1 // SONIC ROBO BLAST 2
2 //-----------------------------------------------------------------------------
3 // Copyright (C) 1993-1996 by id Software, Inc.
4 // Copyright (C) 1998-2000 by DooM Legacy Team.
5 // Copyright (C) 2011-2016 by Matthew "Kaito Sinclaire" Walsh.
6 // Copyright (C) 1999-2020 by Sonic Team Junior.
7 //
8 // This program is free software distributed under the
9 // terms of the GNU General Public License, version 2.
10 // See the 'LICENSE' file for more details.
11 //-----------------------------------------------------------------------------
12 /// \file  m_menu.c
13 /// \brief XMOD's extremely revamped menu system.
14 
15 #ifdef __GNUC__
16 #include <unistd.h>
17 #endif
18 
19 #include "m_menu.h"
20 
21 #include "doomdef.h"
22 #include "d_main.h"
23 #include "d_netcmd.h"
24 #include "console.h"
25 #include "r_local.h"
26 #include "hu_stuff.h"
27 #include "g_game.h"
28 #include "g_input.h"
29 #include "m_argv.h"
30 
31 // Data.
32 #include "sounds.h"
33 #include "s_sound.h"
34 #include "i_system.h"
35 #include "i_threads.h"
36 
37 // Addfile
38 #include "filesrch.h"
39 
40 #include "v_video.h"
41 #include "i_video.h"
42 #include "keys.h"
43 #include "z_zone.h"
44 #include "w_wad.h"
45 #include "p_local.h"
46 #include "p_setup.h"
47 #include "f_finale.h"
48 #include "lua_hook.h"
49 
50 #ifdef HWRENDER
51 #include "hardware/hw_main.h"
52 #endif
53 
54 #include "d_net.h"
55 #include "mserv.h"
56 #include "m_misc.h"
57 #include "m_anigif.h"
58 #include "byteptr.h"
59 #include "st_stuff.h"
60 #include "i_sound.h"
61 #include "fastcmp.h"
62 
63 #include "i_joy.h" // for joystick menu controls
64 
65 // Condition Sets
66 #include "m_cond.h"
67 
68 // And just some randomness for the exits.
69 #include "m_random.h"
70 
71 #if defined(HAVE_SDL)
72 #include "SDL.h"
73 #if SDL_VERSION_ATLEAST(2,0,0)
74 #include "sdl/sdlmain.h" // JOYSTICK_HOTPLUG
75 #endif
76 #endif
77 
78 #if defined (__GNUC__) && (__GNUC__ >= 4)
79 #define FIXUPO0
80 #endif
81 
82 #define SKULLXOFF -32
83 #define LINEHEIGHT 16
84 #define STRINGHEIGHT 8
85 #define FONTBHEIGHT 20
86 #define SMALLLINEHEIGHT 8
87 #define SLIDER_RANGE 9
88 #define SLIDER_WIDTH 78
89 #define SERVERS_PER_PAGE 11
90 
91 typedef enum
92 {
93 	QUITMSG = 0,
94 	QUITMSG1,
95 	QUITMSG2,
96 	QUITMSG3,
97 	QUITMSG4,
98 	QUITMSG5,
99 	QUITMSG6,
100 	QUITMSG7,
101 
102 	QUIT2MSG,
103 	QUIT2MSG1,
104 	QUIT2MSG2,
105 	QUIT2MSG3,
106 	QUIT2MSG4,
107 	QUIT2MSG5,
108 	QUIT2MSG6,
109 
110 	QUIT3MSG,
111 	QUIT3MSG1,
112 	QUIT3MSG2,
113 	QUIT3MSG3,
114 	QUIT3MSG4,
115 	QUIT3MSG5,
116 	QUIT3MSG6,
117 	NUM_QUITMESSAGES
118 } text_enum;
119 
120 #ifdef HAVE_THREADS
121 I_mutex m_menu_mutex;
122 #endif
123 
124 M_waiting_mode_t m_waiting_mode = M_NOT_WAITING;
125 
126 const char *quitmsg[NUM_QUITMESSAGES];
127 
128 // Stuff for customizing the player select screen Tails 09-22-2003
129 description_t description[MAXSKINS];
130 INT16 char_on = -1, startchar = 0;
131 static char *char_notes = NULL;
132 
133 boolean menuactive = false;
134 boolean fromlevelselect = false;
135 
136 typedef enum
137 {
138 	LLM_CREATESERVER,
139 	LLM_LEVELSELECT,
140 	LLM_RECORDATTACK,
141 	LLM_NIGHTSATTACK
142 } levellist_mode_t;
143 
144 levellist_mode_t levellistmode = LLM_CREATESERVER;
145 UINT8 maplistoption = 0;
146 
147 static char joystickInfo[MAX_JOYSTICKS+1][29];
148 #ifndef NONET
149 static UINT32 serverlistpage;
150 #endif
151 
152 static UINT8 numsaves = 0;
153 static saveinfo_t* savegameinfo = NULL; // Extra info about the save games.
154 static patch_t *savselp[7];
155 
156 INT16 startmap; // Mario, NiGHTS, or just a plain old normal game?
157 
158 static INT16 itemOn = 1; // menu item skull is on, Hack by Tails 09-18-2002
159 static INT16 skullAnimCounter = 10; // skull animation counter
160 
161 static  boolean setupcontrols_secondaryplayer;
162 static  INT32   (*setupcontrols)[2];  // pointer to the gamecontrols of the player being edited
163 
164 // shhh... what am I doing... nooooo!
165 static INT32 vidm_testingmode = 0;
166 static INT32 vidm_previousmode;
167 static INT32 vidm_selected = 0;
168 static INT32 vidm_nummodes;
169 static INT32 vidm_column_size;
170 
171 // new menus
172 static tic_t recatkdrawtimer = 0;
173 static tic_t ntsatkdrawtimer = 0;
174 
175 static tic_t charseltimer = 0;
176 static fixed_t char_scroll = 0;
177 #define charscrollamt 128*FRACUNIT
178 
179 static tic_t keydown = 0;
180 
181 //
182 // PROTOTYPES
183 //
184 
185 static void M_GoBack(INT32 choice);
186 static void M_StopMessage(INT32 choice);
187 static boolean stopstopmessage = false;
188 
189 #ifndef NONET
190 static void M_HandleServerPage(INT32 choice);
191 static void M_RoomMenu(INT32 choice);
192 #endif
193 
194 // Prototyping is fun, innit?
195 // ==========================================================================
196 // NEEDED FUNCTION PROTOTYPES GO HERE
197 // ==========================================================================
198 
199 // the haxor message menu
200 menu_t MessageDef;
201 
202 menu_t SPauseDef;
203 
204 // Level Select
205 static levelselect_t levelselect = {0, NULL};
206 static UINT8 levelselectselect[3];
207 static patch_t *levselp[2][3];
208 static INT32 lsoffs[2];
209 
210 #define lsrow levelselectselect[0]
211 #define lscol levelselectselect[1]
212 #define lshli levelselectselect[2]
213 
214 #define lshseperation 101
215 #define lsbasevseperation (62*vid.height)/(BASEVIDHEIGHT*vid.dupy) //62
216 #define lsheadingheight 16
217 #define getheadingoffset(row) (levelselect.rows[row].header[0] ? lsheadingheight : 0)
218 #define lsvseperation(row) lsbasevseperation + getheadingoffset(row)
219 #define lswide(row) levelselect.rows[row].mapavailable[3]
220 
221 #define lsbasex 19
222 #define lsbasey 59+lsheadingheight
223 
224 // Sky Room
225 static void M_CustomLevelSelect(INT32 choice);
226 static void M_CustomWarp(INT32 choice);
227 FUNCNORETURN static ATTRNORETURN void M_UltimateCheat(INT32 choice);
228 static void M_LoadGameLevelSelect(INT32 choice);
229 static void M_AllowSuper(INT32 choice);
230 static void M_GetAllEmeralds(INT32 choice);
231 static void M_DestroyRobots(INT32 choice);
232 static void M_LevelSelectWarp(INT32 choice);
233 static void M_Credits(INT32 choice);
234 static void M_SoundTest(INT32 choice);
235 static void M_PandorasBox(INT32 choice);
236 static void M_EmblemHints(INT32 choice);
237 static void M_HandleEmblemHints(INT32 choice);
238 UINT32 hintpage = 1;
239 static void M_HandleChecklist(INT32 choice);
240 menu_t SR_MainDef, SR_UnlockChecklistDef;
241 
242 static UINT8 check_on;
243 
244 // Misc. Main Menu
245 static void M_SinglePlayerMenu(INT32 choice);
246 static void M_Options(INT32 choice);
247 static void M_SelectableClearMenus(INT32 choice);
248 static void M_Retry(INT32 choice);
249 static void M_EndGame(INT32 choice);
250 static void M_MapChange(INT32 choice);
251 static void M_ChangeLevel(INT32 choice);
252 static void M_ConfirmSpectate(INT32 choice);
253 static void M_ConfirmEnterGame(INT32 choice);
254 static void M_ConfirmTeamScramble(INT32 choice);
255 static void M_ConfirmTeamChange(INT32 choice);
256 static void M_SecretsMenu(INT32 choice);
257 static void M_SetupChoosePlayer(INT32 choice);
258 static UINT8 M_SetupChoosePlayerDirect(INT32 choice);
259 static void M_QuitSRB2(INT32 choice);
260 menu_t SP_MainDef, OP_MainDef;
261 menu_t MISC_ScrambleTeamDef, MISC_ChangeTeamDef;
262 
263 // Single Player
264 static void M_StartTutorial(INT32 choice);
265 static void M_LoadGame(INT32 choice);
266 static void M_HandleTimeAttackLevelSelect(INT32 choice);
267 static void M_TimeAttackLevelSelect(INT32 choice);
268 static void M_TimeAttack(INT32 choice);
269 static void M_NightsAttackLevelSelect(INT32 choice);
270 static void M_NightsAttack(INT32 choice);
271 static void M_Statistics(INT32 choice);
272 static void M_ReplayTimeAttack(INT32 choice);
273 static void M_ChooseTimeAttack(INT32 choice);
274 static void M_ChooseNightsAttack(INT32 choice);
275 static void M_ModeAttackEndGame(INT32 choice);
276 static void M_SetGuestReplay(INT32 choice);
277 static void M_HandleChoosePlayerMenu(INT32 choice);
278 static void M_ChoosePlayer(INT32 choice);
279 static void M_MarathonLiveEventBackup(INT32 choice);
280 static void M_Marathon(INT32 choice);
281 static void M_HandleMarathonChoosePlayer(INT32 choice);
282 static void M_StartMarathon(INT32 choice);
283 menu_t SP_LevelStatsDef;
284 static menu_t SP_TimeAttackDef, SP_ReplayDef, SP_GuestReplayDef, SP_GhostDef;
285 static menu_t SP_NightsAttackDef, SP_NightsReplayDef, SP_NightsGuestReplayDef, SP_NightsGhostDef;
286 static menu_t SP_MarathonDef;
287 
288 // Multiplayer
289 static void M_SetupMultiPlayer(INT32 choice);
290 static void M_SetupMultiPlayer2(INT32 choice);
291 static void M_StartSplitServerMenu(INT32 choice);
292 static void M_StartServer(INT32 choice);
293 static void M_ServerOptions(INT32 choice);
294 #ifndef NONET
295 static void M_StartServerMenu(INT32 choice);
296 static void M_ConnectMenu(INT32 choice);
297 static void M_ConnectMenuModChecks(INT32 choice);
298 static void M_Refresh(INT32 choice);
299 static void M_Connect(INT32 choice);
300 static void M_ChooseRoom(INT32 choice);
301 menu_t MP_MainDef;
302 #endif
303 
304 // Options
305 // Split into multiple parts due to size
306 // Controls
307 menu_t OP_ChangeControlsDef;
308 menu_t OP_MPControlsDef, OP_MiscControlsDef;
309 menu_t OP_P1ControlsDef, OP_P2ControlsDef, OP_MouseOptionsDef;
310 menu_t OP_Mouse2OptionsDef, OP_Joystick1Def, OP_Joystick2Def;
311 menu_t OP_CameraOptionsDef, OP_Camera2OptionsDef;
312 menu_t OP_PlaystyleDef;
313 static void M_VideoModeMenu(INT32 choice);
314 static void M_Setup1PControlsMenu(INT32 choice);
315 static void M_Setup2PControlsMenu(INT32 choice);
316 static void M_Setup1PJoystickMenu(INT32 choice);
317 static void M_Setup2PJoystickMenu(INT32 choice);
318 static void M_Setup1PPlaystyleMenu(INT32 choice);
319 static void M_Setup2PPlaystyleMenu(INT32 choice);
320 static void M_AssignJoystick(INT32 choice);
321 static void M_ChangeControl(INT32 choice);
322 
323 // Video & Sound
324 static void M_VideoOptions(INT32 choice);
325 menu_t OP_VideoOptionsDef, OP_VideoModeDef, OP_ColorOptionsDef;
326 #ifdef HWRENDER
327 static void M_OpenGLOptionsMenu(void);
328 menu_t OP_OpenGLOptionsDef;
329 #ifdef ALAM_LIGHTING
330 menu_t OP_OpenGLLightingDef;
331 #endif // ALAM_LIGHTING
332 #endif // HWRENDER
333 menu_t OP_SoundOptionsDef;
334 menu_t OP_SoundAdvancedDef;
335 
336 //Misc
337 menu_t OP_DataOptionsDef, OP_ScreenshotOptionsDef, OP_EraseDataDef;
338 menu_t OP_ServerOptionsDef;
339 menu_t OP_MonitorToggleDef;
340 static void M_ScreenshotOptions(INT32 choice);
341 static void M_SetupScreenshotMenu(void);
342 static void M_EraseData(INT32 choice);
343 
344 static void M_Addons(INT32 choice);
345 static void M_AddonsOptions(INT32 choice);
346 static patch_t *addonsp[NUM_EXT+5];
347 
348 #define addonmenusize 9 // number of items actually displayed in the addons menu view, formerly (2*numaddonsshown + 1)
349 #define numaddonsshown 4 // number of items to each side of the currently selected item, unless at top/bottom ends of directory
350 
351 static void M_DrawLevelPlatterHeader(INT32 y, const char *header, boolean headerhighlight, boolean allowlowercase);
352 
353 // Drawing functions
354 static void M_DrawGenericMenu(void);
355 static void M_DrawGenericScrollMenu(void);
356 static void M_DrawCenteredMenu(void);
357 static void M_DrawAddons(void);
358 static void M_DrawChecklist(void);
359 static void M_DrawSoundTest(void);
360 static void M_DrawEmblemHints(void);
361 static void M_DrawPauseMenu(void);
362 static void M_DrawServerMenu(void);
363 static void M_DrawLevelPlatterMenu(void);
364 static void M_DrawImageDef(void);
365 static void M_DrawLoad(void);
366 static void M_DrawLevelStats(void);
367 static void M_DrawTimeAttackMenu(void);
368 static void M_DrawNightsAttackMenu(void);
369 static void M_DrawMarathon(void);
370 static void M_DrawSetupChoosePlayerMenu(void);
371 static void M_DrawControlsDefMenu(void);
372 static void M_DrawCameraOptionsMenu(void);
373 static void M_DrawPlaystyleMenu(void);
374 static void M_DrawControl(void);
375 static void M_DrawMainVideoMenu(void);
376 static void M_DrawVideoMode(void);
377 static void M_DrawColorMenu(void);
378 static void M_DrawScreenshotMenu(void);
379 static void M_DrawMonitorToggles(void);
380 #ifndef NONET
381 static void M_DrawConnectMenu(void);
382 static void M_DrawMPMainMenu(void);
383 static void M_DrawRoomMenu(void);
384 #endif
385 static void M_DrawJoystick(void);
386 static void M_DrawSetupMultiPlayerMenu(void);
387 
388 // Handling functions
389 static boolean M_ExitPandorasBox(void);
390 static boolean M_QuitMultiPlayerMenu(void);
391 static void M_HandleAddons(INT32 choice);
392 static void M_HandleLevelPlatter(INT32 choice);
393 static void M_HandleSoundTest(INT32 choice);
394 static void M_HandleImageDef(INT32 choice);
395 static void M_HandleLoadSave(INT32 choice);
396 static void M_HandleLevelStats(INT32 choice);
397 static void M_HandlePlaystyleMenu(INT32 choice);
398 #ifndef NONET
399 static boolean M_CancelConnect(void);
400 static void M_HandleConnectIP(INT32 choice);
401 #endif
402 static void M_HandleSetupMultiPlayer(INT32 choice);
403 static void M_HandleVideoMode(INT32 choice);
404 
405 static void M_ResetCvars(void);
406 
407 // Consvar onchange functions
408 static void Newgametype_OnChange(void);
409 static void Dummymares_OnChange(void);
410 
411 // ==========================================================================
412 // CONSOLE VARIABLES AND THEIR POSSIBLE VALUES GO HERE.
413 // ==========================================================================
414 
415 consvar_t cv_showfocuslost = CVAR_INIT ("showfocuslost", "Yes", CV_SAVE, CV_YesNo, NULL);
416 
417 static CV_PossibleValue_t map_cons_t[] = {
418 	{1,"MIN"},
419 	{NUMMAPS, "MAX"},
420 	{0,NULL}
421 };
422 consvar_t cv_nextmap = CVAR_INIT ("nextmap", "1", CV_HIDEN|CV_CALL, map_cons_t, Nextmap_OnChange);
423 
424 static CV_PossibleValue_t skins_cons_t[MAXSKINS+1] = {{1, DEFAULTSKIN}};
425 consvar_t cv_chooseskin = CVAR_INIT ("chooseskin", DEFAULTSKIN, CV_HIDEN|CV_CALL, skins_cons_t, Nextmap_OnChange);
426 
427 // This gametype list is integral for many different reasons.
428 // When you add gametypes here, don't forget to update them in deh_tables.c and doomstat.h!
429 CV_PossibleValue_t gametype_cons_t[NUMGAMETYPES+1];
430 
431 consvar_t cv_newgametype = CVAR_INIT ("newgametype", "Co-op", CV_HIDEN|CV_CALL, gametype_cons_t, Newgametype_OnChange);
432 
433 static CV_PossibleValue_t serversort_cons_t[] = {
434 	{0,"Ping"},
435 	{1,"Modified State"},
436 	{2,"Most Players"},
437 	{3,"Least Players"},
438 	{4,"Max Player Slots"},
439 	{5,"Gametype"},
440 	{0,NULL}
441 };
442 consvar_t cv_serversort = CVAR_INIT ("serversort", "Ping", CV_HIDEN | CV_CALL, serversort_cons_t, M_SortServerList);
443 
444 // first time memory
445 consvar_t cv_tutorialprompt = CVAR_INIT ("tutorialprompt", "On", CV_SAVE, CV_OnOff, NULL);
446 
447 // autorecord demos for time attack
448 static consvar_t cv_autorecord = CVAR_INIT ("autorecord", "Yes", 0, CV_YesNo, NULL);
449 
450 CV_PossibleValue_t ghost_cons_t[] = {{0, "Hide"}, {1, "Show"}, {2, "Show All"}, {0, NULL}};
451 CV_PossibleValue_t ghost2_cons_t[] = {{0, "Hide"}, {1, "Show"}, {0, NULL}};
452 
453 consvar_t cv_ghost_bestscore = CVAR_INIT ("ghost_bestscore", "Show", CV_SAVE, ghost_cons_t, NULL);
454 consvar_t cv_ghost_besttime  = CVAR_INIT ("ghost_besttime",  "Show", CV_SAVE, ghost_cons_t, NULL);
455 consvar_t cv_ghost_bestrings = CVAR_INIT ("ghost_bestrings", "Show", CV_SAVE, ghost_cons_t, NULL);
456 consvar_t cv_ghost_last      = CVAR_INIT ("ghost_last",      "Show", CV_SAVE, ghost_cons_t, NULL);
457 consvar_t cv_ghost_guest     = CVAR_INIT ("ghost_guest",     "Show", CV_SAVE, ghost2_cons_t, NULL);
458 
459 //Console variables used solely in the menu system.
460 //todo: add a way to use non-console variables in the menu
461 //      or make these consvars legitimate like color or skin.
462 static CV_PossibleValue_t dummyteam_cons_t[] = {{0, "Spectator"}, {1, "Red"}, {2, "Blue"}, {0, NULL}};
463 static CV_PossibleValue_t dummyscramble_cons_t[] = {{0, "Random"}, {1, "Points"}, {0, NULL}};
464 static CV_PossibleValue_t ringlimit_cons_t[] = {{0, "MIN"}, {9999, "MAX"}, {0, NULL}};
465 static CV_PossibleValue_t liveslimit_cons_t[] = {{1, "MIN"}, {99, "MAX"}, {-1, "Infinite"}, {0, NULL}};
466 static CV_PossibleValue_t contlimit_cons_t[] = {{0, "MIN"}, {99, "MAX"}, {0, NULL}};
467 static CV_PossibleValue_t dummymares_cons_t[] = {
468 	{-1, "END"}, {0,"Overall"}, {1,"Mare 1"}, {2,"Mare 2"}, {3,"Mare 3"}, {4,"Mare 4"}, {5,"Mare 5"}, {6,"Mare 6"}, {7,"Mare 7"}, {8,"Mare 8"}, {0,NULL}
469 };
470 
471 static consvar_t cv_dummyteam = CVAR_INIT ("dummyteam", "Spectator", CV_HIDEN, dummyteam_cons_t, NULL);
472 static consvar_t cv_dummyscramble = CVAR_INIT ("dummyscramble", "Random", CV_HIDEN, dummyscramble_cons_t, NULL);
473 static consvar_t cv_dummyrings = CVAR_INIT ("dummyrings", "0", CV_HIDEN, ringlimit_cons_t,	NULL);
474 static consvar_t cv_dummylives = CVAR_INIT ("dummylives", "0", CV_HIDEN, liveslimit_cons_t, NULL);
475 static consvar_t cv_dummycontinues = CVAR_INIT ("dummycontinues", "0", CV_HIDEN, contlimit_cons_t, NULL);
476 static consvar_t cv_dummymares = CVAR_INIT ("dummymares", "Overall", CV_HIDEN|CV_CALL, dummymares_cons_t, Dummymares_OnChange);
477 
478 CV_PossibleValue_t marathon_cons_t[] = {{0, "Standard"}, {1, "Live Event Backup"}, {2, "Ultimate"}, {0, NULL}};
479 CV_PossibleValue_t loadless_cons_t[] = {{0, "Realtime"}, {1, "In-game"}, {0, NULL}};
480 
481 consvar_t cv_dummymarathon = CVAR_INIT ("dummymarathon", "Standard", CV_HIDEN, marathon_cons_t, NULL);
482 consvar_t cv_dummycutscenes = CVAR_INIT ("dummycutscenes", "Off", CV_HIDEN, CV_OnOff, NULL);
483 consvar_t cv_dummyloadless = CVAR_INIT ("dummyloadless", "In-game", CV_HIDEN, loadless_cons_t, NULL);
484 
485 // ==========================================================================
486 // ORGANIZATION START.
487 // ==========================================================================
488 // Note: Never should we be jumping from one category of menu options to another
489 //       without first going to the Main Menu.
490 // Note: Ignore the above if you're working with the Pause menu.
491 // Note: (Prefix)_MainMenu should be the target of all Main Menu options that
492 //       point to submenus.
493 
494 // ---------
495 // Main Menu
496 // ---------
497 static menuitem_t MainMenu[] =
498 {
499 	{IT_STRING|IT_CALL,    NULL, "1  Player",   M_SinglePlayerMenu,      76},
500 #ifndef NONET
501 	{IT_STRING|IT_SUBMENU, NULL, "Multiplayer", &MP_MainDef,             84},
502 #else
503 	{IT_STRING|IT_CALL,    NULL, "Multiplayer", M_StartSplitServerMenu,  84},
504 #endif
505 	{IT_STRING|IT_CALL,    NULL, "Extras",      M_SecretsMenu,           92},
506 	{IT_CALL   |IT_STRING, NULL, "Addons",      M_Addons,               100},
507 	{IT_STRING|IT_CALL,    NULL, "Options",     M_Options,              108},
508 	{IT_STRING|IT_CALL,    NULL, "Quit  Game",  M_QuitSRB2,             116},
509 };
510 
511 typedef enum
512 {
513 	singleplr = 0,
514 	multiplr,
515 	secrets,
516 	addons,
517 	options,
518 	quitdoom
519 } main_e;
520 
521 static menuitem_t MISC_AddonsMenu[] =
522 {
523 	{IT_KEYHANDLER | IT_NOTHING, NULL, "", M_HandleAddons, 0},     // dummy menuitem for the control func
524 };
525 
526 // ---------------------------------
527 // Pause Menu Mode Attacking Edition
528 // ---------------------------------
529 static menuitem_t MAPauseMenu[] =
530 {
531 	{IT_CALL | IT_STRING,    NULL, "Emblem Hints...",      M_EmblemHints,         32},
532 
533 	{IT_CALL | IT_STRING,    NULL, "Continue",             M_SelectableClearMenus,48},
534 	{IT_CALL | IT_STRING,    NULL, "Retry",                M_ModeAttackRetry,     56},
535 	{IT_CALL | IT_STRING,    NULL, "Abort",                M_ModeAttackEndGame,   64},
536 };
537 
538 typedef enum
539 {
540 	mapause_hints,
541 	mapause_continue,
542 	mapause_retry,
543 	mapause_abort
544 } mapause_e;
545 
546 // ---------------------
547 // Pause Menu MP Edition
548 // ---------------------
549 static menuitem_t MPauseMenu[] =
550 {
551 	{IT_STRING | IT_CALL,    NULL, "Add-ons...",                M_Addons,               8},
552 	{IT_STRING | IT_SUBMENU, NULL, "Scramble Teams...",         &MISC_ScrambleTeamDef, 16},
553 	{IT_STRING | IT_CALL,    NULL, "Switch Gametype/Level...",  M_MapChange,           24},
554 
555 	{IT_STRING | IT_CALL,    NULL, "Continue",                  M_SelectableClearMenus,40},
556 	{IT_STRING | IT_CALL,    NULL, "Player 1 Setup",            M_SetupMultiPlayer,    48}, // splitscreen
557 	{IT_STRING | IT_CALL,    NULL, "Player 2 Setup",            M_SetupMultiPlayer2,   56}, // splitscreen
558 
559 	{IT_STRING | IT_CALL,    NULL, "Spectate",                  M_ConfirmSpectate,     48},
560 	{IT_STRING | IT_CALL,    NULL, "Enter Game",                M_ConfirmEnterGame,    48},
561 	{IT_STRING | IT_SUBMENU, NULL, "Switch Team...",            &MISC_ChangeTeamDef,   48},
562 	{IT_STRING | IT_CALL,    NULL, "Player Setup",              M_SetupMultiPlayer,    56}, // alone
563 	{IT_STRING | IT_CALL,    NULL, "Options",                   M_Options,             64},
564 
565 	{IT_STRING | IT_CALL,    NULL, "Return to Title",           M_EndGame,             80},
566 	{IT_STRING | IT_CALL,    NULL, "Quit Game",                 M_QuitSRB2,            88},
567 };
568 
569 typedef enum
570 {
571 	mpause_addons = 0,
572 	mpause_scramble,
573 	mpause_switchmap,
574 
575 	mpause_continue,
576 	mpause_psetupsplit,
577 	mpause_psetupsplit2,
578 	mpause_spectate,
579 	mpause_entergame,
580 	mpause_switchteam,
581 	mpause_psetup,
582 	mpause_options,
583 
584 	mpause_title,
585 	mpause_quit
586 } mpause_e;
587 
588 // ---------------------
589 // Pause Menu SP Edition
590 // ---------------------
591 static menuitem_t SPauseMenu[] =
592 {
593 	// Pandora's Box will be shifted up if both options are available
594 	{IT_CALL | IT_STRING,    NULL, "Pandora's Box...",     M_PandorasBox,         16},
595 	{IT_CALL | IT_STRING,    NULL, "Emblem Hints...",      M_EmblemHints,         24},
596 	{IT_CALL | IT_STRING,    NULL, "Level Select...",      M_LoadGameLevelSelect, 32},
597 
598 	{IT_CALL | IT_STRING,    NULL, "Continue",             M_SelectableClearMenus,48},
599 	{IT_CALL | IT_STRING,    NULL, "Retry",                M_Retry,               56},
600 	{IT_CALL | IT_STRING,    NULL, "Options",              M_Options,             64},
601 
602 	{IT_CALL | IT_STRING,    NULL, "Return to Title",      M_EndGame,             80},
603 	{IT_CALL | IT_STRING,    NULL, "Quit Game",            M_QuitSRB2,            88},
604 };
605 
606 typedef enum
607 {
608 	spause_pandora = 0,
609 	spause_hints,
610 	spause_levelselect,
611 
612 	spause_continue,
613 	spause_retry,
614 	spause_options,
615 
616 	spause_title,
617 	spause_quit
618 } spause_e;
619 
620 // -----------------
621 // Misc menu options
622 // -----------------
623 // Prefix: MISC_
624 static menuitem_t MISC_ScrambleTeamMenu[] =
625 {
626 	{IT_STRING|IT_CVAR,      NULL, "Scramble Method", &cv_dummyscramble,     30},
627 	{IT_WHITESTRING|IT_CALL, NULL, "Confirm",         M_ConfirmTeamScramble, 90},
628 };
629 
630 static menuitem_t MISC_ChangeTeamMenu[] =
631 {
632 	{IT_STRING|IT_CVAR,              NULL, "Select Team",             &cv_dummyteam,    30},
633 	{IT_WHITESTRING|IT_CALL,         NULL, "Confirm",           M_ConfirmTeamChange,    90},
634 };
635 
636 gtdesc_t gametypedesc[NUMGAMETYPES] =
637 {
638 	{{ 54,  54}, "Play through the single-player campaign with your friends, teaming up to beat Dr Eggman's nefarious challenges!"},
639 	{{103, 103}, "Speed your way through the main acts, competing in several different categories to see who's the best."},
640 	{{190, 190}, "There's not much to it - zoom through the level faster than everyone else."},
641 	{{ 66,  66}, "Sling rings at your foes in a free-for-all battle. Use the special weapon rings to your advantage!"},
642 	{{153,  37}, "Sling rings at your foes in a color-coded battle. Use the special weapon rings to your advantage!"},
643 	{{123, 123}, "Whoever's IT has to hunt down everyone else. If you get caught, you have to turn on your former friends!"},
644 	{{150, 150}, "Try and find a good hiding place in these maps - we dare you."},
645 	{{ 37, 153}, "Steal the flag from the enemy's base and bring it back to your own, but watch out - they could just as easily steal yours!"},
646 };
647 
648 static menuitem_t MISC_ChangeLevelMenu[] =
649 {
650 	{IT_KEYHANDLER | IT_NOTHING, NULL, "", M_HandleLevelPlatter, 0},     // dummy menuitem for the control func
651 };
652 
653 static menuitem_t MISC_HelpMenu[] =
654 {
655 	{IT_KEYHANDLER | IT_NOTHING, NULL, "HELPN01", M_HandleImageDef, 0},
656 	{IT_KEYHANDLER | IT_NOTHING, NULL, "HELPN02", M_HandleImageDef, 0},
657 	{IT_KEYHANDLER | IT_NOTHING, NULL, "HELPN03", M_HandleImageDef, 0},
658 	{IT_KEYHANDLER | IT_NOTHING, NULL, "HELPM01", M_HandleImageDef, 0},
659 	{IT_KEYHANDLER | IT_NOTHING, NULL, "HELPM02", M_HandleImageDef, 0},
660 };
661 
662 // --------------------------------
663 // Sky Room and all of its submenus
664 // --------------------------------
665 // Prefix: SR_
666 
667 // Pause Menu Pandora's Box Options
668 static menuitem_t SR_PandorasBox[] =
669 {
670 	{IT_STRING | IT_CALL, NULL, "Mid-game add-ons...", M_Addons,             0},
671 
672 	{IT_STRING | IT_CVAR, NULL, "Rings",               &cv_dummyrings,      20},
673 	{IT_STRING | IT_CVAR, NULL, "Lives",               &cv_dummylives,      30},
674 	{IT_STRING | IT_CVAR, NULL, "Continues",           &cv_dummycontinues,  40},
675 
676 	{IT_STRING | IT_CVAR, NULL, "Gravity",             &cv_gravity,         60},
677 	{IT_STRING | IT_CVAR, NULL, "Throw Rings",         &cv_ringslinger,     70},
678 
679 	{IT_STRING | IT_CALL, NULL, "Enable Super form",   M_AllowSuper,        90},
680 	{IT_STRING | IT_CALL, NULL, "Get All Emeralds",    M_GetAllEmeralds,   100},
681 	{IT_STRING | IT_CALL, NULL, "Destroy All Robots",  M_DestroyRobots,    110},
682 
683 	{IT_STRING | IT_CALL, NULL, "Ultimate Cheat",      M_UltimateCheat,    130},
684 };
685 
686 // Sky Room Custom Unlocks
687 static menuitem_t SR_MainMenu[] =
688 {
689 	{IT_STRING|IT_SUBMENU,NULL, "Extras Checklist", &SR_UnlockChecklistDef, 0},
690 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom1
691 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom2
692 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom3
693 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom4
694 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom5
695 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom6
696 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom7
697 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom8
698 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom9
699 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom10
700 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom11
701 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom12
702 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom13
703 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom14
704 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom15
705 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom16
706 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom17
707 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom18
708 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom19
709 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom20
710 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom21
711 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom22
712 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom23
713 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom24
714 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom25
715 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom26
716 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom27
717 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom28
718 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom29
719 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom30
720 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom31
721 	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom32
722 
723 };
724 
725 static menuitem_t SR_LevelSelectMenu[] =
726 {
727 	{IT_KEYHANDLER | IT_NOTHING, NULL, "", M_HandleLevelPlatter, 0},     // dummy menuitem for the control func
728 };
729 
730 static menuitem_t SR_UnlockChecklistMenu[] =
731 {
732 	{IT_KEYHANDLER | IT_STRING, NULL, "", M_HandleChecklist, 0},
733 };
734 
735 static menuitem_t SR_SoundTestMenu[] =
736 {
737 	{IT_KEYHANDLER | IT_STRING, NULL, "", M_HandleSoundTest, 0},
738 };
739 
740 static menuitem_t SR_EmblemHintMenu[] =
741 {
742 	{IT_STRING | IT_ARROWS,  NULL, "Page",    M_HandleEmblemHints, 10},
743 	{IT_STRING|IT_CVAR,      NULL, "Emblem Radar", &cv_itemfinder, 20},
744 	{IT_WHITESTRING|IT_CALL, NULL, "Back",         M_GoBack,       30}
745 };
746 
747 // --------------------------------
748 // 1 Player and all of its submenus
749 // --------------------------------
750 // Prefix: SP_
751 
752 // Single Player Main
753 static menuitem_t SP_MainMenu[] =
754 {
755 	// Note: If changing the positions here, also change them in M_SinglePlayerMenu()
756 	{IT_CALL | IT_STRING,                       NULL, "Start Game",    M_LoadGame,                 76},
757 	{IT_SECRET,                                 NULL, "Record Attack", M_TimeAttack,               84},
758 	{IT_SECRET,                                 NULL, "NiGHTS Mode",   M_NightsAttack,             92},
759 	{IT_SECRET,                                 NULL, "Marathon Run",  M_Marathon,                100},
760 	{IT_CALL | IT_STRING,                       NULL, "Tutorial",      M_StartTutorial,           108},
761 	{IT_CALL | IT_STRING | IT_CALL_NOTMODIFIED, NULL, "Statistics",    M_Statistics,              116}
762 };
763 
764 enum
765 {
766 	spstartgame,
767 	sprecordattack,
768 	spnightsmode,
769 	spmarathon,
770 	sptutorial,
771 	spstatistics
772 };
773 
774 // Single Player Load Game
775 static menuitem_t SP_LoadGameMenu[] =
776 {
777 	{IT_KEYHANDLER | IT_NOTHING, NULL, "", M_HandleLoadSave, 0},     // dummy menuitem for the control func
778 };
779 
780 // Single Player Level Select
781 static menuitem_t SP_LevelSelectMenu[] =
782 {
783 	{IT_KEYHANDLER | IT_NOTHING, NULL, "", M_HandleLevelPlatter, 0},     // dummy menuitem for the control func
784 };
785 
786 // Single Player Time Attack Level Select
787 static menuitem_t SP_TimeAttackLevelSelectMenu[] =
788 {
789 	{IT_KEYHANDLER | IT_NOTHING, NULL, "", M_HandleLevelPlatter, 0},     // dummy menuitem for the control func
790 };
791 
792 // Single Player Time Attack
793 static menuitem_t SP_TimeAttackMenu[] =
794 {
795 	{IT_STRING|IT_KEYHANDLER,  NULL, "Level Select...", M_HandleTimeAttackLevelSelect,   62},
796 	{IT_STRING|IT_CVAR,        NULL, "Character",       &cv_chooseskin,             72},
797 
798 	{IT_DISABLED,              NULL, "Guest Option...", &SP_GuestReplayDef, 100},
799 	{IT_DISABLED,              NULL, "Replay...",       &SP_ReplayDef,      110},
800 	{IT_DISABLED,              NULL, "Ghosts...",       &SP_GhostDef,       120},
801 	{IT_WHITESTRING|IT_CALL|IT_CALL_NOTMODIFIED,   NULL, "Start",         M_ChooseTimeAttack,   130},
802 };
803 
804 enum
805 {
806 	talevel,
807 	taplayer,
808 
809 	taguest,
810 	tareplay,
811 	taghost,
812 	tastart
813 };
814 
815 static menuitem_t SP_ReplayMenu[] =
816 {
817 	{IT_WHITESTRING|IT_CALL, NULL, "Replay Best Score", M_ReplayTimeAttack, 0},
818 	{IT_WHITESTRING|IT_CALL, NULL, "Replay Best Time",  M_ReplayTimeAttack, 8},
819 	{IT_WHITESTRING|IT_CALL, NULL, "Replay Best Rings", M_ReplayTimeAttack,16},
820 
821 	{IT_WHITESTRING|IT_CALL, NULL, "Replay Last",       M_ReplayTimeAttack,29},
822 	{IT_WHITESTRING|IT_CALL, NULL, "Replay Guest",      M_ReplayTimeAttack,37},
823 
824 	{IT_WHITESTRING|IT_SUBMENU, NULL, "Back",           &SP_TimeAttackDef, 50}
825 };
826 
827 static menuitem_t SP_NightsReplayMenu[] =
828 {
829 	{IT_WHITESTRING|IT_CALL, NULL, "Replay Best Score", M_ReplayTimeAttack, 8},
830 	{IT_WHITESTRING|IT_CALL, NULL, "Replay Best Time",  M_ReplayTimeAttack,16},
831 
832 	{IT_WHITESTRING|IT_CALL, NULL, "Replay Last",       M_ReplayTimeAttack,29},
833 	{IT_WHITESTRING|IT_CALL, NULL, "Replay Guest",      M_ReplayTimeAttack,37},
834 
835 	{IT_WHITESTRING|IT_SUBMENU, NULL, "Back",           &SP_NightsAttackDef, 50}
836 };
837 
838 static menuitem_t SP_GuestReplayMenu[] =
839 {
840 	{IT_WHITESTRING|IT_CALL, NULL, "Save Best Score as Guest", M_SetGuestReplay, 0},
841 	{IT_WHITESTRING|IT_CALL, NULL, "Save Best Time as Guest",  M_SetGuestReplay, 8},
842 	{IT_WHITESTRING|IT_CALL, NULL, "Save Best Rings as Guest", M_SetGuestReplay,16},
843 	{IT_WHITESTRING|IT_CALL, NULL, "Save Last as Guest",       M_SetGuestReplay,24},
844 
845 	{IT_WHITESTRING|IT_CALL, NULL, "Delete Guest Replay",      M_SetGuestReplay,37},
846 
847 	{IT_WHITESTRING|IT_SUBMENU, NULL, "Back",                &SP_TimeAttackDef, 50}
848 };
849 
850 static menuitem_t SP_NightsGuestReplayMenu[] =
851 {
852 	{IT_WHITESTRING|IT_CALL, NULL, "Save Best Score as Guest", M_SetGuestReplay, 8},
853 	{IT_WHITESTRING|IT_CALL, NULL, "Save Best Time as Guest",  M_SetGuestReplay,16},
854 	{IT_WHITESTRING|IT_CALL, NULL, "Save Last as Guest",       M_SetGuestReplay,24},
855 
856 	{IT_WHITESTRING|IT_CALL, NULL, "Delete Guest Replay",      M_SetGuestReplay,37},
857 
858 	{IT_WHITESTRING|IT_SUBMENU, NULL, "Back",                &SP_NightsAttackDef, 50}
859 };
860 
861 static menuitem_t SP_GhostMenu[] =
862 {
863 	{IT_STRING|IT_CVAR,         NULL, "Best Score", &cv_ghost_bestscore, 0},
864 	{IT_STRING|IT_CVAR,         NULL, "Best Time",  &cv_ghost_besttime,  8},
865 	{IT_STRING|IT_CVAR,         NULL, "Best Rings", &cv_ghost_bestrings,16},
866 	{IT_STRING|IT_CVAR,         NULL, "Last",       &cv_ghost_last,     24},
867 
868 	{IT_STRING|IT_CVAR,         NULL, "Guest",      &cv_ghost_guest,    37},
869 
870 	{IT_WHITESTRING|IT_SUBMENU, NULL, "Back",       &SP_TimeAttackDef,  50}
871 };
872 
873 static menuitem_t SP_NightsGhostMenu[] =
874 {
875 	{IT_STRING|IT_CVAR,         NULL, "Best Score", &cv_ghost_bestscore, 8},
876 	{IT_STRING|IT_CVAR,         NULL, "Best Time",  &cv_ghost_besttime, 16},
877 	{IT_STRING|IT_CVAR,         NULL, "Last",       &cv_ghost_last,     24},
878 
879 	{IT_STRING|IT_CVAR,         NULL, "Guest",      &cv_ghost_guest,    37},
880 
881 	{IT_WHITESTRING|IT_SUBMENU, NULL, "Back",       &SP_NightsAttackDef,  50}
882 };
883 
884 // Single Player Nights Attack Level Select
885 static menuitem_t SP_NightsAttackLevelSelectMenu[] =
886 {
887 	{IT_KEYHANDLER | IT_NOTHING, NULL, "", M_HandleLevelPlatter, 0},     // dummy menuitem for the control func
888 };
889 
890 // Single Player Nights Attack
891 static menuitem_t SP_NightsAttackMenu[] =
892 {
893 	{IT_STRING|IT_KEYHANDLER,        NULL, "Level Select...",  &M_HandleTimeAttackLevelSelect,  52},
894 	{IT_STRING|IT_CVAR,        NULL, "Character",       &cv_chooseskin,             62},
895 	{IT_STRING|IT_CVAR,        NULL, "Show Records For", &cv_dummymares,              72},
896 
897 	{IT_DISABLED,              NULL, "Guest Option...",  &SP_NightsGuestReplayDef,    100},
898 	{IT_DISABLED,              NULL, "Replay...",        &SP_NightsReplayDef,         110},
899 	{IT_DISABLED,              NULL, "Ghosts...",        &SP_NightsGhostDef,          120},
900 	{IT_WHITESTRING|IT_CALL|IT_CALL_NOTMODIFIED, NULL, "Start", M_ChooseNightsAttack, 130},
901 };
902 
903 enum
904 {
905 	nalevel,
906 	nachar,
907 	narecords,
908 
909 	naguest,
910 	nareplay,
911 	naghost,
912 	nastart
913 };
914 
915 // Marathon
916 static menuitem_t SP_MarathonMenu[] =
917 {
918 	{IT_STRING|IT_KEYHANDLER,  NULL, "Character", M_HandleMarathonChoosePlayer,  90},
919 	{IT_STRING|IT_CVAR,        NULL, "Category",  &cv_dummymarathon,            100},
920 	{IT_STRING|IT_CVAR,        NULL, "Timer",     &cv_dummyloadless,            110},
921 	{IT_STRING|IT_CVAR,        NULL, "Cutscenes", &cv_dummycutscenes,           120},
922 	{IT_WHITESTRING|IT_CALL,   NULL, "Start",     M_StartMarathon,              130},
923 };
924 
925 enum
926 {
927 	marathonplayer,
928 	marathonultimate,
929 	marathonloadless,
930 	marathoncutscenes,
931 	marathonstart
932 };
933 
934 // Statistics
935 static menuitem_t SP_LevelStatsMenu[] =
936 {
937 	{IT_KEYHANDLER | IT_NOTHING, NULL, "", M_HandleLevelStats, 0},     // dummy menuitem for the control func
938 };
939 
940 // Player menu dummy
941 static menuitem_t SP_PlayerMenu[] =
942 {
943 	{IT_NOTHING | IT_KEYHANDLER, NULL, "", M_HandleChoosePlayerMenu, 0},     // dummy menuitem for the control func
944 };
945 
946 // -----------------------------------
947 // Multiplayer and all of its submenus
948 // -----------------------------------
949 // Prefix: MP_
950 
951 // Separated splitscreen and normal servers.
952 static menuitem_t MP_SplitServerMenu[] =
953 {
954 	{IT_STRING|IT_CALL,              NULL, "Select Gametype/Level...", M_MapChange,         100},
955 #ifdef NONET // In order to keep player setup accessible.
956 	{IT_STRING|IT_CALL,              NULL, "Player 1 setup...",        M_SetupMultiPlayer,  110},
957 	{IT_STRING|IT_CALL,              NULL, "Player 2 setup...",        M_SetupMultiPlayer2, 120},
958 #endif
959 	{IT_STRING|IT_CALL,              NULL, "More Options...",          M_ServerOptions,     130},
960 	{IT_WHITESTRING|IT_CALL,         NULL, "Start",                    M_StartServer,       140},
961 };
962 
963 #ifndef NONET
964 
965 static menuitem_t MP_MainMenu[] =
966 {
967 	{IT_HEADER, NULL, "Join a game", NULL, 0},
968 	{IT_STRING|IT_CALL,       NULL, "Server browser...",     M_ConnectMenuModChecks,          12},
969 	{IT_STRING|IT_KEYHANDLER, NULL, "Specify IPv4 address:", M_HandleConnectIP,      22},
970 	{IT_HEADER, NULL, "Host a game", NULL, 54},
971 	{IT_STRING|IT_CALL,       NULL, "Internet/LAN...",       M_StartServerMenu,      66},
972 	{IT_STRING|IT_CALL,       NULL, "Splitscreen...",        M_StartSplitServerMenu, 76},
973 	{IT_HEADER, NULL, "Player setup", NULL, 94},
974 	{IT_STRING|IT_CALL,       NULL, "Player 1...",           M_SetupMultiPlayer,    106},
975 	{IT_STRING|IT_CALL,       NULL, "Player 2... ",          M_SetupMultiPlayer2,   116},
976 };
977 
978 static menuitem_t MP_ServerMenu[] =
979 {
980 	{IT_STRING|IT_CALL,              NULL, "Room...",                  M_RoomMenu,          10},
981 	{IT_STRING|IT_CVAR|IT_CV_STRING, NULL, "Server Name",              &cv_servername,      20},
982 	{IT_STRING|IT_CVAR,              NULL, "Max Players",              &cv_maxplayers,      46},
983 	{IT_STRING|IT_CVAR,              NULL, "Allow Add-on Downloading", &cv_downloading,     56},
984 	{IT_STRING|IT_CALL,              NULL, "Select Gametype/Level...", M_MapChange,        100},
985 	{IT_STRING|IT_CALL,              NULL, "More Options...",          M_ServerOptions,    130},
986 	{IT_WHITESTRING|IT_CALL,         NULL, "Start",                    M_StartServer,      140},
987 };
988 
989 enum
990 {
991 	mp_server_room = 0,
992 	mp_server_name,
993 	mp_server_maxpl,
994 	mp_server_waddl,
995 	mp_server_levelgt,
996 	mp_server_options,
997 	mp_server_start
998 };
999 
1000 static menuitem_t MP_ConnectMenu[] =
1001 {
1002 	{IT_STRING | IT_CALL,       NULL, "Room...",  M_RoomMenu,         4},
1003 	{IT_STRING | IT_CVAR,       NULL, "Sort By",  &cv_serversort,     12},
1004 	{IT_STRING | IT_KEYHANDLER, NULL, "Page",     M_HandleServerPage, 20},
1005 	{IT_STRING | IT_CALL,       NULL, "Refresh",  M_Refresh,          28},
1006 
1007 	{IT_STRING | IT_SPACE, NULL, "",              M_Connect,          48-4},
1008 	{IT_STRING | IT_SPACE, NULL, "",              M_Connect,          60-4},
1009 	{IT_STRING | IT_SPACE, NULL, "",              M_Connect,          72-4},
1010 	{IT_STRING | IT_SPACE, NULL, "",              M_Connect,          84-4},
1011 	{IT_STRING | IT_SPACE, NULL, "",              M_Connect,          96-4},
1012 	{IT_STRING | IT_SPACE, NULL, "",              M_Connect,         108-4},
1013 	{IT_STRING | IT_SPACE, NULL, "",              M_Connect,         120-4},
1014 	{IT_STRING | IT_SPACE, NULL, "",              M_Connect,         132-4},
1015 	{IT_STRING | IT_SPACE, NULL, "",              M_Connect,         144-4},
1016 	{IT_STRING | IT_SPACE, NULL, "",              M_Connect,         156-4},
1017 	{IT_STRING | IT_SPACE, NULL, "",              M_Connect,         168-4},
1018 };
1019 
1020 enum
1021 {
1022 	mp_connect_room,
1023 	mp_connect_sort,
1024 	mp_connect_page,
1025 	mp_connect_refresh,
1026 	FIRSTSERVERLINE
1027 };
1028 
1029 menuitem_t MP_RoomMenu[] =
1030 {
1031 	{IT_STRING | IT_CALL, NULL, "<Unlisted Mode>", M_ChooseRoom,   9},
1032 	{IT_DISABLED,         NULL, "",               M_ChooseRoom,  18},
1033 	{IT_DISABLED,         NULL, "",               M_ChooseRoom,  27},
1034 	{IT_DISABLED,         NULL, "",               M_ChooseRoom,  36},
1035 	{IT_DISABLED,         NULL, "",               M_ChooseRoom,  45},
1036 	{IT_DISABLED,         NULL, "",               M_ChooseRoom,  54},
1037 	{IT_DISABLED,         NULL, "",               M_ChooseRoom,  63},
1038 	{IT_DISABLED,         NULL, "",               M_ChooseRoom,  72},
1039 	{IT_DISABLED,         NULL, "",               M_ChooseRoom,  81},
1040 	{IT_DISABLED,         NULL, "",               M_ChooseRoom,  90},
1041 	{IT_DISABLED,         NULL, "",               M_ChooseRoom,  99},
1042 	{IT_DISABLED,         NULL, "",               M_ChooseRoom, 108},
1043 	{IT_DISABLED,         NULL, "",               M_ChooseRoom, 117},
1044 	{IT_DISABLED,         NULL, "",               M_ChooseRoom, 126},
1045 	{IT_DISABLED,         NULL, "",               M_ChooseRoom, 135},
1046 	{IT_DISABLED,         NULL, "",               M_ChooseRoom, 144},
1047 	{IT_DISABLED,         NULL, "",               M_ChooseRoom, 153},
1048 	{IT_DISABLED,         NULL, "",               M_ChooseRoom, 162},
1049 };
1050 
1051 #endif
1052 
1053 static menuitem_t MP_PlayerSetupMenu[] =
1054 {
1055 	{IT_KEYHANDLER, NULL, "", M_HandleSetupMultiPlayer, 0}, // name
1056 	{IT_KEYHANDLER, NULL, "", M_HandleSetupMultiPlayer, 0}, // skin
1057 	{IT_KEYHANDLER, NULL, "", M_HandleSetupMultiPlayer, 0}, // colour
1058 	{IT_KEYHANDLER, NULL, "", M_HandleSetupMultiPlayer, 0}, // default
1059 };
1060 
1061 // ------------------------------------
1062 // Options and most (?) of its submenus
1063 // ------------------------------------
1064 // Prefix: OP_
1065 static menuitem_t OP_MainMenu[] =
1066 {
1067 	{IT_SUBMENU | IT_STRING, NULL, "Player 1 Controls...", &OP_P1ControlsDef,   10},
1068 	{IT_SUBMENU | IT_STRING, NULL, "Player 2 Controls...", &OP_P2ControlsDef,   20},
1069 	{IT_CVAR    | IT_STRING, NULL, "Controls per key",     &cv_controlperkey,   30},
1070 
1071 	{IT_CALL    | IT_STRING, NULL, "Video Options...",     M_VideoOptions,      50},
1072 	{IT_SUBMENU | IT_STRING, NULL, "Sound Options...",     &OP_SoundOptionsDef, 60},
1073 
1074 	{IT_CALL    | IT_STRING, NULL, "Server Options...",    M_ServerOptions,     80},
1075 
1076 	{IT_SUBMENU | IT_STRING, NULL, "Data Options...",      &OP_DataOptionsDef, 100},
1077 };
1078 
1079 static menuitem_t OP_P1ControlsMenu[] =
1080 {
1081 	{IT_CALL    | IT_STRING, NULL, "Control Configuration...", M_Setup1PControlsMenu,   10},
1082 	{IT_SUBMENU | IT_STRING, NULL, "Mouse Options...", &OP_MouseOptionsDef, 20},
1083 	{IT_SUBMENU | IT_STRING, NULL, "Gamepad Options...", &OP_Joystick1Def  ,  30},
1084 
1085 	{IT_SUBMENU | IT_STRING, NULL, "Camera Options...", &OP_CameraOptionsDef,	50},
1086 
1087 	{IT_STRING  | IT_CVAR, NULL, "Automatic braking", &cv_autobrake,  70},
1088 	{IT_CALL    | IT_STRING, NULL, "Play Style...", M_Setup1PPlaystyleMenu, 80},
1089 };
1090 
1091 static menuitem_t OP_P2ControlsMenu[] =
1092 {
1093 	{IT_CALL    | IT_STRING, NULL, "Control Configuration...", M_Setup2PControlsMenu,   10},
1094 	{IT_SUBMENU | IT_STRING, NULL, "Second Mouse Options...", &OP_Mouse2OptionsDef, 20},
1095 	{IT_SUBMENU | IT_STRING, NULL, "Second Gamepad Options...", &OP_Joystick2Def  ,  30},
1096 
1097 	{IT_SUBMENU | IT_STRING, NULL, "Camera Options...", &OP_Camera2OptionsDef,	50},
1098 
1099 	{IT_STRING  | IT_CVAR, NULL, "Automatic braking", &cv_autobrake2,  70},
1100 	{IT_CALL    | IT_STRING, NULL, "Play Style...", M_Setup2PPlaystyleMenu, 80},
1101 };
1102 
1103 static menuitem_t OP_ChangeControlsMenu[] =
1104 {
1105 	{IT_HEADER, NULL, "Movement", NULL, 0},
1106 	{IT_SPACE, NULL, NULL, NULL, 0}, // padding
1107 	{IT_CALL | IT_STRING2, NULL, "Move Forward",     M_ChangeControl, gc_forward     },
1108 	{IT_CALL | IT_STRING2, NULL, "Move Backward",    M_ChangeControl, gc_backward    },
1109 	{IT_CALL | IT_STRING2, NULL, "Move Left",        M_ChangeControl, gc_strafeleft  },
1110 	{IT_CALL | IT_STRING2, NULL, "Move Right",       M_ChangeControl, gc_straferight },
1111 	{IT_CALL | IT_STRING2, NULL, "Jump",             M_ChangeControl, gc_jump      },
1112 	{IT_CALL | IT_STRING2, NULL, "Spin",             M_ChangeControl, gc_spin     },
1113 	{IT_HEADER, NULL, "Camera", NULL, 0},
1114 	{IT_SPACE, NULL, NULL, NULL, 0}, // padding
1115 	{IT_CALL | IT_STRING2, NULL, "Look Up",        M_ChangeControl, gc_lookup      },
1116 	{IT_CALL | IT_STRING2, NULL, "Look Down",      M_ChangeControl, gc_lookdown    },
1117 	{IT_CALL | IT_STRING2, NULL, "Look Left",      M_ChangeControl, gc_turnleft    },
1118 	{IT_CALL | IT_STRING2, NULL, "Look Right",     M_ChangeControl, gc_turnright   },
1119 	{IT_CALL | IT_STRING2, NULL, "Center View",      M_ChangeControl, gc_centerview  },
1120 	{IT_CALL | IT_STRING2, NULL, "Toggle Mouselook", M_ChangeControl, gc_mouseaiming },
1121 	{IT_CALL | IT_STRING2, NULL, "Toggle Third-Person", M_ChangeControl, gc_camtoggle},
1122 	{IT_CALL | IT_STRING2, NULL, "Reset Camera",     M_ChangeControl, gc_camreset    },
1123 	{IT_HEADER, NULL, "Meta", NULL, 0},
1124 	{IT_SPACE, NULL, NULL, NULL, 0}, // padding
1125 	{IT_CALL | IT_STRING2, NULL, "Game Status",
1126     M_ChangeControl, gc_scores      },
1127 	{IT_CALL | IT_STRING2, NULL, "Pause / Run Retry", M_ChangeControl, gc_pause      },
1128 	{IT_CALL | IT_STRING2, NULL, "Screenshot",            M_ChangeControl, gc_screenshot },
1129 	{IT_CALL | IT_STRING2, NULL, "Toggle GIF Recording",  M_ChangeControl, gc_recordgif  },
1130 	{IT_CALL | IT_STRING2, NULL, "Open/Close Menu (ESC)", M_ChangeControl, gc_systemmenu },
1131 	{IT_CALL | IT_STRING2, NULL, "Change Viewpoint",      M_ChangeControl, gc_viewpoint  },
1132 	{IT_CALL | IT_STRING2, NULL, "Console",          M_ChangeControl, gc_console     },
1133 	{IT_HEADER, NULL, "Multiplayer", NULL, 0},
1134 	{IT_SPACE, NULL, NULL, NULL, 0}, // padding
1135 	{IT_CALL | IT_STRING2, NULL, "Talk",             M_ChangeControl, gc_talkkey     },
1136 	{IT_CALL | IT_STRING2, NULL, "Talk (Team only)", M_ChangeControl, gc_teamkey     },
1137 	{IT_HEADER, NULL, "Ringslinger (Match, CTF, Tag, H&S)", NULL, 0},
1138 	{IT_SPACE, NULL, NULL, NULL, 0}, // padding
1139 	{IT_CALL | IT_STRING2, NULL, "Fire",             M_ChangeControl, gc_fire        },
1140 	{IT_CALL | IT_STRING2, NULL, "Fire Normal",      M_ChangeControl, gc_firenormal  },
1141 	{IT_CALL | IT_STRING2, NULL, "Toss Flag",        M_ChangeControl, gc_tossflag    },
1142 	{IT_CALL | IT_STRING2, NULL, "Next Weapon",      M_ChangeControl, gc_weaponnext  },
1143 	{IT_CALL | IT_STRING2, NULL, "Prev Weapon",      M_ChangeControl, gc_weaponprev  },
1144 	{IT_CALL | IT_STRING2, NULL, "Normal / Infinity",   M_ChangeControl, gc_wepslot1    },
1145 	{IT_CALL | IT_STRING2, NULL, "Automatic",        M_ChangeControl, gc_wepslot2    },
1146 	{IT_CALL | IT_STRING2, NULL, "Bounce",           M_ChangeControl, gc_wepslot3    },
1147 	{IT_CALL | IT_STRING2, NULL, "Scatter",          M_ChangeControl, gc_wepslot4    },
1148 	{IT_CALL | IT_STRING2, NULL, "Grenade",          M_ChangeControl, gc_wepslot5    },
1149 	{IT_CALL | IT_STRING2, NULL, "Explosion",        M_ChangeControl, gc_wepslot6    },
1150 	{IT_CALL | IT_STRING2, NULL, "Rail",             M_ChangeControl, gc_wepslot7    },
1151 	{IT_HEADER, NULL, "Add-ons", NULL, 0},
1152 	{IT_SPACE, NULL, NULL, NULL, 0}, // padding
1153 	{IT_CALL | IT_STRING2, NULL, "Custom Action 1",  M_ChangeControl, gc_custom1     },
1154 	{IT_CALL | IT_STRING2, NULL, "Custom Action 2",  M_ChangeControl, gc_custom2     },
1155 	{IT_CALL | IT_STRING2, NULL, "Custom Action 3",  M_ChangeControl, gc_custom3     },
1156 };
1157 
1158 static menuitem_t OP_Joystick1Menu[] =
1159 {
1160 	{IT_STRING | IT_CALL,  NULL, "Select Gamepad...", M_Setup1PJoystickMenu, 10},
1161 	{IT_STRING | IT_CVAR,  NULL, "Move \x17 Axis"    , &cv_moveaxis         , 30},
1162 	{IT_STRING | IT_CVAR,  NULL, "Move \x18 Axis"    , &cv_sideaxis         , 40},
1163 	{IT_STRING | IT_CVAR,  NULL, "Camera \x17 Axis"  , &cv_lookaxis         , 50},
1164 	{IT_STRING | IT_CVAR,  NULL, "Camera \x18 Axis"  , &cv_turnaxis         , 60},
1165 	{IT_STRING | IT_CVAR,  NULL, "Jump Axis"         , &cv_jumpaxis         , 70},
1166 	{IT_STRING | IT_CVAR,  NULL, "Spin Axis"         , &cv_spinaxis         , 80},
1167 	{IT_STRING | IT_CVAR,  NULL, "Fire Axis"         , &cv_fireaxis         , 90},
1168 	{IT_STRING | IT_CVAR,  NULL, "Fire Normal Axis"  , &cv_firenaxis        ,100},
1169 
1170 	{IT_STRING | IT_CVAR, NULL, "First-Person Vert-Look", &cv_alwaysfreelook, 120},
1171 	{IT_STRING | IT_CVAR, NULL, "Third-Person Vert-Look", &cv_chasefreelook,  130},
1172 	{IT_STRING | IT_CVAR | IT_CV_FLOATSLIDER, NULL, "Analog Deadzone", &cv_deadzone, 140},
1173 	{IT_STRING | IT_CVAR | IT_CV_FLOATSLIDER, NULL, "Digital Deadzone", &cv_digitaldeadzone, 150},
1174 };
1175 
1176 static menuitem_t OP_Joystick2Menu[] =
1177 {
1178 	{IT_STRING | IT_CALL,  NULL, "Select Gamepad...", M_Setup2PJoystickMenu, 10},
1179 	{IT_STRING | IT_CVAR,  NULL, "Move \x17 Axis"    , &cv_moveaxis2        , 30},
1180 	{IT_STRING | IT_CVAR,  NULL, "Move \x18 Axis"    , &cv_sideaxis2        , 40},
1181 	{IT_STRING | IT_CVAR,  NULL, "Camera \x17 Axis"  , &cv_lookaxis2        , 50},
1182 	{IT_STRING | IT_CVAR,  NULL, "Camera \x18 Axis"  , &cv_turnaxis2        , 60},
1183 	{IT_STRING | IT_CVAR,  NULL, "Jump Axis"         , &cv_jumpaxis2        , 70},
1184 	{IT_STRING | IT_CVAR,  NULL, "Spin Axis"         , &cv_spinaxis2        , 80},
1185 	{IT_STRING | IT_CVAR,  NULL, "Fire Axis"         , &cv_fireaxis2        , 90},
1186 	{IT_STRING | IT_CVAR,  NULL, "Fire Normal Axis"  , &cv_firenaxis2       ,100},
1187 
1188 	{IT_STRING | IT_CVAR, NULL, "First-Person Vert-Look", &cv_alwaysfreelook2,120},
1189 	{IT_STRING | IT_CVAR, NULL, "Third-Person Vert-Look", &cv_chasefreelook2, 130},
1190 	{IT_STRING | IT_CVAR | IT_CV_FLOATSLIDER, NULL, "Analog Deadzone", &cv_deadzone2,140},
1191 	{IT_STRING | IT_CVAR | IT_CV_FLOATSLIDER, NULL, "Digital Deadzone", &cv_digitaldeadzone2,150},
1192 };
1193 
1194 static menuitem_t OP_JoystickSetMenu[1+MAX_JOYSTICKS];
1195 
1196 static menuitem_t OP_MouseOptionsMenu[] =
1197 {
1198 	{IT_STRING | IT_CVAR, NULL, "Use Mouse",        &cv_usemouse,         10},
1199 
1200 
1201 	{IT_STRING | IT_CVAR, NULL, "First-Person MouseLook", &cv_alwaysfreelook,   30},
1202 	{IT_STRING | IT_CVAR, NULL, "Third-Person MouseLook", &cv_chasefreelook,   40},
1203 	{IT_STRING | IT_CVAR, NULL, "Mouse Move",       &cv_mousemove,        50},
1204 	{IT_STRING | IT_CVAR, NULL, "Invert Y Axis",     &cv_invertmouse,      60},
1205 	{IT_STRING | IT_CVAR | IT_CV_SLIDER,
1206 	                      NULL, "Mouse X Sensitivity",    &cv_mousesens,        70},
1207 	{IT_STRING | IT_CVAR | IT_CV_SLIDER,
1208 	                      NULL, "Mouse Y Sensitivity",    &cv_mouseysens,        80},
1209 };
1210 
1211 static menuitem_t OP_Mouse2OptionsMenu[] =
1212 {
1213 	{IT_STRING | IT_CVAR, NULL, "Use Mouse 2",      &cv_usemouse2,        10},
1214 	{IT_STRING | IT_CVAR, NULL, "Second Mouse Serial Port",
1215 	                                                &cv_mouse2port,       20},
1216 	{IT_STRING | IT_CVAR, NULL, "First-Person MouseLook", &cv_alwaysfreelook2,  30},
1217 	{IT_STRING | IT_CVAR, NULL, "Third-Person MouseLook", &cv_chasefreelook2,  40},
1218 	{IT_STRING | IT_CVAR, NULL, "Mouse Move",       &cv_mousemove2,       50},
1219 	{IT_STRING | IT_CVAR, NULL, "Invert Y Axis",     &cv_invertmouse2,     60},
1220 	{IT_STRING | IT_CVAR | IT_CV_SLIDER,
1221 	                      NULL, "Mouse X Sensitivity",    &cv_mousesens2,       70},
1222 	{IT_STRING | IT_CVAR | IT_CV_SLIDER,
1223 	                      NULL, "Mouse Y Sensitivity",    &cv_mouseysens2,      80},
1224 };
1225 
1226 static menuitem_t OP_CameraOptionsMenu[] =
1227 {
1228 	{IT_HEADER,            NULL, "General Toggles", NULL, 0},
1229 	{IT_STRING  | IT_CVAR, NULL, "Third-person Camera"  , &cv_chasecam , 6},
1230 	{IT_STRING  | IT_CVAR, NULL, "Flip Camera with Gravity"  , &cv_flipcam , 11},
1231 	{IT_STRING  | IT_CVAR, NULL, "Orbital Looking"  , &cv_cam_orbit , 16},
1232 	{IT_STRING  | IT_CVAR, NULL, "Downhill Slope Adjustment", &cv_cam_adjust, 21},
1233 
1234 	{IT_HEADER,                                NULL, "Camera Positioning", NULL, 30},
1235 	{IT_STRING  | IT_CVAR | IT_CV_INTEGERSTEP, NULL, "Camera Distance", &cv_cam_savedist[0][0], 36},
1236 	{IT_STRING  | IT_CVAR | IT_CV_INTEGERSTEP, NULL, "Camera Height", &cv_cam_saveheight[0][0], 41},
1237 	{IT_STRING  | IT_CVAR | IT_CV_FLOATSLIDER, NULL, "Camera Spacial Speed", &cv_cam_speed, 46},
1238 	{IT_STRING  | IT_CVAR | IT_CV_FLOATSLIDER, NULL, "Rotation Speed", &cv_cam_turnmultiplier, 51},
1239 
1240 	{IT_HEADER,            NULL, "Display Options", NULL, 60},
1241 	{IT_STRING  | IT_CVAR, NULL, "Crosshair", &cv_crosshair, 66},
1242 };
1243 
1244 static menuitem_t OP_Camera2OptionsMenu[] =
1245 {
1246 	{IT_HEADER,            NULL, "General Toggles", NULL, 0},
1247 	{IT_STRING  | IT_CVAR, NULL, "Third-person Camera"  , &cv_chasecam2 , 6},
1248 	{IT_STRING  | IT_CVAR, NULL, "Flip Camera with Gravity"  , &cv_flipcam2 , 11},
1249 	{IT_STRING  | IT_CVAR, NULL, "Orbital Looking"  , &cv_cam2_orbit , 16},
1250 	{IT_STRING  | IT_CVAR, NULL, "Downhill Slope Adjustment", &cv_cam2_adjust, 21},
1251 
1252 	{IT_HEADER,                                NULL, "Camera Positioning", NULL, 30},
1253 	{IT_STRING  | IT_CVAR | IT_CV_INTEGERSTEP, NULL, "Camera Distance", &cv_cam_savedist[0][1], 36},
1254 	{IT_STRING  | IT_CVAR | IT_CV_INTEGERSTEP, NULL, "Camera Height", &cv_cam_saveheight[0][1], 41},
1255 	{IT_STRING  | IT_CVAR | IT_CV_FLOATSLIDER, NULL, "Camera Spacial Speed", &cv_cam2_speed, 46},
1256 	{IT_STRING  | IT_CVAR | IT_CV_FLOATSLIDER, NULL, "Rotation Speed", &cv_cam2_turnmultiplier, 51},
1257 
1258 	{IT_HEADER,            NULL, "Display Options", NULL, 60},
1259 	{IT_STRING  | IT_CVAR, NULL, "Crosshair", &cv_crosshair2, 66},
1260 };
1261 
1262 static menuitem_t OP_CameraExtendedOptionsMenu[] =
1263 {
1264 	{IT_HEADER,            NULL, "General Toggles", NULL, 0},
1265 	{IT_STRING  | IT_CVAR, NULL, "Third-person Camera"  , &cv_chasecam , 6},
1266 	{IT_STRING  | IT_CVAR, NULL, "Flip Camera with Gravity"  , &cv_flipcam , 11},
1267 	{IT_STRING  | IT_CVAR, NULL, "Orbital Looking"  , &cv_cam_orbit , 16},
1268 	{IT_STRING  | IT_CVAR, NULL, "Downhill Slope Adjustment", &cv_cam_adjust, 21},
1269 
1270 	{IT_HEADER,                                NULL, "Camera Positioning", NULL, 30},
1271 	{IT_STRING  | IT_CVAR | IT_CV_INTEGERSTEP, NULL, "Camera Distance", &cv_cam_savedist[1][0], 36},
1272 	{IT_STRING  | IT_CVAR | IT_CV_INTEGERSTEP, NULL, "Camera Height", &cv_cam_saveheight[1][0], 41},
1273 	{IT_STRING  | IT_CVAR | IT_CV_FLOATSLIDER, NULL, "Camera Spacial Speed", &cv_cam_speed, 46},
1274 	{IT_STRING  | IT_CVAR | IT_CV_FLOATSLIDER, NULL, "Rotation Speed", &cv_cam_turnmultiplier, 51},
1275 
1276 	{IT_HEADER,                           NULL, "Automatic Camera Options", NULL, 60},
1277 	{IT_STRING  | IT_CVAR | IT_CV_SLIDER, NULL, "Shift to player angle", &cv_cam_shiftfacing[0],  66},
1278 	{IT_STRING  | IT_CVAR | IT_CV_SLIDER, NULL, "Turn to player angle", &cv_cam_turnfacing[0],  71},
1279 	{IT_STRING  | IT_CVAR | IT_CV_SLIDER, NULL, "Turn to ability", &cv_cam_turnfacingability[0],  76},
1280 	{IT_STRING  | IT_CVAR | IT_CV_SLIDER, NULL, "Turn to spindash", &cv_cam_turnfacingspindash[0],  81},
1281 	{IT_STRING  | IT_CVAR | IT_CV_SLIDER, NULL, "Turn to input", &cv_cam_turnfacinginput[0],  86},
1282 
1283 	{IT_HEADER,            NULL, "Locked Camera Options", NULL, 95},
1284 	{IT_STRING  | IT_CVAR, NULL, "Lock button behavior", &cv_cam_centertoggle[0],  101},
1285 	{IT_STRING  | IT_CVAR, NULL, "Sideways movement", &cv_cam_lockedinput[0],  106},
1286 	{IT_STRING  | IT_CVAR, NULL, "Targeting assist", &cv_cam_lockonboss[0],  111},
1287 
1288 	{IT_HEADER,            NULL, "Display Options", NULL, 120},
1289 	{IT_STRING  | IT_CVAR, NULL, "Crosshair", &cv_crosshair, 126},
1290 };
1291 
1292 static menuitem_t OP_Camera2ExtendedOptionsMenu[] =
1293 {
1294 	{IT_HEADER,            NULL, "General Toggles", NULL, 0},
1295 	{IT_STRING  | IT_CVAR, NULL, "Third-person Camera"  , &cv_chasecam2 , 6},
1296 	{IT_STRING  | IT_CVAR, NULL, "Flip Camera with Gravity"  , &cv_flipcam2 , 11},
1297 	{IT_STRING  | IT_CVAR, NULL, "Orbital Looking"  , &cv_cam2_orbit , 16},
1298 	{IT_STRING  | IT_CVAR, NULL, "Downhill Slope Adjustment", &cv_cam2_adjust, 21},
1299 
1300 	{IT_HEADER,                                NULL, "Camera Positioning", NULL, 30},
1301 	{IT_STRING  | IT_CVAR | IT_CV_INTEGERSTEP, NULL, "Camera Distance", &cv_cam_savedist[1][1], 36},
1302 	{IT_STRING  | IT_CVAR | IT_CV_INTEGERSTEP, NULL, "Camera Height", &cv_cam_saveheight[1][1], 41},
1303 	{IT_STRING  | IT_CVAR | IT_CV_FLOATSLIDER, NULL, "Camera Spacial Speed", &cv_cam2_speed, 46},
1304 	{IT_STRING  | IT_CVAR | IT_CV_FLOATSLIDER, NULL, "Rotation Speed", &cv_cam2_turnmultiplier, 51},
1305 
1306 	{IT_HEADER,                           NULL, "Automatic Camera Options", NULL, 60},
1307 	{IT_STRING  | IT_CVAR | IT_CV_SLIDER, NULL, "Shift to player angle", &cv_cam_shiftfacing[1],  66},
1308 	{IT_STRING  | IT_CVAR | IT_CV_SLIDER, NULL, "Turn to player angle", &cv_cam_turnfacing[1],  71},
1309 	{IT_STRING  | IT_CVAR | IT_CV_SLIDER, NULL, "Turn to ability", &cv_cam_turnfacingability[1],  76},
1310 	{IT_STRING  | IT_CVAR | IT_CV_SLIDER, NULL, "Turn to spindash", &cv_cam_turnfacingspindash[1],  81},
1311 	{IT_STRING  | IT_CVAR | IT_CV_SLIDER, NULL, "Turn to input", &cv_cam_turnfacinginput[1],  86},
1312 
1313 	{IT_HEADER,            NULL, "Locked Camera Options", NULL, 95},
1314 	{IT_STRING  | IT_CVAR, NULL, "Lock button behavior", &cv_cam_centertoggle[1],  101},
1315 	{IT_STRING  | IT_CVAR, NULL, "Sideways movement", &cv_cam_lockedinput[1],  106},
1316 	{IT_STRING  | IT_CVAR, NULL, "Targeting assist", &cv_cam_lockonboss[1],  111},
1317 
1318 	{IT_HEADER,            NULL, "Display Options", NULL, 120},
1319 	{IT_STRING  | IT_CVAR, NULL, "Crosshair", &cv_crosshair2, 126},
1320 };
1321 
1322 enum
1323 {
1324 	op_video_resolution = 1,
1325 #if (defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL)
1326 	op_video_fullscreen,
1327 #endif
1328 	op_video_vsync,
1329 	op_video_renderer,
1330 };
1331 
1332 static menuitem_t OP_VideoOptionsMenu[] =
1333 {
1334 	{IT_HEADER, NULL, "Screen", NULL, 0},
1335 	{IT_STRING | IT_CALL,  NULL, "Set Resolution...",       M_VideoModeMenu,          6},
1336 
1337 #if (defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL)
1338 	{IT_STRING|IT_CVAR,      NULL, "Fullscreen",             &cv_fullscreen,         11},
1339 #endif
1340 	{IT_STRING | IT_CVAR, NULL, "Vertical Sync",                &cv_vidwait,         16},
1341 #ifdef HWRENDER
1342 	{IT_STRING | IT_CVAR, NULL, "Renderer",                     &cv_renderer,        21},
1343 #else
1344 	{IT_TRANSTEXT | IT_PAIR, "Renderer", "Software",            &cv_renderer,           21},
1345 #endif
1346 
1347 	{IT_HEADER, NULL, "Color Profile", NULL, 30},
1348 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Brightness (F11)", &cv_globalgamma,36},
1349 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Saturation", &cv_globalsaturation, 41},
1350 	{IT_SUBMENU|IT_STRING, NULL, "Advanced Settings...",     &OP_ColorOptionsDef,  46},
1351 
1352 	{IT_HEADER, NULL, "Heads-Up Display", NULL, 55},
1353 	{IT_STRING | IT_CVAR, NULL, "Show HUD",                  &cv_showhud,          61},
1354 	{IT_STRING | IT_CVAR | IT_CV_SLIDER,
1355 	                      NULL, "HUD Transparency",          &cv_translucenthud,   66},
1356 	{IT_STRING | IT_CVAR, NULL, "Score/Time/Rings",          &cv_timetic,          71},
1357 	{IT_STRING | IT_CVAR, NULL, "Show Powerups",             &cv_powerupdisplay,   76},
1358 	{IT_STRING | IT_CVAR, NULL, "Local ping display",		&cv_showping,			81}, // shows ping next to framerate if we want to.
1359 	{IT_STRING | IT_CVAR, NULL, "Show player names",         &cv_seenames,         86},
1360 
1361 	{IT_HEADER, NULL, "Console", NULL, 95},
1362 	{IT_STRING | IT_CVAR, NULL, "Background color",          &cons_backcolor,      101},
1363 	{IT_STRING | IT_CVAR, NULL, "Text Size",                 &cv_constextsize,    106},
1364 
1365 	{IT_HEADER, NULL, "Chat", NULL, 115},
1366 	{IT_STRING | IT_CVAR, NULL, "Chat Mode",            		 	 &cv_consolechat,  121},
1367 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Chat Box Width",    &cv_chatwidth,     126},
1368 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Chat Box Height",   &cv_chatheight,    131},
1369 	{IT_STRING | IT_CVAR, NULL, "Message Fadeout Time",              &cv_chattime,    136},
1370 	{IT_STRING | IT_CVAR, NULL, "Chat Notifications",           	 &cv_chatnotifications,  141},
1371 	{IT_STRING | IT_CVAR, NULL, "Spam Protection",           		 &cv_chatspamprotection,  146},
1372 	{IT_STRING | IT_CVAR, NULL, "Chat background tint",           	 &cv_chatbacktint,  151},
1373 
1374 	{IT_HEADER, NULL, "Level", NULL, 160},
1375 	{IT_STRING | IT_CVAR, NULL, "Draw Distance",             &cv_drawdist,        166},
1376 	{IT_STRING | IT_CVAR, NULL, "Weather Draw Dist.",        &cv_drawdist_precip, 171},
1377 	{IT_STRING | IT_CVAR, NULL, "NiGHTS Hoop Draw Dist.",    &cv_drawdist_nights, 176},
1378 
1379 	{IT_HEADER, NULL, "Diagnostic", NULL, 184},
1380 	{IT_STRING | IT_CVAR, NULL, "Show FPS",                  &cv_ticrate,         190},
1381 	{IT_STRING | IT_CVAR, NULL, "Clear Before Redraw",       &cv_homremoval,      195},
1382 	{IT_STRING | IT_CVAR, NULL, "Show \"FOCUS LOST\"",       &cv_showfocuslost,   200},
1383 
1384 #ifdef HWRENDER
1385 	{IT_HEADER, NULL, "Renderer", NULL, 208},
1386 	{IT_CALL | IT_STRING, NULL, "OpenGL Options...",         M_OpenGLOptionsMenu, 214},
1387 #endif
1388 };
1389 
1390 static menuitem_t OP_VideoModeMenu[] =
1391 {
1392 	{IT_KEYHANDLER | IT_NOTHING, NULL, "", M_HandleVideoMode, 0},     // dummy menuitem for the control func
1393 };
1394 
1395 static menuitem_t OP_ColorOptionsMenu[] =
1396 {
1397 	{IT_STRING | IT_CALL, NULL, "Reset to defaults", M_ResetCvars, 0},
1398 
1399 	{IT_HEADER, NULL, "Red", NULL, 9},
1400 	{IT_DISABLED, NULL, NULL, NULL, 35},
1401 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Hue",          &cv_rhue,         15},
1402 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Saturation",   &cv_rsaturation,  20},
1403 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Brightness",   &cv_rgamma,       25},
1404 
1405 	{IT_HEADER, NULL, "Yellow", NULL, 34},
1406 	{IT_DISABLED, NULL, NULL, NULL, 73},
1407 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Hue",          &cv_yhue,         40},
1408 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Saturation",   &cv_ysaturation,  45},
1409 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Brightness",   &cv_ygamma,       50},
1410 
1411 	{IT_HEADER, NULL, "Green", NULL, 59},
1412 	{IT_DISABLED, NULL, NULL, NULL, 112},
1413 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Hue",          &cv_ghue,         65},
1414 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Saturation",   &cv_gsaturation,  70},
1415 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Brightness",   &cv_ggamma,       75},
1416 
1417 	{IT_HEADER, NULL, "Cyan", NULL, 84},
1418 	{IT_DISABLED, NULL, NULL, NULL, 255},
1419 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Hue",          &cv_chue,         90},
1420 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Saturation",   &cv_csaturation,  95},
1421 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Brightness",   &cv_cgamma,      100},
1422 
1423 	{IT_HEADER, NULL, "Blue", NULL, 109},
1424 	{IT_DISABLED, NULL, NULL, NULL, 152},
1425 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Hue",          &cv_bhue,        115},
1426 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Saturation",   &cv_bsaturation, 120},
1427 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Brightness",   &cv_bgamma,      125},
1428 
1429 	{IT_HEADER, NULL, "Magenta", NULL, 134},
1430 	{IT_DISABLED, NULL, NULL, NULL, 181},
1431 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Hue",          &cv_mhue,        140},
1432 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Saturation",   &cv_msaturation, 145},
1433 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Brightness",   &cv_mgamma,      150},
1434 };
1435 
1436 #ifdef HWRENDER
1437 static menuitem_t OP_OpenGLOptionsMenu[] =
1438 {
1439 	{IT_HEADER, NULL, "3D Models", NULL, 0},
1440 	{IT_STRING|IT_CVAR,         NULL, "Models",              &cv_glmodels,             12},
1441 	{IT_STRING|IT_CVAR,         NULL, "Frame interpolation", &cv_glmodelinterpolation, 22},
1442 	{IT_STRING|IT_CVAR,         NULL, "Ambient lighting",    &cv_glmodellighting,      32},
1443 
1444 	{IT_HEADER, NULL, "General", NULL, 51},
1445 	{IT_STRING|IT_CVAR,         NULL, "Shaders",             &cv_glshaders,            63},
1446 	{IT_STRING|IT_CVAR,         NULL, "Lack of perspective", &cv_glshearing,           73},
1447 	{IT_STRING|IT_CVAR,         NULL, "Field of view",       &cv_fov,                  83},
1448 
1449 	{IT_HEADER, NULL, "Miscellaneous", NULL, 102},
1450 	{IT_STRING|IT_CVAR,         NULL, "Bit depth",           &cv_scr_depth,           114},
1451 	{IT_STRING|IT_CVAR,         NULL, "Texture filter",      &cv_glfiltermode,        124},
1452 	{IT_STRING|IT_CVAR,         NULL, "Anisotropic",         &cv_glanisotropicmode,   134},
1453 #ifdef ALAM_LIGHTING
1454 	{IT_SUBMENU|IT_STRING,      NULL, "Lighting...",         &OP_OpenGLLightingDef,   144},
1455 #endif
1456 #if defined (_WINDOWS) && (!((defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL)))
1457 	{IT_STRING|IT_CVAR,         NULL, "Fullscreen",          &cv_fullscreen,          154},
1458 #endif
1459 };
1460 
1461 #ifdef ALAM_LIGHTING
1462 static menuitem_t OP_OpenGLLightingMenu[] =
1463 {
1464 	{IT_STRING|IT_CVAR, NULL, "Coronas",          &cv_glcoronas,          0},
1465 	{IT_STRING|IT_CVAR, NULL, "Coronas size",     &cv_glcoronasize,      10},
1466 	{IT_STRING|IT_CVAR, NULL, "Dynamic lighting", &cv_gldynamiclighting, 20},
1467 	{IT_STRING|IT_CVAR, NULL, "Static lighting",  &cv_glstaticlighting,  30},
1468 };
1469 #endif // ALAM_LIGHTING
1470 
1471 #endif
1472 
1473 static menuitem_t OP_SoundOptionsMenu[] =
1474 {
1475 	{IT_HEADER, NULL, "Game Audio", NULL, 0},
1476 	{IT_STRING | IT_CVAR,  NULL,  "Sound Effects", &cv_gamesounds, 6},
1477 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Sound Volume", &cv_soundvolume, 11},
1478 
1479 	{IT_STRING | IT_CVAR,  NULL,  "Digital Music", &cv_gamedigimusic, 21},
1480 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Digital Music Volume", &cv_digmusicvolume,  26},
1481 
1482 	{IT_STRING | IT_CVAR,  NULL,  "MIDI Music", &cv_gamemidimusic, 36},
1483 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "MIDI Music Volume", &cv_midimusicvolume, 41},
1484 
1485 	{IT_STRING | IT_CVAR,  NULL,  "Music Preference", &cv_musicpref, 51},
1486 
1487 	{IT_HEADER, NULL, "Miscellaneous", NULL, 61},
1488 	{IT_STRING | IT_CVAR, NULL, "Closed Captioning", &cv_closedcaptioning, 67},
1489 	{IT_STRING | IT_CVAR, NULL, "Reset Music Upon Dying", &cv_resetmusic, 72},
1490 	{IT_STRING | IT_CVAR, NULL, "Default 1-Up sound", &cv_1upsound, 77},
1491 
1492 	{IT_STRING | IT_SUBMENU, NULL, "Advanced Settings...", &OP_SoundAdvancedDef, 87},
1493 };
1494 
1495 #ifdef HAVE_OPENMPT
1496 #define OPENMPT_MENUOFFSET 32
1497 #else
1498 #define OPENMPT_MENUOFFSET 0
1499 #endif
1500 
1501 #ifdef HAVE_MIXERX
1502 #define MIXERX_MENUOFFSET 81
1503 #else
1504 #define MIXERX_MENUOFFSET 0
1505 #endif
1506 
1507 static menuitem_t OP_SoundAdvancedMenu[] =
1508 {
1509 #ifdef HAVE_OPENMPT
1510 	{IT_HEADER, NULL, "OpenMPT Settings", NULL, 0},
1511 	{IT_STRING | IT_CVAR, NULL, "Instrument Filter", &cv_modfilter, 12},
1512 #endif
1513 
1514 #ifdef HAVE_MIXERX
1515 	{IT_HEADER, NULL, "MIDI Settings", NULL, OPENMPT_MENUOFFSET},
1516 	{IT_STRING | IT_CVAR, NULL, "MIDI Player", &cv_midiplayer, OPENMPT_MENUOFFSET+12},
1517 	{IT_STRING | IT_CVAR | IT_CV_STRING, NULL, "FluidSynth Sound Font File", &cv_midisoundfontpath, OPENMPT_MENUOFFSET+24},
1518 	{IT_STRING | IT_CVAR | IT_CV_STRING, NULL, "TiMidity++ Config Folder", &cv_miditimiditypath, OPENMPT_MENUOFFSET+51},
1519 #endif
1520 
1521 	{IT_HEADER, NULL, "Miscellaneous", NULL, OPENMPT_MENUOFFSET+MIXERX_MENUOFFSET},
1522 	{IT_STRING | IT_CVAR, NULL, "Play Sound Effects if Unfocused", &cv_playsoundsifunfocused, OPENMPT_MENUOFFSET+MIXERX_MENUOFFSET+12},
1523 	{IT_STRING | IT_CVAR, NULL, "Play Music if Unfocused", &cv_playmusicifunfocused, OPENMPT_MENUOFFSET+MIXERX_MENUOFFSET+22},
1524 	{IT_STRING | IT_CVAR, NULL, "Let Levels Force Reset Music", &cv_resetmusicbyheader, OPENMPT_MENUOFFSET+MIXERX_MENUOFFSET+32},
1525 };
1526 
1527 #undef OPENMPT_MENUOFFSET
1528 #undef MIXERX_MENUOFFSET
1529 
1530 static menuitem_t OP_DataOptionsMenu[] =
1531 {
1532 	{IT_STRING | IT_CALL,    NULL, "Add-on Options...",     M_AddonsOptions,     10},
1533 	{IT_STRING | IT_CALL,    NULL, "Screenshot Options...", M_ScreenshotOptions, 20},
1534 
1535 	{IT_STRING | IT_SUBMENU, NULL, "\x85" "Erase Data...",  &OP_EraseDataDef,    40},
1536 };
1537 
1538 static menuitem_t OP_ScreenshotOptionsMenu[] =
1539 {
1540 	{IT_HEADER, NULL, "General", NULL, 0},
1541 	{IT_STRING|IT_CVAR, NULL, "Use color profile", &cv_screenshot_colorprofile,     6},
1542 
1543 	{IT_HEADER, NULL, "Screenshots (F8)", NULL, 16},
1544 	{IT_STRING|IT_CVAR, NULL, "Storage Location",  &cv_screenshot_option,          22},
1545 	{IT_STRING|IT_CVAR|IT_CV_STRING, NULL, "Custom Folder", &cv_screenshot_folder, 27},
1546 	{IT_STRING|IT_CVAR, NULL, "Memory Level",      &cv_zlib_memory,                42},
1547 	{IT_STRING|IT_CVAR, NULL, "Compression Level", &cv_zlib_level,                 47},
1548 	{IT_STRING|IT_CVAR, NULL, "Strategy",          &cv_zlib_strategy,              52},
1549 	{IT_STRING|IT_CVAR, NULL, "Window Size",       &cv_zlib_window_bits,           57},
1550 
1551 	{IT_HEADER, NULL, "Movie Mode (F9)", NULL, 64},
1552 	{IT_STRING|IT_CVAR, NULL, "Storage Location",  &cv_movie_option,               70},
1553 	{IT_STRING|IT_CVAR|IT_CV_STRING, NULL, "Custom Folder", &cv_movie_folder, 	   75},
1554 	{IT_STRING|IT_CVAR, NULL, "Capture Mode",      &cv_moviemode,                  90},
1555 
1556 	{IT_STRING|IT_CVAR, NULL, "Downscaling",       &cv_gif_downscale,              95},
1557 	{IT_STRING|IT_CVAR, NULL, "Region Optimizing", &cv_gif_optimize,              100},
1558 	{IT_STRING|IT_CVAR, NULL, "Local Color Table", &cv_gif_localcolortable,       105},
1559 
1560 	{IT_STRING|IT_CVAR, NULL, "Downscaling",       &cv_apng_downscale,             95},
1561 	{IT_STRING|IT_CVAR, NULL, "Memory Level",      &cv_zlib_memorya,              100},
1562 	{IT_STRING|IT_CVAR, NULL, "Compression Level", &cv_zlib_levela,               105},
1563 	{IT_STRING|IT_CVAR, NULL, "Strategy",          &cv_zlib_strategya,            110},
1564 	{IT_STRING|IT_CVAR, NULL, "Window Size",       &cv_zlib_window_bitsa,         115},
1565 };
1566 
1567 enum
1568 {
1569 	op_screenshot_colorprofile = 1,
1570 	op_screenshot_storagelocation = 3,
1571 	op_screenshot_folder = 4,
1572 	op_movie_folder = 11,
1573 	op_screenshot_capture = 12,
1574 	op_screenshot_gif_start = 13,
1575 	op_screenshot_gif_end = 15,
1576 	op_screenshot_apng_start = 16,
1577 	op_screenshot_apng_end = 20,
1578 };
1579 
1580 static menuitem_t OP_EraseDataMenu[] =
1581 {
1582 	{IT_STRING | IT_CALL, NULL, "Erase Record Data", M_EraseData, 10},
1583 	{IT_STRING | IT_CALL, NULL, "Erase Extras Data", M_EraseData, 20},
1584 
1585 	{IT_STRING | IT_CALL, NULL, "\x85" "Erase ALL Data", M_EraseData, 40},
1586 };
1587 
1588 static menuitem_t OP_AddonsOptionsMenu[] =
1589 {
1590 	{IT_HEADER,                      NULL, "Menu",                        NULL,                     0},
1591 	{IT_STRING|IT_CVAR,              NULL, "Location",                    &cv_addons_option,       12},
1592 	{IT_STRING|IT_CVAR|IT_CV_STRING, NULL, "Custom Folder",               &cv_addons_folder,       22},
1593 	{IT_STRING|IT_CVAR,              NULL, "Identify add-ons via",        &cv_addons_md5,          50},
1594 	{IT_STRING|IT_CVAR,              NULL, "Show unsupported file types", &cv_addons_showall,      60},
1595 
1596 	{IT_HEADER,                      NULL, "Search",                      NULL,                    78},
1597 	{IT_STRING|IT_CVAR,              NULL, "Matching",                    &cv_addons_search_type,  90},
1598 	{IT_STRING|IT_CVAR,              NULL, "Case-sensitive",              &cv_addons_search_case, 100},
1599 };
1600 
1601 enum
1602 {
1603 	op_addons_folder = 2,
1604 };
1605 
1606 static menuitem_t OP_ServerOptionsMenu[] =
1607 {
1608 	{IT_HEADER, NULL, "General", NULL, 0},
1609 #ifndef NONET
1610 	{IT_STRING | IT_CVAR | IT_CV_STRING,
1611 	                         NULL, "Server name",                      &cv_servername,           7},
1612 	{IT_STRING | IT_CVAR,    NULL, "Max Players",                      &cv_maxplayers,          21},
1613 	{IT_STRING | IT_CVAR,    NULL, "Allow Add-on Downloading",         &cv_downloading,         26},
1614 	{IT_STRING | IT_CVAR,    NULL, "Allow players to join",            &cv_allownewplayer,      31},
1615 	{IT_STRING | IT_CVAR,    NULL, "Minutes for reconnecting",         &cv_rejointimeout,       36},
1616 #endif
1617 	{IT_STRING | IT_CVAR,    NULL, "Map progression",                  &cv_advancemap,          41},
1618 	{IT_STRING | IT_CVAR,    NULL, "Intermission Timer",               &cv_inttime,             46},
1619 
1620 	{IT_HEADER, NULL, "Characters", NULL, 55},
1621 	{IT_STRING | IT_CVAR,    NULL, "Force a character",                &cv_forceskin,           61},
1622 	{IT_STRING | IT_CVAR,    NULL, "Restrict character changes",       &cv_restrictskinchange,  66},
1623 
1624 	{IT_HEADER, NULL, "Items", NULL, 75},
1625 	{IT_STRING | IT_CVAR,    NULL, "Item respawn delay",               &cv_itemrespawntime,     81},
1626 	{IT_STRING | IT_SUBMENU, NULL, "Mystery Item Monitor Toggles...",  &OP_MonitorToggleDef,    86},
1627 
1628 	{IT_HEADER, NULL, "Cooperative", NULL, 95},
1629 	{IT_STRING | IT_CVAR,    NULL, "Players required for exit",        &cv_playersforexit,     101},
1630 	{IT_STRING | IT_CVAR,    NULL, "Starposts",                        &cv_coopstarposts,      106},
1631 	{IT_STRING | IT_CVAR,    NULL, "Life sharing",                     &cv_cooplives,          111},
1632 	{IT_STRING | IT_CVAR,    NULL, "Post-goal free roaming",           &cv_exitmove,           116},
1633 
1634 	{IT_HEADER, NULL, "Race, Competition", NULL, 125},
1635 	{IT_STRING | IT_CVAR,    NULL, "Level completion countdown",       &cv_countdowntime,      131},
1636 	{IT_STRING | IT_CVAR,    NULL, "Item Monitors",                    &cv_competitionboxes,   136},
1637 
1638 	{IT_HEADER, NULL, "Ringslinger (Match, CTF, Tag, H&S)", NULL, 145},
1639 	{IT_STRING | IT_CVAR,    NULL, "Time Limit",                       &cv_timelimit,          151},
1640 	{IT_STRING | IT_CVAR,    NULL, "Score Limit",                      &cv_pointlimit,         156},
1641 	{IT_STRING | IT_CVAR,    NULL, "Overtime on Tie",                  &cv_overtime,           161},
1642 	{IT_STRING | IT_CVAR,    NULL, "Player respawn delay",             &cv_respawntime,        166},
1643 
1644 	{IT_STRING | IT_CVAR,    NULL, "Item Monitors",                    &cv_matchboxes,         176},
1645 	{IT_STRING | IT_CVAR,    NULL, "Weapon Rings",                     &cv_specialrings,       181},
1646 	{IT_STRING | IT_CVAR,    NULL, "Power Stones",                     &cv_powerstones,        186},
1647 
1648 	{IT_STRING | IT_CVAR,    NULL, "Flag respawn delay",               &cv_flagtime,           196},
1649 	{IT_STRING | IT_CVAR,    NULL, "Hiding time",                      &cv_hidetime,           201},
1650 
1651 	{IT_HEADER, NULL, "Teams", NULL, 210},
1652 	{IT_STRING | IT_CVAR,    NULL, "Autobalance sizes",                &cv_autobalance,        216},
1653 	{IT_STRING | IT_CVAR,    NULL, "Scramble on Map Change",           &cv_scrambleonchange,   221},
1654 
1655 #ifndef NONET
1656 	{IT_HEADER, NULL, "Advanced", NULL, 230},
1657 	{IT_STRING | IT_CVAR | IT_CV_STRING, NULL, "Master server",        &cv_masterserver,       236},
1658 
1659 	{IT_STRING | IT_CVAR,    NULL, "Join delay",                       &cv_joindelay,          251},
1660 	{IT_STRING | IT_CVAR,    NULL, "Attempts to resynchronise",        &cv_resynchattempts,    256},
1661 
1662 	{IT_STRING | IT_CVAR,    NULL, "Show IP Address of Joiners",       &cv_showjoinaddress,    261},
1663 #endif
1664 };
1665 
1666 static menuitem_t OP_MonitorToggleMenu[] =
1667 {
1668 	// Printing handled by drawing function
1669 	{IT_STRING|IT_CALL, NULL, "Reset to defaults", M_ResetCvars, 15},
1670 	{IT_STRING|IT_CVAR|IT_CV_INVISSLIDER, NULL, "Recycler",          &cv_recycler,      30},
1671 	{IT_STRING|IT_CVAR|IT_CV_INVISSLIDER, NULL, "Teleport",          &cv_teleporters,   40},
1672 	{IT_STRING|IT_CVAR|IT_CV_INVISSLIDER, NULL, "Super Ring",        &cv_superring,     50},
1673 	{IT_STRING|IT_CVAR|IT_CV_INVISSLIDER, NULL, "Super Sneakers",    &cv_supersneakers, 60},
1674 	{IT_STRING|IT_CVAR|IT_CV_INVISSLIDER, NULL, "Invincibility",     &cv_invincibility, 70},
1675 	{IT_STRING|IT_CVAR|IT_CV_INVISSLIDER, NULL, "Whirlwind Shield",  &cv_jumpshield,    80},
1676 	{IT_STRING|IT_CVAR|IT_CV_INVISSLIDER, NULL, "Elemental Shield",  &cv_watershield,   90},
1677 	{IT_STRING|IT_CVAR|IT_CV_INVISSLIDER, NULL, "Attraction Shield", &cv_ringshield,   100},
1678 	{IT_STRING|IT_CVAR|IT_CV_INVISSLIDER, NULL, "Force Shield",      &cv_forceshield,  110},
1679 	{IT_STRING|IT_CVAR|IT_CV_INVISSLIDER, NULL, "Armageddon Shield", &cv_bombshield,   120},
1680 	{IT_STRING|IT_CVAR|IT_CV_INVISSLIDER, NULL, "1 Up",              &cv_1up,          130},
1681 	{IT_STRING|IT_CVAR|IT_CV_INVISSLIDER, NULL, "Eggman Box",        &cv_eggmanbox,    140},
1682 };
1683 
1684 // ==========================================================================
1685 // ALL MENU DEFINITIONS GO HERE
1686 // ==========================================================================
1687 
1688 // Main Menu and related
1689 menu_t MainDef = CENTERMENUSTYLE(MN_MAIN, NULL, MainMenu, NULL, 72);
1690 
1691 menu_t MISC_AddonsDef =
1692 {
1693 	MN_AD_MAIN,
1694 	NULL,
1695 	sizeof (MISC_AddonsMenu)/sizeof (menuitem_t),
1696 	&MainDef,
1697 	MISC_AddonsMenu,
1698 	M_DrawAddons,
1699 	50, 28,
1700 	0,
1701 	NULL
1702 };
1703 
1704 menu_t MAPauseDef = PAUSEMENUSTYLE(MAPauseMenu, 40, 72);
1705 menu_t SPauseDef = PAUSEMENUSTYLE(SPauseMenu, 40, 72);
1706 menu_t MPauseDef = PAUSEMENUSTYLE(MPauseMenu, 40, 72);
1707 
1708 // Misc Main Menu
1709 menu_t MISC_ScrambleTeamDef = DEFAULTMENUSTYLE(MN_SPECIAL, NULL, MISC_ScrambleTeamMenu, &MPauseDef, 27, 40);
1710 menu_t MISC_ChangeTeamDef = DEFAULTMENUSTYLE(MN_SPECIAL, NULL, MISC_ChangeTeamMenu, &MPauseDef, 27, 40);
1711 
1712 // MP Gametype and map change menu
1713 menu_t MISC_ChangeLevelDef =
1714 {
1715 	MN_SPECIAL,
1716 	NULL,
1717 	sizeof (MISC_ChangeLevelMenu)/sizeof (menuitem_t),
1718 	&MainDef,  // Doesn't matter.
1719 	MISC_ChangeLevelMenu,
1720 	M_DrawLevelPlatterMenu,
1721 	0, 0,
1722 	0,
1723 	NULL
1724 };
1725 
1726 menu_t MISC_HelpDef = IMAGEDEF(MISC_HelpMenu);
1727 
1728 static INT32 highlightflags, recommendedflags, warningflags;
1729 
1730 
1731 // Sky Room
1732 menu_t SR_PandoraDef =
1733 {
1734 	MTREE2(MN_SR_MAIN, MN_SR_PANDORA),
1735 	"M_PANDRA",
1736 	sizeof (SR_PandorasBox)/sizeof (menuitem_t),
1737 	&SPauseDef,
1738 	SR_PandorasBox,
1739 	M_DrawGenericMenu,
1740 	60, 30,
1741 	0,
1742 	M_ExitPandorasBox
1743 };
1744 
1745 menu_t SR_MainDef = DEFAULTMENUSTYLE(MN_SR_MAIN, "M_SECRET", SR_MainMenu, &MainDef, 60, 40);
1746 
1747 menu_t SR_LevelSelectDef = MAPPLATTERMENUSTYLE(
1748 	MTREE2(MN_SR_MAIN, MN_SR_LEVELSELECT),
1749 	NULL, SR_LevelSelectMenu);
1750 
1751 menu_t SR_UnlockChecklistDef =
1752 {
1753 	MTREE2(MN_SR_MAIN, MN_SR_UNLOCKCHECKLIST),
1754 	"M_SECRET",
1755 	1,
1756 	&SR_MainDef,
1757 	SR_UnlockChecklistMenu,
1758 	M_DrawChecklist,
1759 	30, 30,
1760 	0,
1761 	NULL
1762 };
1763 
1764 menu_t SR_SoundTestDef =
1765 {
1766 	MTREE2(MN_SR_MAIN, MN_SR_SOUNDTEST),
1767 	NULL,
1768 	sizeof (SR_SoundTestMenu)/sizeof (menuitem_t),
1769 	&SR_MainDef,
1770 	SR_SoundTestMenu,
1771 	M_DrawSoundTest,
1772 	60, 150,
1773 	0,
1774 	NULL
1775 };
1776 
1777 menu_t SR_EmblemHintDef =
1778 {
1779 	MTREE2(MN_SR_MAIN, MN_SR_EMBLEMHINT),
1780 	NULL,
1781 	sizeof (SR_EmblemHintMenu)/sizeof (menuitem_t),
1782 	&SPauseDef,
1783 	SR_EmblemHintMenu,
1784 	M_DrawEmblemHints,
1785 	60, 150,
1786 	0,
1787 	NULL
1788 };
1789 
1790 // Single Player
1791 menu_t SP_MainDef = //CENTERMENUSTYLE(NULL, SP_MainMenu, &MainDef, 72);
1792 {
1793 	MN_SP_MAIN,
1794 	NULL,
1795 	sizeof(SP_MainMenu)/sizeof(menuitem_t),
1796 	&MainDef,
1797 	SP_MainMenu,
1798 	M_DrawCenteredMenu,
1799 	BASEVIDWIDTH/2, 72,
1800 	0,
1801 	NULL
1802 };
1803 
1804 menu_t SP_LoadDef =
1805 {
1806 	MTREE2(MN_SP_MAIN, MN_SP_LOAD),
1807 	"M_PICKG",
1808 	1,
1809 	&SP_MainDef,
1810 	SP_LoadGameMenu,
1811 	M_DrawLoad,
1812 	68, 46,
1813 	0,
1814 	NULL
1815 };
1816 
1817 menu_t SP_LevelSelectDef = MAPPLATTERMENUSTYLE(
1818 	MTREE4(MN_SP_MAIN, MN_SP_LOAD, MN_SP_PLAYER, MN_SP_LEVELSELECT),
1819 	NULL, SP_LevelSelectMenu);
1820 
1821 menu_t SP_LevelStatsDef =
1822 {
1823 	MTREE2(MN_SP_MAIN, MN_SP_LEVELSTATS),
1824 	"M_STATS",
1825 	1,
1826 	&SP_MainDef,
1827 	SP_LevelStatsMenu,
1828 	M_DrawLevelStats,
1829 	280, 185,
1830 	0,
1831 	NULL
1832 };
1833 
1834 menu_t SP_TimeAttackLevelSelectDef = MAPPLATTERMENUSTYLE(
1835 	MTREE3(MN_SP_MAIN, MN_SP_TIMEATTACK, MN_SP_TIMEATTACK_LEVELSELECT),
1836 	"M_ATTACK", SP_TimeAttackLevelSelectMenu);
1837 
1838 static menu_t SP_TimeAttackDef =
1839 {
1840 	MTREE2(MN_SP_MAIN, MN_SP_TIMEATTACK),
1841 	"M_ATTACK",
1842 	sizeof (SP_TimeAttackMenu)/sizeof (menuitem_t),
1843 	&MainDef,  // Doesn't matter.
1844 	SP_TimeAttackMenu,
1845 	M_DrawTimeAttackMenu,
1846 	32, 40,
1847 	0,
1848 	NULL
1849 };
1850 static menu_t SP_ReplayDef =
1851 {
1852 	MTREE3(MN_SP_MAIN, MN_SP_TIMEATTACK, MN_SP_REPLAY),
1853 	"M_ATTACK",
1854 	sizeof(SP_ReplayMenu)/sizeof(menuitem_t),
1855 	&SP_TimeAttackDef,
1856 	SP_ReplayMenu,
1857 	M_DrawTimeAttackMenu,
1858 	32, 120,
1859 	0,
1860 	NULL
1861 };
1862 static menu_t SP_GuestReplayDef =
1863 {
1864 	MTREE3(MN_SP_MAIN, MN_SP_TIMEATTACK, MN_SP_GUESTREPLAY),
1865 	"M_ATTACK",
1866 	sizeof(SP_GuestReplayMenu)/sizeof(menuitem_t),
1867 	&SP_TimeAttackDef,
1868 	SP_GuestReplayMenu,
1869 	M_DrawTimeAttackMenu,
1870 	32, 120,
1871 	0,
1872 	NULL
1873 };
1874 static menu_t SP_GhostDef =
1875 {
1876 	MTREE3(MN_SP_MAIN, MN_SP_TIMEATTACK, MN_SP_GHOST),
1877 	"M_ATTACK",
1878 	sizeof(SP_GhostMenu)/sizeof(menuitem_t),
1879 	&SP_TimeAttackDef,
1880 	SP_GhostMenu,
1881 	M_DrawTimeAttackMenu,
1882 	32, 120,
1883 	0,
1884 	NULL
1885 };
1886 
1887 menu_t SP_NightsAttackLevelSelectDef = MAPPLATTERMENUSTYLE(
1888 	MTREE3(MN_SP_MAIN, MN_SP_NIGHTSATTACK, MN_SP_NIGHTS_LEVELSELECT),
1889 	 "M_NIGHTS", SP_NightsAttackLevelSelectMenu);
1890 
1891 static menu_t SP_NightsAttackDef =
1892 {
1893 	MTREE2(MN_SP_MAIN, MN_SP_NIGHTSATTACK),
1894 	"M_NIGHTS",
1895 	sizeof (SP_NightsAttackMenu)/sizeof (menuitem_t),
1896 	&MainDef,  // Doesn't matter.
1897 	SP_NightsAttackMenu,
1898 	M_DrawNightsAttackMenu,
1899 	32, 40,
1900 	0,
1901 	NULL
1902 };
1903 static menu_t SP_NightsReplayDef =
1904 {
1905 	MTREE3(MN_SP_MAIN, MN_SP_NIGHTSATTACK, MN_SP_NIGHTS_REPLAY),
1906 	"M_NIGHTS",
1907 	sizeof(SP_NightsReplayMenu)/sizeof(menuitem_t),
1908 	&SP_NightsAttackDef,
1909 	SP_NightsReplayMenu,
1910 	M_DrawNightsAttackMenu,
1911 	32, 120,
1912 	0,
1913 	NULL
1914 };
1915 static menu_t SP_NightsGuestReplayDef =
1916 {
1917 	MTREE3(MN_SP_MAIN, MN_SP_NIGHTSATTACK, MN_SP_NIGHTS_GUESTREPLAY),
1918 	"M_NIGHTS",
1919 	sizeof(SP_NightsGuestReplayMenu)/sizeof(menuitem_t),
1920 	&SP_NightsAttackDef,
1921 	SP_NightsGuestReplayMenu,
1922 	M_DrawNightsAttackMenu,
1923 	32, 120,
1924 	0,
1925 	NULL
1926 };
1927 static menu_t SP_NightsGhostDef =
1928 {
1929 	MTREE3(MN_SP_MAIN, MN_SP_NIGHTSATTACK, MN_SP_NIGHTS_GHOST),
1930 	"M_NIGHTS",
1931 	sizeof(SP_NightsGhostMenu)/sizeof(menuitem_t),
1932 	&SP_NightsAttackDef,
1933 	SP_NightsGhostMenu,
1934 	M_DrawNightsAttackMenu,
1935 	32, 120,
1936 	0,
1937 	NULL
1938 };
1939 
1940 static menu_t SP_MarathonDef =
1941 {
1942 	MTREE2(MN_SP_MAIN, MN_SP_MARATHON),
1943 	"M_RATHON",
1944 	sizeof(SP_MarathonMenu)/sizeof(menuitem_t),
1945 	&MainDef,  // Doesn't matter.
1946 	SP_MarathonMenu,
1947 	M_DrawMarathon,
1948 	32, 40,
1949 	0,
1950 	NULL
1951 };
1952 
1953 menu_t SP_PlayerDef =
1954 {
1955 	MTREE3(MN_SP_MAIN, MN_SP_LOAD, MN_SP_PLAYER),
1956 	"M_PICKP",
1957 	sizeof (SP_PlayerMenu)/sizeof (menuitem_t),
1958 	&SP_MainDef,
1959 	SP_PlayerMenu,
1960 	M_DrawSetupChoosePlayerMenu,
1961 	24, 32,
1962 	0,
1963 	NULL
1964 };
1965 
1966 // Multiplayer
1967 
1968 menu_t MP_SplitServerDef =
1969 {
1970 	MTREE2(MN_MP_MAIN, MN_MP_SPLITSCREEN),
1971 	"M_MULTI",
1972 	sizeof (MP_SplitServerMenu)/sizeof (menuitem_t),
1973 #ifndef NONET
1974 	&MP_MainDef,
1975 #else
1976 	&MainDef,
1977 #endif
1978 	MP_SplitServerMenu,
1979 	M_DrawServerMenu,
1980 	27, 30 - 50,
1981 	0,
1982 	NULL
1983 };
1984 
1985 #ifndef NONET
1986 
1987 menu_t MP_MainDef =
1988 {
1989 	MN_MP_MAIN,
1990 	"M_MULTI",
1991 	sizeof (MP_MainMenu)/sizeof (menuitem_t),
1992 	&MainDef,
1993 	MP_MainMenu,
1994 	M_DrawMPMainMenu,
1995 	27, 40,
1996 	0,
1997 	M_CancelConnect
1998 };
1999 
2000 menu_t MP_ServerDef =
2001 {
2002 	MTREE2(MN_MP_MAIN, MN_MP_SERVER),
2003 	"M_MULTI",
2004 	sizeof (MP_ServerMenu)/sizeof (menuitem_t),
2005 	&MP_MainDef,
2006 	MP_ServerMenu,
2007 	M_DrawServerMenu,
2008 	27, 30,
2009 	0,
2010 	NULL
2011 };
2012 
2013 menu_t MP_ConnectDef =
2014 {
2015 	MTREE2(MN_MP_MAIN, MN_MP_CONNECT),
2016 	"M_MULTI",
2017 	sizeof (MP_ConnectMenu)/sizeof (menuitem_t),
2018 	&MP_MainDef,
2019 	MP_ConnectMenu,
2020 	M_DrawConnectMenu,
2021 	27,24,
2022 	0,
2023 	M_CancelConnect
2024 };
2025 
2026 menu_t MP_RoomDef =
2027 {
2028 	MTREE2(MN_MP_MAIN, MN_MP_ROOM),
2029 	"M_MULTI",
2030 	sizeof (MP_RoomMenu)/sizeof (menuitem_t),
2031 	&MP_ConnectDef,
2032 	MP_RoomMenu,
2033 	M_DrawRoomMenu,
2034 	27, 32,
2035 	0,
2036 	NULL
2037 };
2038 #endif
2039 
2040 menu_t MP_PlayerSetupDef =
2041 {
2042 #ifdef NONET
2043 	MTREE2(MN_MP_MAIN, MN_MP_PLAYERSETUP),
2044 #else
2045 	MTREE3(MN_MP_MAIN, MN_MP_SPLITSCREEN, MN_MP_PLAYERSETUP),
2046 #endif
2047 	"M_SPLAYR",
2048 	sizeof (MP_PlayerSetupMenu)/sizeof (menuitem_t),
2049 	&MainDef, // doesn't matter
2050 	MP_PlayerSetupMenu,
2051 	M_DrawSetupMultiPlayerMenu,
2052 	19, 22,
2053 	0,
2054 	M_QuitMultiPlayerMenu
2055 };
2056 
2057 // Options
2058 menu_t OP_MainDef = DEFAULTMENUSTYLE(
2059 	MN_OP_MAIN,
2060 	"M_OPTTTL", OP_MainMenu, &MainDef, 50, 30);
2061 
2062 menu_t OP_ChangeControlsDef = CONTROLMENUSTYLE(
2063 	MTREE3(MN_OP_MAIN, 0, MN_OP_CHANGECONTROLS), // second level set on runtime
2064 	OP_ChangeControlsMenu, &OP_MainDef);
2065 
2066 menu_t OP_P1ControlsDef = {
2067 	MTREE2(MN_OP_MAIN, MN_OP_P1CONTROLS),
2068 	"M_CONTRO",
2069 	sizeof(OP_P1ControlsMenu)/sizeof(menuitem_t),
2070 	&OP_MainDef,
2071 	OP_P1ControlsMenu,
2072 	M_DrawControlsDefMenu,
2073 	50, 30, 0, NULL};
2074 menu_t OP_P2ControlsDef = {
2075 	MTREE2(MN_OP_MAIN, MN_OP_P2CONTROLS),
2076 	"M_CONTRO",
2077 	sizeof(OP_P2ControlsMenu)/sizeof(menuitem_t),
2078 	&OP_MainDef,
2079 	OP_P2ControlsMenu,
2080 	M_DrawControlsDefMenu,
2081 	50, 30, 0, NULL};
2082 
2083 menu_t OP_MouseOptionsDef = DEFAULTMENUSTYLE(
2084 	MTREE3(MN_OP_MAIN, MN_OP_P1CONTROLS, MN_OP_P1MOUSE),
2085 	"M_CONTRO", OP_MouseOptionsMenu, &OP_P1ControlsDef, 35, 30);
2086 menu_t OP_Mouse2OptionsDef = DEFAULTMENUSTYLE(
2087 	MTREE3(MN_OP_MAIN, MN_OP_P2CONTROLS, MN_OP_P2MOUSE),
2088 	"M_CONTRO", OP_Mouse2OptionsMenu, &OP_P2ControlsDef, 35, 30);
2089 
2090 menu_t OP_Joystick1Def = DEFAULTMENUSTYLE(
2091 	MTREE3(MN_OP_MAIN, MN_OP_P1CONTROLS, MN_OP_P1JOYSTICK),
2092 	"M_CONTRO", OP_Joystick1Menu, &OP_P1ControlsDef, 50, 30);
2093 menu_t OP_Joystick2Def = DEFAULTMENUSTYLE(
2094 	MTREE3(MN_OP_MAIN, MN_OP_P2CONTROLS, MN_OP_P2JOYSTICK),
2095 	"M_CONTRO", OP_Joystick2Menu, &OP_P2ControlsDef, 50, 30);
2096 
2097 menu_t OP_JoystickSetDef =
2098 {
2099 	MTREE4(MN_OP_MAIN, 0, 0, MN_OP_JOYSTICKSET), // second and third level set on runtime
2100 	"M_CONTRO",
2101 	sizeof (OP_JoystickSetMenu)/sizeof (menuitem_t),
2102 	&OP_Joystick1Def,
2103 	OP_JoystickSetMenu,
2104 	M_DrawJoystick,
2105 	60, 40,
2106 	0,
2107 	NULL
2108 };
2109 
2110 menu_t OP_CameraOptionsDef = {
2111 	MTREE3(MN_OP_MAIN, MN_OP_P1CONTROLS, MN_OP_P1CAMERA),
2112 	"M_CONTRO",
2113 	sizeof (OP_CameraOptionsMenu)/sizeof (menuitem_t),
2114 	&OP_P1ControlsDef,
2115 	OP_CameraOptionsMenu,
2116 	M_DrawCameraOptionsMenu,
2117 	35, 30,
2118 	0,
2119 	NULL
2120 };
2121 menu_t OP_Camera2OptionsDef = {
2122 	MTREE3(MN_OP_MAIN, MN_OP_P2CONTROLS, MN_OP_P2CAMERA),
2123 	"M_CONTRO",
2124 	sizeof (OP_Camera2OptionsMenu)/sizeof (menuitem_t),
2125 	&OP_P2ControlsDef,
2126 	OP_Camera2OptionsMenu,
2127 	M_DrawCameraOptionsMenu,
2128 	35, 30,
2129 	0,
2130 	NULL
2131 };
2132 
2133 static menuitem_t OP_PlaystyleMenu[] = {{IT_KEYHANDLER | IT_NOTHING, NULL, "", M_HandlePlaystyleMenu, 0}};
2134 
2135 menu_t OP_PlaystyleDef = {
2136 	MTREE3(MN_OP_MAIN, MN_OP_P1CONTROLS, MN_OP_PLAYSTYLE), ///@TODO the second level should be set in runtime
2137 	NULL,
2138 	1,
2139 	&OP_P1ControlsDef,
2140 	OP_PlaystyleMenu,
2141 	M_DrawPlaystyleMenu,
2142 	0, 0, 0, NULL
2143 };
2144 
M_VideoOptions(INT32 choice)2145 static void M_VideoOptions(INT32 choice)
2146 {
2147 	(void)choice;
2148 
2149 	OP_VideoOptionsMenu[op_video_renderer].status = (IT_TRANSTEXT | IT_PAIR);
2150 	OP_VideoOptionsMenu[op_video_renderer].patch = "Renderer";
2151 	OP_VideoOptionsMenu[op_video_renderer].text = "Software";
2152 
2153 #ifdef HWRENDER
2154 	if (vid.glstate != VID_GL_LIBRARY_ERROR)
2155 	{
2156 		OP_VideoOptionsMenu[op_video_renderer].status = (IT_STRING | IT_CVAR);
2157 		OP_VideoOptionsMenu[op_video_renderer].patch = NULL;
2158 		OP_VideoOptionsMenu[op_video_renderer].text = "Renderer";
2159 	}
2160 #endif
2161 
2162 	M_SetupNextMenu(&OP_VideoOptionsDef);
2163 }
2164 
2165 menu_t OP_VideoOptionsDef =
2166 {
2167 	MTREE2(MN_OP_MAIN, MN_OP_VIDEO),
2168 	"M_VIDEO",
2169 	sizeof (OP_VideoOptionsMenu)/sizeof (menuitem_t),
2170 	&OP_MainDef,
2171 	OP_VideoOptionsMenu,
2172 	M_DrawMainVideoMenu,
2173 	30, 30,
2174 	0,
2175 	NULL
2176 };
2177 menu_t OP_VideoModeDef =
2178 {
2179 	MTREE3(MN_OP_MAIN, MN_OP_VIDEO, MN_OP_VIDEOMODE),
2180 	"M_VIDEO",
2181 	1,
2182 	&OP_VideoOptionsDef,
2183 	OP_VideoModeMenu,
2184 	M_DrawVideoMode,
2185 	48, 26,
2186 	0,
2187 	NULL
2188 };
2189 menu_t OP_ColorOptionsDef =
2190 {
2191 	MTREE3(MN_OP_MAIN, MN_OP_VIDEO, MN_OP_COLOR),
2192 	"M_VIDEO",
2193 	sizeof (OP_ColorOptionsMenu)/sizeof (menuitem_t),
2194 	&OP_VideoOptionsDef,
2195 	OP_ColorOptionsMenu,
2196 	M_DrawColorMenu,
2197 	30, 30,
2198 	0,
2199 	NULL
2200 };
2201 menu_t OP_SoundOptionsDef = DEFAULTSCROLLMENUSTYLE(
2202 	MTREE2(MN_OP_MAIN, MN_OP_SOUND),
2203 	"M_SOUND", OP_SoundOptionsMenu, &OP_MainDef, 30, 30);
2204 menu_t OP_SoundAdvancedDef = DEFAULTMENUSTYLE(
2205 	MTREE2(MN_OP_MAIN, MN_OP_SOUND),
2206 	"M_SOUND", OP_SoundAdvancedMenu, &OP_SoundOptionsDef, 30, 30);
2207 
2208 menu_t OP_ServerOptionsDef = DEFAULTSCROLLMENUSTYLE(
2209 	MTREE2(MN_OP_MAIN, MN_OP_SERVER),
2210 	"M_SERVER", OP_ServerOptionsMenu, &OP_MainDef, 30, 30);
2211 
2212 menu_t OP_MonitorToggleDef =
2213 {
2214 	MTREE3(MN_OP_MAIN, MN_OP_SOUND, MN_OP_MONITORTOGGLE),
2215 	"M_SERVER",
2216 	sizeof (OP_MonitorToggleMenu)/sizeof (menuitem_t),
2217 	&OP_ServerOptionsDef,
2218 	OP_MonitorToggleMenu,
2219 	M_DrawMonitorToggles,
2220 	30, 30,
2221 	0,
2222 	NULL
2223 };
2224 
2225 #ifdef HWRENDER
M_OpenGLOptionsMenu(void)2226 static void M_OpenGLOptionsMenu(void)
2227 {
2228 	if (rendermode == render_opengl)
2229 		M_SetupNextMenu(&OP_OpenGLOptionsDef);
2230 	else
2231 		M_StartMessage(M_GetText("You must be in OpenGL mode\nto access this menu.\n\n(Press a key)\n"), NULL, MM_NOTHING);
2232 }
2233 
2234 menu_t OP_OpenGLOptionsDef = DEFAULTMENUSTYLE(
2235 	MTREE3(MN_OP_MAIN, MN_OP_VIDEO, MN_OP_OPENGL),
2236 	"M_VIDEO", OP_OpenGLOptionsMenu, &OP_VideoOptionsDef, 30, 30);
2237 #ifdef ALAM_LIGHTING
2238 menu_t OP_OpenGLLightingDef = DEFAULTMENUSTYLE(
2239 	MTREE4(MN_OP_MAIN, MN_OP_VIDEO, MN_OP_OPENGL, MN_OP_OPENGL_LIGHTING),
2240 	"M_VIDEO", OP_OpenGLLightingMenu, &OP_OpenGLOptionsDef, 60, 40);
2241 #endif // ALAM_LIGHTING
2242 #endif // HWRENDER
2243 
2244 menu_t OP_DataOptionsDef = DEFAULTMENUSTYLE(
2245 	MTREE2(MN_OP_MAIN, MN_OP_DATA),
2246 	"M_DATA", OP_DataOptionsMenu, &OP_MainDef, 60, 30);
2247 
2248 menu_t OP_ScreenshotOptionsDef =
2249 {
2250 	MTREE3(MN_OP_MAIN, MN_OP_DATA, MN_OP_SCREENSHOTS),
2251 	"M_SCREEN",
2252 	sizeof (OP_ScreenshotOptionsMenu)/sizeof (menuitem_t),
2253 	&OP_DataOptionsDef,
2254 	OP_ScreenshotOptionsMenu,
2255 	M_DrawScreenshotMenu,
2256 	30, 30,
2257 	0,
2258 	NULL
2259 };
2260 
2261 menu_t OP_AddonsOptionsDef = DEFAULTMENUSTYLE(
2262 	MTREE3(MN_OP_MAIN, MN_OP_DATA, MN_OP_ADDONS),
2263 	"M_ADDONS", OP_AddonsOptionsMenu, &OP_DataOptionsDef, 30, 30);
2264 
2265 menu_t OP_EraseDataDef = DEFAULTMENUSTYLE(
2266 	MTREE3(MN_OP_MAIN, MN_OP_DATA, MN_OP_ERASEDATA),
2267 	"M_DATA", OP_EraseDataMenu, &OP_DataOptionsDef, 60, 30);
2268 
2269 // ==========================================================================
2270 // CVAR ONCHANGE EVENTS GO HERE
2271 // ==========================================================================
2272 // (there's only a couple anyway)
2273 
2274 // Prototypes
2275 static INT32 M_GetFirstLevelInList(INT32 gt);
2276 static boolean M_CanShowLevelOnPlatter(INT32 mapnum, INT32 gt);
2277 
2278 // Nextmap.  Used for Level select.
Nextmap_OnChange(void)2279 void Nextmap_OnChange(void)
2280 {
2281 	char *leveltitle;
2282 	char tabase[256];
2283 #ifdef OLDNREPLAYNAME
2284 	char tabaseold[256];
2285 #endif
2286 	short i;
2287 	boolean active;
2288 
2289 	// Update the string in the consvar.
2290 	Z_Free(cv_nextmap.zstring);
2291 	leveltitle = G_BuildMapTitle(cv_nextmap.value);
2292 	cv_nextmap.string = cv_nextmap.zstring = leveltitle ? leveltitle : Z_StrDup(G_BuildMapName(cv_nextmap.value));
2293 
2294 	if (currentMenu == &SP_NightsAttackDef)
2295 	{
2296 		CV_StealthSetValue(&cv_dummymares, 0);
2297 		// Hide the record changing CVAR if only one mare is available.
2298 		if (!nightsrecords[cv_nextmap.value-1] || nightsrecords[cv_nextmap.value-1]->nummares < 2)
2299 			SP_NightsAttackMenu[narecords].status = IT_DISABLED;
2300 		else
2301 			SP_NightsAttackMenu[narecords].status = IT_STRING|IT_CVAR;
2302 
2303 		// Do the replay things.
2304 		active = false;
2305 		SP_NightsAttackMenu[naguest].status = IT_DISABLED;
2306 		SP_NightsAttackMenu[nareplay].status = IT_DISABLED;
2307 		SP_NightsAttackMenu[naghost].status = IT_DISABLED;
2308 
2309 		// Check if file exists, if not, disable REPLAY option
2310 		sprintf(tabase,"%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s",srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), skins[cv_chooseskin.value-1].name);
2311 
2312 #ifdef OLDNREPLAYNAME
2313 		sprintf(tabaseold,"%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s",srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value));
2314 #endif
2315 
2316 		for (i = 0; i < 4; i++) {
2317 			SP_NightsReplayMenu[i].status = IT_DISABLED;
2318 			SP_NightsGuestReplayMenu[i].status = IT_DISABLED;
2319 		}
2320 
2321 		if (FIL_FileExists(va("%s-score-best.lmp", tabase))) {
2322 			SP_NightsReplayMenu[0].status = IT_WHITESTRING|IT_CALL;
2323 			SP_NightsGuestReplayMenu[0].status = IT_WHITESTRING|IT_CALL;
2324 			active = true;
2325 		}
2326 		if (FIL_FileExists(va("%s-time-best.lmp", tabase))) {
2327 			SP_NightsReplayMenu[1].status = IT_WHITESTRING|IT_CALL;
2328 			SP_NightsGuestReplayMenu[1].status = IT_WHITESTRING|IT_CALL;
2329 			active = true;
2330 		}
2331 		if (FIL_FileExists(va("%s-last.lmp", tabase))) {
2332 			SP_NightsReplayMenu[2].status = IT_WHITESTRING|IT_CALL;
2333 			SP_NightsGuestReplayMenu[2].status = IT_WHITESTRING|IT_CALL;
2334 			active = true;
2335 		}
2336 		if (FIL_FileExists(va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-guest.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value))))  {
2337 			SP_NightsReplayMenu[3].status = IT_WHITESTRING|IT_CALL;
2338 			SP_NightsGuestReplayMenu[3].status = IT_WHITESTRING|IT_CALL;
2339 			active = true;
2340 		}
2341 
2342 		// Old style name compatibility
2343 #ifdef OLDNREPLAYNAME
2344 		if (FIL_FileExists(va("%s-score-best.lmp", tabaseold))) {
2345 			SP_NightsReplayMenu[0].status = IT_WHITESTRING|IT_CALL;
2346 			SP_NightsGuestReplayMenu[0].status = IT_WHITESTRING|IT_CALL;
2347 			active = true;
2348 		}
2349 		if (FIL_FileExists(va("%s-time-best.lmp", tabaseold))) {
2350 			SP_NightsReplayMenu[1].status = IT_WHITESTRING|IT_CALL;
2351 			SP_NightsGuestReplayMenu[1].status = IT_WHITESTRING|IT_CALL;
2352 			active = true;
2353 		}
2354 		if (FIL_FileExists(va("%s-last.lmp", tabaseold))) {
2355 			SP_NightsReplayMenu[2].status = IT_WHITESTRING|IT_CALL;
2356 			SP_NightsGuestReplayMenu[2].status = IT_WHITESTRING|IT_CALL;
2357 			active = true;
2358 		}
2359 #endif
2360 
2361 		if (active) {
2362 			SP_NightsAttackMenu[naguest].status = IT_WHITESTRING|IT_SUBMENU;
2363 			SP_NightsAttackMenu[nareplay].status = IT_WHITESTRING|IT_SUBMENU;
2364 			SP_NightsAttackMenu[naghost].status = IT_WHITESTRING|IT_SUBMENU;
2365 		}
2366 
2367 		else if(itemOn == nareplay) // Reset lastOn so replay isn't still selected when not available.
2368 		{
2369 			currentMenu->lastOn = itemOn;
2370 			itemOn = nastart;
2371 		}
2372 	}
2373 	else if (currentMenu == &SP_TimeAttackDef)
2374 	{
2375 		active = false;
2376 		SP_TimeAttackMenu[taguest].status = IT_DISABLED;
2377 		SP_TimeAttackMenu[tareplay].status = IT_DISABLED;
2378 		SP_TimeAttackMenu[taghost].status = IT_DISABLED;
2379 
2380 		// Check if file exists, if not, disable REPLAY option
2381 		sprintf(tabase,"%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s",srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), skins[cv_chooseskin.value-1].name);
2382 		for (i = 0; i < 5; i++) {
2383 			SP_ReplayMenu[i].status = IT_DISABLED;
2384 			SP_GuestReplayMenu[i].status = IT_DISABLED;
2385 		}
2386 		if (FIL_FileExists(va("%s-time-best.lmp", tabase))) {
2387 			SP_ReplayMenu[0].status = IT_WHITESTRING|IT_CALL;
2388 			SP_GuestReplayMenu[0].status = IT_WHITESTRING|IT_CALL;
2389 			active = true;
2390 		}
2391 		if (FIL_FileExists(va("%s-score-best.lmp", tabase))) {
2392 			SP_ReplayMenu[1].status = IT_WHITESTRING|IT_CALL;
2393 			SP_GuestReplayMenu[1].status = IT_WHITESTRING|IT_CALL;
2394 			active = true;
2395 		}
2396 		if (FIL_FileExists(va("%s-rings-best.lmp", tabase))) {
2397 			SP_ReplayMenu[2].status = IT_WHITESTRING|IT_CALL;
2398 			SP_GuestReplayMenu[2].status = IT_WHITESTRING|IT_CALL;
2399 			active = true;
2400 		}
2401 		if (FIL_FileExists(va("%s-last.lmp", tabase))) {
2402 			SP_ReplayMenu[3].status = IT_WHITESTRING|IT_CALL;
2403 			SP_GuestReplayMenu[3].status = IT_WHITESTRING|IT_CALL;
2404 			active = true;
2405 		}
2406 		if (FIL_FileExists(va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-guest.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value)))) {
2407 			SP_ReplayMenu[4].status = IT_WHITESTRING|IT_CALL;
2408 			SP_GuestReplayMenu[4].status = IT_WHITESTRING|IT_CALL;
2409 			active = true;
2410 		}
2411 		if (active) {
2412 			SP_TimeAttackMenu[taguest].status = IT_WHITESTRING|IT_SUBMENU;
2413 			SP_TimeAttackMenu[tareplay].status = IT_WHITESTRING|IT_SUBMENU;
2414 			SP_TimeAttackMenu[taghost].status = IT_WHITESTRING|IT_SUBMENU;
2415 		}
2416 		else if(itemOn == tareplay) // Reset lastOn so replay isn't still selected when not available.
2417 		{
2418 			currentMenu->lastOn = itemOn;
2419 			itemOn = tastart;
2420 		}
2421 
2422 		if (mapheaderinfo[cv_nextmap.value-1] && mapheaderinfo[cv_nextmap.value-1]->forcecharacter[0] != '\0')
2423 			CV_Set(&cv_chooseskin, mapheaderinfo[cv_nextmap.value-1]->forcecharacter);
2424 	}
2425 }
2426 
Dummymares_OnChange(void)2427 static void Dummymares_OnChange(void)
2428 {
2429 	if (!nightsrecords[cv_nextmap.value-1])
2430 	{
2431 		CV_StealthSetValue(&cv_dummymares, 0);
2432 		return;
2433 	}
2434 	else
2435 	{
2436 		UINT8 mares = nightsrecords[cv_nextmap.value-1]->nummares;
2437 
2438 		if (cv_dummymares.value < 0)
2439 			CV_StealthSetValue(&cv_dummymares, mares);
2440 		else if (cv_dummymares.value > mares)
2441 			CV_StealthSetValue(&cv_dummymares, 0);
2442 	}
2443 }
2444 
2445 // Newgametype.  Used for gametype changes.
Newgametype_OnChange(void)2446 static void Newgametype_OnChange(void)
2447 {
2448 	if (menuactive)
2449 	{
2450 		if(!mapheaderinfo[cv_nextmap.value-1])
2451 			P_AllocMapHeader((INT16)(cv_nextmap.value-1));
2452 
2453 		if (!M_CanShowLevelOnPlatter(cv_nextmap.value-1, cv_newgametype.value))
2454 			CV_SetValue(&cv_nextmap, M_GetFirstLevelInList(cv_newgametype.value));
2455 	}
2456 }
2457 
Screenshot_option_Onchange(void)2458 void Screenshot_option_Onchange(void)
2459 {
2460 	OP_ScreenshotOptionsMenu[op_screenshot_folder].status =
2461 		(cv_screenshot_option.value == 3 ? IT_CVAR|IT_STRING|IT_CV_STRING : IT_DISABLED);
2462 }
2463 
Moviemode_mode_Onchange(void)2464 void Moviemode_mode_Onchange(void)
2465 {
2466 	INT32 i, cstart, cend;
2467 	for (i = op_screenshot_gif_start; i <= op_screenshot_apng_end; ++i)
2468 		OP_ScreenshotOptionsMenu[i].status = IT_DISABLED;
2469 
2470 	switch (cv_moviemode.value)
2471 	{
2472 		case MM_GIF:
2473 			cstart = op_screenshot_gif_start;
2474 			cend = op_screenshot_gif_end;
2475 			break;
2476 		case MM_APNG:
2477 			cstart = op_screenshot_apng_start;
2478 			cend = op_screenshot_apng_end;
2479 			break;
2480 		default:
2481 			return;
2482 	}
2483 	for (i = cstart; i <= cend; ++i)
2484 		OP_ScreenshotOptionsMenu[i].status = IT_STRING|IT_CVAR;
2485 }
2486 
Addons_option_Onchange(void)2487 void Addons_option_Onchange(void)
2488 {
2489 	OP_AddonsOptionsMenu[op_addons_folder].status =
2490 		(cv_addons_option.value == 3 ? IT_CVAR|IT_STRING|IT_CV_STRING : IT_DISABLED);
2491 }
2492 
Moviemode_option_Onchange(void)2493 void Moviemode_option_Onchange(void)
2494 {
2495 	OP_ScreenshotOptionsMenu[op_movie_folder].status =
2496 		(cv_movie_option.value == 3 ? IT_CVAR|IT_STRING|IT_CV_STRING : IT_DISABLED);
2497 }
2498 
2499 // ==========================================================================
2500 // END ORGANIZATION STUFF.
2501 // ==========================================================================
2502 
2503 // current menudef
2504 menu_t *currentMenu = &MainDef;
2505 
2506 // =========================================================================
2507 // MENU PRESENTATION PARAMETER HANDLING (BACKGROUNDS)
2508 // =========================================================================
2509 
2510 // menu IDs are equal to current/prevMenu in most cases, except MN_SPECIAL when we don't want to operate on Message, Pause, etc.
2511 UINT32 prevMenuId = 0;
2512 UINT32 activeMenuId = 0;
2513 
2514 menupres_t menupres[NUMMENUTYPES];
2515 
M_InitMenuPresTables(void)2516 void M_InitMenuPresTables(void)
2517 {
2518 	INT32 i;
2519 
2520 	// Called in d_main before SOC can get to the tables
2521 	// Set menupres defaults
2522 	for (i = 0; i < NUMMENUTYPES; i++)
2523 	{
2524 		// so-called "undefined"
2525 		menupres[i].fadestrength = -1;
2526 		menupres[i].hidetitlepics = -1; // inherits global hidetitlepics
2527 		menupres[i].ttmode = TTMODE_NONE;
2528 		menupres[i].ttscale = UINT8_MAX;
2529 		menupres[i].ttname[0] = 0;
2530 		menupres[i].ttx = INT16_MAX;
2531 		menupres[i].tty = INT16_MAX;
2532 		menupres[i].ttloop = INT16_MAX;
2533 		menupres[i].tttics = UINT16_MAX;
2534 		menupres[i].enterwipe = -1;
2535 		menupres[i].exitwipe = -1;
2536 		menupres[i].bgcolor = -1;
2537 		menupres[i].titlescrollxspeed = INT32_MAX;
2538 		menupres[i].titlescrollyspeed = INT32_MAX;
2539 		menupres[i].bghide = true;
2540 		// default true
2541 		menupres[i].enterbubble = true;
2542 		menupres[i].exitbubble = true;
2543 
2544 		if (i != MN_MAIN)
2545 		{
2546 			menupres[i].muslooping = true;
2547 		}
2548 		if (i == MN_SP_TIMEATTACK)
2549 			strncpy(menupres[i].musname, "_recat", 7);
2550 		else if (i == MN_SP_NIGHTSATTACK)
2551 			strncpy(menupres[i].musname, "_nitat", 7);
2552 		else if (i == MN_SP_MARATHON)
2553 			strncpy(menupres[i].musname, "spec8", 6);
2554 		else if (i == MN_SP_PLAYER || i == MN_SR_PLAYER)
2555 			strncpy(menupres[i].musname, "_chsel", 7);
2556 		else if (i == MN_SR_SOUNDTEST)
2557 		{
2558 			*menupres[i].musname = '\0';
2559 			menupres[i].musstop = true;
2560 		}
2561 	}
2562 }
2563 
2564 // ====================================
2565 // TREE ITERATION
2566 // ====================================
2567 
2568 // UINT32 menutype - current menutype_t
2569 // INT32 level - current level up the tree, higher means younger
2570 // INT32 *retval - Return value
2571 // void *input - Pointer to input of any type
2572 //
2573 // return true - stop iterating
2574 // return false - continue
2575 typedef boolean (*menutree_iterator)(UINT32, INT32, INT32 *, void **, boolean fromoldest);
2576 
2577 // HACK: Used in the ChangeMusic iterator because we only allow
2578 // a single input. Maybe someday use this struct program-wide.
2579 typedef struct
2580 {
2581 	char musname[7];
2582 	UINT16 mustrack;
2583 	boolean muslooping;
2584 } menupresmusic_t;
2585 
M_IterateMenuTree(menutree_iterator itfunc,void * input)2586 static INT32 M_IterateMenuTree(menutree_iterator itfunc, void *input)
2587 {
2588 	INT32 i, retval = 0;
2589 	UINT32 bitmask, menutype;
2590 
2591 	for (i = NUMMENULEVELS; i >= 0; i--)
2592 	{
2593 		bitmask = ((1 << MENUBITS) - 1) << (MENUBITS*i);
2594 		menutype = (activeMenuId & bitmask) >> (MENUBITS*i);
2595 		if (itfunc(menutype, i, &retval, &input, false))
2596 			break;
2597 	}
2598 
2599 	return retval;
2600 }
2601 
2602 // ====================================
2603 // ITERATORS
2604 // ====================================
2605 
MIT_GetMenuAtLevel(UINT32 menutype,INT32 level,INT32 * retval,void ** input,boolean fromoldest)2606 static boolean MIT_GetMenuAtLevel(UINT32 menutype, INT32 level, INT32 *retval, void **input, boolean fromoldest)
2607 {
2608 	INT32 *inputptr = (INT32*)*input;
2609 	INT32 targetlevel = *inputptr;
2610 	if (menutype)
2611 	{
2612 		if (level == targetlevel || targetlevel < 0)
2613 		{
2614 			*retval = menutype;
2615 			return true;
2616 		}
2617 	}
2618 	else if (targetlevel >= 0)
2619 	{
2620 		// offset targetlevel by failed attempts; this should only happen in beginning of iteration
2621 		if (fromoldest)
2622 			(*inputptr)++;
2623 		else
2624 			(*inputptr)--; // iterating backwards, so count from highest
2625 	}
2626 	return false;
2627 }
2628 
MIT_SetCurBackground(UINT32 menutype,INT32 level,INT32 * retval,void ** input,boolean fromoldest)2629 static boolean MIT_SetCurBackground(UINT32 menutype, INT32 level, INT32 *retval, void **input, boolean fromoldest)
2630 {
2631 	char *defaultname = (char*)*input;
2632 
2633 	(void)retval;
2634 	(void)fromoldest;
2635 
2636 	if (!menutype) // if there's nothing in this level, do nothing
2637 		return false;
2638 
2639 	if (menupres[menutype].bgcolor >= 0)
2640 	{
2641 		curbgcolor = menupres[menutype].bgcolor;
2642 		return true;
2643 	}
2644 	else if (menupres[menutype].bghide && titlemapinaction) // hide the background
2645 	{
2646 		curbghide = true;
2647 		return true;
2648 	}
2649 	else if (menupres[menutype].bgname[0])
2650 	{
2651 		strncpy(curbgname, menupres[menutype].bgname, 8);
2652 		curbgxspeed = menupres[menutype].titlescrollxspeed != INT32_MAX ? menupres[menutype].titlescrollxspeed : titlescrollxspeed;
2653 		curbgyspeed = menupres[menutype].titlescrollyspeed != INT32_MAX ? menupres[menutype].titlescrollyspeed : titlescrollyspeed;
2654 		return true;
2655 	}
2656 	else if (!level)
2657 	{
2658 		if (M_GetYoungestChildMenu() == MN_SP_PLAYER || !defaultname || !defaultname[0])
2659 			curbgcolor = 31;
2660 		else if (titlemapinaction) // hide the background by default in titlemap
2661 			curbghide = true;
2662 		else
2663 		{
2664 			strncpy(curbgname, defaultname, 9);
2665 			curbgxspeed = (gamestate == GS_TIMEATTACK) ? 0 : titlescrollxspeed;
2666 			curbgyspeed = (gamestate == GS_TIMEATTACK) ? 0 : titlescrollyspeed;
2667 		}
2668 	}
2669 	return false;
2670 }
2671 
MIT_ChangeMusic(UINT32 menutype,INT32 level,INT32 * retval,void ** input,boolean fromoldest)2672 static boolean MIT_ChangeMusic(UINT32 menutype, INT32 level, INT32 *retval, void **input, boolean fromoldest)
2673 {
2674 	menupresmusic_t *defaultmusic = (menupresmusic_t*)*input;
2675 
2676 	(void)retval;
2677 	(void)fromoldest;
2678 
2679 	if (!menutype) // if there's nothing in this level, do nothing
2680 		return false;
2681 
2682 	if (menupres[menutype].musname[0])
2683 	{
2684 		S_ChangeMusic(menupres[menutype].musname, menupres[menutype].mustrack, menupres[menutype].muslooping);
2685 		return true;
2686 	}
2687 	else if (menupres[menutype].musstop)
2688 	{
2689 		S_StopMusic();
2690 		return true;
2691 	}
2692 	else if (menupres[menutype].musignore)
2693 		return true;
2694 	else if (!level && defaultmusic && defaultmusic->musname[0])
2695 		S_ChangeMusic(defaultmusic->musname, defaultmusic->mustrack, defaultmusic->muslooping);
2696 	return false;
2697 }
2698 
MIT_SetCurFadeValue(UINT32 menutype,INT32 level,INT32 * retval,void ** input,boolean fromoldest)2699 static boolean MIT_SetCurFadeValue(UINT32 menutype, INT32 level, INT32 *retval, void **input, boolean fromoldest)
2700 {
2701 	UINT8 defaultvalue = *(UINT8*)*input;
2702 
2703 	(void)retval;
2704 	(void)fromoldest;
2705 
2706 	if (!menutype) // if there's nothing in this level, do nothing
2707 		return false;
2708 
2709 	if (menupres[menutype].fadestrength >= 0)
2710 	{
2711 		curfadevalue = (menupres[menutype].fadestrength % 32);
2712 		return true;
2713 	}
2714 	else if (!level)
2715 		curfadevalue = (gamestate == GS_TIMEATTACK) ? 0 : (defaultvalue % 32);
2716 	return false;
2717 }
2718 
MIT_SetCurTitlePics(UINT32 menutype,INT32 level,INT32 * retval,void ** input,boolean fromoldest)2719 static boolean MIT_SetCurTitlePics(UINT32 menutype, INT32 level, INT32 *retval, void **input, boolean fromoldest)
2720 {
2721 	(void)input;
2722 	(void)retval;
2723 	(void)fromoldest;
2724 
2725 	if (!menutype) // if there's nothing in this level, do nothing
2726 		return false;
2727 
2728 	if (menupres[menutype].hidetitlepics >= 0)
2729 	{
2730 		curhidepics = menupres[menutype].hidetitlepics;
2731 		return true;
2732 	}
2733 	else if (menupres[menutype].ttmode == TTMODE_USER)
2734 	{
2735 		if (menupres[menutype].ttname[0])
2736 		{
2737 			curhidepics = menupres[menutype].hidetitlepics;
2738 			curttmode = menupres[menutype].ttmode;
2739 			curttscale = (menupres[menutype].ttscale != UINT8_MAX ? menupres[menutype].ttscale : ttscale);
2740 			strncpy(curttname, menupres[menutype].ttname, 9);
2741 			curttx = (menupres[menutype].ttx != INT16_MAX ? menupres[menutype].ttx : ttx);
2742 			curtty = (menupres[menutype].tty != INT16_MAX ? menupres[menutype].tty : tty);
2743 			curttloop = (menupres[menutype].ttloop != INT16_MAX ? menupres[menutype].ttloop : ttloop);
2744 			curtttics = (menupres[menutype].tttics != UINT16_MAX ? menupres[menutype].tttics : tttics);
2745 		}
2746 		else
2747 			curhidepics = menupres[menutype].hidetitlepics;
2748 		return true;
2749 	}
2750 	else if (menupres[menutype].ttmode != TTMODE_NONE)
2751 	{
2752 		curhidepics = menupres[menutype].hidetitlepics;
2753 		curttmode = menupres[menutype].ttmode;
2754 		curttscale = (menupres[menutype].ttscale != UINT8_MAX ? menupres[menutype].ttscale : ttscale);
2755 		return true;
2756 	}
2757 	else if (!level)
2758 	{
2759 		curhidepics = hidetitlepics;
2760 		curttmode = ttmode;
2761 		curttscale = ttscale;
2762 		strncpy(curttname, ttname, 9);
2763 		curttx = ttx;
2764 		curtty = tty;
2765 		curttloop = ttloop;
2766 		curtttics = tttics;
2767 	}
2768 	return false;
2769 }
2770 
2771 // ====================================
2772 // TREE RETRIEVAL
2773 // ====================================
2774 
M_GetYoungestChildMenu(void)2775 UINT8 M_GetYoungestChildMenu(void) // aka the active menu
2776 {
2777 	INT32 targetlevel = -1;
2778 	return M_IterateMenuTree(MIT_GetMenuAtLevel, &targetlevel);
2779 }
2780 
2781 // ====================================
2782 // EFFECTS
2783 // ====================================
2784 
M_ChangeMenuMusic(const char * defaultmusname,boolean defaultmuslooping)2785 void M_ChangeMenuMusic(const char *defaultmusname, boolean defaultmuslooping)
2786 {
2787 	menupresmusic_t defaultmusic;
2788 
2789 	if (!defaultmusname)
2790 		defaultmusname = "";
2791 
2792 	strncpy(defaultmusic.musname, defaultmusname, 7);
2793 	defaultmusic.musname[6] = 0;
2794 	defaultmusic.mustrack = 0;
2795 	defaultmusic.muslooping = defaultmuslooping;
2796 
2797 	M_IterateMenuTree(MIT_ChangeMusic, &defaultmusic);
2798 }
2799 
M_SetMenuCurBackground(const char * defaultname)2800 void M_SetMenuCurBackground(const char *defaultname)
2801 {
2802 	char name[9];
2803 	strncpy(name, defaultname, 8);
2804 	M_IterateMenuTree(MIT_SetCurBackground, &name);
2805 }
2806 
M_SetMenuCurFadeValue(UINT8 defaultvalue)2807 void M_SetMenuCurFadeValue(UINT8 defaultvalue)
2808 {
2809 	M_IterateMenuTree(MIT_SetCurFadeValue, &defaultvalue);
2810 }
2811 
M_SetMenuCurTitlePics(void)2812 void M_SetMenuCurTitlePics(void)
2813 {
2814 	M_IterateMenuTree(MIT_SetCurTitlePics, NULL);
2815 }
2816 
2817 // ====================================
2818 // MENU STATE
2819 // ====================================
2820 
2821 static INT32 exitlevel, enterlevel, anceslevel;
2822 static INT16 exittype, entertype;
2823 static INT16 exitwipe, enterwipe;
2824 static boolean exitbubble, enterbubble;
2825 static INT16 exittag, entertag;
2826 
M_HandleMenuPresState(menu_t * newMenu)2827 static void M_HandleMenuPresState(menu_t *newMenu)
2828 {
2829 	INT32 i;
2830 	UINT32 bitmask;
2831 	SINT8 prevtype, activetype, menutype;
2832 
2833 	if (!newMenu)
2834 		return;
2835 
2836 	// Look for MN_SPECIAL here, because our iterators can't look at new menu IDs
2837 	for (i = 0; i <= NUMMENULEVELS; i++)
2838 	{
2839 		bitmask = ((1 << MENUBITS) - 1) << (MENUBITS*i);
2840 		menutype = (newMenu->menuid & bitmask) >> (MENUBITS*i);
2841 		prevtype = (currentMenu->menuid & bitmask) >> (MENUBITS*i);
2842 		if (menutype == MN_SPECIAL || prevtype == MN_SPECIAL)
2843 			return;
2844 	}
2845 
2846 	if (currentMenu && newMenu && currentMenu->menuid == newMenu->menuid) // same menu?
2847 		return;
2848 
2849 	exittype = entertype = exitlevel = enterlevel = anceslevel = exitwipe = enterwipe = -1;
2850 	exitbubble = enterbubble = true;
2851 
2852 	prevMenuId = currentMenu ? currentMenu->menuid : 0;
2853 	activeMenuId = newMenu ? newMenu->menuid : 0;
2854 
2855 	// Set defaults for presentation values
2856 	strncpy(curbgname, "TITLESKY", 9);
2857 	curfadevalue = 16;
2858 	curhidepics = hidetitlepics;
2859 	curbgcolor = -1;
2860 	curbgxspeed = titlescrollxspeed;
2861 	curbgyspeed = titlescrollyspeed;
2862 	curbghide = (gamestate != GS_TIMEATTACK); // show in time attack, hide in other menus
2863 
2864 	curttmode = ttmode;
2865 	curttscale = ttscale;
2866 	strncpy(curttname, ttname, 9);
2867 	curttx = ttx;
2868 	curtty = tty;
2869 	curttloop = ttloop;
2870 	curtttics = tttics;
2871 
2872 	// don't do the below during the in-game menus
2873 	if (gamestate != GS_TITLESCREEN && gamestate != GS_TIMEATTACK)
2874 		return;
2875 
2876 	M_SetMenuCurFadeValue(16);
2877 	M_SetMenuCurTitlePics();
2878 
2879 	// Loop through both menu IDs in parallel and look for type changes
2880 	// The youngest child in activeMenuId is the entered menu
2881 	// The youngest child in prevMenuId is the exited menu
2882 
2883 	// 0. Get the type and level of each menu, and level of common ancestor
2884 	// 1. Get the wipes for both, then run the exit wipe
2885 	// 2. Change music (so that execs can change it again later)
2886 	// 3. Run each exit exec on the prevMenuId up to the common ancestor (UNLESS NoBubbleExecs)
2887 	// 4. Run each entrance exec on the activeMenuId down from the common ancestor (UNLESS NoBubbleExecs)
2888 	// 5. Run the entrance wipe
2889 
2890 	// Get the parameters for each menu
2891 	for (i = NUMMENULEVELS; i >= 0; i--)
2892 	{
2893 		bitmask = ((1 << MENUBITS) - 1) << (MENUBITS*i);
2894 		prevtype = (prevMenuId & bitmask) >> (MENUBITS*i);
2895 		activetype = (activeMenuId & bitmask) >> (MENUBITS*i);
2896 
2897 		if (prevtype && (exittype < 0))
2898 		{
2899 			exittype = prevtype;
2900 			exitlevel = i;
2901 			exitwipe = menupres[exittype].exitwipe;
2902 			exitbubble = menupres[exittype].exitbubble;
2903 			exittag = menupres[exittype].exittag;
2904 		}
2905 
2906 		if (activetype && (entertype < 0))
2907 		{
2908 			entertype = activetype;
2909 			enterlevel = i;
2910 			enterwipe = menupres[entertype].enterwipe;
2911 			enterbubble = menupres[entertype].enterbubble;
2912 			entertag = menupres[entertype].entertag;
2913 		}
2914 
2915 		if (prevtype && activetype && prevtype == activetype && anceslevel < 0)
2916 		{
2917 			anceslevel = i;
2918 			break;
2919 		}
2920 	}
2921 
2922 	// if no common ancestor (top menu), force a wipe. Look for a specified wipe first.
2923 	// Don't force a wipe if you're actually going to/from the main menu
2924 	if (anceslevel < 0 && exitwipe < 0 && newMenu != &MainDef && currentMenu != &MainDef)
2925 	{
2926 		for (i = NUMMENULEVELS; i >= 0; i--)
2927 		{
2928 			bitmask = ((1 << MENUBITS) - 1) << (MENUBITS*i);
2929 			prevtype = (prevMenuId & bitmask) >> (MENUBITS*i);
2930 
2931 			if (menupres[prevtype].exitwipe >= 0)
2932 			{
2933 				exitwipe = menupres[prevtype].exitwipe;
2934 				break;
2935 			}
2936 		}
2937 
2938 		if (exitwipe < 0)
2939 			exitwipe = menupres[MN_MAIN].exitwipe;
2940 	}
2941 
2942 	// do the same for enter wipe
2943 	if (anceslevel < 0 && enterwipe < 0 && newMenu != &MainDef && currentMenu != &MainDef)
2944 	{
2945 		for (i = NUMMENULEVELS; i >= 0; i--)
2946 		{
2947 			bitmask = ((1 << MENUBITS) - 1) << (MENUBITS*i);
2948 			activetype = (activeMenuId & bitmask) >> (MENUBITS*i);
2949 
2950 			if (menupres[activetype].enterwipe >= 0)
2951 			{
2952 				exitwipe = menupres[activetype].enterwipe;
2953 				break;
2954 			}
2955 		}
2956 
2957 		if (enterwipe < 0)
2958 			enterwipe = menupres[MN_MAIN].enterwipe;
2959 	}
2960 
2961 	// Change the music
2962 	M_ChangeMenuMusic("_title", false);
2963 
2964 	// Run the linedef execs
2965 	if (titlemapinaction)
2966 	{
2967 		// Run the exit tags
2968 		if (enterlevel <= exitlevel) // equals is an edge case
2969 		{
2970 			if (exitbubble)
2971 			{
2972 				for (i = exitlevel; i > anceslevel; i--) // don't run the common ancestor's exit tag
2973 				{
2974 					bitmask = ((1 << MENUBITS) - 1) << (MENUBITS*i);
2975 					menutype = (prevMenuId & bitmask) >> (MENUBITS*i);
2976 					if (menupres[menutype].exittag)
2977 						P_LinedefExecute(menupres[menutype].exittag, players[displayplayer].mo, NULL);
2978 				}
2979 			}
2980 			else if (exittag)
2981 				P_LinedefExecute(exittag, players[displayplayer].mo, NULL);
2982 		}
2983 
2984 		// Run the enter tags
2985 		if (enterlevel >= exitlevel) // equals is an edge case
2986 		{
2987 			if (enterbubble)
2988 			{
2989 				for (i = anceslevel+1; i <= enterlevel; i++) // don't run the common ancestor's enter tag
2990 				{
2991 					bitmask = ((1 << MENUBITS) - 1) << (MENUBITS*i);
2992 					menutype = (activeMenuId & bitmask) >> (MENUBITS*i);
2993 					if (menupres[menutype].entertag)
2994 						P_LinedefExecute(menupres[menutype].entertag, players[displayplayer].mo, NULL);
2995 				}
2996 			}
2997 			else if (entertag)
2998 				P_LinedefExecute(entertag, players[displayplayer].mo, NULL);
2999 		}
3000 	}
3001 
3002 
3003 	// Set the wipes for next frame
3004 	if (
3005 		(exitwipe >= 0 && enterlevel <= exitlevel) ||
3006 		(enterwipe >= 0 && enterlevel >= exitlevel) ||
3007 		(anceslevel < 0 && newMenu != &MainDef && currentMenu != &MainDef)
3008 	)
3009 	{
3010 		if (gamestate == GS_TIMEATTACK)
3011 			wipetypepre = ((exitwipe && enterlevel <= exitlevel) || anceslevel < 0) ? exitwipe : -1; // force default
3012 		else
3013 			// HACK: INT16_MAX signals to not wipe
3014 			// because 0 is a valid index and -1 means default
3015 			wipetypepre = ((exitwipe && enterlevel <= exitlevel) || anceslevel < 0) ? exitwipe : INT16_MAX;
3016 		wipetypepost = ((enterwipe && enterlevel >= exitlevel) || anceslevel < 0) ? enterwipe : INT16_MAX;
3017 		wipegamestate = FORCEWIPE;
3018 
3019 		// If just one of the above is a force not-wipe,
3020 		// mirror the other wipe.
3021 		if (wipetypepre != INT16_MAX && wipetypepost == INT16_MAX)
3022 			wipetypepost = wipetypepre;
3023 		else if (wipetypepost != INT16_MAX && wipetypepre == INT16_MAX)
3024 			wipetypepre = wipetypepost;
3025 
3026 		// D_Display runs the next step of processing
3027 	}
3028 }
3029 
3030 // =========================================================================
3031 // BASIC MENU HANDLING
3032 // =========================================================================
3033 
M_GoBack(INT32 choice)3034 static void M_GoBack(INT32 choice)
3035 {
3036 	(void)choice;
3037 
3038 	if (currentMenu->prevMenu)
3039 	{
3040 		//If we entered the game search menu, but didn't enter a game,
3041 		//make sure the game doesn't still think we're in a netgame.
3042 		if (!Playing() && netgame && multiplayer)
3043 		{
3044 			netgame = multiplayer = false;
3045 		}
3046 
3047 		if ((currentMenu->prevMenu == &MainDef) && (currentMenu == &SP_TimeAttackDef || currentMenu == &SP_NightsAttackDef || currentMenu == &SP_MarathonDef))
3048 		{
3049 			// D_StartTitle does its own wipe, since GS_TIMEATTACK is now a complete gamestate.
3050 
3051 			if (levelselect.rows)
3052 			{
3053 				Z_Free(levelselect.rows);
3054 				levelselect.rows = NULL;
3055 			}
3056 
3057 			menuactive = false;
3058 			wipetypepre = menupres[M_GetYoungestChildMenu()].exitwipe;
3059 			I_UpdateMouseGrab();
3060 			D_StartTitle();
3061 		}
3062 		else
3063 			M_SetupNextMenu(currentMenu->prevMenu);
3064 	}
3065 	else
3066 		M_ClearMenus(true);
3067 }
3068 
M_ChangeCvar(INT32 choice)3069 static void M_ChangeCvar(INT32 choice)
3070 {
3071 	consvar_t *cv = (consvar_t *)currentMenu->menuitems[itemOn].itemaction;
3072 
3073 	if (choice == -1)
3074 	{
3075 		if (cv == &cv_playercolor)
3076 		{
3077 			SINT8 skinno = R_SkinAvailable(cv_chooseskin.string);
3078 			if (skinno != -1)
3079 				CV_SetValue(cv,skins[skinno].prefcolor);
3080 			return;
3081 		}
3082 		CV_Set(cv,cv->defaultvalue);
3083 		return;
3084 	}
3085 
3086 	choice = (choice<<1) - 1;
3087 
3088 	if (cv->flags & CV_FLOAT)
3089 	{
3090 		if (((currentMenu->menuitems[itemOn].status & IT_CVARTYPE) == IT_CV_SLIDER)
3091 			||((currentMenu->menuitems[itemOn].status & IT_CVARTYPE) == IT_CV_INVISSLIDER)
3092 			||((currentMenu->menuitems[itemOn].status & IT_CVARTYPE) == IT_CV_NOMOD)
3093 			|| !(currentMenu->menuitems[itemOn].status & IT_CV_INTEGERSTEP))
3094 		{
3095 			char s[20];
3096 			float n = FIXED_TO_FLOAT(cv->value)+(choice)*(1.0f/16.0f);
3097 			sprintf(s,"%ld%s",(long)n,M_Ftrim(n));
3098 			CV_Set(cv,s);
3099 		}
3100 		else
3101 			CV_SetValue(cv,FIXED_TO_FLOAT(cv->value)+(choice));
3102 	}
3103 	else
3104 		CV_AddValue(cv,choice);
3105 }
3106 
M_ChangeStringCvar(INT32 choice)3107 static boolean M_ChangeStringCvar(INT32 choice)
3108 {
3109 	consvar_t *cv = (consvar_t *)currentMenu->menuitems[itemOn].itemaction;
3110 	char buf[MAXSTRINGLENGTH];
3111 	size_t len;
3112 
3113 	if (shiftdown && choice >= 32 && choice <= 127)
3114 		choice = shiftxform[choice];
3115 
3116 	switch (choice)
3117 	{
3118 		case KEY_BACKSPACE:
3119 			len = strlen(cv->string);
3120 			if (len > 0)
3121 			{
3122 				M_Memcpy(buf, cv->string, len);
3123 				buf[len-1] = 0;
3124 				CV_Set(cv, buf);
3125 			}
3126 			return true;
3127 		default:
3128 			if (choice >= 32 && choice <= 127)
3129 			{
3130 				len = strlen(cv->string);
3131 				if (len < MAXSTRINGLENGTH - 1)
3132 				{
3133 					M_Memcpy(buf, cv->string, len);
3134 					buf[len++] = (char)choice;
3135 					buf[len] = 0;
3136 					CV_Set(cv, buf);
3137 				}
3138 				return true;
3139 			}
3140 			break;
3141 	}
3142 	return false;
3143 }
3144 
3145 // resets all cvars on a menu - assumes that all that have itemactions are cvars
M_ResetCvars(void)3146 static void M_ResetCvars(void)
3147 {
3148 	INT32 i;
3149 	consvar_t *cv;
3150 	for (i = 0; i < currentMenu->numitems; i++)
3151 	{
3152 		if (!(currentMenu->menuitems[i].status & IT_CVAR) || !(cv = (consvar_t *)currentMenu->menuitems[i].itemaction))
3153 			continue;
3154 		CV_SetValue(cv, atoi(cv->defaultvalue));
3155 	}
3156 }
3157 
M_NextOpt(void)3158 static void M_NextOpt(void)
3159 {
3160 	INT16 oldItemOn = itemOn; // prevent infinite loop
3161 	do
3162 	{
3163 		if (itemOn + 1 > currentMenu->numitems - 1)
3164 			itemOn = 0;
3165 		else
3166 			itemOn++;
3167 	} while (oldItemOn != itemOn && ( (currentMenu->menuitems[itemOn].status & IT_TYPE) & IT_SPACE ));
3168 }
3169 
M_PrevOpt(void)3170 static void M_PrevOpt(void)
3171 {
3172 	INT16 oldItemOn = itemOn; // prevent infinite loop
3173 	do
3174 	{
3175 		if (!itemOn)
3176 			itemOn = currentMenu->numitems - 1;
3177 		else
3178 			itemOn--;
3179 	} while (oldItemOn != itemOn && ( (currentMenu->menuitems[itemOn].status & IT_TYPE) & IT_SPACE ));
3180 }
3181 
3182 // lock out further input in a tic when important buttons are pressed
3183 // (in other words -- stop bullshit happening by mashing buttons in fades)
3184 static boolean noFurtherInput = false;
3185 
Command_Manual_f(void)3186 static void Command_Manual_f(void)
3187 {
3188 	if (modeattacking)
3189 		return;
3190 	M_StartControlPanel();
3191 	currentMenu = &MISC_HelpDef;
3192 	itemOn = 0;
3193 }
3194 
3195 //
3196 // M_Responder
3197 //
M_Responder(event_t * ev)3198 boolean M_Responder(event_t *ev)
3199 {
3200 	INT32 ch = -1;
3201 //	INT32 i;
3202 	static tic_t joywait = 0, mousewait = 0;
3203 	static INT32 pjoyx = 0, pjoyy = 0;
3204 	static INT32 pmousex = 0, pmousey = 0;
3205 	static INT32 lastx = 0, lasty = 0;
3206 	void (*routine)(INT32 choice); // for some casting problem
3207 
3208 	if (dedicated || (demoplayback && titledemo)
3209 	|| gamestate == GS_INTRO || gamestate == GS_ENDING || gamestate == GS_CUTSCENE
3210 	|| gamestate == GS_CREDITS || gamestate == GS_EVALUATION || gamestate == GS_GAMEEND)
3211 		return false;
3212 
3213 	if (gamestate == GS_TITLESCREEN && finalecount < TICRATE)
3214 		return false;
3215 
3216 	if (CON_Ready())
3217 		return false;
3218 
3219 	if (noFurtherInput)
3220 	{
3221 		// Ignore input after enter/escape/other buttons
3222 		// (but still allow shift keyup so caps doesn't get stuck)
3223 		return false;
3224 	}
3225 	else if (menuactive)
3226 	{
3227 		if (ev->type == ev_keydown)
3228 		{
3229 			keydown++;
3230 			ch = ev->data1;
3231 
3232 			// added 5-2-98 remap virtual keys (mouse & joystick buttons)
3233 			switch (ch)
3234 			{
3235 				case KEY_MOUSE1:
3236 				case KEY_JOY1:
3237 					ch = KEY_ENTER;
3238 					break;
3239 				case KEY_JOY1 + 3:
3240 					ch = 'n';
3241 					break;
3242 				case KEY_MOUSE1 + 1:
3243 				case KEY_JOY1 + 1:
3244 					ch = KEY_ESCAPE;
3245 					break;
3246 				case KEY_JOY1 + 2:
3247 					ch = KEY_BACKSPACE;
3248 					break;
3249 				case KEY_HAT1:
3250 					ch = KEY_UPARROW;
3251 					break;
3252 				case KEY_HAT1 + 1:
3253 					ch = KEY_DOWNARROW;
3254 					break;
3255 				case KEY_HAT1 + 2:
3256 					ch = KEY_LEFTARROW;
3257 					break;
3258 				case KEY_HAT1 + 3:
3259 					ch = KEY_RIGHTARROW;
3260 					break;
3261 			}
3262 		}
3263 		else if (ev->type == ev_joystick  && ev->data1 == 0 && joywait < I_GetTime())
3264 		{
3265 			const INT32 jdeadzone = (JOYAXISRANGE * cv_digitaldeadzone.value) / FRACUNIT;
3266 			if (ev->data3 != INT32_MAX)
3267 			{
3268 				if (Joystick.bGamepadStyle || abs(ev->data3) > jdeadzone)
3269 				{
3270 					if (ev->data3 < 0 && pjoyy >= 0)
3271 					{
3272 						ch = KEY_UPARROW;
3273 						joywait = I_GetTime() + NEWTICRATE/7;
3274 					}
3275 					else if (ev->data3 > 0 && pjoyy <= 0)
3276 					{
3277 						ch = KEY_DOWNARROW;
3278 						joywait = I_GetTime() + NEWTICRATE/7;
3279 					}
3280 					pjoyy = ev->data3;
3281 				}
3282 				else
3283 					pjoyy = 0;
3284 			}
3285 
3286 			if (ev->data2 != INT32_MAX)
3287 			{
3288 				if (Joystick.bGamepadStyle || abs(ev->data2) > jdeadzone)
3289 				{
3290 					if (ev->data2 < 0 && pjoyx >= 0)
3291 					{
3292 						ch = KEY_LEFTARROW;
3293 						joywait = I_GetTime() + NEWTICRATE/17;
3294 					}
3295 					else if (ev->data2 > 0 && pjoyx <= 0)
3296 					{
3297 						ch = KEY_RIGHTARROW;
3298 						joywait = I_GetTime() + NEWTICRATE/17;
3299 					}
3300 					pjoyx = ev->data2;
3301 				}
3302 				else
3303 					pjoyx = 0;
3304 			}
3305 		}
3306 		else if (ev->type == ev_mouse && mousewait < I_GetTime())
3307 		{
3308 			pmousey += ev->data3;
3309 			if (pmousey < lasty-30)
3310 			{
3311 				ch = KEY_DOWNARROW;
3312 				mousewait = I_GetTime() + NEWTICRATE/7;
3313 				pmousey = lasty -= 30;
3314 			}
3315 			else if (pmousey > lasty + 30)
3316 			{
3317 				ch = KEY_UPARROW;
3318 				mousewait = I_GetTime() + NEWTICRATE/7;
3319 				pmousey = lasty += 30;
3320 			}
3321 
3322 			pmousex += ev->data2;
3323 			if (pmousex < lastx - 30)
3324 			{
3325 				ch = KEY_LEFTARROW;
3326 				mousewait = I_GetTime() + NEWTICRATE/7;
3327 				pmousex = lastx -= 30;
3328 			}
3329 			else if (pmousex > lastx+30)
3330 			{
3331 				ch = KEY_RIGHTARROW;
3332 				mousewait = I_GetTime() + NEWTICRATE/7;
3333 				pmousex = lastx += 30;
3334 			}
3335 		}
3336 		else if (ev->type == ev_keyup) // Preserve event for other responders
3337 			keydown = 0;
3338 	}
3339 	else if (ev->type == ev_keydown) // Preserve event for other responders
3340 		ch = ev->data1;
3341 
3342 	if (ch == -1)
3343 		return false;
3344 	else if (ch == gamecontrol[gc_systemmenu][0] || ch == gamecontrol[gc_systemmenu][1]) // allow remappable ESC key
3345 		ch = KEY_ESCAPE;
3346 
3347 	// F-Keys
3348 	if (!menuactive)
3349 	{
3350 		noFurtherInput = true;
3351 		switch (ch)
3352 		{
3353 			case KEY_F1: // Help key
3354 				Command_Manual_f();
3355 				return true;
3356 
3357 			case KEY_F2: // Empty
3358 				return true;
3359 
3360 			case KEY_F3: // Toggle HUD
3361 				CV_SetValue(&cv_showhud, !cv_showhud.value);
3362 				return true;
3363 
3364 			case KEY_F4: // Sound Volume
3365 				if (modeattacking)
3366 					return true;
3367 				M_StartControlPanel();
3368 				M_Options(0);
3369 				// Uncomment the below if you want the menu to reset to the top each time like before. M_SetupNextMenu will fix it automatically.
3370 				//OP_SoundOptionsDef.lastOn = 0;
3371 				M_SetupNextMenu(&OP_SoundOptionsDef);
3372 				return true;
3373 
3374 			case KEY_F5: // Video Mode
3375 				if (modeattacking)
3376 					return true;
3377 				M_StartControlPanel();
3378 				M_Options(0);
3379 				M_VideoModeMenu(0);
3380 				return true;
3381 
3382 			case KEY_F6: // Empty
3383 				return true;
3384 
3385 			case KEY_F7: // Options
3386 				if (modeattacking)
3387 					return true;
3388 				M_StartControlPanel();
3389 				M_Options(0);
3390 				M_SetupNextMenu(&OP_MainDef);
3391 				return true;
3392 
3393 			// Screenshots on F8 now handled elsewhere
3394 			// Same with Moviemode on F9
3395 
3396 			case KEY_F10: // Quit SRB2
3397 				M_QuitSRB2(0);
3398 				return true;
3399 
3400 			case KEY_F11: // Gamma Level
3401 				CV_AddValue(&cv_globalgamma, 1);
3402 				return true;
3403 
3404 			// Spymode on F12 handled in game logic
3405 
3406 			case KEY_ESCAPE: // Pop up menu
3407 				if (chat_on)
3408 					HU_clearChatChars();
3409 				else
3410 					M_StartControlPanel();
3411 				return true;
3412 		}
3413 		noFurtherInput = false; // turns out we didn't care
3414 		return false;
3415 	}
3416 
3417 	routine = currentMenu->menuitems[itemOn].itemaction;
3418 
3419 	// Handle menuitems which need a specific key handling
3420 	if (routine && (currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_KEYHANDLER)
3421 	{
3422 		if (shiftdown && ch >= 32 && ch <= 127)
3423 			ch = shiftxform[ch];
3424 		routine(ch);
3425 		return true;
3426 	}
3427 
3428 	if (currentMenu->menuitems[itemOn].status == IT_MSGHANDLER)
3429 	{
3430 		if (currentMenu->menuitems[itemOn].alphaKey != MM_EVENTHANDLER)
3431 		{
3432 			if (ch == ' ' || ch == 'n' || ch == 'y' || ch == KEY_ESCAPE || ch == KEY_ENTER || ch == KEY_DEL)
3433 			{
3434 				if (routine)
3435 					routine(ch);
3436 				if (stopstopmessage)
3437 					stopstopmessage = false;
3438 				else
3439 					M_StopMessage(0);
3440 				noFurtherInput = true;
3441 				return true;
3442 			}
3443 			return true;
3444 		}
3445 		else
3446 		{
3447 			// dirty hack: for customising controls, I want only buttons/keys, not moves
3448 			if (ev->type == ev_mouse || ev->type == ev_mouse2 || ev->type == ev_joystick
3449 				|| ev->type == ev_joystick2)
3450 				return true;
3451 			if (routine)
3452 			{
3453 				void (*otherroutine)(event_t *sev) = currentMenu->menuitems[itemOn].itemaction;
3454 				otherroutine(ev); //Alam: what a hack
3455 			}
3456 			return true;
3457 		}
3458 	}
3459 
3460 	// BP: one of the more big hack i have never made
3461 	if (routine && (currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_CVAR)
3462 	{
3463 		if ((currentMenu->menuitems[itemOn].status & IT_CVARTYPE) == IT_CV_STRING)
3464 		{
3465 			if (M_ChangeStringCvar(ch))
3466 				return true;
3467 			else
3468 				routine = NULL;
3469 		}
3470 		else
3471 			routine = M_ChangeCvar;
3472 	}
3473 
3474 	// Keys usable within menu
3475 	switch (ch)
3476 	{
3477 		case KEY_DOWNARROW:
3478 			M_NextOpt();
3479 			S_StartSound(NULL, sfx_menu1);
3480 			return true;
3481 
3482 		case KEY_UPARROW:
3483 			M_PrevOpt();
3484 			S_StartSound(NULL, sfx_menu1);
3485 			return true;
3486 
3487 		case KEY_LEFTARROW:
3488 			if (routine && ((currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_ARROWS
3489 				|| (currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_CVAR))
3490 			{
3491 				S_StartSound(NULL, sfx_menu1);
3492 				routine(0);
3493 			}
3494 			return true;
3495 
3496 		case KEY_RIGHTARROW:
3497 			if (routine && ((currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_ARROWS
3498 				|| (currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_CVAR))
3499 			{
3500 				S_StartSound(NULL, sfx_menu1);
3501 				routine(1);
3502 			}
3503 			return true;
3504 
3505 		case KEY_ENTER:
3506 			noFurtherInput = true;
3507 			currentMenu->lastOn = itemOn;
3508 			if (routine)
3509 			{
3510 				if (((currentMenu->menuitems[itemOn].status & IT_TYPE)==IT_CALL
3511 				 || (currentMenu->menuitems[itemOn].status & IT_TYPE)==IT_SUBMENU)
3512                  && (currentMenu->menuitems[itemOn].status & IT_CALLTYPE))
3513 				{
3514 #ifndef DEVELOP
3515 					if (((currentMenu->menuitems[itemOn].status & IT_CALLTYPE) & IT_CALL_NOTMODIFIED) && modifiedgame && !savemoddata)
3516 					{
3517 						S_StartSound(NULL, sfx_skid);
3518 						M_StartMessage(M_GetText("This cannot be done in a modified game.\n\n(Press a key)\n"), NULL, MM_NOTHING);
3519 						return true;
3520 					}
3521 #endif
3522 				}
3523 				S_StartSound(NULL, sfx_menu1);
3524 				switch (currentMenu->menuitems[itemOn].status & IT_TYPE)
3525 				{
3526 					case IT_CVAR:
3527 					case IT_ARROWS:
3528 						routine(1); // right arrow
3529 						break;
3530 					case IT_CALL:
3531 						routine(itemOn);
3532 						break;
3533 					case IT_SUBMENU:
3534 						currentMenu->lastOn = itemOn;
3535 						M_SetupNextMenu((menu_t *)currentMenu->menuitems[itemOn].itemaction);
3536 						break;
3537 				}
3538 			}
3539 			return true;
3540 
3541 		case KEY_ESCAPE:
3542 			noFurtherInput = true;
3543 			currentMenu->lastOn = itemOn;
3544 
3545 			M_GoBack(0);
3546 
3547 			return true;
3548 
3549 		case KEY_BACKSPACE:
3550 			if ((currentMenu->menuitems[itemOn].status) == IT_CONTROL)
3551 			{
3552 				// detach any keys associated with the game control
3553 				G_ClearControlKeys(setupcontrols, currentMenu->menuitems[itemOn].alphaKey);
3554 				S_StartSound(NULL, sfx_shldls);
3555 				return true;
3556 			}
3557 
3558 			if (routine && ((currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_ARROWS
3559 				|| (currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_CVAR))
3560 			{
3561 				consvar_t *cv = (consvar_t *)currentMenu->menuitems[itemOn].itemaction;
3562 
3563 				if (cv == &cv_chooseskin
3564 					|| cv == &cv_nextmap
3565 					|| cv == &cv_newgametype)
3566 					return true;
3567 
3568 				if (currentMenu != &OP_SoundOptionsDef || itemOn > 3)
3569 					S_StartSound(NULL, sfx_menu1);
3570 				routine(-1);
3571 				return true;
3572 			}
3573 
3574 			// Why _does_ backspace go back anyway?
3575 			//currentMenu->lastOn = itemOn;
3576 			//if (currentMenu->prevMenu)
3577 			//	M_SetupNextMenu(currentMenu->prevMenu);
3578 			return false;
3579 
3580 		default:
3581 			CON_Responder(ev);
3582 			break;
3583 	}
3584 
3585 	return true;
3586 }
3587 
3588 //
3589 // M_Drawer
3590 // Called after the view has been rendered,
3591 // but before it has been blitted.
3592 //
M_Drawer(void)3593 void M_Drawer(void)
3594 {
3595 	boolean wipe = WipeInAction;
3596 
3597 	if (currentMenu == &MessageDef)
3598 		menuactive = true;
3599 
3600 	if (menuactive)
3601 	{
3602 		// now that's more readable with a faded background (yeah like Quake...)
3603 		if (!wipe && (curfadevalue || (gamestate != GS_TITLESCREEN && gamestate != GS_TIMEATTACK)))
3604 			V_DrawFadeScreen(0xFF00, (gamestate != GS_TITLESCREEN && gamestate != GS_TIMEATTACK) ? 16 : curfadevalue);
3605 
3606 		if (currentMenu->drawroutine)
3607 			currentMenu->drawroutine(); // call current menu Draw routine
3608 
3609 		// Draw version down in corner
3610 		// ... but only in the MAIN MENU.  I'm a picky bastard.
3611 		if (currentMenu == &MainDef)
3612 		{
3613 			if (customversionstring[0] != '\0')
3614 			{
3615 				V_DrawThinString(vid.dupx, vid.height - 17*vid.dupy, V_NOSCALESTART|V_TRANSLUCENT, "Mod version:");
3616 				V_DrawThinString(vid.dupx, vid.height - 9*vid.dupy, V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, customversionstring);
3617 			}
3618 			else
3619 			{
3620 #ifdef DEVELOP // Development -- show revision / branch info
3621 				V_DrawThinString(vid.dupx, vid.height - 17*vid.dupy, V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, compbranch);
3622 				V_DrawThinString(vid.dupx, vid.height - 9*vid.dupy,  V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, comprevision);
3623 #else // Regular build
3624 				V_DrawThinString(vid.dupx, vid.height - 9*vid.dupy, V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, va("%s", VERSIONSTRING));
3625 #endif
3626 			}
3627 		}
3628 	}
3629 
3630 	// focus lost notification goes on top of everything, even the former everything
3631 	if (window_notinfocus && cv_showfocuslost.value)
3632 	{
3633 		M_DrawTextBox((BASEVIDWIDTH/2) - (60), (BASEVIDHEIGHT/2) - (16), 13, 2);
3634 		if (gamestate == GS_LEVEL && (P_AutoPause() || paused))
3635 			V_DrawCenteredString(BASEVIDWIDTH/2, (BASEVIDHEIGHT/2) - (4), V_YELLOWMAP, "Game Paused");
3636 		else
3637 			V_DrawCenteredString(BASEVIDWIDTH/2, (BASEVIDHEIGHT/2) - (4), V_YELLOWMAP, "Focus Lost");
3638 	}
3639 }
3640 
3641 //
3642 // M_StartControlPanel
3643 //
M_StartControlPanel(void)3644 void M_StartControlPanel(void)
3645 {
3646 	// time attack HACK
3647 	if (modeattacking && demoplayback)
3648 	{
3649 		G_CheckDemoStatus();
3650 		return;
3651 	}
3652 
3653 	// intro might call this repeatedly
3654 	if (menuactive)
3655 	{
3656 		CON_ToggleOff(); // move away console
3657 		return;
3658 	}
3659 
3660 	menuactive = true;
3661 
3662 	if (!Playing())
3663 	{
3664 		// Secret menu!
3665 		MainMenu[singleplr].alphaKey = (M_AnySecretUnlocked()) ? 76 : 84;
3666 		MainMenu[multiplr].alphaKey = (M_AnySecretUnlocked()) ? 84 : 92;
3667 		MainMenu[secrets].status = (M_AnySecretUnlocked()) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
3668 
3669 		currentMenu = &MainDef;
3670 		itemOn = singleplr;
3671 	}
3672 	else if (modeattacking)
3673 	{
3674 		currentMenu = &MAPauseDef;
3675 		MAPauseMenu[mapause_hints].status = (M_SecretUnlocked(SECRET_EMBLEMHINTS)) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
3676 		itemOn = mapause_continue;
3677 	}
3678 	else if (!(netgame || multiplayer)) // Single Player
3679 	{
3680 		if (gamestate != GS_LEVEL || ultimatemode) // intermission, so gray out stuff.
3681 		{
3682 			SPauseMenu[spause_pandora].status = (M_SecretUnlocked(SECRET_PANDORA)) ? (IT_GRAYEDOUT) : (IT_DISABLED);
3683 			SPauseMenu[spause_retry].status = IT_GRAYEDOUT;
3684 		}
3685 		else
3686 		{
3687 			INT32 numlives = 2;
3688 
3689 			SPauseMenu[spause_pandora].status = (M_SecretUnlocked(SECRET_PANDORA) && !marathonmode) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
3690 
3691 			if (&players[consoleplayer])
3692 			{
3693 				numlives = players[consoleplayer].lives;
3694 				if (players[consoleplayer].playerstate != PST_LIVE)
3695 					++numlives;
3696 			}
3697 
3698 			// The list of things that can disable retrying is (was?) a little too complex
3699 			// for me to want to use the short if statement syntax
3700 			if (numlives <= 1 || G_IsSpecialStage(gamemap))
3701 				SPauseMenu[spause_retry].status = (IT_GRAYEDOUT);
3702 			else
3703 				SPauseMenu[spause_retry].status = (IT_STRING | IT_CALL);
3704 		}
3705 
3706 		// We can always use level select though. :33
3707 		SPauseMenu[spause_levelselect].status = (gamecomplete == 1) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
3708 
3709 		// And emblem hints.
3710 		SPauseMenu[spause_hints].status = (M_SecretUnlocked(SECRET_EMBLEMHINTS) && !marathonmode) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
3711 
3712 		// Shift up Pandora's Box if both pandora and levelselect are active
3713 		/*if (SPauseMenu[spause_pandora].status != (IT_DISABLED)
3714 		 && SPauseMenu[spause_levelselect].status != (IT_DISABLED))
3715 			SPauseMenu[spause_pandora].alphaKey = 24;
3716 		else
3717 			SPauseMenu[spause_pandora].alphaKey = 32;*/
3718 
3719 		currentMenu = &SPauseDef;
3720 		itemOn = spause_continue;
3721 	}
3722 	else // multiplayer
3723 	{
3724 		MPauseMenu[mpause_switchmap].status = IT_DISABLED;
3725 		MPauseMenu[mpause_addons].status = IT_DISABLED;
3726 		MPauseMenu[mpause_scramble].status = IT_DISABLED;
3727 		MPauseMenu[mpause_psetupsplit].status = IT_DISABLED;
3728 		MPauseMenu[mpause_psetupsplit2].status = IT_DISABLED;
3729 		MPauseMenu[mpause_spectate].status = IT_DISABLED;
3730 		MPauseMenu[mpause_entergame].status = IT_DISABLED;
3731 		MPauseMenu[mpause_switchteam].status = IT_DISABLED;
3732 		MPauseMenu[mpause_psetup].status = IT_DISABLED;
3733 
3734 		if ((server || IsPlayerAdmin(consoleplayer)))
3735 		{
3736 			MPauseMenu[mpause_switchmap].status = IT_STRING | IT_CALL;
3737 			MPauseMenu[mpause_addons].status = IT_STRING | IT_CALL;
3738 			if (G_GametypeHasTeams())
3739 				MPauseMenu[mpause_scramble].status = IT_STRING | IT_SUBMENU;
3740 		}
3741 
3742 		if (splitscreen)
3743 		{
3744 			MPauseMenu[mpause_psetupsplit].status = MPauseMenu[mpause_psetupsplit2].status = IT_STRING | IT_CALL;
3745 			MPauseMenu[mpause_psetup].text = "Player 1 Setup";
3746 		}
3747 		else
3748 		{
3749 			MPauseMenu[mpause_psetup].status = IT_STRING | IT_CALL;
3750 			MPauseMenu[mpause_psetup].text = "Player Setup";
3751 
3752 			if (G_GametypeHasTeams())
3753 				MPauseMenu[mpause_switchteam].status = IT_STRING | IT_SUBMENU;
3754 			else if (G_GametypeHasSpectators())
3755 				MPauseMenu[((&players[consoleplayer] && players[consoleplayer].spectator) ? mpause_entergame : mpause_spectate)].status = IT_STRING | IT_CALL;
3756 			else // in this odd case, we still want something to be on the menu even if it's useless
3757 				MPauseMenu[mpause_spectate].status = IT_GRAYEDOUT;
3758 		}
3759 
3760 		currentMenu = &MPauseDef;
3761 		itemOn = mpause_continue;
3762 	}
3763 
3764 	CON_ToggleOff(); // move away console
3765 }
3766 
M_EndModeAttackRun(void)3767 void M_EndModeAttackRun(void)
3768 {
3769 	G_ClearModeAttackRetryFlag();
3770 	M_ModeAttackEndGame(0);
3771 }
3772 
3773 //
3774 // M_ClearMenus
3775 //
M_ClearMenus(boolean callexitmenufunc)3776 void M_ClearMenus(boolean callexitmenufunc)
3777 {
3778 	if (!menuactive)
3779 		return;
3780 
3781 	if (currentMenu->quitroutine && callexitmenufunc && !currentMenu->quitroutine())
3782 		return; // we can't quit this menu (also used to set parameter from the menu)
3783 
3784 	// Save the config file. I'm sick of crashing the game later and losing all my changes!
3785 	COM_BufAddText(va("saveconfig \"%s\" -silent\n", configfile));
3786 
3787 	if (currentMenu == &MessageDef) // Oh sod off!
3788 		currentMenu = &MainDef; // Not like it matters
3789 	menuactive = false;
3790 	hidetitlemap = false;
3791 
3792 	I_UpdateMouseGrab();
3793 }
3794 
3795 //
3796 // M_SetupNextMenu
3797 //
M_SetupNextMenu(menu_t * menudef)3798 void M_SetupNextMenu(menu_t *menudef)
3799 {
3800 	INT16 i;
3801 
3802 #if defined (MASTERSERVER) && defined (HAVE_THREADS)
3803 	if (currentMenu == &MP_RoomDef || currentMenu == &MP_ConnectDef)
3804 	{
3805 		I_lock_mutex(&ms_QueryId_mutex);
3806 		{
3807 			ms_QueryId++;
3808 		}
3809 		I_unlock_mutex(ms_QueryId_mutex);
3810 	}
3811 
3812 	if (currentMenu == &MP_ConnectDef)
3813 	{
3814 		I_lock_mutex(&ms_ServerList_mutex);
3815 		{
3816 			if (ms_ServerList)
3817 			{
3818 				free(ms_ServerList);
3819 				ms_ServerList = NULL;
3820 			}
3821 		}
3822 		I_unlock_mutex(ms_ServerList_mutex);
3823 	}
3824 #endif/*HAVE_THREADS*/
3825 
3826 	if (currentMenu->quitroutine)
3827 	{
3828 		// If you're going from a menu to itself, why are you running the quitroutine? You're not quitting it! -SH
3829 		if (currentMenu != menudef && !currentMenu->quitroutine())
3830 			return; // we can't quit this menu (also used to set parameter from the menu)
3831 	}
3832 
3833 	M_HandleMenuPresState(menudef);
3834 
3835 	currentMenu = menudef;
3836 	itemOn = currentMenu->lastOn;
3837 
3838 	// in case of...
3839 	if (itemOn >= currentMenu->numitems)
3840 		itemOn = currentMenu->numitems - 1;
3841 
3842 	// the curent item can be disabled,
3843 	// this code go up until an enabled item found
3844 	if (( (currentMenu->menuitems[itemOn].status & IT_TYPE) & IT_SPACE ))
3845 	{
3846 		for (i = 0; i < currentMenu->numitems; i++)
3847 		{
3848 			if (!( (currentMenu->menuitems[i].status & IT_TYPE) & IT_SPACE ))
3849 			{
3850 				itemOn = i;
3851 				break;
3852 			}
3853 		}
3854 	}
3855 
3856 	hidetitlemap = false;
3857 }
3858 
3859 // Guess I'll put this here, idk
M_MouseNeeded(void)3860 boolean M_MouseNeeded(void)
3861 {
3862 	return (currentMenu == &MessageDef && currentMenu->prevMenu == &OP_ChangeControlsDef);
3863 }
3864 
3865 //
3866 // M_Ticker
3867 //
M_Ticker(void)3868 void M_Ticker(void)
3869 {
3870 	// reset input trigger
3871 	noFurtherInput = false;
3872 
3873 	if (dedicated)
3874 		return;
3875 
3876 	if (--skullAnimCounter <= 0)
3877 		skullAnimCounter = 8;
3878 
3879 	//added : 30-01-98 : test mode for five seconds
3880 	if (vidm_testingmode > 0)
3881 	{
3882 		// restore the previous video mode
3883 		if (--vidm_testingmode == 0)
3884 			setmodeneeded = vidm_previousmode + 1;
3885 	}
3886 
3887 	if (currentMenu == &OP_ScreenshotOptionsDef)
3888 		M_SetupScreenshotMenu();
3889 
3890 #if defined (MASTERSERVER) && defined (HAVE_THREADS)
3891 	I_lock_mutex(&ms_ServerList_mutex);
3892 	{
3893 		if (ms_ServerList)
3894 		{
3895 			CL_QueryServerList(ms_ServerList);
3896 			free(ms_ServerList);
3897 			ms_ServerList = NULL;
3898 		}
3899 	}
3900 	I_unlock_mutex(ms_ServerList_mutex);
3901 #endif
3902 }
3903 
3904 //
3905 // M_Init
3906 //
M_Init(void)3907 void M_Init(void)
3908 {
3909 	int i;
3910 
3911 	COM_AddCommand("manual", Command_Manual_f);
3912 
3913 	CV_RegisterVar(&cv_nextmap);
3914 	CV_RegisterVar(&cv_newgametype);
3915 	CV_RegisterVar(&cv_chooseskin);
3916 	CV_RegisterVar(&cv_autorecord);
3917 
3918 	if (dedicated)
3919 		return;
3920 
3921 	// Menu hacks
3922 	CV_RegisterVar(&cv_dummyteam);
3923 	CV_RegisterVar(&cv_dummyscramble);
3924 	CV_RegisterVar(&cv_dummyrings);
3925 	CV_RegisterVar(&cv_dummylives);
3926 	CV_RegisterVar(&cv_dummycontinues);
3927 	CV_RegisterVar(&cv_dummymares);
3928 	CV_RegisterVar(&cv_dummymarathon);
3929 	CV_RegisterVar(&cv_dummyloadless);
3930 	CV_RegisterVar(&cv_dummycutscenes);
3931 
3932 	quitmsg[QUITMSG] = M_GetText("Eggman's tied explosives\nto your girlfriend, and\nwill activate them if\nyou press the 'Y' key!\nPress 'N' to save her!\n\n(Press 'Y' to quit)");
3933 	quitmsg[QUITMSG1] = M_GetText("What would Tails say if\nhe saw you quitting the game?\n\n(Press 'Y' to quit)");
3934 	quitmsg[QUITMSG2] = M_GetText("Hey!\nWhere do ya think you're goin'?\n\n(Press 'Y' to quit)");
3935 	quitmsg[QUITMSG3] = M_GetText("Forget your studies!\nPlay some more!\n\n(Press 'Y' to quit)");
3936 	quitmsg[QUITMSG4] = M_GetText("You're trying to say you\nlike Sonic 2K6 better than\nthis, right?\n\n(Press 'Y' to quit)");
3937 	quitmsg[QUITMSG5] = M_GetText("Don't leave yet -- there's a\nsuper emerald around that corner!\n\n(Press 'Y' to quit)");
3938 	quitmsg[QUITMSG6] = M_GetText("You'd rather work than play?\n\n(Press 'Y' to quit)");
3939 	quitmsg[QUITMSG7] = M_GetText("Go ahead and leave. See if I care...\n*sniffle*\n\n(Press 'Y' to quit)");
3940 
3941 	quitmsg[QUIT2MSG] = M_GetText("If you leave now,\nEggman will take over the world!\n\n(Press 'Y' to quit)");
3942 	quitmsg[QUIT2MSG1] = M_GetText("Don't quit!\nThere are animals\nto save!\n\n(Press 'Y' to quit)");
3943 	quitmsg[QUIT2MSG2] = M_GetText("Aw c'mon, just bop\na few more robots!\n\n(Press 'Y' to quit)");
3944 	quitmsg[QUIT2MSG3] = M_GetText("Did you get all those Chaos Emeralds?\n\n(Press 'Y' to quit)");
3945 	quitmsg[QUIT2MSG4] = M_GetText("If you leave, I'll use\nmy spin attack on you!\n\n(Press 'Y' to quit)");
3946 	quitmsg[QUIT2MSG5] = M_GetText("Don't go!\nYou might find the hidden\nlevels!\n\n(Press 'Y' to quit)");
3947 	quitmsg[QUIT2MSG6] = M_GetText("Hit the 'N' key, Sonic!\nThe 'N' key!\n\n(Press 'Y' to quit)");
3948 
3949 	quitmsg[QUIT3MSG] = M_GetText("Are you really going to give up?\nWe certainly would never give you up.\n\n(Press 'Y' to quit)");
3950 	quitmsg[QUIT3MSG1] = M_GetText("Come on, just ONE more netgame!\n\n(Press 'Y' to quit)");
3951 	quitmsg[QUIT3MSG2] = M_GetText("Press 'N' to unlock\nthe Ultimate Cheat!\n\n(Press 'Y' to quit)");
3952 	quitmsg[QUIT3MSG3] = M_GetText("Why don't you go back and try\njumping on that house to\nsee what happens?\n\n(Press 'Y' to quit)");
3953 	quitmsg[QUIT3MSG4] = M_GetText("Every time you press 'Y', an\nSRB2 Developer cries...\n\n(Press 'Y' to quit)");
3954 	quitmsg[QUIT3MSG5] = M_GetText("You'll be back to play soon, though...\n......right?\n\n(Press 'Y' to quit)");
3955 	quitmsg[QUIT3MSG6] = M_GetText("Aww, is Egg Rock Zone too\ndifficult for you?\n\n(Press 'Y' to quit)");
3956 
3957 	/*
3958 	Well the menu sucks for forcing us to have an item set
3959 	at all if every item just calls the same function, and
3960 	nothing more. Now just automate the definition.
3961 	*/
3962 	for (i = 0; i <= MAX_JOYSTICKS; ++i)
3963 	{
3964 		OP_JoystickSetMenu[i].status = ( IT_NOTHING|IT_CALL );
3965 		OP_JoystickSetMenu[i].itemaction = M_AssignJoystick;
3966 	}
3967 
3968 #ifndef NONET
3969 	CV_RegisterVar(&cv_serversort);
3970 #endif
3971 }
3972 
M_InitCharacterTables(void)3973 void M_InitCharacterTables(void)
3974 {
3975 	UINT8 i;
3976 
3977 	// Setup description table
3978 	for (i = 0; i < MAXSKINS; i++)
3979 	{
3980 		description[i].used = false;
3981 		strcpy(description[i].notes, "???");
3982 		strcpy(description[i].picname, "");
3983 		strcpy(description[i].nametag, "");
3984 		strcpy(description[i].skinname, "");
3985 		strcpy(description[i].displayname, "");
3986 		description[i].prev = description[i].next = 0;
3987 		description[i].charpic = NULL;
3988 		description[i].namepic = NULL;
3989 		description[i].oppositecolor = description[i].tagtextcolor = description[i].tagoutlinecolor = 0;
3990 	}
3991 }
3992 
3993 // ==========================================================================
3994 // SPECIAL MENU OPTION DRAW ROUTINES GO HERE
3995 // ==========================================================================
3996 
3997 // Converts a string into question marks.
3998 // Used for the secrets menu, to hide yet-to-be-unlocked stuff.
M_CreateSecretMenuOption(const char * str)3999 static const char *M_CreateSecretMenuOption(const char *str)
4000 {
4001 	static char qbuf[32];
4002 	int i;
4003 
4004 	for (i = 0; i < 31; ++i)
4005 	{
4006 		if (!str[i])
4007 		{
4008 			qbuf[i] = '\0';
4009 			return qbuf;
4010 		}
4011 		else if (str[i] != ' ')
4012 			qbuf[i] = '?';
4013 		else
4014 			qbuf[i] = ' ';
4015 	}
4016 
4017 	qbuf[31] = '\0';
4018 	return qbuf;
4019 }
4020 
M_DrawThermo(INT32 x,INT32 y,consvar_t * cv)4021 static void M_DrawThermo(INT32 x, INT32 y, consvar_t *cv)
4022 {
4023 	INT32 xx = x, i;
4024 	lumpnum_t leftlump, rightlump, centerlump[2], cursorlump;
4025 	patch_t *p;
4026 
4027 	leftlump = W_GetNumForName("M_THERML");
4028 	rightlump = W_GetNumForName("M_THERMR");
4029 	centerlump[0] = W_GetNumForName("M_THERMM");
4030 	centerlump[1] = W_GetNumForName("M_THERMM");
4031 	cursorlump = W_GetNumForName("M_THERMO");
4032 
4033 	V_DrawScaledPatch(xx, y, 0, p = W_CachePatchNum(leftlump,PU_PATCH));
4034 	xx += p->width - p->leftoffset;
4035 	for (i = 0; i < 16; i++)
4036 	{
4037 		V_DrawScaledPatch(xx, y, V_WRAPX, W_CachePatchNum(centerlump[i & 1], PU_PATCH));
4038 		xx += 8;
4039 	}
4040 	V_DrawScaledPatch(xx, y, 0, W_CachePatchNum(rightlump, PU_PATCH));
4041 
4042 	xx = (cv->value - cv->PossibleValue[0].value) * (15*8) /
4043 		(cv->PossibleValue[1].value - cv->PossibleValue[0].value);
4044 
4045 	V_DrawScaledPatch((x + 8) + xx, y, 0, W_CachePatchNum(cursorlump, PU_PATCH));
4046 }
4047 
4048 //  A smaller 'Thermo', with range given as percents (0-100)
M_DrawSlider(INT32 x,INT32 y,const consvar_t * cv,boolean ontop)4049 static void M_DrawSlider(INT32 x, INT32 y, const consvar_t *cv, boolean ontop)
4050 {
4051 	INT32 i;
4052 	INT32 range;
4053 	patch_t *p;
4054 
4055 	x = BASEVIDWIDTH - x - SLIDER_WIDTH;
4056 
4057 	V_DrawScaledPatch(x, y, 0, W_CachePatchName("M_SLIDEL", PU_PATCH));
4058 
4059 	p =  W_CachePatchName("M_SLIDEM", PU_PATCH);
4060 	for (i = 1; i < SLIDER_RANGE; i++)
4061 		V_DrawScaledPatch (x+i*8, y, 0,p);
4062 
4063 	if (ontop)
4064 	{
4065 		V_DrawCharacter(x - 6 - (skullAnimCounter/5), y,
4066 			'\x1C' | V_YELLOWMAP, false);
4067 		V_DrawCharacter(x+i*8 + 8 + (skullAnimCounter/5), y,
4068 			'\x1D' | V_YELLOWMAP, false);
4069 	}
4070 
4071 	p = W_CachePatchName("M_SLIDER", PU_PATCH);
4072 	V_DrawScaledPatch(x+i*8, y, 0, p);
4073 
4074 	// draw the slider cursor
4075 	p = W_CachePatchName("M_SLIDEC", PU_PATCH);
4076 
4077 	for (i = 0; cv->PossibleValue[i+1].strvalue; i++);
4078 
4079 	if (cv->flags & CV_FLOAT)
4080 		range = (INT32)(atof(cv->defaultvalue)*FRACUNIT);
4081 	else
4082 		range = atoi(cv->defaultvalue);
4083 
4084 	if (range != cv->value)
4085 	{
4086 		range = ((range - cv->PossibleValue[0].value) * 100 /
4087 		 (cv->PossibleValue[i].value - cv->PossibleValue[0].value));
4088 
4089 		if (range < 0)
4090 			range = 0;
4091 		else if (range > 100)
4092 			range = 100;
4093 
4094 		V_DrawMappedPatch(x + 2 + (SLIDER_RANGE*8*range)/100, y, V_TRANSLUCENT, p, yellowmap);
4095 	}
4096 
4097 	range = ((cv->value - cv->PossibleValue[0].value) * 100 /
4098 	 (cv->PossibleValue[i].value - cv->PossibleValue[0].value));
4099 
4100 	if (range < 0)
4101 		range = 0;
4102 	else if (range > 100)
4103 		range = 100;
4104 
4105 	V_DrawMappedPatch(x + 2 + (SLIDER_RANGE*8*range)/100, y, 0, p, yellowmap);
4106 }
4107 
4108 //
4109 //  Draw a textbox, like Quake does, because sometimes it's difficult
4110 //  to read the text with all the stuff in the background...
4111 //
M_DrawTextBox(INT32 x,INT32 y,INT32 width,INT32 boxlines)4112 void M_DrawTextBox(INT32 x, INT32 y, INT32 width, INT32 boxlines)
4113 {
4114 	// Solid color textbox.
4115 	V_DrawFill(x+5, y+5, width*8+6, boxlines*8+6, 159);
4116 	//V_DrawFill(x+8, y+8, width*8, boxlines*8, 31);
4117 /*
4118 	patch_t *p;
4119 	INT32 cx, cy, n;
4120 	INT32 step, boff;
4121 
4122 	step = 8;
4123 	boff = 8;
4124 
4125 	// draw left side
4126 	cx = x;
4127 	cy = y;
4128 	V_DrawScaledPatch(cx, cy, 0, W_CachePatchNum(viewborderlump[BRDR_TL], PU_PATCH));
4129 	cy += boff;
4130 	p = W_CachePatchNum(viewborderlump[BRDR_L], PU_PATCH);
4131 	for (n = 0; n < boxlines; n++)
4132 	{
4133 		V_DrawScaledPatch(cx, cy, V_WRAPY, p);
4134 		cy += step;
4135 	}
4136 	V_DrawScaledPatch(cx, cy, 0, W_CachePatchNum(viewborderlump[BRDR_BL], PU_PATCH));
4137 
4138 	// draw middle
4139 	V_DrawFlatFill(x + boff, y + boff, width*step, boxlines*step, st_borderpatchnum);
4140 
4141 	cx += boff;
4142 	cy = y;
4143 	while (width > 0)
4144 	{
4145 		V_DrawScaledPatch(cx, cy, V_WRAPX, W_CachePatchNum(viewborderlump[BRDR_T], PU_PATCH));
4146 		V_DrawScaledPatch(cx, y + boff + boxlines*step, V_WRAPX, W_CachePatchNum(viewborderlump[BRDR_B], PU_PATCH));
4147 		width--;
4148 		cx += step;
4149 	}
4150 
4151 	// draw right side
4152 	cy = y;
4153 	V_DrawScaledPatch(cx, cy, 0, W_CachePatchNum(viewborderlump[BRDR_TR], PU_PATCH));
4154 	cy += boff;
4155 	p = W_CachePatchNum(viewborderlump[BRDR_R], PU_PATCH);
4156 	for (n = 0; n < boxlines; n++)
4157 	{
4158 		V_DrawScaledPatch(cx, cy, V_WRAPY, p);
4159 		cy += step;
4160 	}
4161 	V_DrawScaledPatch(cx, cy, 0, W_CachePatchNum(viewborderlump[BRDR_BR], PU_PATCH));
4162 */
4163 }
4164 
4165 static fixed_t staticalong = 0;
4166 
M_DrawStaticBox(fixed_t x,fixed_t y,INT32 flags,fixed_t w,fixed_t h)4167 static void M_DrawStaticBox(fixed_t x, fixed_t y, INT32 flags, fixed_t w, fixed_t h)
4168 {
4169 	patch_t *patch;
4170 	fixed_t sw, pw;
4171 
4172 	patch = W_CachePatchName("LSSTATIC", PU_PATCH);
4173 	pw = patch->width - (sw = w*2); //FixedDiv(w, scale); -- for scale FRACUNIT/2
4174 
4175 	/*if (pw > 0) -- model code for modders providing weird LSSTATIC
4176 	{
4177 		if (staticalong > pw)
4178 			staticalong -= pw;
4179 	}
4180 	else
4181 		staticalong = 0;*/
4182 
4183 	if (staticalong > pw) // simplified for base LSSTATIC
4184 		staticalong -= pw;
4185 
4186 	V_DrawCroppedPatch(x<<FRACBITS, y<<FRACBITS, FRACUNIT/2, flags, patch, staticalong, 0, sw, h*2); // FixedDiv(h, scale)); -- for scale FRACUNIT/2
4187 
4188 	staticalong += sw; //M_RandomRange(sw/2, 2*sw); -- turns out less randomisation looks better because immediately adjacent frames can't end up close to each other
4189 
4190 	W_UnlockCachedPatch(patch);
4191 }
4192 
4193 //
4194 // Draw border for the savegame description
4195 //
4196 #if 0 // once used for joysticks and savegames, now no longer
M_DrawSaveLoadBorder(INT32 x,INT32 y)4197 static void M_DrawSaveLoadBorder(INT32 x,INT32 y)
4198 {
4199 	INT32 i;
4200 
4201 	V_DrawScaledPatch (x-8,y+7,0,W_CachePatchName("M_LSLEFT",PU_PATCH));
4202 
4203 	for (i = 0;i < 24;i++)
4204 	{
4205 		V_DrawScaledPatch (x,y+7,0,W_CachePatchName("M_LSCNTR",PU_PATCH));
4206 		x += 8;
4207 	}
4208 
4209 	V_DrawScaledPatch (x,y+7,0,W_CachePatchName("M_LSRGHT",PU_PATCH));
4210 }
4211 #endif
4212 
4213 // horizontally centered text
M_CentreText(INT32 y,const char * string)4214 static void M_CentreText(INT32 y, const char *string)
4215 {
4216 	INT32 x;
4217 	//added : 02-02-98 : centre on 320, because V_DrawString centers on vid.width...
4218 	x = (BASEVIDWIDTH - V_StringWidth(string, V_OLDSPACING))>>1;
4219 	V_DrawString(x,y,V_OLDSPACING,string);
4220 }
4221 
4222 //
4223 // M_DrawMapEmblems
4224 //
4225 // used by pause & statistics to draw a row of emblems for a map
4226 //
M_DrawMapEmblems(INT32 mapnum,INT32 x,INT32 y)4227 static void M_DrawMapEmblems(INT32 mapnum, INT32 x, INT32 y)
4228 {
4229 	UINT8 lasttype = UINT8_MAX, curtype;
4230 	emblem_t *emblem = M_GetLevelEmblems(mapnum);
4231 
4232 	while (emblem)
4233 	{
4234 		switch (emblem->type)
4235 		{
4236 			case ET_SCORE: case ET_TIME: case ET_RINGS:
4237 				curtype = 1; break;
4238 			case ET_NGRADE: case ET_NTIME:
4239 				curtype = 2; break;
4240 			case ET_MAP:
4241 				curtype = 3; break;
4242 			default:
4243 				curtype = 0; break;
4244 		}
4245 
4246 		// Shift over if emblem is of a different discipline
4247 		if (lasttype != UINT8_MAX && lasttype != curtype)
4248 			x -= 4;
4249 		lasttype = curtype;
4250 
4251 		if (emblem->collected)
4252 			V_DrawSmallMappedPatch(x, y, 0, W_CachePatchName(M_GetEmblemPatch(emblem, false), PU_PATCH),
4253 			                       R_GetTranslationColormap(TC_DEFAULT, M_GetEmblemColor(emblem), GTC_CACHE));
4254 		else
4255 			V_DrawSmallScaledPatch(x, y, 0, W_CachePatchName("NEEDIT", PU_PATCH));
4256 
4257 		emblem = M_GetLevelEmblems(-1);
4258 		x -= 12;
4259 	}
4260 }
4261 
M_DrawMenuTitle(void)4262 static void M_DrawMenuTitle(void)
4263 {
4264 	if (currentMenu->menutitlepic)
4265 	{
4266 		patch_t *p = W_CachePatchName(currentMenu->menutitlepic, PU_PATCH);
4267 
4268 		if (p->height > 24) // title is larger than normal
4269 		{
4270 			INT32 xtitle = (BASEVIDWIDTH - (p->width/2))/2;
4271 			INT32 ytitle = (30 - (p->height/2))/2;
4272 
4273 			if (xtitle < 0)
4274 				xtitle = 0;
4275 			if (ytitle < 0)
4276 				ytitle = 0;
4277 
4278 			V_DrawSmallScaledPatch(xtitle, ytitle, 0, p);
4279 		}
4280 		else
4281 		{
4282 			INT32 xtitle = (BASEVIDWIDTH - p->width)/2;
4283 			INT32 ytitle = (30 - p->height)/2;
4284 
4285 			if (xtitle < 0)
4286 				xtitle = 0;
4287 			if (ytitle < 0)
4288 				ytitle = 0;
4289 
4290 			V_DrawScaledPatch(xtitle, ytitle, 0, p);
4291 		}
4292 	}
4293 }
4294 
M_DrawGenericMenu(void)4295 static void M_DrawGenericMenu(void)
4296 {
4297 	INT32 x, y, i, cursory = 0;
4298 
4299 	// DRAW MENU
4300 	x = currentMenu->x;
4301 	y = currentMenu->y;
4302 
4303 	// draw title (or big pic)
4304 	M_DrawMenuTitle();
4305 
4306 	for (i = 0; i < currentMenu->numitems; i++)
4307 	{
4308 		if (i == itemOn)
4309 			cursory = y;
4310 		switch (currentMenu->menuitems[i].status & IT_DISPLAY)
4311 		{
4312 			case IT_PATCH:
4313 				if (currentMenu->menuitems[i].patch && currentMenu->menuitems[i].patch[0])
4314 				{
4315 					if (currentMenu->menuitems[i].status & IT_CENTER)
4316 					{
4317 						patch_t *p;
4318 						p = W_CachePatchName(currentMenu->menuitems[i].patch, PU_PATCH);
4319 						V_DrawScaledPatch((BASEVIDWIDTH - p->width)/2, y, 0, p);
4320 					}
4321 					else
4322 					{
4323 						V_DrawScaledPatch(x, y, 0,
4324 							W_CachePatchName(currentMenu->menuitems[i].patch, PU_PATCH));
4325 					}
4326 				}
4327 				/* FALLTHRU */
4328 			case IT_NOTHING:
4329 			case IT_DYBIGSPACE:
4330 				y += LINEHEIGHT;
4331 				break;
4332 			case IT_BIGSLIDER:
4333 				M_DrawThermo(x, y, (consvar_t *)currentMenu->menuitems[i].itemaction);
4334 				y += LINEHEIGHT;
4335 				break;
4336 			case IT_STRING:
4337 			case IT_WHITESTRING:
4338 				if (currentMenu->menuitems[i].alphaKey)
4339 					y = currentMenu->y+currentMenu->menuitems[i].alphaKey;
4340 				if (i == itemOn)
4341 					cursory = y;
4342 
4343 				if ((currentMenu->menuitems[i].status & IT_DISPLAY)==IT_STRING)
4344 					V_DrawString(x, y, 0, currentMenu->menuitems[i].text);
4345 				else
4346 					V_DrawString(x, y, V_YELLOWMAP, currentMenu->menuitems[i].text);
4347 
4348 				// Cvar specific handling
4349 				switch (currentMenu->menuitems[i].status & IT_TYPE)
4350 					case IT_CVAR:
4351 					{
4352 						consvar_t *cv = (consvar_t *)currentMenu->menuitems[i].itemaction;
4353 						switch (currentMenu->menuitems[i].status & IT_CVARTYPE)
4354 						{
4355 							case IT_CV_SLIDER:
4356 								M_DrawSlider(x, y, cv, (i == itemOn));
4357 							case IT_CV_NOPRINT: // color use this
4358 							case IT_CV_INVISSLIDER: // monitor toggles use this
4359 								break;
4360 							case IT_CV_STRING:
4361 								M_DrawTextBox(x, y + 4, MAXSTRINGLENGTH, 1);
4362 								V_DrawString(x + 8, y + 12, V_ALLOWLOWERCASE, cv->string);
4363 								if (skullAnimCounter < 4 && i == itemOn)
4364 									V_DrawCharacter(x + 8 + V_StringWidth(cv->string, 0), y + 12,
4365 										'_' | 0x80, false);
4366 								y += 16;
4367 								break;
4368 							default:
4369 								V_DrawRightAlignedString(BASEVIDWIDTH - x, y,
4370 									((cv->flags & CV_CHEAT) && !CV_IsSetToDefault(cv) ? V_REDMAP : V_YELLOWMAP), cv->string);
4371 								if (i == itemOn)
4372 								{
4373 									V_DrawCharacter(BASEVIDWIDTH - x - 10 - V_StringWidth(cv->string, 0) - (skullAnimCounter/5), y,
4374 											'\x1C' | V_YELLOWMAP, false);
4375 									V_DrawCharacter(BASEVIDWIDTH - x + 2 + (skullAnimCounter/5), y,
4376 											'\x1D' | V_YELLOWMAP, false);
4377 								}
4378 								break;
4379 						}
4380 						break;
4381 					}
4382 					y += STRINGHEIGHT;
4383 					break;
4384 			case IT_STRING2:
4385 				V_DrawString(x, y, 0, currentMenu->menuitems[i].text);
4386 				/* FALLTHRU */
4387 			case IT_DYLITLSPACE:
4388 				y += SMALLLINEHEIGHT;
4389 				break;
4390 			case IT_GRAYPATCH:
4391 				if (currentMenu->menuitems[i].patch && currentMenu->menuitems[i].patch[0])
4392 					V_DrawMappedPatch(x, y, 0,
4393 						W_CachePatchName(currentMenu->menuitems[i].patch,PU_PATCH), graymap);
4394 				y += LINEHEIGHT;
4395 				break;
4396 			case IT_TRANSTEXT:
4397 				if (currentMenu->menuitems[i].alphaKey)
4398 					y = currentMenu->y+currentMenu->menuitems[i].alphaKey;
4399 				/* FALLTHRU */
4400 			case IT_TRANSTEXT2:
4401 				V_DrawString(x, y, V_TRANSLUCENT, currentMenu->menuitems[i].text);
4402 				y += SMALLLINEHEIGHT;
4403 				break;
4404 			case IT_QUESTIONMARKS:
4405 				if (currentMenu->menuitems[i].alphaKey)
4406 					y = currentMenu->y+currentMenu->menuitems[i].alphaKey;
4407 
4408 				V_DrawString(x, y, V_TRANSLUCENT|V_OLDSPACING, M_CreateSecretMenuOption(currentMenu->menuitems[i].text));
4409 				y += SMALLLINEHEIGHT;
4410 				break;
4411 			case IT_HEADERTEXT: // draws 16 pixels to the left, in yellow text
4412 				if (currentMenu->menuitems[i].alphaKey)
4413 					y = currentMenu->y+currentMenu->menuitems[i].alphaKey;
4414 
4415 				//V_DrawString(x-16, y, V_YELLOWMAP, currentMenu->menuitems[i].text);
4416 				M_DrawLevelPlatterHeader(y - (lsheadingheight - 12), currentMenu->menuitems[i].text, true, false);
4417 				y += SMALLLINEHEIGHT;
4418 				break;
4419 		}
4420 	}
4421 
4422 	// DRAW THE SKULL CURSOR
4423 	if (((currentMenu->menuitems[itemOn].status & IT_DISPLAY) == IT_PATCH)
4424 		|| ((currentMenu->menuitems[itemOn].status & IT_DISPLAY) == IT_NOTHING))
4425 	{
4426 		V_DrawScaledPatch(currentMenu->x + SKULLXOFF, cursory - 5, 0,
4427 			W_CachePatchName("M_CURSOR", PU_PATCH));
4428 	}
4429 	else
4430 	{
4431 		V_DrawScaledPatch(currentMenu->x - 24, cursory, 0,
4432 			W_CachePatchName("M_CURSOR", PU_PATCH));
4433 		V_DrawString(currentMenu->x, cursory, V_YELLOWMAP, currentMenu->menuitems[itemOn].text);
4434 	}
4435 }
4436 
4437 const char *PlaystyleNames[4] = {"Strafe", "Standard", "Simple", "Old Analog??"};
4438 const char *PlaystyleDesc[4] = {
4439 	// Legacy
4440 	"The play style used for\n"
4441 	"old-school SRB2.\n"
4442 	"\n"
4443 	"This play style is identical\n"
4444 	"to Standard, except that the\n"
4445 	"player always looks in the\n"
4446 	"direction of the camera."
4447 	,
4448 
4449 	// Standard
4450 	"The default play style,\n"
4451 	"designed for full control\n"
4452 	"with a keyboard and mouse.\n"
4453 	"\n"
4454 	"The camera rotates only when\n"
4455 	"you tell it to. The player\n"
4456 	"looks in the direction they're\n"
4457 	"moving, but acts in the direction\n"
4458 	"the camera is facing.\n"
4459 	"\n"
4460 	"Mastery of this play style will\n"
4461 	"open up the highest level of play!"
4462 	,
4463 
4464 	// Simple
4465 	"A play style designed for\n"
4466 	"gamepads and hassle-free play.\n"
4467 	"\n"
4468 	"The camera rotates automatically\n"
4469 	"as you move, and the player faces\n"
4470 	"and acts in the direction\n"
4471 	"they're moving.\n"
4472 	"\n"
4473 	"Hold \x82" "Center View\x80 to lock the\n"
4474 	"camera behind the player!\n"
4475 	,
4476 
4477 	// Old Analog
4478 	"I see.\n"
4479 	"\n"
4480 	"You really liked the old analog mode,\n"
4481 	"so when 2.2 came out, you opened up\n"
4482 	"your config file and brought it back.\n"
4483 	"\n"
4484 	"That's absolutely valid, but I implore\n"
4485 	"you to try the new Simple play style\n"
4486 	"instead!"
4487 };
4488 
4489 static UINT8 playstyle_activeplayer = 0, playstyle_currentchoice = 0;
4490 
M_DrawControlsDefMenu(void)4491 static void M_DrawControlsDefMenu(void)
4492 {
4493 	UINT8 opt = 0;
4494 
4495 	M_DrawGenericMenu();
4496 
4497 	if (currentMenu == &OP_P1ControlsDef)
4498 	{
4499 		opt = cv_directionchar[0].value ? 1 : 0;
4500 		opt = playstyle_currentchoice = cv_useranalog[0].value ? 3 - opt : opt;
4501 
4502 		if (opt == 2)
4503 		{
4504 			OP_CameraOptionsDef.menuitems = OP_CameraExtendedOptionsMenu;
4505 			OP_CameraOptionsDef.numitems = sizeof (OP_CameraExtendedOptionsMenu) / sizeof (menuitem_t);
4506 		}
4507 		else
4508 		{
4509 			OP_CameraOptionsDef.menuitems = OP_CameraOptionsMenu;
4510 			OP_CameraOptionsDef.numitems = sizeof (OP_CameraOptionsMenu) / sizeof (menuitem_t);
4511 		}
4512 	}
4513 	else
4514 	{
4515 		opt = cv_directionchar[1].value ? 1 : 0;
4516 		opt = playstyle_currentchoice = cv_useranalog[1].value ? 3 - opt : opt;
4517 
4518 		if (opt == 2)
4519 		{
4520 			OP_Camera2OptionsDef.menuitems = OP_Camera2ExtendedOptionsMenu;
4521 			OP_Camera2OptionsDef.numitems = sizeof (OP_Camera2ExtendedOptionsMenu) / sizeof (menuitem_t);
4522 		}
4523 		else
4524 		{
4525 			OP_Camera2OptionsDef.menuitems = OP_Camera2OptionsMenu;
4526 			OP_Camera2OptionsDef.numitems = sizeof (OP_Camera2OptionsMenu) / sizeof (menuitem_t);
4527 		}
4528 	}
4529 
4530 	V_DrawRightAlignedString(BASEVIDWIDTH - currentMenu->x, currentMenu->y + 80, V_YELLOWMAP, PlaystyleNames[opt]);
4531 }
4532 
4533 #define scrollareaheight 72
4534 
4535 // note that alphakey is multiplied by 2 for scrolling menus to allow greater usage in UINT8 range.
M_DrawGenericScrollMenu(void)4536 static void M_DrawGenericScrollMenu(void)
4537 {
4538 	INT32 x, y, i, max, bottom, tempcentery, cursory = 0;
4539 
4540 	// DRAW MENU
4541 	x = currentMenu->x;
4542 	y = currentMenu->y;
4543 
4544 	if (currentMenu->menuitems[currentMenu->numitems-1].alphaKey < scrollareaheight)
4545 		tempcentery = currentMenu->y; // Not tall enough to scroll, but this thinker is used in case it becomes so
4546 	else if ((currentMenu->menuitems[itemOn].alphaKey*2 - currentMenu->menuitems[0].alphaKey*2) <= scrollareaheight)
4547 		tempcentery = currentMenu->y - currentMenu->menuitems[0].alphaKey*2;
4548 	else if ((currentMenu->menuitems[currentMenu->numitems-1].alphaKey*2 - currentMenu->menuitems[itemOn].alphaKey*2) <= scrollareaheight)
4549 		tempcentery = currentMenu->y - currentMenu->menuitems[currentMenu->numitems-1].alphaKey*2 + 2*scrollareaheight;
4550 	else
4551 		tempcentery = currentMenu->y - currentMenu->menuitems[itemOn].alphaKey*2 + scrollareaheight;
4552 
4553 	for (i = 0; i < currentMenu->numitems; i++)
4554 	{
4555 		if (currentMenu->menuitems[i].status != IT_DISABLED && currentMenu->menuitems[i].alphaKey*2 + tempcentery >= currentMenu->y)
4556 			break;
4557 	}
4558 
4559 	for (bottom = currentMenu->numitems; bottom > 0; bottom--)
4560 	{
4561 		if (currentMenu->menuitems[bottom-1].status != IT_DISABLED)
4562 			break;
4563 	}
4564 
4565 	for (max = bottom; max > 0; max--)
4566 	{
4567 		if (currentMenu->menuitems[max-1].status != IT_DISABLED && currentMenu->menuitems[max-1].alphaKey*2 + tempcentery <= (currentMenu->y + 2*scrollareaheight))
4568 			break;
4569 	}
4570 
4571 	if (i)
4572 		V_DrawString(currentMenu->x - 20, currentMenu->y - (skullAnimCounter/5), V_YELLOWMAP, "\x1A"); // up arrow
4573 	if (max != bottom)
4574 		V_DrawString(currentMenu->x - 20, currentMenu->y + 2*scrollareaheight + (skullAnimCounter/5), V_YELLOWMAP, "\x1B"); // down arrow
4575 
4576 	// draw title (or big pic)
4577 	M_DrawMenuTitle();
4578 
4579 	for (; i < max; i++)
4580 	{
4581 		y = currentMenu->menuitems[i].alphaKey*2 + tempcentery;
4582 		if (i == itemOn)
4583 			cursory = y;
4584 		switch (currentMenu->menuitems[i].status & IT_DISPLAY)
4585 		{
4586 			case IT_PATCH:
4587 			case IT_DYBIGSPACE:
4588 			case IT_BIGSLIDER:
4589 			case IT_STRING2:
4590 			case IT_DYLITLSPACE:
4591 			case IT_GRAYPATCH:
4592 			case IT_TRANSTEXT2:
4593 				// unsupported
4594 				break;
4595 			case IT_NOTHING:
4596 				break;
4597 			case IT_STRING:
4598 			case IT_WHITESTRING:
4599 				if (i != itemOn && (currentMenu->menuitems[i].status & IT_DISPLAY)==IT_STRING)
4600 					V_DrawString(x, y, 0, currentMenu->menuitems[i].text);
4601 				else
4602 					V_DrawString(x, y, V_YELLOWMAP, currentMenu->menuitems[i].text);
4603 
4604 				// Cvar specific handling
4605 				switch (currentMenu->menuitems[i].status & IT_TYPE)
4606 					case IT_CVAR:
4607 					{
4608 						consvar_t *cv = (consvar_t *)currentMenu->menuitems[i].itemaction;
4609 						switch (currentMenu->menuitems[i].status & IT_CVARTYPE)
4610 						{
4611 							case IT_CV_SLIDER:
4612 								M_DrawSlider(x, y, cv, (i == itemOn));
4613 							case IT_CV_NOPRINT: // color use this
4614 							case IT_CV_INVISSLIDER: // monitor toggles use this
4615 								break;
4616 							case IT_CV_STRING:
4617 #if 1
4618 								if (y + 12 > (currentMenu->y + 2*scrollareaheight))
4619 									break;
4620 								M_DrawTextBox(x, y + 4, MAXSTRINGLENGTH, 1);
4621 								V_DrawString(x + 8, y + 12, V_ALLOWLOWERCASE, cv->string);
4622 								if (skullAnimCounter < 4 && i == itemOn)
4623 									V_DrawCharacter(x + 8 + V_StringWidth(cv->string, 0), y + 12,
4624 										'_' | 0x80, false);
4625 #else // cool new string type stuff, not ready for limelight
4626 								if (i == itemOn)
4627 								{
4628 									V_DrawFill(x-2, y-1, MAXSTRINGLENGTH*8 + 4, 8+3, 159);
4629 									V_DrawString(x, y, V_ALLOWLOWERCASE, cv->string);
4630 									if (skullAnimCounter < 4)
4631 										V_DrawCharacter(x + V_StringWidth(cv->string, 0), y, '_' | 0x80, false);
4632 								}
4633 								else
4634 									V_DrawRightAlignedString(BASEVIDWIDTH - x, y,
4635 									V_YELLOWMAP|V_ALLOWLOWERCASE, cv->string);
4636 #endif
4637 								break;
4638 							default:
4639 								V_DrawRightAlignedString(BASEVIDWIDTH - x, y,
4640 									((cv->flags & CV_CHEAT) && !CV_IsSetToDefault(cv) ? V_REDMAP : V_YELLOWMAP), cv->string);
4641 								if (i == itemOn)
4642 								{
4643 									V_DrawCharacter(BASEVIDWIDTH - x - 10 - V_StringWidth(cv->string, 0) - (skullAnimCounter/5), y,
4644 											'\x1C' | V_YELLOWMAP, false);
4645 									V_DrawCharacter(BASEVIDWIDTH - x + 2 + (skullAnimCounter/5), y,
4646 											'\x1D' | V_YELLOWMAP, false);
4647 								}
4648 								break;
4649 						}
4650 						break;
4651 					}
4652 					break;
4653 			case IT_TRANSTEXT:
4654 				switch (currentMenu->menuitems[i].status & IT_TYPE)
4655 				{
4656 					case IT_PAIR:
4657 						V_DrawString(x, y,
4658 								V_TRANSLUCENT, currentMenu->menuitems[i].patch);
4659 						V_DrawRightAlignedString(BASEVIDWIDTH - x, y,
4660 								V_TRANSLUCENT, currentMenu->menuitems[i].text);
4661 						break;
4662 					default:
4663 						V_DrawString(x, y,
4664 								V_TRANSLUCENT, currentMenu->menuitems[i].text);
4665 				}
4666 				break;
4667 			case IT_QUESTIONMARKS:
4668 				V_DrawString(x, y, V_TRANSLUCENT|V_OLDSPACING, M_CreateSecretMenuOption(currentMenu->menuitems[i].text));
4669 				break;
4670 			case IT_HEADERTEXT:
4671 				//V_DrawString(x-16, y, V_YELLOWMAP, currentMenu->menuitems[i].text);
4672 				M_DrawLevelPlatterHeader(y - (lsheadingheight - 12), currentMenu->menuitems[i].text, true, false);
4673 				break;
4674 		}
4675 	}
4676 
4677 	// DRAW THE SKULL CURSOR
4678 	V_DrawScaledPatch(currentMenu->x - 24, cursory, 0,
4679 		W_CachePatchName("M_CURSOR", PU_PATCH));
4680 }
4681 
M_DrawPauseMenu(void)4682 static void M_DrawPauseMenu(void)
4683 {
4684 	if (!netgame && !multiplayer && (gamestate == GS_LEVEL || gamestate == GS_INTERMISSION))
4685 	{
4686 		emblem_t *emblem_detail[3] = {NULL, NULL, NULL};
4687 		char emblem_text[3][20];
4688 		INT32 i;
4689 
4690 		M_DrawTextBox(27, 16, 32, 6);
4691 
4692 		// Draw any and all emblems at the top.
4693 		M_DrawMapEmblems(gamemap, 272, 28);
4694 
4695 		if (mapheaderinfo[gamemap-1]->actnum != 0)
4696 			V_DrawString(40, 28, V_YELLOWMAP, va("%s %d", mapheaderinfo[gamemap-1]->lvlttl, mapheaderinfo[gamemap-1]->actnum));
4697 		else
4698 			V_DrawString(40, 28, V_YELLOWMAP, mapheaderinfo[gamemap-1]->lvlttl);
4699 
4700 		// Set up the detail boxes.
4701 		{
4702 			emblem_t *emblem = M_GetLevelEmblems(gamemap);
4703 			while (emblem)
4704 			{
4705 				INT32 emblemslot;
4706 				char targettext[9], currenttext[9];
4707 
4708 				switch (emblem->type)
4709 				{
4710 					case ET_SCORE:
4711 						snprintf(targettext, 9, "%d", emblem->var);
4712 						snprintf(currenttext, 9, "%u", G_GetBestScore(gamemap));
4713 
4714 						targettext[8] = 0;
4715 						currenttext[8] = 0;
4716 
4717 						emblemslot = 0;
4718 						break;
4719 					case ET_TIME:
4720 						emblemslot = emblem->var; // dumb hack
4721 						snprintf(targettext, 9, "%i:%02i.%02i",
4722 							G_TicsToMinutes((tic_t)emblemslot, false),
4723 							G_TicsToSeconds((tic_t)emblemslot),
4724 							G_TicsToCentiseconds((tic_t)emblemslot));
4725 
4726 						emblemslot = (INT32)G_GetBestTime(gamemap); // dumb hack pt ii
4727 						if ((tic_t)emblemslot == UINT32_MAX)
4728 							snprintf(currenttext, 9, "-:--.--");
4729 						else
4730 							snprintf(currenttext, 9, "%i:%02i.%02i",
4731 								G_TicsToMinutes((tic_t)emblemslot, false),
4732 								G_TicsToSeconds((tic_t)emblemslot),
4733 								G_TicsToCentiseconds((tic_t)emblemslot));
4734 
4735 						targettext[8] = 0;
4736 						currenttext[8] = 0;
4737 
4738 						emblemslot = 1;
4739 						break;
4740 					case ET_RINGS:
4741 						snprintf(targettext, 9, "%d", emblem->var);
4742 						snprintf(currenttext, 9, "%u", G_GetBestRings(gamemap));
4743 
4744 						targettext[8] = 0;
4745 						currenttext[8] = 0;
4746 
4747 						emblemslot = 2;
4748 						break;
4749 					case ET_NGRADE:
4750 						snprintf(targettext, 9, "%u", P_GetScoreForGrade(gamemap, 0, emblem->var));
4751 						snprintf(currenttext, 9, "%u", G_GetBestNightsScore(gamemap, 0));
4752 
4753 						targettext[8] = 0;
4754 						currenttext[8] = 0;
4755 
4756 						emblemslot = 1;
4757 						break;
4758 					case ET_NTIME:
4759 						emblemslot = emblem->var; // dumb hack pt iii
4760 						snprintf(targettext, 9, "%i:%02i.%02i",
4761 							G_TicsToMinutes((tic_t)emblemslot, false),
4762 							G_TicsToSeconds((tic_t)emblemslot),
4763 							G_TicsToCentiseconds((tic_t)emblemslot));
4764 
4765 						emblemslot = (INT32)G_GetBestNightsTime(gamemap, 0); // dumb hack pt iv
4766 						if ((tic_t)emblemslot == UINT32_MAX)
4767 							snprintf(currenttext, 9, "-:--.--");
4768 						else
4769 							snprintf(currenttext, 9, "%i:%02i.%02i",
4770 								G_TicsToMinutes((tic_t)emblemslot, false),
4771 								G_TicsToSeconds((tic_t)emblemslot),
4772 								G_TicsToCentiseconds((tic_t)emblemslot));
4773 
4774 						targettext[8] = 0;
4775 						currenttext[8] = 0;
4776 
4777 						emblemslot = 2;
4778 						break;
4779 					default:
4780 						goto bademblem;
4781 				}
4782 				if (emblem_detail[emblemslot])
4783 					goto bademblem;
4784 
4785 				emblem_detail[emblemslot] = emblem;
4786 				snprintf(emblem_text[emblemslot], 20, "%8s /%8s", currenttext, targettext);
4787 				emblem_text[emblemslot][19] = 0;
4788 
4789 				bademblem:
4790 				emblem = M_GetLevelEmblems(-1);
4791 			}
4792 		}
4793 		for (i = 0; i < 3; ++i)
4794 		{
4795 			emblem_t *emblem = emblem_detail[i];
4796 			if (!emblem)
4797 				continue;
4798 
4799 			if (emblem->collected)
4800 				V_DrawSmallMappedPatch(40, 44 + (i*8), 0, W_CachePatchName(M_GetEmblemPatch(emblem, false), PU_PATCH),
4801 				                       R_GetTranslationColormap(TC_DEFAULT, M_GetEmblemColor(emblem), GTC_CACHE));
4802 			else
4803 				V_DrawSmallScaledPatch(40, 44 + (i*8), 0, W_CachePatchName("NEEDIT", PU_PATCH));
4804 
4805 			switch (emblem->type)
4806 			{
4807 				case ET_SCORE:
4808 				case ET_NGRADE:
4809 					V_DrawString(56, 44 + (i*8), V_YELLOWMAP, "SCORE:");
4810 					break;
4811 				case ET_TIME:
4812 				case ET_NTIME:
4813 					V_DrawString(56, 44 + (i*8), V_YELLOWMAP, "TIME:");
4814 					break;
4815 				case ET_RINGS:
4816 					V_DrawString(56, 44 + (i*8), V_YELLOWMAP, "RINGS:");
4817 					break;
4818 			}
4819 			V_DrawRightAlignedString(284, 44 + (i*8), V_MONOSPACE, emblem_text[i]);
4820 		}
4821 	}
4822 
4823 	M_DrawGenericMenu();
4824 }
4825 
M_DrawCenteredMenu(void)4826 static void M_DrawCenteredMenu(void)
4827 {
4828 	INT32 x, y, i, cursory = 0;
4829 
4830 	// DRAW MENU
4831 	x = currentMenu->x;
4832 	y = currentMenu->y;
4833 
4834 	// draw title (or big pic)
4835 	M_DrawMenuTitle();
4836 
4837 	for (i = 0; i < currentMenu->numitems; i++)
4838 	{
4839 		if (i == itemOn)
4840 			cursory = y;
4841 		switch (currentMenu->menuitems[i].status & IT_DISPLAY)
4842 		{
4843 			case IT_PATCH:
4844 				if (currentMenu->menuitems[i].patch && currentMenu->menuitems[i].patch[0])
4845 				{
4846 					if (currentMenu->menuitems[i].status & IT_CENTER)
4847 					{
4848 						patch_t *p;
4849 						p = W_CachePatchName(currentMenu->menuitems[i].patch, PU_PATCH);
4850 						V_DrawScaledPatch((BASEVIDWIDTH - p->width)/2, y, 0, p);
4851 					}
4852 					else
4853 					{
4854 						V_DrawScaledPatch(x, y, 0,
4855 							W_CachePatchName(currentMenu->menuitems[i].patch, PU_PATCH));
4856 					}
4857 				}
4858 				/* FALLTHRU */
4859 			case IT_NOTHING:
4860 			case IT_DYBIGSPACE:
4861 				y += LINEHEIGHT;
4862 				break;
4863 			case IT_BIGSLIDER:
4864 				M_DrawThermo(x, y, (consvar_t *)currentMenu->menuitems[i].itemaction);
4865 				y += LINEHEIGHT;
4866 				break;
4867 			case IT_STRING:
4868 			case IT_WHITESTRING:
4869 				if (currentMenu->menuitems[i].alphaKey)
4870 					y = currentMenu->y+currentMenu->menuitems[i].alphaKey;
4871 				if (i == itemOn)
4872 					cursory = y;
4873 
4874 				if ((currentMenu->menuitems[i].status & IT_DISPLAY)==IT_STRING)
4875 					V_DrawCenteredString(x, y, 0, currentMenu->menuitems[i].text);
4876 				else
4877 					V_DrawCenteredString(x, y, V_YELLOWMAP, currentMenu->menuitems[i].text);
4878 
4879 				// Cvar specific handling
4880 				switch(currentMenu->menuitems[i].status & IT_TYPE)
4881 					case IT_CVAR:
4882 					{
4883 						consvar_t *cv = (consvar_t *)currentMenu->menuitems[i].itemaction;
4884 						switch(currentMenu->menuitems[i].status & IT_CVARTYPE)
4885 						{
4886 							case IT_CV_SLIDER:
4887 								M_DrawSlider(x, y, cv, (i == itemOn));
4888 							case IT_CV_NOPRINT: // color use this
4889 								break;
4890 							case IT_CV_STRING:
4891 								M_DrawTextBox(x, y + 4, MAXSTRINGLENGTH, 1);
4892 								V_DrawString(x + 8, y + 12, V_ALLOWLOWERCASE, cv->string);
4893 								if (skullAnimCounter < 4 && i == itemOn)
4894 									V_DrawCharacter(x + 8 + V_StringWidth(cv->string, 0), y + 12,
4895 										'_' | 0x80, false);
4896 								y += 16;
4897 								break;
4898 							default:
4899 								V_DrawString(BASEVIDWIDTH - x - V_StringWidth(cv->string, 0), y,
4900 									((cv->flags & CV_CHEAT) && !CV_IsSetToDefault(cv) ? V_REDMAP : V_YELLOWMAP), cv->string);
4901 								if (i == itemOn)
4902 								{
4903 									V_DrawCharacter(BASEVIDWIDTH - x - 10 - V_StringWidth(cv->string, 0) - (skullAnimCounter/5), y,
4904 											'\x1C' | V_YELLOWMAP, false);
4905 									V_DrawCharacter(BASEVIDWIDTH - x + 2 + (skullAnimCounter/5), y,
4906 											'\x1D' | V_YELLOWMAP, false);
4907 								}
4908 								break;
4909 						}
4910 						break;
4911 					}
4912 					y += STRINGHEIGHT;
4913 					break;
4914 			case IT_STRING2:
4915 				V_DrawCenteredString(x, y, 0, currentMenu->menuitems[i].text);
4916 				/* FALLTHRU */
4917 			case IT_DYLITLSPACE:
4918 				y += SMALLLINEHEIGHT;
4919 				break;
4920 			case IT_QUESTIONMARKS:
4921 				if (currentMenu->menuitems[i].alphaKey)
4922 					y = currentMenu->y+currentMenu->menuitems[i].alphaKey;
4923 
4924 				V_DrawCenteredString(x, y, V_TRANSLUCENT|V_OLDSPACING, M_CreateSecretMenuOption(currentMenu->menuitems[i].text));
4925 				y += SMALLLINEHEIGHT;
4926 				break;
4927 			case IT_GRAYPATCH:
4928 				if (currentMenu->menuitems[i].patch && currentMenu->menuitems[i].patch[0])
4929 					V_DrawMappedPatch(x, y, 0,
4930 						W_CachePatchName(currentMenu->menuitems[i].patch,PU_PATCH), graymap);
4931 				y += LINEHEIGHT;
4932 				break;
4933 		}
4934 	}
4935 
4936 	// DRAW THE SKULL CURSOR
4937 	if (((currentMenu->menuitems[itemOn].status & IT_DISPLAY) == IT_PATCH)
4938 		|| ((currentMenu->menuitems[itemOn].status & IT_DISPLAY) == IT_NOTHING))
4939 	{
4940 		V_DrawScaledPatch(x + SKULLXOFF, cursory - 5, 0,
4941 			W_CachePatchName("M_CURSOR", PU_PATCH));
4942 	}
4943 	else
4944 	{
4945 		V_DrawScaledPatch(x - V_StringWidth(currentMenu->menuitems[itemOn].text, 0)/2 - 24, cursory, 0,
4946 			W_CachePatchName("M_CURSOR", PU_PATCH));
4947 		V_DrawCenteredString(x, cursory, V_YELLOWMAP, currentMenu->menuitems[itemOn].text);
4948 	}
4949 }
4950 
4951 //
4952 // M_StringHeight
4953 //
4954 // Find string height from hu_font chars
4955 //
M_StringHeight(const char * string)4956 static inline size_t M_StringHeight(const char *string)
4957 {
4958 	size_t h = 8, i;
4959 
4960 	for (i = 0; i < strlen(string); i++)
4961 		if (string[i] == '\n')
4962 			h += 8;
4963 
4964 	return h;
4965 }
4966 
4967 // ==========================================================================
4968 // Extraneous menu patching functions
4969 // ==========================================================================
4970 
4971 //
4972 // M_PatchSkinNameTable
4973 //
4974 // Like M_PatchLevelNameTable, but for cv_chooseskin
4975 //
M_PatchSkinNameTable(void)4976 static void M_PatchSkinNameTable(void)
4977 {
4978 	INT32 j;
4979 
4980 	memset(skins_cons_t, 0, sizeof (skins_cons_t));
4981 
4982 	for (j = 0; j < MAXSKINS; j++)
4983 	{
4984 		if (skins[j].name[0] != '\0' && R_SkinUsable(-1, j))
4985 		{
4986 			skins_cons_t[j].strvalue = skins[j].realname;
4987 			skins_cons_t[j].value = j+1;
4988 		}
4989 		else
4990 		{
4991 			skins_cons_t[j].strvalue = NULL;
4992 			skins_cons_t[j].value = 0;
4993 		}
4994 	}
4995 
4996 	CV_SetValue(&cv_chooseskin, 1);
4997 	Nextmap_OnChange();
4998 
4999 	return;
5000 }
5001 
5002 //
5003 // M_LevelAvailableOnPlatter
5004 //
5005 // Okay, you know that the level SHOULD show up on the platter already.
5006 // The only question is whether it should be as a question mark,
5007 // (hinting as to its existence), or as its pure, unfettered self.
5008 //
M_LevelAvailableOnPlatter(INT32 mapnum)5009 static boolean M_LevelAvailableOnPlatter(INT32 mapnum)
5010 {
5011 	if (M_MapLocked(mapnum+1))
5012 		return false; // not unlocked
5013 
5014 	switch (levellistmode)
5015 	{
5016 		case LLM_CREATESERVER:
5017 			if (!(mapheaderinfo[mapnum]->typeoflevel & TOL_COOP))
5018 				return true;
5019 
5020 			if (mapnum+1 == spstage_start)
5021 				return true;
5022 
5023 #ifndef DEVELOP
5024 			if (mapvisited[mapnum]) // MV_MP
5025 #endif
5026 				return true;
5027 
5028 			/* FALLTHRU */
5029 		case LLM_RECORDATTACK:
5030 		case LLM_NIGHTSATTACK:
5031 #ifndef DEVELOP
5032 			if (mapvisited[mapnum] & MV_MAX)
5033 				return true;
5034 
5035 			if (mapheaderinfo[mapnum]->menuflags & LF2_NOVISITNEEDED)
5036 #endif
5037 				return true;
5038 
5039 			return false;
5040 		case LLM_LEVELSELECT:
5041 		default:
5042 			return true;
5043 	}
5044 	return true;
5045 }
5046 
5047 //
5048 // M_CanShowLevelOnPlatter
5049 //
5050 // Determines whether to show a given map in the various level-select lists.
5051 // Set gt = -1 to ignore gametype.
5052 //
M_CanShowLevelOnPlatter(INT32 mapnum,INT32 gt)5053 static boolean M_CanShowLevelOnPlatter(INT32 mapnum, INT32 gt)
5054 {
5055 	// Does the map exist?
5056 	if (!mapheaderinfo[mapnum])
5057 		return false;
5058 
5059 	// Does the map have a name?
5060 	if (!mapheaderinfo[mapnum]->lvlttl[0])
5061 		return false;
5062 
5063 	/*if (M_MapLocked(mapnum+1))
5064 		return false; // not unlocked*/
5065 
5066 	switch (levellistmode)
5067 	{
5068 		case LLM_CREATESERVER:
5069 			// Should the map be hidden?
5070 			if (mapheaderinfo[mapnum]->menuflags & LF2_HIDEINMENU)
5071 				return false;
5072 
5073 			if (G_IsSpecialStage(mapnum+1))
5074 				return false;
5075 
5076 			if (gt == GT_COOP && (mapheaderinfo[mapnum]->typeoflevel & TOL_COOP))
5077 				return true;
5078 
5079 			if (gt == GT_COMPETITION && (mapheaderinfo[mapnum]->typeoflevel & TOL_COMPETITION))
5080 				return true;
5081 
5082 			if (gt == GT_CTF && (mapheaderinfo[mapnum]->typeoflevel & TOL_CTF))
5083 				return true;
5084 
5085 			if ((gt == GT_MATCH || gt == GT_TEAMMATCH) && (mapheaderinfo[mapnum]->typeoflevel & TOL_MATCH))
5086 				return true;
5087 
5088 			if ((gt == GT_TAG || gt == GT_HIDEANDSEEK) && (mapheaderinfo[mapnum]->typeoflevel & TOL_TAG))
5089 				return true;
5090 
5091 			if (gt == GT_RACE && (mapheaderinfo[mapnum]->typeoflevel & TOL_RACE))
5092 				return true;
5093 
5094 			if (gt >= 0 && gt < gametypecount && (mapheaderinfo[mapnum]->typeoflevel & gametypetol[gt]))
5095 				return true;
5096 
5097 			return false;
5098 
5099 		case LLM_LEVELSELECT:
5100 			if (!(mapheaderinfo[mapnum]->levelselect & maplistoption))
5101 				return false;
5102 
5103 			return true;
5104 		case LLM_RECORDATTACK:
5105 			if (!(mapheaderinfo[mapnum]->menuflags & LF2_RECORDATTACK))
5106 				return false;
5107 
5108 			return true;
5109 		case LLM_NIGHTSATTACK:
5110 			if (!(mapheaderinfo[mapnum]->menuflags & LF2_NIGHTSATTACK))
5111 				return false;
5112 
5113 			return true;
5114 	}
5115 
5116 	// Hmm? Couldn't decide?
5117 	return false;
5118 }
5119 
5120 #if 0
M_CountLevelsToShowOnPlatter(INT32 gt)5121 static INT32 M_CountLevelsToShowOnPlatter(INT32 gt)
5122 {
5123 	INT32 mapnum, count = 0;
5124 
5125 	for (mapnum = 0; mapnum < NUMMAPS; mapnum++)
5126 		if (M_CanShowLevelOnPlatter(mapnum, gt))
5127 			count++;
5128 
5129 	return count;
5130 }
5131 #endif
5132 
5133 #if 0
M_SetNextMapOnPlatter(void)5134 static boolean M_SetNextMapOnPlatter(void)
5135 {
5136 	INT32 row, col = 0;
5137 	while (col < 3)
5138 	{
5139 		row = 0;
5140 		while (row < levelselect.numrows)
5141 		{
5142 			if (levelselect.rows[row].maplist[col] == cv_nextmap.value)
5143 			{
5144 				lsrow = row;
5145 				lscol = col;
5146 				return true;
5147 			}
5148 			row++;
5149 		}
5150 		col++;
5151 	}
5152 	return true;
5153 }
5154 #endif
5155 
M_GametypeHasLevels(INT32 gt)5156 static boolean M_GametypeHasLevels(INT32 gt)
5157 {
5158 	INT32 mapnum;
5159 
5160 	for (mapnum = 0; mapnum < NUMMAPS; mapnum++)
5161 		if (M_CanShowLevelOnPlatter(mapnum, gt))
5162 			return true;
5163 
5164 	return false;
5165 }
5166 
M_CountRowsToShowOnPlatter(INT32 gt)5167 static INT32 M_CountRowsToShowOnPlatter(INT32 gt)
5168 {
5169 	INT32 mapnum = 0, prevmapnum = 0, col = 0, rows = 0;
5170 
5171 	while (mapnum < NUMMAPS)
5172 	{
5173 		if (M_CanShowLevelOnPlatter(mapnum, gt))
5174 		{
5175 			if (rows == 0)
5176 				rows++;
5177 			else
5178 			{
5179 				if (col == 2
5180 				|| (mapheaderinfo[prevmapnum]->menuflags & LF2_WIDEICON)
5181 				|| (mapheaderinfo[mapnum]->menuflags & LF2_WIDEICON)
5182 				|| !(fastcmp(mapheaderinfo[mapnum]->selectheading, mapheaderinfo[prevmapnum]->selectheading)))
5183 				{
5184 					col = 0;
5185 					rows++;
5186 				}
5187 				else
5188 					col++;
5189 			}
5190 			prevmapnum = mapnum;
5191 		}
5192 		mapnum++;
5193 	}
5194 
5195 	if (levellistmode == LLM_CREATESERVER)
5196 		rows++;
5197 
5198 	return rows;
5199 }
5200 
5201 //
5202 // M_CacheLevelPlatter
5203 //
5204 // Cache every patch used by the level platter.
5205 //
M_CacheLevelPlatter(void)5206 static void M_CacheLevelPlatter(void)
5207 {
5208 	levselp[0][0] = W_CachePatchName("SLCT1LVL", PU_PATCH);
5209 	levselp[0][1] = W_CachePatchName("SLCT2LVL", PU_PATCH);
5210 	levselp[0][2] = W_CachePatchName("BLANKLVL", PU_PATCH);
5211 
5212 	levselp[1][0] = W_CachePatchName("SLCT1LVW", PU_PATCH);
5213 	levselp[1][1] = W_CachePatchName("SLCT2LVW", PU_PATCH);
5214 	levselp[1][2] = W_CachePatchName("BLANKLVW", PU_PATCH);
5215 }
5216 
5217 //
5218 // M_PrepareLevelPlatter
5219 //
5220 // Prepares a tasty dish of zones and acts!
5221 // Call before any attempt to access a level platter.
5222 //
M_PrepareLevelPlatter(INT32 gt,boolean nextmappick)5223 static boolean M_PrepareLevelPlatter(INT32 gt, boolean nextmappick)
5224 {
5225 	INT32 numrows = M_CountRowsToShowOnPlatter(gt);
5226 	INT32 mapnum = 0, prevmapnum = 0, col = 0, row = 0, startrow = 0;
5227 
5228 	if (!numrows)
5229 		return false;
5230 
5231 	if (levelselect.rows)
5232 		Z_Free(levelselect.rows);
5233 	levelselect.rows = NULL;
5234 
5235 	levelselect.numrows = numrows;
5236 	levelselect.rows = Z_Realloc(levelselect.rows, numrows*sizeof(levelselectrow_t), PU_STATIC, NULL);
5237 	if (!levelselect.rows)
5238 		I_Error("Insufficient memory to prepare level platter");
5239 
5240 	// done here so lsrow and lscol can be set if cv_nextmap is on the platter
5241 	lsrow = lscol = lshli = lsoffs[0] = lsoffs[1] = 0;
5242 
5243 	if (levellistmode == LLM_CREATESERVER)
5244 	{
5245 		sprintf(levelselect.rows[0].header, "Gametype");
5246 		lswide(0) = true;
5247 		levelselect.rows[row].mapavailable[2] = levelselect.rows[row].mapavailable[1] = levelselect.rows[row].mapavailable[0] = false;
5248 		startrow = row = 1;
5249 
5250 		Z_Free(char_notes);
5251 		char_notes = NULL;
5252 	}
5253 
5254 	while (mapnum < NUMMAPS)
5255 	{
5256 		if (M_CanShowLevelOnPlatter(mapnum, gt))
5257 		{
5258 			const UINT8 actnum = mapheaderinfo[mapnum]->actnum;
5259 			const boolean headingisname = (fastcmp(mapheaderinfo[mapnum]->selectheading, mapheaderinfo[mapnum]->lvlttl));
5260 			const boolean wide = (mapheaderinfo[mapnum]->menuflags & LF2_WIDEICON);
5261 
5262 			// preparing next position to drop mapnum into
5263 			if (levelselect.rows[startrow].maplist[0])
5264 			{
5265 				if (col == 2 // no more space on the row?
5266 				|| wide
5267 				|| (mapheaderinfo[prevmapnum]->menuflags & LF2_WIDEICON)
5268 				|| !(fastcmp(mapheaderinfo[mapnum]->selectheading, mapheaderinfo[prevmapnum]->selectheading))) // a new heading is starting?
5269 				{
5270 					col = 0;
5271 					row++;
5272 				}
5273 				else
5274 					col++;
5275 			}
5276 
5277 			levelselect.rows[row].maplist[col] = mapnum+1; // putting the map on the platter
5278 			levelselect.rows[row].mapavailable[col] = M_LevelAvailableOnPlatter(mapnum);
5279 
5280 			if ((lswide(row) = wide)) // intentionally assignment
5281 			{
5282 				levelselect.rows[row].maplist[2] = levelselect.rows[row].maplist[1] = levelselect.rows[row].maplist[0];
5283 				levelselect.rows[row].mapavailable[2] = levelselect.rows[row].mapavailable[1] = levelselect.rows[row].mapavailable[0];
5284 			}
5285 
5286 			if (nextmappick && cv_nextmap.value == mapnum+1) // A little quality of life improvement.
5287 			{
5288 				lsrow = row;
5289 				lscol = col;
5290 			}
5291 
5292 			// individual map name
5293 			if (levelselect.rows[row].mapavailable[col])
5294 			{
5295 				if (headingisname)
5296 				{
5297 					if (actnum)
5298 						sprintf(levelselect.rows[row].mapnames[col], "ACT %d", actnum);
5299 					else
5300 						sprintf(levelselect.rows[row].mapnames[col], "THE ACT");
5301 				}
5302 				else if (wide)
5303 				{
5304 					// Yes, with LF2_WIDEICON it'll continue on over into the next 17+1 char block. That's alright; col is always zero, the string is contiguous, and the maximum length is lvlttl[22] + ' ' + ZONE + ' ' + INT32, which is about 39 or so - barely crossing into the third column.
5305 					char* mapname = G_BuildMapTitle(mapnum+1);
5306 					strcpy(levelselect.rows[row].mapnames[col], (const char *)mapname);
5307 					Z_Free(mapname);
5308 				}
5309 				else
5310 				{
5311 					char mapname[22+1+11]; // lvlttl[22] + ' ' + INT32
5312 
5313 					if (actnum)
5314 						sprintf(mapname, "%s %d", mapheaderinfo[mapnum]->lvlttl, actnum);
5315 					else
5316 						strcpy(mapname, mapheaderinfo[mapnum]->lvlttl);
5317 
5318 					if (strlen(mapname) >= 17)
5319 						strcpy(mapname+17-3, "...");
5320 
5321 					strcpy(levelselect.rows[row].mapnames[col], (const char *)mapname);
5322 				}
5323 			}
5324 			else
5325 				sprintf(levelselect.rows[row].mapnames[col], "???");
5326 
5327 			// creating header text
5328 			if (!col && ((row == startrow) || !(fastcmp(mapheaderinfo[mapnum]->selectheading, mapheaderinfo[levelselect.rows[row-1].maplist[0]-1]->selectheading))))
5329 			{
5330 				if (!levelselect.rows[row].mapavailable[col])
5331 					sprintf(levelselect.rows[row].header, "???");
5332 				else
5333 				{
5334 					sprintf(levelselect.rows[row].header, "%s", mapheaderinfo[mapnum]->selectheading);
5335 					if (!(mapheaderinfo[mapnum]->levelflags & LF_NOZONE) && headingisname)
5336 					{
5337 						sprintf(levelselect.rows[row].header + strlen(levelselect.rows[row].header), " ZONE");
5338 					}
5339 				}
5340 			}
5341 
5342 			prevmapnum = mapnum;
5343 		}
5344 
5345 		mapnum++;
5346 	}
5347 
5348 #ifdef SYMMETRICAL_PLATTER
5349 	// horizontally space out rows with missing right sides
5350 	for (; row >= 0; row--)
5351 	{
5352 		if (!levelselect.rows[row].maplist[2] // no right side
5353 		&& levelselect.rows[row].maplist[0] && levelselect.rows[row].maplist[1]) // all the left filled in
5354 		{
5355 			levelselect.rows[row].maplist[2] = levelselect.rows[row].maplist[1];
5356 			STRBUFCPY(levelselect.rows[row].mapnames[2], levelselect.rows[row].mapnames[1]);
5357 			levelselect.rows[row].mapavailable[2] = levelselect.rows[row].mapavailable[1];
5358 
5359 			levelselect.rows[row].maplist[1] = -1; // diamond
5360 			levelselect.rows[row].mapnames[1][0] = '\0';
5361 			levelselect.rows[row].mapavailable[1] = false;
5362 		}
5363 	}
5364 #endif
5365 
5366 	M_CacheLevelPlatter();
5367 
5368 	return true;
5369 }
5370 
5371 #define ifselectvalnextmapnobrace(column) if ((selectval = levelselect.rows[lsrow].maplist[column]) && levelselect.rows[lsrow].mapavailable[column])\
5372 			{\
5373 				CV_SetValue(&cv_nextmap, selectval);
5374 
5375 #define ifselectvalnextmap(column) ifselectvalnextmapnobrace(column)}
5376 
5377 //
5378 // M_HandleLevelPlatter
5379 //
5380 // Reacts to your key inputs. Basically a mini menu thinker.
5381 //
M_HandleLevelPlatter(INT32 choice)5382 static void M_HandleLevelPlatter(INT32 choice)
5383 {
5384 	boolean exitmenu = false;  // exit to previous menu
5385 	INT32 selectval;
5386 	UINT8 iter;
5387 
5388 	switch (choice)
5389 	{
5390 		case KEY_DOWNARROW:
5391 			if (lsrow == levelselect.numrows-1)
5392 			{
5393 				if (levelselect.numrows < 3)
5394 				{
5395 					if (!lsoffs[0]) // prevent sound spam
5396 					{
5397 						lsoffs[0] = -8;
5398 						S_StartSound(NULL,sfx_s3kb7);
5399 					}
5400 					return;
5401 				}
5402 				lsrow = UINT8_MAX;
5403 			}
5404 			lsrow++;
5405 
5406 			lsoffs[0] = lsvseperation(lsrow);
5407 
5408 			if (levelselect.rows[lsrow].header[0])
5409 				lshli = lsrow;
5410 			// no else needed - headerless lines associate upwards, so moving down to a row without a header is identity
5411 
5412 			S_StartSound(NULL,sfx_s3kb7);
5413 
5414 			ifselectvalnextmap(lscol) else ifselectvalnextmap(0)
5415 			break;
5416 
5417 		case KEY_UPARROW:
5418 			iter = lsrow;
5419 			if (!lsrow)
5420 			{
5421 				if (levelselect.numrows < 3)
5422 				{
5423 					if (!lsoffs[0]) // prevent sound spam
5424 					{
5425 						lsoffs[0] = 8;
5426 						S_StartSound(NULL,sfx_s3kb7);
5427 					}
5428 					return;
5429 				}
5430 				lsrow = levelselect.numrows;
5431 			}
5432 			lsrow--;
5433 
5434 			lsoffs[0] = -lsvseperation(iter);
5435 
5436 			if (levelselect.rows[lsrow].header[0])
5437 				lshli = lsrow;
5438 			else
5439 			{
5440 				iter = lsrow;
5441 				do
5442 					iter = ((iter == 0) ? levelselect.numrows-1 : iter-1);
5443 				while ((iter != lsrow) && !(levelselect.rows[iter].header[0]));
5444 				lshli = iter;
5445 			}
5446 
5447 			S_StartSound(NULL,sfx_s3kb7);
5448 
5449 			ifselectvalnextmap(lscol) else ifselectvalnextmap(0)
5450 			break;
5451 
5452 		case KEY_ENTER:
5453 			if (!(levellistmode == LLM_CREATESERVER && !lsrow))
5454 			{
5455 				ifselectvalnextmapnobrace(lscol)
5456 					lsoffs[0] = lsoffs[1] = 0;
5457 					S_StartSound(NULL,sfx_menu1);
5458 					if (gamestate == GS_TIMEATTACK)
5459 						M_SetupNextMenu(currentMenu->prevMenu);
5460 					else if (currentMenu == &MISC_ChangeLevelDef)
5461 					{
5462 						if (currentMenu->prevMenu && currentMenu->prevMenu != &MPauseDef)
5463 							M_SetupNextMenu(currentMenu->prevMenu);
5464 						else
5465 							M_ChangeLevel(0);
5466 						Z_Free(levelselect.rows);
5467 						levelselect.rows = NULL;
5468 					}
5469 					else
5470 						M_LevelSelectWarp(0);
5471 					Nextmap_OnChange();
5472 				}
5473 				else if (!lsoffs[0]) // prevent sound spam
5474 				{
5475 					lsoffs[0] = -8;
5476 					S_StartSound(NULL,sfx_s3kb2);
5477 				}
5478 				break;
5479 			}
5480 			/* FALLTHRU */
5481 		case KEY_RIGHTARROW:
5482 			if (levellistmode == LLM_CREATESERVER && !lsrow)
5483 			{
5484 				INT32 startinggametype = cv_newgametype.value;
5485 				do
5486 					CV_AddValue(&cv_newgametype, 1);
5487 				while (cv_newgametype.value != startinggametype && !M_GametypeHasLevels(cv_newgametype.value));
5488 				S_StartSound(NULL,sfx_menu1);
5489 				lscol = 0;
5490 
5491 				Z_Free(char_notes);
5492 				char_notes = NULL;
5493 
5494 				if (!M_PrepareLevelPlatter(cv_newgametype.value, false))
5495 					I_Error("Unidentified level platter failure!");
5496 			}
5497 			else if (lscol < 2)
5498 			{
5499 				lscol++;
5500 
5501 				lsoffs[1] = (lswide(lsrow) ? 8 : -lshseperation);
5502 				S_StartSound(NULL,sfx_s3kb7);
5503 
5504 				ifselectvalnextmap(lscol) else ifselectvalnextmap(0)
5505 			}
5506 			else if (!lsoffs[1]) // prevent sound spam
5507 			{
5508 				lsoffs[1] = 8;
5509 				S_StartSound(NULL,sfx_s3kb7);
5510 			}
5511 			break;
5512 
5513 		case KEY_LEFTARROW:
5514 			if (levellistmode == LLM_CREATESERVER && !lsrow)
5515 			{
5516 				INT32 startinggametype = cv_newgametype.value;
5517 				do
5518 					CV_AddValue(&cv_newgametype, -1);
5519 				while (cv_newgametype.value != startinggametype && !M_GametypeHasLevels(cv_newgametype.value));
5520 				S_StartSound(NULL,sfx_menu1);
5521 				lscol = 0;
5522 
5523 				Z_Free(char_notes);
5524 				char_notes = NULL;
5525 
5526 				if (!M_PrepareLevelPlatter(cv_newgametype.value, false))
5527 					I_Error("Unidentified level platter failure!");
5528 			}
5529 			else if (lscol > 0)
5530 			{
5531 				lscol--;
5532 
5533 				lsoffs[1] = (lswide(lsrow) ? -8 : lshseperation);
5534 				S_StartSound(NULL,sfx_s3kb7);
5535 
5536 				ifselectvalnextmap(lscol) else ifselectvalnextmap(0)
5537 			}
5538 			else if (!lsoffs[1]) // prevent sound spam
5539 			{
5540 				lsoffs[1] = -8;
5541 				S_StartSound(NULL,sfx_s3kb7);
5542 			}
5543 			break;
5544 
5545 		case KEY_ESCAPE:
5546 			exitmenu = true;
5547 			break;
5548 
5549 		default:
5550 			break;
5551 	}
5552 
5553 	if (exitmenu)
5554 	{
5555 		if (gamestate != GS_TIMEATTACK)
5556 		{
5557 			Z_Free(levelselect.rows);
5558 			levelselect.rows = NULL;
5559 		}
5560 
5561 		if (currentMenu->prevMenu)
5562 		{
5563 			M_SetupNextMenu(currentMenu->prevMenu);
5564 			Nextmap_OnChange();
5565 		}
5566 		else
5567 			M_ClearMenus(true);
5568 
5569 		Z_Free(char_notes);
5570 		char_notes = NULL;
5571 	}
5572 }
5573 
5574 void M_DrawLevelPlatterHeader(INT32 y, const char *header, boolean headerhighlight, boolean allowlowercase)
5575 {
5576 	y += lsheadingheight - 12;
5577 	V_DrawString(19, y, (headerhighlight ? V_YELLOWMAP : 0)|(allowlowercase ? V_ALLOWLOWERCASE : 0), header);
5578 	y += 9;
5579 	V_DrawFill(19, y, 281, 1, (headerhighlight ? yellowmap[3] : 3));
5580 	V_DrawFill(300, y, 1, 1, 26);
5581 	y++;
5582 	V_DrawFill(19, y, 282, 1, 26);
5583 }
5584 
5585 static void M_DrawLevelPlatterWideMap(UINT8 row, UINT8 col, INT32 x, INT32 y, boolean highlight)
5586 {
5587 	patch_t *patch;
5588 
5589 	INT32 map = levelselect.rows[row].maplist[col];
5590 	if (map <= 0)
5591 		return;
5592 
5593 	//  A 564x100 image of the level as entry MAPxxW
5594 	if (!(levelselect.rows[row].mapavailable[col]))
5595 	{
5596 		V_DrawSmallScaledPatch(x, y, 0, levselp[1][2]);
5597 		M_DrawStaticBox(x, y, V_80TRANS, 282, 50);
5598 	}
5599 	else
5600 	{
5601 		if (W_CheckNumForName(va("%sW", G_BuildMapName(map))) != LUMPERROR)
5602 			patch = W_CachePatchName(va("%sW", G_BuildMapName(map)), PU_PATCH);
5603 		else
5604 			patch = levselp[1][2]; // don't static to indicate that it's just a normal level
5605 
5606 		V_DrawSmallScaledPatch(x, y, 0, patch);
5607 	}
5608 
5609 	V_DrawFill(x, y+50, 282, 8,
5610 		((mapheaderinfo[map-1]->unlockrequired < 0)
5611 		? 159 : 63));
5612 
5613 	V_DrawString(x, y+50, (highlight ? V_YELLOWMAP : 0), levelselect.rows[row].mapnames[col]);
5614 }
5615 
5616 static void M_DrawLevelPlatterMap(UINT8 row, UINT8 col, INT32 x, INT32 y, boolean highlight)
5617 {
5618 	patch_t *patch;
5619 
5620 	INT32 map = levelselect.rows[row].maplist[col];
5621 	if (map <= 0)
5622 		return;
5623 
5624 	//  A 160x100 image of the level as entry MAPxxP
5625 	if (!(levelselect.rows[row].mapavailable[col]))
5626 	{
5627 		V_DrawSmallScaledPatch(x, y, 0, levselp[0][2]);
5628 		M_DrawStaticBox(x, y, V_80TRANS, 80, 50);
5629 	}
5630 	else
5631 	{
5632 		if (W_CheckNumForName(va("%sP", G_BuildMapName(map))) != LUMPERROR)
5633 			patch = W_CachePatchName(va("%sP", G_BuildMapName(map)), PU_PATCH);
5634 		else
5635 			patch = levselp[0][2]; // don't static to indicate that it's just a normal level
5636 
5637 		V_DrawSmallScaledPatch(x, y, 0, patch);
5638 	}
5639 
5640 	V_DrawFill(x, y+50, 80, 8,
5641 		((mapheaderinfo[map-1]->unlockrequired < 0)
5642 		? 159 : 63));
5643 
5644 	if (strlen(levelselect.rows[row].mapnames[col]) > 6) // "AERIAL GARDEN" vs "ACT 18" - "THE ACT" intentionally compressed
5645 		V_DrawThinString(x, y+50, (highlight ? V_YELLOWMAP : 0), levelselect.rows[row].mapnames[col]);
5646 	else
5647 		V_DrawString(x, y+50, (highlight ? V_YELLOWMAP : 0), levelselect.rows[row].mapnames[col]);
5648 }
5649 
5650 static void M_DrawLevelPlatterRow(UINT8 row, INT32 y)
5651 {
5652 	UINT8 col;
5653 	const boolean rowhighlight = (row == lsrow);
5654 	if (levelselect.rows[row].header[0])
5655 	{
5656 		M_DrawLevelPlatterHeader(y, levelselect.rows[row].header, (rowhighlight || (row == lshli)), false);
5657 		y += lsheadingheight;
5658 	}
5659 
5660 	if (levellistmode == LLM_CREATESERVER && !row)
5661 	{
5662 		if (!char_notes)
5663 			char_notes = V_WordWrap(0, 282 - 8, V_ALLOWLOWERCASE, gametypedesc[cv_newgametype.value].notes);
5664 
5665 		V_DrawFill(lsbasex, y, 282, 50, 27);
5666 		V_DrawString(lsbasex + 4, y + 4, V_RETURN8|V_ALLOWLOWERCASE, char_notes);
5667 
5668 		V_DrawFill(lsbasex,     y+50, 141, 8, gametypedesc[cv_newgametype.value].col[0]);
5669 		V_DrawFill(lsbasex+141, y+50, 141, 8, gametypedesc[cv_newgametype.value].col[1]);
5670 
5671 		V_DrawString(lsbasex, y+50, 0, gametype_cons_t[cv_newgametype.value].strvalue);
5672 
5673 		if (!lsrow)
5674 		{
5675 			V_DrawCharacter(lsbasex - 10 - (skullAnimCounter/5), y+25,
5676 				'\x1C' | V_YELLOWMAP, false);
5677 			V_DrawCharacter(lsbasex+282 + 2 + (skullAnimCounter/5), y+25,
5678 				'\x1D' | V_YELLOWMAP, false);
5679 		}
5680 	}
5681 	else if (lswide(row))
5682 		M_DrawLevelPlatterWideMap(row, 0, lsbasex, y, rowhighlight);
5683 	else
5684 	{
5685 		for (col = 0; col < 3; col++)
5686 			M_DrawLevelPlatterMap(row, col, lsbasex+(col*lshseperation), y, (rowhighlight && (col == lscol)));
5687 	}
5688 }
5689 
5690 // new menus
5691 static void M_DrawRecordAttackForeground(void)
5692 {
5693 	patch_t *fg = W_CachePatchName("RECATKFG", PU_PATCH);
5694 	patch_t *clock = W_CachePatchName("RECCLOCK", PU_PATCH);
5695 	angle_t fa;
5696 
5697 	INT32 i;
5698 	INT32 height = (fg->height / 2);
5699 	INT32 dupz = (vid.dupx < vid.dupy ? vid.dupx : vid.dupy);
5700 
5701 	for (i = -12; i < (BASEVIDHEIGHT/height) + 12; i++)
5702 	{
5703 		INT32 y = ((i*height) - (height - ((recatkdrawtimer*2)%height)));
5704 		// don't draw above the screen
5705 		{
5706 			INT32 sy = FixedMul(y, dupz<<FRACBITS) >> FRACBITS;
5707 			if (vid.height != BASEVIDHEIGHT * dupz)
5708 				sy += (vid.height - (BASEVIDHEIGHT * dupz)) / 2;
5709 			if ((sy+height) < 0)
5710 				continue;
5711 		}
5712 		V_DrawFixedPatch(0, y<<FRACBITS, FRACUNIT/2, V_SNAPTOLEFT, fg, NULL);
5713 		V_DrawFixedPatch(BASEVIDWIDTH<<FRACBITS, y<<FRACBITS, FRACUNIT/2, V_SNAPTORIGHT|V_FLIP, fg, NULL);
5714 		// don't draw below the screen
5715 		if (y > vid.height)
5716 			break;
5717 	}
5718 
5719 	// draw clock
5720 	fa = (FixedAngle(((recatkdrawtimer * 4) % 360)<<FRACBITS)>>ANGLETOFINESHIFT) & FINEMASK;
5721 	V_DrawSciencePatch(160<<FRACBITS, (80<<FRACBITS) + (4*FINESINE(fa)), 0, clock, FRACUNIT);
5722 
5723 	// Increment timer.
5724 	recatkdrawtimer++;
5725 }
5726 
5727 // NiGHTS Attack background.
5728 static void M_DrawNightsAttackMountains(void)
5729 {
5730 	static INT32 bgscrollx;
5731 	INT32 dupz = (vid.dupx < vid.dupy ? vid.dupx : vid.dupy);
5732 	patch_t *background = W_CachePatchName(curbgname, PU_PATCH);
5733 	INT16 w = background->width;
5734 	INT32 x = FixedInt(-bgscrollx) % w;
5735 	INT32 y = BASEVIDHEIGHT - (background->height * 2);
5736 
5737 	if (vid.height != BASEVIDHEIGHT * dupz)
5738 		V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 158);
5739 	V_DrawFill(0, y+50, vid.width, BASEVIDHEIGHT, V_SNAPTOLEFT|31);
5740 
5741 	V_DrawScaledPatch(x, y, V_SNAPTOLEFT, background);
5742 	x += w;
5743 	if (x < BASEVIDWIDTH)
5744 		V_DrawScaledPatch(x, y, V_SNAPTOLEFT, background);
5745 
5746 	bgscrollx += (FRACUNIT/2);
5747 	if (bgscrollx > w<<FRACBITS)
5748 		bgscrollx &= 0xFFFF;
5749 }
5750 
5751 // NiGHTS Attack foreground.
5752 static void M_DrawNightsAttackBackground(void)
5753 {
5754 	INT32 x, y = 0;
5755 	INT32 i;
5756 
5757 	// top
5758 	patch_t *backtopfg = W_CachePatchName("NTSATKT1", PU_PATCH);
5759 	patch_t *fronttopfg = W_CachePatchName("NTSATKT2", PU_PATCH);
5760 	INT32 backtopwidth = backtopfg->width;
5761 	//INT32 backtopheight = backtopfg->height;
5762 	INT32 fronttopwidth = fronttopfg->width;
5763 	//INT32 fronttopheight = fronttopfg->height;
5764 
5765 	// bottom
5766 	patch_t *backbottomfg = W_CachePatchName("NTSATKB1", PU_PATCH);
5767 	patch_t *frontbottomfg = W_CachePatchName("NTSATKB2", PU_PATCH);
5768 	INT32 backbottomwidth = backbottomfg->width;
5769 	INT32 backbottomheight = backbottomfg->height;
5770 	INT32 frontbottomwidth = frontbottomfg->width;
5771 	INT32 frontbottomheight = frontbottomfg->height;
5772 
5773 	// background
5774 	M_DrawNightsAttackMountains();
5775 
5776 	// back top foreground patch
5777 	x = 0-(ntsatkdrawtimer%backtopwidth);
5778 	V_DrawScaledPatch(x, y, V_SNAPTOTOP|V_SNAPTOLEFT, backtopfg);
5779 	for (i = 0; i < 3; i++)
5780 	{
5781 		x += (backtopwidth);
5782 		if (x >= vid.width)
5783 			break;
5784 		V_DrawScaledPatch(x, y, V_SNAPTOTOP|V_SNAPTOLEFT, backtopfg);
5785 	}
5786 
5787 	// front top foreground patch
5788 	x = 0-((ntsatkdrawtimer*2)%fronttopwidth);
5789 	V_DrawScaledPatch(x, y, V_SNAPTOTOP|V_SNAPTOLEFT, fronttopfg);
5790 	for (i = 0; i < 3; i++)
5791 	{
5792 		x += (fronttopwidth);
5793 		if (x >= vid.width)
5794 			break;
5795 		V_DrawScaledPatch(x, y, V_SNAPTOTOP|V_SNAPTOLEFT, fronttopfg);
5796 	}
5797 
5798 	// back bottom foreground patch
5799 	x = 0-(ntsatkdrawtimer%backbottomwidth);
5800 	y = BASEVIDHEIGHT - backbottomheight;
5801 	V_DrawScaledPatch(x, y, V_SNAPTOBOTTOM|V_SNAPTOLEFT, backbottomfg);
5802 	for (i = 0; i < 3; i++)
5803 	{
5804 		x += (backbottomwidth);
5805 		if (x >= vid.width)
5806 			break;
5807 		V_DrawScaledPatch(x, y, V_SNAPTOBOTTOM|V_SNAPTOLEFT, backbottomfg);
5808 	}
5809 
5810 	// front bottom foreground patch
5811 	x = 0-((ntsatkdrawtimer*2)%frontbottomwidth);
5812 	y = BASEVIDHEIGHT - frontbottomheight;
5813 	V_DrawScaledPatch(x, y, V_SNAPTOBOTTOM|V_SNAPTOLEFT, frontbottomfg);
5814 	for (i = 0; i < 3; i++)
5815 	{
5816 		x += (frontbottomwidth);
5817 		if (x >= vid.width)
5818 			break;
5819 		V_DrawScaledPatch(x, y, V_SNAPTOBOTTOM|V_SNAPTOLEFT, frontbottomfg);
5820 	}
5821 
5822 	// Increment timer.
5823 	ntsatkdrawtimer++;
5824 }
5825 
5826 // NiGHTS Attack floating Super Sonic.
5827 static patch_t *ntssupersonic[2];
5828 static void M_DrawNightsAttackSuperSonic(void)
5829 {
5830 	const UINT8 *colormap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_YELLOW, GTC_CACHE);
5831 	INT32 timer = (ntsatkdrawtimer/4) % 2;
5832 	angle_t fa = (FixedAngle(((ntsatkdrawtimer * 4) % 360)<<FRACBITS)>>ANGLETOFINESHIFT) & FINEMASK;
5833 	V_DrawFixedPatch(235<<FRACBITS, (120<<FRACBITS) - (8*FINESINE(fa)), FRACUNIT, 0, ntssupersonic[timer], colormap);
5834 }
5835 
5836 static void M_DrawLevelPlatterMenu(void)
5837 {
5838 	UINT8 iter = lsrow, sizeselect = (lswide(lsrow) ? 1 : 0);
5839 	INT32 y = lsbasey + lsoffs[0] - getheadingoffset(lsrow);
5840 	const INT32 cursorx = (sizeselect ? 0 : (lscol*lshseperation));
5841 
5842 	if (currentMenu->prevMenu == &SP_TimeAttackDef)
5843 	{
5844 		M_SetMenuCurBackground("RECATKBG");
5845 
5846 		curbgxspeed = 0;
5847 		curbgyspeed = 18;
5848 
5849 		if (curbgcolor >= 0)
5850 			V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, curbgcolor);
5851 		else if (!curbghide || !titlemapinaction)
5852 		{
5853 			F_SkyScroll(curbgxspeed, curbgyspeed, curbgname);
5854 			// Draw and animate foreground
5855 			if (!strncmp("RECATKBG", curbgname, 8))
5856 				M_DrawRecordAttackForeground();
5857 		}
5858 
5859 		if (curfadevalue)
5860 			V_DrawFadeScreen(0xFF00, curfadevalue);
5861 	}
5862 
5863 	if (currentMenu->prevMenu == &SP_NightsAttackDef)
5864 	{
5865 		M_SetMenuCurBackground("NTSATKBG");
5866 
5867 		if (curbgcolor >= 0)
5868 			V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, curbgcolor);
5869 		else if (!curbghide || !titlemapinaction)
5870 		{
5871 			V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 158);
5872 			M_DrawNightsAttackMountains();
5873 		}
5874 		if (curfadevalue)
5875 			V_DrawFadeScreen(0xFF00, curfadevalue);
5876 	}
5877 
5878 	// finds row at top of the screen
5879 	while (y > -8)
5880 	{
5881 		if (iter == 0)
5882 		{
5883 			if (levelselect.numrows < 3)
5884 				break;
5885 			iter = levelselect.numrows;
5886 		}
5887 		iter--;
5888 		y -= lsvseperation(iter);
5889 	}
5890 
5891 	// draw from top to bottom
5892 	while (y < (vid.height/vid.dupy))
5893 	{
5894 		M_DrawLevelPlatterRow(iter, y);
5895 		y += lsvseperation(iter);
5896 		if (iter == levelselect.numrows-1)
5897 		{
5898 			if (levelselect.numrows < 3)
5899 				break;
5900 			iter = UINT8_MAX;
5901 		}
5902 		iter++;
5903 	}
5904 
5905 	// draw cursor box
5906 	if (levellistmode != LLM_CREATESERVER || lsrow)
5907 		V_DrawSmallScaledPatch(lsbasex + cursorx + lsoffs[1], lsbasey+lsoffs[0], 0, (levselp[sizeselect][((skullAnimCounter/4) ? 1 : 0)]));
5908 
5909 #if 0
5910 	if (levelselect.rows[lsrow].maplist[lscol] > 0)
5911 		V_DrawScaledPatch(lsbasex + cursorx-17, lsbasey+50+lsoffs[0], 0, W_CachePatchName("M_CURSOR", PU_PATCH));
5912 #endif
5913 
5914 	// handle movement of cursor box
5915 	if (lsoffs[0] > 1 || lsoffs[0] < -1)
5916 		lsoffs[0] = 2*lsoffs[0]/3;
5917 	else
5918 		lsoffs[0] = 0;
5919 
5920 	if (lsoffs[1] > 1 || lsoffs[1] < -1)
5921 		lsoffs[1] = 2*lsoffs[1]/3;
5922 	else
5923 		lsoffs[1] = 0;
5924 
5925 	M_DrawMenuTitle();
5926 }
5927 
5928 //
5929 // M_CanShowLevelInList
5930 //
5931 // Determines whether to show a given map in level-select lists where you don't want to see locked levels.
5932 // Set gt = -1 to ignore gametype.
5933 //
5934 boolean M_CanShowLevelInList(INT32 mapnum, INT32 gt)
5935 {
5936 	return (M_CanShowLevelOnPlatter(mapnum, gt) && M_LevelAvailableOnPlatter(mapnum));
5937 }
5938 
5939 static INT32 M_GetFirstLevelInList(INT32 gt)
5940 {
5941 	INT32 mapnum;
5942 
5943 	for (mapnum = 0; mapnum < NUMMAPS; mapnum++)
5944 		if (M_CanShowLevelInList(mapnum, gt))
5945 			return mapnum + 1;
5946 
5947 	return 1;
5948 }
5949 
5950 // ==================================================
5951 // MESSAGE BOX (aka: a hacked, cobbled together menu)
5952 // ==================================================
5953 static void M_DrawMessageMenu(void);
5954 
5955 // Because this is just a hack-ish 'menu', I'm not putting this with the others
5956 static menuitem_t MessageMenu[] =
5957 {
5958 	// TO HACK
5959 	{0,NULL, NULL, NULL,0}
5960 };
5961 
5962 menu_t MessageDef =
5963 {
5964 	MN_SPECIAL,
5965 	NULL,               // title
5966 	1,                  // # of menu items
5967 	NULL,               // previous menu       (TO HACK)
5968 	MessageMenu,        // menuitem_t ->
5969 	M_DrawMessageMenu,  // drawing routine ->
5970 	0, 0,               // x, y                (TO HACK)
5971 	0,                  // lastOn, flags       (TO HACK)
5972 	NULL
5973 };
5974 
5975 
5976 void M_StartMessage(const char *string, void *routine,
5977 	menumessagetype_t itemtype)
5978 {
5979 	size_t max = 0, start = 0, i, strlines;
5980 	static char *message = NULL;
5981 	Z_Free(message);
5982 	message = Z_StrDup(string);
5983 	DEBFILE(message);
5984 
5985 	// Rudementary word wrapping.
5986 	// Simple and effective. Does not handle nonuniform letter sizes, colors, etc. but who cares.
5987 	strlines = 0;
5988 	for (i = 0; message[i]; i++)
5989 	{
5990 		if (message[i] == ' ')
5991 		{
5992 			start = i;
5993 			max += 4;
5994 		}
5995 		else if (message[i] == '\n')
5996 		{
5997 			strlines = i;
5998 			start = 0;
5999 			max = 0;
6000 			continue;
6001 		}
6002 		else
6003 			max += 8;
6004 
6005 		// Start trying to wrap if presumed length exceeds the screen width.
6006 		if (max >= BASEVIDWIDTH && start > 0)
6007 		{
6008 			message[start] = '\n';
6009 			max -= (start-strlines)*8;
6010 			strlines = start;
6011 			start = 0;
6012 		}
6013 	}
6014 
6015 	start = 0;
6016 	max = 0;
6017 
6018 	M_StartControlPanel(); // can't put menuactive to true
6019 
6020 	if (currentMenu == &MessageDef) // Prevent recursion
6021 		MessageDef.prevMenu = &MainDef;
6022 	else
6023 		MessageDef.prevMenu = currentMenu;
6024 
6025 	MessageDef.menuitems[0].text     = message;
6026 	MessageDef.menuitems[0].alphaKey = (UINT8)itemtype;
6027 	if (!routine && itemtype != MM_NOTHING) itemtype = MM_NOTHING;
6028 	switch (itemtype)
6029 	{
6030 		case MM_NOTHING:
6031 			MessageDef.menuitems[0].status     = IT_MSGHANDLER;
6032 			MessageDef.menuitems[0].itemaction = M_StopMessage;
6033 			break;
6034 		case MM_YESNO:
6035 			MessageDef.menuitems[0].status     = IT_MSGHANDLER;
6036 			MessageDef.menuitems[0].itemaction = routine;
6037 			break;
6038 		case MM_EVENTHANDLER:
6039 			MessageDef.menuitems[0].status     = IT_MSGHANDLER;
6040 			MessageDef.menuitems[0].itemaction = routine;
6041 			break;
6042 	}
6043 	//added : 06-02-98: now draw a textbox around the message
6044 	// compute lenght max and the numbers of lines
6045 	for (strlines = 0; *(message+start); strlines++)
6046 	{
6047 		for (i = 0;i < strlen(message+start);i++)
6048 		{
6049 			if (*(message+start+i) == '\n')
6050 			{
6051 				if (i > max)
6052 					max = i;
6053 				start += i;
6054 				i = (size_t)-1; //added : 07-02-98 : damned!
6055 				start++;
6056 				break;
6057 			}
6058 		}
6059 
6060 		if (i == strlen(message+start))
6061 			start += i;
6062 	}
6063 
6064 	MessageDef.x = (INT16)((BASEVIDWIDTH  - 8*max-16)/2);
6065 	MessageDef.y = (INT16)((BASEVIDHEIGHT - M_StringHeight(message))/2);
6066 
6067 	MessageDef.lastOn = (INT16)((strlines<<8)+max);
6068 
6069 	//M_SetupNextMenu();
6070 	currentMenu = &MessageDef;
6071 	itemOn = 0;
6072 }
6073 
6074 #define MAXMSGLINELEN 256
6075 
6076 static void M_DrawMessageMenu(void)
6077 {
6078 	INT32 y = currentMenu->y;
6079 	size_t i, start = 0;
6080 	INT16 max;
6081 	char string[MAXMSGLINELEN];
6082 	INT32 mlines;
6083 	const char *msg = currentMenu->menuitems[0].text;
6084 
6085 	mlines = currentMenu->lastOn>>8;
6086 	max = (INT16)((UINT8)(currentMenu->lastOn & 0xFF)*8);
6087 
6088 	// hack: draw RA background in RA menus
6089 	if (gamestate == GS_TIMEATTACK)
6090 	{
6091 		if (curbgcolor >= 0)
6092 			V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, curbgcolor);
6093 		else if (!curbghide || !titlemapinaction)
6094 		{
6095 			if (levellistmode == LLM_NIGHTSATTACK)
6096 			{
6097 				V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 158);
6098 				M_DrawNightsAttackMountains();
6099 			}
6100 			else
6101 			{
6102 				F_SkyScroll(curbgxspeed, curbgyspeed, curbgname);
6103 				if (!strncmp("RECATKBG", curbgname, 8))
6104 					M_DrawRecordAttackForeground();
6105 			}
6106 		}
6107 		if (curfadevalue)
6108 			V_DrawFadeScreen(0xFF00, curfadevalue);
6109 	}
6110 
6111 	M_DrawTextBox(currentMenu->x, y - 8, (max+7)>>3, mlines);
6112 
6113 	while (*(msg+start))
6114 	{
6115 		size_t len = strlen(msg+start);
6116 
6117 		for (i = 0; i < len; i++)
6118 		{
6119 			if (*(msg+start+i) == '\n')
6120 			{
6121 				memset(string, 0, MAXMSGLINELEN);
6122 				if (i >= MAXMSGLINELEN)
6123 				{
6124 					CONS_Printf("M_DrawMessageMenu: too long segment in %s\n", msg);
6125 					return;
6126 				}
6127 				else
6128 				{
6129 					strncpy(string,msg+start, i);
6130 					string[i] = '\0';
6131 					start += i;
6132 					i = (size_t)-1; //added : 07-02-98 : damned!
6133 					start++;
6134 				}
6135 				break;
6136 			}
6137 		}
6138 
6139 		if (i == strlen(msg+start))
6140 		{
6141 			if (i >= MAXMSGLINELEN)
6142 			{
6143 				CONS_Printf("M_DrawMessageMenu: too long segment in %s\n", msg);
6144 				return;
6145 			}
6146 			else
6147 			{
6148 				strcpy(string, msg + start);
6149 				start += i;
6150 			}
6151 		}
6152 
6153 		V_DrawString((BASEVIDWIDTH - V_StringWidth(string, 0))/2,y,V_ALLOWLOWERCASE,string);
6154 		y += 8; //hu_font[0]->height;
6155 	}
6156 }
6157 
6158 // default message handler
6159 static void M_StopMessage(INT32 choice)
6160 {
6161 	(void)choice;
6162 	if (menuactive)
6163 		M_SetupNextMenu(MessageDef.prevMenu);
6164 }
6165 
6166 // =========
6167 // IMAGEDEFS
6168 // =========
6169 
6170 // Draw an Image Def.  Aka, Help images.
6171 // Defines what image is used in (menuitem_t)->text.
6172 // You can even put multiple images in one menu!
6173 static void M_DrawImageDef(void)
6174 {
6175 	// Grr.  Need to autodetect for pic_ts.
6176 	pic_t *pictest = (pic_t *)W_CacheLumpName(currentMenu->menuitems[itemOn].text,PU_CACHE);
6177 	if (!pictest->zero)
6178 		V_DrawScaledPic(0,0,0,W_GetNumForName(currentMenu->menuitems[itemOn].text));
6179 	else
6180 	{
6181 		patch_t *patch = W_CachePatchName(currentMenu->menuitems[itemOn].text,PU_PATCH);
6182 		if (patch->width <= BASEVIDWIDTH)
6183 			V_DrawScaledPatch(0,0,0,patch);
6184 		else
6185 			V_DrawSmallScaledPatch(0,0,0,patch);
6186 	}
6187 
6188 	if (currentMenu->numitems > 1)
6189 		V_DrawString(0,192,V_TRANSLUCENT, va("PAGE %d of %hd", itemOn+1, currentMenu->numitems));
6190 }
6191 
6192 // Handles the ImageDefs.  Just a specialized function that
6193 // uses left and right movement.
6194 static void M_HandleImageDef(INT32 choice)
6195 {
6196 	switch (choice)
6197 	{
6198 		case KEY_RIGHTARROW:
6199 			if (currentMenu->numitems == 1)
6200 				break;
6201 
6202 			S_StartSound(NULL, sfx_menu1);
6203 			if (itemOn >= (INT16)(currentMenu->numitems-1))
6204 				itemOn = 0;
6205             else itemOn++;
6206 			break;
6207 
6208 		case KEY_LEFTARROW:
6209 			if (currentMenu->numitems == 1)
6210 				break;
6211 
6212 			S_StartSound(NULL, sfx_menu1);
6213 			if (!itemOn)
6214 				itemOn = currentMenu->numitems - 1;
6215 			else itemOn--;
6216 			break;
6217 
6218 		case KEY_ESCAPE:
6219 		case KEY_ENTER:
6220 			M_ClearMenus(true);
6221 			break;
6222 	}
6223 }
6224 
6225 // ======================
6226 // MISC MAIN MENU OPTIONS
6227 // ======================
6228 
6229 static void M_AddonsOptions(INT32 choice)
6230 {
6231 	(void)choice;
6232 	Addons_option_Onchange();
6233 
6234 	M_SetupNextMenu(&OP_AddonsOptionsDef);
6235 }
6236 
6237 #define LOCATIONSTRING1 "Visit \x83SRB2.ORG/ADDONS\x80 to get & make addons!"
6238 //#define LOCATIONSTRING2 "Visit \x88SRB2.ORG/ADDONS\x80 to get & make addons!"
6239 
6240 static void M_LoadAddonsPatches(void)
6241 {
6242 	addonsp[EXT_FOLDER] = W_CachePatchName("M_FFLDR", PU_PATCH);
6243 	addonsp[EXT_UP] = W_CachePatchName("M_FBACK", PU_PATCH);
6244 	addonsp[EXT_NORESULTS] = W_CachePatchName("M_FNOPE", PU_PATCH);
6245 	addonsp[EXT_TXT] = W_CachePatchName("M_FTXT", PU_PATCH);
6246 	addonsp[EXT_CFG] = W_CachePatchName("M_FCFG", PU_PATCH);
6247 	addonsp[EXT_WAD] = W_CachePatchName("M_FWAD", PU_PATCH);
6248 #ifdef USE_KART
6249 	addonsp[EXT_KART] = W_CachePatchName("M_FKART", PU_PATCH);
6250 #endif
6251 	addonsp[EXT_PK3] = W_CachePatchName("M_FPK3", PU_PATCH);
6252 	addonsp[EXT_SOC] = W_CachePatchName("M_FSOC", PU_PATCH);
6253 	addonsp[EXT_LUA] = W_CachePatchName("M_FLUA", PU_PATCH);
6254 	addonsp[NUM_EXT] = W_CachePatchName("M_FUNKN", PU_PATCH);
6255 	addonsp[NUM_EXT+1] = W_CachePatchName("M_FSEL", PU_PATCH);
6256 	addonsp[NUM_EXT+2] = W_CachePatchName("M_FLOAD", PU_PATCH);
6257 	addonsp[NUM_EXT+3] = W_CachePatchName("M_FSRCH", PU_PATCH);
6258 	addonsp[NUM_EXT+4] = W_CachePatchName("M_FSAVE", PU_PATCH);
6259 }
6260 
6261 static void M_Addons(INT32 choice)
6262 {
6263 	const char *pathname = ".";
6264 
6265 	(void)choice;
6266 
6267 	// If M_GetGameypeColor() is ever ported from Kart, then remove this.
6268 	highlightflags = V_YELLOWMAP;
6269 	recommendedflags = V_GREENMAP;
6270 	warningflags = V_REDMAP;
6271 
6272 #if 1
6273 	if (cv_addons_option.value == 0)
6274 		pathname = usehome ? srb2home : srb2path;
6275 	else if (cv_addons_option.value == 1)
6276 		pathname = srb2home;
6277 	else if (cv_addons_option.value == 2)
6278 		pathname = srb2path;
6279 	else
6280 #endif
6281 	if (cv_addons_option.value == 3 && *cv_addons_folder.string != '\0')
6282 		pathname = cv_addons_folder.string;
6283 
6284 	strlcpy(menupath, pathname, 1024);
6285 	menupathindex[(menudepthleft = menudepth-1)] = strlen(menupath) + 1;
6286 
6287 	if (menupath[menupathindex[menudepthleft]-2] != PATHSEP[0])
6288 	{
6289 		menupath[menupathindex[menudepthleft]-1] = PATHSEP[0];
6290 		menupath[menupathindex[menudepthleft]] = 0;
6291 	}
6292 	else
6293 		--menupathindex[menudepthleft];
6294 
6295 	if (!preparefilemenu(false))
6296 	{
6297 		M_StartMessage(va("No files/folders found.\n\n%s\n\n(Press a key)\n",LOCATIONSTRING1),NULL,MM_NOTHING);
6298 			// (recommendedflags == V_SKYMAP ? LOCATIONSTRING2 : LOCATIONSTRING1))
6299 		return;
6300 	}
6301 	else
6302 		dir_on[menudepthleft] = 0;
6303 
6304 	M_LoadAddonsPatches();
6305 
6306 	MISC_AddonsDef.prevMenu = currentMenu;
6307 	M_SetupNextMenu(&MISC_AddonsDef);
6308 }
6309 
6310 #define width 4
6311 #define vpadding 27
6312 #define h (BASEVIDHEIGHT-(2*vpadding))
6313 #define NUMCOLOURS 8 // when toast's coding it's british english hacker fucker
6314 static void M_DrawTemperature(INT32 x, fixed_t t)
6315 {
6316 	INT32 y;
6317 
6318 	// bounds check
6319 	if (t > FRACUNIT)
6320 		t = FRACUNIT;
6321 	/*else if (t < 0) -- not needed
6322 		t = 0;*/
6323 
6324 	// scale
6325 	if (t > 1)
6326 		t = (FixedMul(h<<FRACBITS, t)>>FRACBITS);
6327 
6328 	// border
6329 	V_DrawFill(x - 1, vpadding, 1, h, 3);
6330 	V_DrawFill(x + width, vpadding, 1, h, 3);
6331 	V_DrawFill(x - 1, vpadding-1, width+2, 1, 3);
6332 	V_DrawFill(x - 1, vpadding+h, width+2, 1, 3);
6333 
6334 	// bar itself
6335 	y = h;
6336 	if (t)
6337 		for (t = h - t; y > 0; y--)
6338 		{
6339 			UINT8 colours[NUMCOLOURS] = {42, 40, 58, 222, 65, 90, 97, 98};
6340 			UINT8 c;
6341 			if (y <= t) break;
6342 			if (y+vpadding >= BASEVIDHEIGHT/2)
6343 				c = 113;
6344 			else
6345 				c = colours[(NUMCOLOURS*(y-1))/(h/2)];
6346 			V_DrawFill(x, y-1 + vpadding, width, 1, c);
6347 		}
6348 
6349 	// fill the rest of the backing
6350 	if (y)
6351 		V_DrawFill(x, vpadding, width, y, 27);
6352 }
6353 #undef width
6354 #undef vpadding
6355 #undef h
6356 #undef NUMCOLOURS
6357 
6358 static char *M_AddonsHeaderPath(void)
6359 {
6360 	UINT32 len;
6361 	static char header[1024];
6362 
6363 	strlcpy(header, va("%s folder%s", cv_addons_option.string, menupath+menupathindex[menudepth-1]-1), 1024);
6364 	len = strlen(header);
6365 	if (len > 34)
6366 	{
6367 		len = len-34;
6368 		header[len] = header[len+1] = header[len+2] = '.';
6369 	}
6370 	else
6371 		len = 0;
6372 
6373 	return header+len;
6374 }
6375 
6376 #define UNEXIST S_StartSound(NULL, sfx_lose);\
6377 		M_SetupNextMenu(MISC_AddonsDef.prevMenu);\
6378 		M_StartMessage(va("\x82%s\x80\nThis folder no longer exists!\nAborting to main menu.\n\n(Press a key)\n", M_AddonsHeaderPath()),NULL,MM_NOTHING)
6379 
6380 #define CLEARNAME Z_Free(refreshdirname);\
6381 					refreshdirname = NULL
6382 
6383 static void M_AddonsClearName(INT32 choice)
6384 {
6385 	(void)choice;
6386 	CLEARNAME;
6387 }
6388 
6389 // returns whether to do message draw
6390 static boolean M_AddonsRefresh(void)
6391 {
6392 	if ((refreshdirmenu & REFRESHDIR_NORMAL) && !preparefilemenu(true))
6393 	{
6394 		UNEXIST;
6395 		return true;
6396 	}
6397 
6398 	if (refreshdirmenu & REFRESHDIR_ADDFILE)
6399 	{
6400 		char *message = NULL;
6401 
6402 		if (refreshdirmenu & REFRESHDIR_NOTLOADED)
6403 		{
6404 			S_StartSound(NULL, sfx_lose);
6405 			if (refreshdirmenu & REFRESHDIR_MAX)
6406 				message = va("%c%s\x80\nMaximum number of add-ons reached.\nA file could not be loaded.\nIf you wish to play with this add-on, restart the game to clear existing ones.\n\n(Press a key)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), refreshdirname);
6407 			else
6408 				message = va("%c%s\x80\nA file was not loaded.\nCheck the console log for more information.\n\n(Press a key)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), refreshdirname);
6409 		}
6410 		else if (refreshdirmenu & (REFRESHDIR_WARNING|REFRESHDIR_ERROR))
6411 		{
6412 			S_StartSound(NULL, sfx_skid);
6413 			message = va("%c%s\x80\nA file was loaded with %s.\nCheck the console log for more information.\n\n(Press a key)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), refreshdirname, ((refreshdirmenu & REFRESHDIR_ERROR) ? "errors" : "warnings"));
6414 		}
6415 
6416 		if (message)
6417 		{
6418 			M_StartMessage(message,M_AddonsClearName,MM_NOTHING);
6419 			return true;
6420 		}
6421 
6422 		S_StartSound(NULL, sfx_strpst);
6423 		CLEARNAME;
6424 	}
6425 
6426 	return false;
6427 }
6428 
6429 static void M_DrawAddons(void)
6430 {
6431 	INT32 x, y;
6432 	size_t i, m;
6433 	size_t t, b; // top and bottom item #s to draw in directory
6434 	const UINT8 *flashcol = NULL;
6435 	UINT8 hilicol;
6436 
6437 	// hack - need to refresh at end of frame to handle addfile...
6438 	if (refreshdirmenu & M_AddonsRefresh())
6439 	{
6440 		M_DrawMessageMenu();
6441 		return;
6442 	}
6443 
6444 	if (Playing())
6445 		V_DrawCenteredString(BASEVIDWIDTH/2, 5, warningflags, "Adding files mid-game may cause problems.");
6446 	else
6447 		V_DrawCenteredString(BASEVIDWIDTH/2, 5, 0, LOCATIONSTRING1);
6448 			// (recommendedflags == V_SKYMAP ? LOCATIONSTRING2 : LOCATIONSTRING1)
6449 
6450 	if (numwadfiles <= mainwads+1)
6451 		y = 0;
6452 	else if (numwadfiles >= MAX_WADFILES)
6453 		y = FRACUNIT;
6454 	else
6455 	{
6456 		x = FixedDiv(((ssize_t)(numwadfiles) - (ssize_t)(mainwads+1))<<FRACBITS, ((ssize_t)MAX_WADFILES - (ssize_t)(mainwads+1))<<FRACBITS);
6457 		y = FixedDiv((((ssize_t)packetsizetally-(ssize_t)mainwadstally)<<FRACBITS), ((((ssize_t)MAXFILENEEDED*sizeof(UINT8)-(ssize_t)mainwadstally)-(5+22))<<FRACBITS)); // 5+22 = (a.ext + checksum length) is minimum addition to packet size tally
6458 		if (x > y)
6459 			y = x;
6460 		if (y > FRACUNIT) // happens because of how we're shrinkin' it a little
6461 			y = FRACUNIT;
6462 	}
6463 
6464 	M_DrawTemperature(BASEVIDWIDTH - 19 - 5, y);
6465 
6466 	// DRAW MENU
6467 	x = currentMenu->x;
6468 	y = currentMenu->y + 1;
6469 
6470 	hilicol = 0; // white
6471 
6472 #define boxwidth (MAXSTRINGLENGTH*8+6)
6473 
6474 	// draw the file path and the top white + black lines of the box
6475 	V_DrawString(x-21, (y - 16) + (lsheadingheight - 12), highlightflags|V_ALLOWLOWERCASE, M_AddonsHeaderPath());
6476 	V_DrawFill(x-21, (y - 16) + (lsheadingheight - 3), boxwidth, 1, hilicol);
6477 	V_DrawFill(x-21, (y - 16) + (lsheadingheight - 2), boxwidth, 1, 30);
6478 
6479 	m = (BASEVIDHEIGHT - currentMenu->y + 2) - (y - 1);
6480 	// addons menu back color
6481 	V_DrawFill(x-21, y - 1, boxwidth, m, 159);
6482 
6483 	// The directory is too small for a scrollbar, so just draw a tall white line
6484 	if (sizedirmenu <= addonmenusize)
6485 	{
6486 		t = 0; // first item
6487 		b = sizedirmenu - 1; // last item
6488 		i = 0; // "scrollbar" at "top" position
6489 	}
6490 	else
6491 	{
6492 		size_t q = m;
6493 		m = (addonmenusize * m)/sizedirmenu; // height of scroll bar
6494 		if (dir_on[menudepthleft] <= numaddonsshown) // all the way up
6495 		{
6496 			t = 0; // first item
6497 			b = addonmenusize - 1; //9th item
6498 			i = 0; // scrollbar at top position
6499 		}
6500 		else if (dir_on[menudepthleft] >= sizedirmenu - (numaddonsshown + 1)) // all the way down
6501 		{
6502 			t = sizedirmenu - addonmenusize; // # 9th last
6503 			b = sizedirmenu - 1; // last item
6504 			i = q-m; // scrollbar at bottom position
6505 		}
6506 		else // somewhere in the middle
6507 		{
6508 			t = dir_on[menudepthleft] - numaddonsshown; // 4 items above
6509 			b = dir_on[menudepthleft] + numaddonsshown; // 4 items below
6510 			i = (t * (q-m))/(sizedirmenu - addonmenusize); // calculate position of scrollbar
6511 		}
6512 	}
6513 
6514 	// draw the scrollbar!
6515 	V_DrawFill((x-21) + boxwidth-1, (y - 1) + i, 1, m, hilicol);
6516 
6517 #undef boxwidth
6518 
6519 	// draw up arrow that bobs up and down
6520 	if (t != 0)
6521 		V_DrawString(19, y+4 - (skullAnimCounter/5), highlightflags, "\x1A");
6522 
6523 	// make the selection box flash yellow
6524 	if (skullAnimCounter < 4)
6525 		flashcol = V_GetStringColormap(highlightflags);
6526 
6527 	// draw icons and item names
6528 	for (i = t; i <= b; i++)
6529 	{
6530 		UINT32 flags = V_ALLOWLOWERCASE;
6531 		if (y > BASEVIDHEIGHT) break;
6532 		if (dirmenu[i])
6533 #define type (UINT8)(dirmenu[i][DIR_TYPE])
6534 		{
6535 			if (type & EXT_LOADED)
6536 			{
6537 				flags |= V_TRANSLUCENT;
6538 				V_DrawSmallScaledPatch(x-(16+4), y, V_TRANSLUCENT, addonsp[(type & ~EXT_LOADED)]);
6539 				V_DrawSmallScaledPatch(x-(16+4), y, 0, addonsp[NUM_EXT+2]);
6540 			}
6541 			else
6542 				V_DrawSmallScaledPatch(x-(16+4), y, 0, addonsp[(type & ~EXT_LOADED)]);
6543 
6544 			// draw selection box for the item currently selected
6545 			if ((size_t)i == dir_on[menudepthleft])
6546 			{
6547 				V_DrawFixedPatch((x-(16+4))<<FRACBITS, (y)<<FRACBITS, FRACUNIT/2, 0, addonsp[NUM_EXT+1], flashcol);
6548 				flags = V_ALLOWLOWERCASE|highlightflags;
6549 			}
6550 
6551 			// draw name of the item, use ... if too long
6552 #define charsonside 14
6553 			if (dirmenu[i][DIR_LEN] > (charsonside*2 + 3))
6554 				V_DrawString(x, y+4, flags, va("%.*s...%s", charsonside, dirmenu[i]+DIR_STRING, dirmenu[i]+DIR_STRING+dirmenu[i][DIR_LEN]-(charsonside+1)));
6555 #undef charsonside
6556 			else
6557 				V_DrawString(x, y+4, flags, dirmenu[i]+DIR_STRING);
6558 		}
6559 #undef type
6560 		y += 16;
6561 	}
6562 
6563 	// draw down arrow that bobs down and up
6564 	if (b != sizedirmenu)
6565 		V_DrawString(19, y-12 + (skullAnimCounter/5), highlightflags, "\x1B");
6566 
6567 	// draw search box
6568 	y = BASEVIDHEIGHT - currentMenu->y + 1;
6569 
6570 	M_DrawTextBox(x - (21 + 5), y, MAXSTRINGLENGTH, 1);
6571 	if (menusearch[0])
6572 		V_DrawString(x - 18, y + 8, V_ALLOWLOWERCASE, menusearch+1);
6573 	else
6574 		V_DrawString(x - 18, y + 8, V_ALLOWLOWERCASE|V_TRANSLUCENT, "Type to search...");
6575 	if (skullAnimCounter < 4)
6576 		V_DrawCharacter(x - 18 + V_StringWidth(menusearch+1, 0), y + 8,
6577 			'_' | 0x80, false);
6578 
6579 	// draw search icon
6580 	x -= (21 + 5 + 16);
6581 	V_DrawSmallScaledPatch(x, y + 4, (menusearch[0] ? 0 : V_TRANSLUCENT), addonsp[NUM_EXT+3]);
6582 
6583 	// draw save icon
6584 	x = BASEVIDWIDTH - x - 16;
6585 	V_DrawSmallScaledPatch(x, y + 4, ((!modifiedgame || savemoddata) ? 0 : V_TRANSLUCENT), addonsp[NUM_EXT+4]);
6586 
6587 	if (modifiedgame)
6588 		V_DrawSmallScaledPatch(x, y + 4, 0, addonsp[NUM_EXT+2]);
6589 }
6590 
6591 static void M_AddonExec(INT32 ch)
6592 {
6593 	if (ch != 'y' && ch != KEY_ENTER)
6594 		return;
6595 
6596 	S_StartSound(NULL, sfx_zoom);
6597 	COM_BufAddText(va("exec \"%s%s\"", menupath, dirmenu[dir_on[menudepthleft]]+DIR_STRING));
6598 }
6599 
6600 #define len menusearch[0]
6601 static boolean M_ChangeStringAddons(INT32 choice)
6602 {
6603 	if (shiftdown && choice >= 32 && choice <= 127)
6604 		choice = shiftxform[choice];
6605 
6606 	switch (choice)
6607 	{
6608 		case KEY_DEL:
6609 			if (len)
6610 			{
6611 				len = menusearch[1] = 0;
6612 				return true;
6613 			}
6614 			break;
6615 		case KEY_BACKSPACE:
6616 			if (len)
6617 			{
6618 				menusearch[1+--len] = 0;
6619 				return true;
6620 			}
6621 			break;
6622 		default:
6623 			if (choice >= 32 && choice <= 127)
6624 			{
6625 				if (len < MAXSTRINGLENGTH - 1)
6626 				{
6627 					menusearch[1+len++] = (char)choice;
6628 					menusearch[1+len] = 0;
6629 					return true;
6630 				}
6631 			}
6632 			break;
6633 	}
6634 	return false;
6635 }
6636 #undef len
6637 
6638 static void M_HandleAddons(INT32 choice)
6639 {
6640 	boolean exitmenu = false; // exit to previous menu
6641 
6642 	if (M_ChangeStringAddons(choice))
6643 	{
6644 		char *tempname = NULL;
6645 		if (dirmenu && dirmenu[dir_on[menudepthleft]])
6646 			tempname = Z_StrDup(dirmenu[dir_on[menudepthleft]]+DIR_STRING); // don't need to I_Error if can't make - not important, just QoL
6647 #if 0 // much slower
6648 		if (!preparefilemenu(true))
6649 		{
6650 			UNEXIST;
6651 			return;
6652 		}
6653 #else // streamlined
6654 		searchfilemenu(tempname);
6655 #endif
6656 	}
6657 
6658 	switch (choice)
6659 	{
6660 		case KEY_DOWNARROW:
6661 			if (dir_on[menudepthleft] < sizedirmenu-1)
6662 				dir_on[menudepthleft]++;
6663 			S_StartSound(NULL, sfx_menu1);
6664 			break;
6665 		case KEY_UPARROW:
6666 			if (dir_on[menudepthleft])
6667 				dir_on[menudepthleft]--;
6668 			S_StartSound(NULL, sfx_menu1);
6669 			break;
6670 		case KEY_PGDN:
6671 			{
6672 				UINT8 i;
6673 				for (i = numaddonsshown; i && (dir_on[menudepthleft] < sizedirmenu-1); i--)
6674 					dir_on[menudepthleft]++;
6675 			}
6676 			S_StartSound(NULL, sfx_menu1);
6677 			break;
6678 		case KEY_PGUP:
6679 			{
6680 				UINT8 i;
6681 				for (i = numaddonsshown; i && (dir_on[menudepthleft]); i--)
6682 					dir_on[menudepthleft]--;
6683 			}
6684 			S_StartSound(NULL, sfx_menu1);
6685 			break;
6686 		case KEY_ENTER:
6687 			{
6688 				boolean refresh = true;
6689 				if (!dirmenu[dir_on[menudepthleft]])
6690 					S_StartSound(NULL, sfx_lose);
6691 				else
6692 				{
6693 					switch (dirmenu[dir_on[menudepthleft]][DIR_TYPE])
6694 					{
6695 						case EXT_FOLDER:
6696 							strcpy(&menupath[menupathindex[menudepthleft]],dirmenu[dir_on[menudepthleft]]+DIR_STRING);
6697 							if (menudepthleft)
6698 							{
6699 								menupathindex[--menudepthleft] = strlen(menupath);
6700 								menupath[menupathindex[menudepthleft]] = 0;
6701 
6702 								if (!preparefilemenu(false))
6703 								{
6704 									S_StartSound(NULL, sfx_skid);
6705 									M_StartMessage(va("%c%s\x80\nThis folder is empty.\n\n(Press a key)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), M_AddonsHeaderPath()),NULL,MM_NOTHING);
6706 									menupath[menupathindex[++menudepthleft]] = 0;
6707 
6708 									if (!preparefilemenu(true))
6709 									{
6710 										UNEXIST;
6711 										return;
6712 									}
6713 								}
6714 								else
6715 								{
6716 									S_StartSound(NULL, sfx_menu1);
6717 									dir_on[menudepthleft] = 1;
6718 								}
6719 								refresh = false;
6720 							}
6721 							else
6722 							{
6723 								S_StartSound(NULL, sfx_lose);
6724 								M_StartMessage(va("%c%s\x80\nThis folder is too deep to navigate to!\n\n(Press a key)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), M_AddonsHeaderPath()),NULL,MM_NOTHING);
6725 								menupath[menupathindex[menudepthleft]] = 0;
6726 							}
6727 							break;
6728 						case EXT_UP:
6729 							S_StartSound(NULL, sfx_menu1);
6730 							menupath[menupathindex[++menudepthleft]] = 0;
6731 							if (!preparefilemenu(false))
6732 							{
6733 								UNEXIST;
6734 								return;
6735 							}
6736 							break;
6737 						case EXT_TXT:
6738 							M_StartMessage(va("%c%s\x80\nThis file may not be a console script.\nAttempt to run anyways? \n\n(Press 'Y' to confirm)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), dirmenu[dir_on[menudepthleft]]+DIR_STRING),M_AddonExec,MM_YESNO);
6739 							break;
6740 						case EXT_CFG:
6741 							M_AddonExec(KEY_ENTER);
6742 							break;
6743 						case EXT_LUA:
6744 						case EXT_SOC:
6745 						case EXT_WAD:
6746 #ifdef USE_KART
6747 						case EXT_KART:
6748 #endif
6749 						case EXT_PK3:
6750 							COM_BufAddText(va("addfile \"%s%s\"", menupath, dirmenu[dir_on[menudepthleft]]+DIR_STRING));
6751 							break;
6752 						default:
6753 							S_StartSound(NULL, sfx_lose);
6754 					}
6755 				}
6756 				if (refresh)
6757 					refreshdirmenu |= REFRESHDIR_NORMAL;
6758 			}
6759 			break;
6760 
6761 		case KEY_ESCAPE:
6762 			exitmenu = true;
6763 			break;
6764 
6765 		default:
6766 			break;
6767 	}
6768 	if (exitmenu)
6769 	{
6770 		closefilemenu(true);
6771 
6772 		// secrets disabled by addfile...
6773 		MainMenu[secrets].status = (M_AnySecretUnlocked()) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
6774 
6775 		if (currentMenu->prevMenu)
6776 			M_SetupNextMenu(currentMenu->prevMenu);
6777 		else
6778 			M_ClearMenus(true);
6779 	}
6780 }
6781 
6782 static void M_PandorasBox(INT32 choice)
6783 {
6784 	(void)choice;
6785 	if (maptol & TOL_NIGHTS)
6786 		CV_StealthSetValue(&cv_dummyrings, max(players[consoleplayer].spheres, 0));
6787 	else
6788 		CV_StealthSetValue(&cv_dummyrings, max(players[consoleplayer].rings, 0));
6789 	if (players[consoleplayer].lives == INFLIVES)
6790 		CV_StealthSet(&cv_dummylives, "Infinite");
6791 	else
6792 		CV_StealthSetValue(&cv_dummylives, max(players[consoleplayer].lives, 1));
6793 	CV_StealthSetValue(&cv_dummycontinues, players[consoleplayer].continues);
6794 	SR_PandorasBox[3].status = (continuesInSession) ? (IT_STRING | IT_CVAR) : (IT_GRAYEDOUT);
6795 	SR_PandorasBox[6].status = (players[consoleplayer].charflags & SF_SUPER) ? (IT_GRAYEDOUT) : (IT_STRING | IT_CALL);
6796 	SR_PandorasBox[7].status = (emeralds == ((EMERALD7)*2)-1) ? (IT_GRAYEDOUT) : (IT_STRING | IT_CALL);
6797 	M_SetupNextMenu(&SR_PandoraDef);
6798 }
6799 
6800 static boolean M_ExitPandorasBox(void)
6801 {
6802 	if (cv_dummyrings.value != max(players[consoleplayer].rings, 0))
6803 	{
6804 		if (maptol & TOL_NIGHTS)
6805 			COM_ImmedExecute(va("setspheres %d", cv_dummyrings.value));
6806 		else
6807 			COM_ImmedExecute(va("setrings %d", cv_dummyrings.value));
6808 	}
6809 	if (cv_dummylives.value != players[consoleplayer].lives)
6810 		COM_ImmedExecute(va("setlives %d", cv_dummylives.value));
6811 	if (continuesInSession && cv_dummycontinues.value != players[consoleplayer].continues)
6812 		COM_ImmedExecute(va("setcontinues %d", cv_dummycontinues.value));
6813 	return true;
6814 }
6815 
6816 static void M_ChangeLevel(INT32 choice)
6817 {
6818 	char mapname[6];
6819 	(void)choice;
6820 
6821 	strlcpy(mapname, G_BuildMapName(cv_nextmap.value), sizeof (mapname));
6822 	strlwr(mapname);
6823 	mapname[5] = '\0';
6824 
6825 	M_ClearMenus(true);
6826 	COM_BufAddText(va("map %s -gametype \"%s\"\n", mapname, cv_newgametype.string));
6827 }
6828 
6829 static void M_ConfirmSpectate(INT32 choice)
6830 {
6831 	(void)choice;
6832 	// We allow switching to spectator even if team changing is not allowed
6833 	M_ClearMenus(true);
6834 	COM_ImmedExecute("changeteam spectator");
6835 }
6836 
6837 static void M_ConfirmEnterGame(INT32 choice)
6838 {
6839 	(void)choice;
6840 	if (!cv_allowteamchange.value)
6841 	{
6842 		M_StartMessage(M_GetText("The server is not allowing\nteam changes at this time.\nPress a key.\n"), NULL, MM_NOTHING);
6843 		return;
6844 	}
6845 	M_ClearMenus(true);
6846 	COM_ImmedExecute("changeteam playing");
6847 }
6848 
6849 static void M_ConfirmTeamScramble(INT32 choice)
6850 {
6851 	(void)choice;
6852 	M_ClearMenus(true);
6853 
6854 	switch (cv_dummyscramble.value)
6855 	{
6856 		case 0:
6857 			COM_ImmedExecute("teamscramble 1");
6858 			break;
6859 		case 1:
6860 			COM_ImmedExecute("teamscramble 2");
6861 			break;
6862 	}
6863 }
6864 
6865 static void M_ConfirmTeamChange(INT32 choice)
6866 {
6867 	(void)choice;
6868 	if (!cv_allowteamchange.value && cv_dummyteam.value)
6869 	{
6870 		M_StartMessage(M_GetText("The server is not allowing\nteam changes at this time.\nPress a key.\n"), NULL, MM_NOTHING);
6871 		return;
6872 	}
6873 
6874 	M_ClearMenus(true);
6875 
6876 	switch (cv_dummyteam.value)
6877 	{
6878 		case 0:
6879 			COM_ImmedExecute("changeteam spectator");
6880 			break;
6881 		case 1:
6882 			COM_ImmedExecute("changeteam red");
6883 			break;
6884 		case 2:
6885 			COM_ImmedExecute("changeteam blue");
6886 			break;
6887 	}
6888 }
6889 
6890 static void M_Options(INT32 choice)
6891 {
6892 	(void)choice;
6893 
6894 	// if the player is not admin or server, disable server options
6895 	OP_MainMenu[5].status = (Playing() && !(server || IsPlayerAdmin(consoleplayer))) ? (IT_GRAYEDOUT) : (IT_STRING|IT_CALL);
6896 
6897 	// if the player is playing _at all_, disable the erase data options
6898 	OP_DataOptionsMenu[2].status = (Playing()) ? (IT_GRAYEDOUT) : (IT_STRING|IT_SUBMENU);
6899 
6900 	OP_MainDef.prevMenu = currentMenu;
6901 	M_SetupNextMenu(&OP_MainDef);
6902 }
6903 
6904 static void M_RetryResponse(INT32 ch)
6905 {
6906 	if (ch != 'y' && ch != KEY_ENTER)
6907 		return;
6908 
6909 	if (!&players[consoleplayer] || netgame || multiplayer) // Should never happen!
6910 		return;
6911 
6912 	M_ClearMenus(true);
6913 	G_SetRetryFlag();
6914 }
6915 
6916 static void M_Retry(INT32 choice)
6917 {
6918 	(void)choice;
6919 	if (marathonmode)
6920 	{
6921 		M_RetryResponse(KEY_ENTER);
6922 		return;
6923 	}
6924 	M_StartMessage(M_GetText("Retry this act from the last starpost?\n\n(Press 'Y' to confirm)\n"),M_RetryResponse,MM_YESNO);
6925 }
6926 
6927 static void M_SelectableClearMenus(INT32 choice)
6928 {
6929 	(void)choice;
6930 	M_ClearMenus(true);
6931 }
6932 
6933 // ======
6934 // CHEATS
6935 // ======
6936 
6937 static void M_UltimateCheat(INT32 choice)
6938 {
6939 	(void)choice;
6940 	LUAh_GameQuit(true);
6941 	I_Quit();
6942 }
6943 
6944 static void M_AllowSuper(INT32 choice)
6945 {
6946 	(void)choice;
6947 
6948 	players[consoleplayer].charflags |= SF_SUPER;
6949 	M_StartMessage(M_GetText("You are now capable of turning super.\nRemember to get all the emeralds!\n"),NULL,MM_NOTHING);
6950 	SR_PandorasBox[6].status = IT_GRAYEDOUT;
6951 
6952 	G_SetGameModified(multiplayer);
6953 }
6954 
6955 static void M_GetAllEmeralds(INT32 choice)
6956 {
6957 	(void)choice;
6958 
6959 	emeralds = ((EMERALD7)*2)-1;
6960 	M_StartMessage(M_GetText("You now have all 7 emeralds.\nUse them wisely.\nWith great power comes great ring drain.\n"),NULL,MM_NOTHING);
6961 	SR_PandorasBox[7].status = IT_GRAYEDOUT;
6962 
6963 	G_SetGameModified(multiplayer);
6964 }
6965 
6966 static void M_DestroyRobotsResponse(INT32 ch)
6967 {
6968 	if (ch != 'y' && ch != KEY_ENTER)
6969 		return;
6970 
6971 	// Destroy all robots
6972 	P_DestroyRobots();
6973 
6974 	G_SetGameModified(multiplayer);
6975 }
6976 
6977 static void M_DestroyRobots(INT32 choice)
6978 {
6979 	(void)choice;
6980 
6981 	M_StartMessage(M_GetText("Do you want to destroy all\nrobots in the current level?\n\n(Press 'Y' to confirm)\n"),M_DestroyRobotsResponse,MM_YESNO);
6982 }
6983 
6984 static void M_LevelSelectWarp(INT32 choice)
6985 {
6986 	boolean fromloadgame = (currentMenu == &SP_LevelSelectDef);
6987 
6988 	(void)choice;
6989 
6990 	if (W_CheckNumForName(G_BuildMapName(cv_nextmap.value)) == LUMPERROR)
6991 	{
6992 		CONS_Alert(CONS_WARNING, "Internal game map '%s' not found\n", G_BuildMapName(cv_nextmap.value));
6993 		return;
6994 	}
6995 
6996 	startmap = (INT16)(cv_nextmap.value);
6997 
6998 	fromlevelselect = true;
6999 
7000 	if (fromloadgame)
7001 		G_LoadGame((UINT32)cursaveslot, startmap);
7002 	else
7003 	{
7004 		cursaveslot = 0;
7005 		M_SetupChoosePlayer(0);
7006 	}
7007 }
7008 
7009 // ========
7010 // SKY ROOM
7011 // ========
7012 
7013 UINT8 skyRoomMenuTranslations[MAXUNLOCKABLES];
7014 
7015 static boolean checklist_cangodown; // uuuueeerggghhhh HACK
7016 
7017 static void M_HandleChecklist(INT32 choice)
7018 {
7019 	INT32 j;
7020 	switch (choice)
7021 	{
7022 		case KEY_DOWNARROW:
7023 			S_StartSound(NULL, sfx_menu1);
7024 			if ((check_on != MAXUNLOCKABLES) && checklist_cangodown)
7025 			{
7026 				for (j = check_on+1; j < MAXUNLOCKABLES; j++)
7027 				{
7028 					if (!unlockables[j].name[0])
7029 						continue;
7030 					// if (unlockables[j].nochecklist)
7031 					//	continue;
7032 					if (!unlockables[j].conditionset)
7033 						continue;
7034 					if (unlockables[j].conditionset > MAXCONDITIONSETS)
7035 						continue;
7036 					if (!unlockables[j].unlocked && unlockables[j].showconditionset && !M_Achieved(unlockables[j].showconditionset))
7037 						continue;
7038 					if (unlockables[j].conditionset == unlockables[check_on].conditionset)
7039 						continue;
7040 					break;
7041 				}
7042 				if (j != MAXUNLOCKABLES)
7043 					check_on = j;
7044 			}
7045 			return;
7046 
7047 		case KEY_UPARROW:
7048 			S_StartSound(NULL, sfx_menu1);
7049 			if (check_on)
7050 			{
7051 				for (j = check_on-1; j > -1; j--)
7052 				{
7053 					if (!unlockables[j].name[0])
7054 						continue;
7055 					// if (unlockables[j].nochecklist)
7056 					//	continue;
7057 					if (!unlockables[j].conditionset)
7058 						continue;
7059 					if (unlockables[j].conditionset > MAXCONDITIONSETS)
7060 						continue;
7061 					if (!unlockables[j].unlocked && unlockables[j].showconditionset && !M_Achieved(unlockables[j].showconditionset))
7062 						continue;
7063 					if (j && unlockables[j].conditionset == unlockables[j-1].conditionset)
7064 						continue;
7065 					break;
7066 				}
7067 				if (j != -1)
7068 					check_on = j;
7069 			}
7070 			return;
7071 
7072 		case KEY_ESCAPE:
7073 			if (currentMenu->prevMenu)
7074 				M_SetupNextMenu(currentMenu->prevMenu);
7075 			else
7076 				M_ClearMenus(true);
7077 			return;
7078 		default:
7079 			break;
7080 	}
7081 }
7082 
7083 #define addy(add) { y += add; if ((y - currentMenu->y) > (scrollareaheight*2)) goto finishchecklist; }
7084 
7085 static void M_DrawChecklist(void)
7086 {
7087 	INT32 i = check_on, j = 0, y = currentMenu->y;
7088 	UINT32 condnum, previd, maxcond;
7089 	condition_t *cond;
7090 
7091 	// draw title (or big pic)
7092 	M_DrawMenuTitle();
7093 
7094 	if (check_on)
7095 		V_DrawString(10, y-(skullAnimCounter/5), V_YELLOWMAP, "\x1A");
7096 
7097 	while (i < MAXUNLOCKABLES)
7098 	{
7099 		if (unlockables[i].name[0] == 0 //|| unlockables[i].nochecklist
7100 		|| !unlockables[i].conditionset || unlockables[i].conditionset > MAXCONDITIONSETS
7101 		|| (!unlockables[i].unlocked && unlockables[i].showconditionset && !M_Achieved(unlockables[i].showconditionset)))
7102 		{
7103 			i += 1;
7104 			continue;
7105 		}
7106 
7107 		V_DrawString(currentMenu->x, y, ((unlockables[i].unlocked) ? V_GREENMAP : V_TRANSLUCENT)|V_ALLOWLOWERCASE, ((unlockables[i].unlocked || !unlockables[i].nochecklist) ? unlockables[i].name : M_CreateSecretMenuOption(unlockables[i].name)));
7108 
7109 		for (j = i+1; j < MAXUNLOCKABLES; j++)
7110 		{
7111 			if (!(unlockables[j].name[0] == 0 //|| unlockables[j].nochecklist
7112 			|| !unlockables[j].conditionset || unlockables[j].conditionset > MAXCONDITIONSETS))
7113 				break;
7114 		}
7115 		if ((j != MAXUNLOCKABLES) && (unlockables[i].conditionset == unlockables[j].conditionset))
7116 			addy(8)
7117 		else
7118 		{
7119 			if ((maxcond = conditionSets[unlockables[i].conditionset-1].numconditions))
7120 			{
7121 				cond = conditionSets[unlockables[i].conditionset-1].condition;
7122 				previd = cond[0].id;
7123 				addy(2);
7124 
7125 				if (unlockables[i].objective[0] != '/')
7126 				{
7127 					addy(16);
7128 					V_DrawString(currentMenu->x, y-8,
7129 						V_ALLOWLOWERCASE,
7130 						va("\x1E %s", unlockables[i].objective));
7131 					y -= 8;
7132 				}
7133 				else
7134 				{
7135 					for (condnum = 0; condnum < maxcond; condnum++)
7136 					{
7137 						const char *beat = "!";
7138 
7139 						if (cond[condnum].id != previd)
7140 						{
7141 							addy(8);
7142 							V_DrawString(currentMenu->x + 4, y, V_YELLOWMAP, "OR");
7143 						}
7144 
7145 						addy(8);
7146 
7147 						switch (cond[condnum].type)
7148 						{
7149 							case UC_PLAYTIME:
7150 								{
7151 									UINT32 hours = G_TicsToHours(cond[condnum].requirement);
7152 									UINT32 minutes = G_TicsToMinutes(cond[condnum].requirement, false);
7153 									UINT32 seconds = G_TicsToSeconds(cond[condnum].requirement);
7154 
7155 #define getplural(field) ((field == 1) ? "" : "s")
7156 									if (hours)
7157 									{
7158 										if (minutes)
7159 											beat = va("Play the game for %d hour%s %d minute%s", hours, getplural(hours), minutes, getplural(minutes));
7160 										else
7161 											beat = va("Play the game for %d hour%s", hours, getplural(hours));
7162 									}
7163 									else
7164 									{
7165 										if (minutes && seconds)
7166 											beat = va("Play the game for %d minute%s %d second%s", minutes, getplural(minutes), seconds, getplural(seconds));
7167 										else if (minutes)
7168 											beat = va("Play the game for %d minute%s", minutes, getplural(minutes));
7169 										else
7170 											beat = va("Play the game for %d second%s", seconds, getplural(seconds));
7171 									}
7172 #undef getplural
7173 								}
7174 								break;
7175 							case UC_MAPVISITED:
7176 							case UC_MAPBEATEN:
7177 							case UC_MAPALLEMERALDS:
7178 							case UC_MAPULTIMATE:
7179 							case UC_MAPPERFECT:
7180 								{
7181 									char *title = G_BuildMapTitle(cond[condnum].requirement);
7182 
7183 									if (title)
7184 									{
7185 										const char *level = ((M_MapLocked(cond[condnum].requirement) || !((mapheaderinfo[cond[condnum].requirement-1]->menuflags & LF2_NOVISITNEEDED) || (mapvisited[cond[condnum].requirement-1] & MV_MAX))) ? M_CreateSecretMenuOption(title) : title);
7186 
7187 										switch (cond[condnum].type)
7188 										{
7189 											case UC_MAPVISITED:
7190 												beat = va("Visit %s", level);
7191 												break;
7192 											case UC_MAPALLEMERALDS:
7193 												beat = va("Beat %s with all emeralds", level);
7194 												break;
7195 											case UC_MAPULTIMATE:
7196 												beat = va("Beat %s in Ultimate mode", level);
7197 												break;
7198 											case UC_MAPPERFECT:
7199 												beat = va("Get all rings in %s", level);
7200 												break;
7201 											case UC_MAPBEATEN:
7202 											default:
7203 												beat = va("Beat %s", level);
7204 												break;
7205 										}
7206 										Z_Free(title);
7207 									}
7208 								}
7209 								break;
7210 							case UC_MAPSCORE:
7211 							case UC_MAPTIME:
7212 							case UC_MAPRINGS:
7213 								{
7214 									char *title = G_BuildMapTitle(cond[condnum].extrainfo1);
7215 
7216 									if (title)
7217 									{
7218 										const char *level = ((M_MapLocked(cond[condnum].extrainfo1) || !((mapheaderinfo[cond[condnum].extrainfo1-1]->menuflags & LF2_NOVISITNEEDED) || (mapvisited[cond[condnum].extrainfo1-1] & MV_MAX))) ? M_CreateSecretMenuOption(title) : title);
7219 
7220 										switch (cond[condnum].type)
7221 										{
7222 											case UC_MAPSCORE:
7223 												beat = va("Get %d points in %s", cond[condnum].requirement, level);
7224 												break;
7225 											case UC_MAPTIME:
7226 												beat = va("Beat %s in %d:%02d.%02d", level,
7227 												G_TicsToMinutes(cond[condnum].requirement, true),
7228 												G_TicsToSeconds(cond[condnum].requirement),
7229 												G_TicsToCentiseconds(cond[condnum].requirement));
7230 												break;
7231 											case UC_MAPRINGS:
7232 												beat = va("Get %d rings in %s", cond[condnum].requirement, level);
7233 												break;
7234 											default:
7235 												break;
7236 										}
7237 										Z_Free(title);
7238 									}
7239 								}
7240 								break;
7241 							case UC_OVERALLSCORE:
7242 							case UC_OVERALLTIME:
7243 							case UC_OVERALLRINGS:
7244 								{
7245 									switch (cond[condnum].type)
7246 									{
7247 										case UC_OVERALLSCORE:
7248 											beat = va("Get %d points over all maps", cond[condnum].requirement);
7249 											break;
7250 										case UC_OVERALLTIME:
7251 											beat = va("Get a total time of less than %d:%02d.%02d",
7252 											G_TicsToMinutes(cond[condnum].requirement, true),
7253 											G_TicsToSeconds(cond[condnum].requirement),
7254 											G_TicsToCentiseconds(cond[condnum].requirement));
7255 											break;
7256 										case UC_OVERALLRINGS:
7257 											beat = va("Get %d rings over all maps", cond[condnum].requirement);
7258 											break;
7259 										default:
7260 											break;
7261 									}
7262 								}
7263 								break;
7264 							case UC_GAMECLEAR:
7265 							case UC_ALLEMERALDS:
7266 								{
7267 									const char *emeraldtext = ((cond[condnum].type == UC_ALLEMERALDS) ? " with all emeralds" : "");
7268 									if (cond[condnum].requirement != 1)
7269 										beat = va("Beat the game %d times%s",
7270 										cond[condnum].requirement, emeraldtext);
7271 									else
7272 										beat = va("Beat the game%s",
7273 										emeraldtext);
7274 								}
7275 								break;
7276 							case UC_TOTALEMBLEMS:
7277 								beat = va("Collect %s%d emblems", ((numemblems+numextraemblems == cond[condnum].requirement) ? "all " : ""), cond[condnum].requirement);
7278 								break;
7279 							case UC_NIGHTSTIME:
7280 							case UC_NIGHTSSCORE:
7281 							case UC_NIGHTSGRADE:
7282 								{
7283 									char *title = G_BuildMapTitle(cond[condnum].extrainfo1);
7284 
7285 									if (title)
7286 									{
7287 										const char *level = ((M_MapLocked(cond[condnum].extrainfo1) || !((mapheaderinfo[cond[condnum].extrainfo1-1]->menuflags & LF2_NOVISITNEEDED) || (mapvisited[cond[condnum].extrainfo1-1] & MV_MAX))) ? M_CreateSecretMenuOption(title) : title);
7288 
7289 										switch (cond[condnum].type)
7290 										{
7291 											case UC_NIGHTSSCORE:
7292 												if (cond[condnum].extrainfo2)
7293 													beat = va("Get %d points in %s, mare %d", cond[condnum].requirement, level, cond[condnum].extrainfo2);
7294 												else
7295 													beat = va("Get %d points in %s", cond[condnum].requirement, level);
7296 												break;
7297 											case UC_NIGHTSTIME:
7298 												if (cond[condnum].extrainfo2)
7299 													beat = va("Beat %s, mare %d in %d:%02d.%02d", level, cond[condnum].extrainfo2,
7300 													G_TicsToMinutes(cond[condnum].requirement, true),
7301 													G_TicsToSeconds(cond[condnum].requirement),
7302 													G_TicsToCentiseconds(cond[condnum].requirement));
7303 												else
7304 													beat = va("Beat %s in %d:%02d.%02d",
7305 													level,
7306 													G_TicsToMinutes(cond[condnum].requirement, true),
7307 													G_TicsToSeconds(cond[condnum].requirement),
7308 													G_TicsToCentiseconds(cond[condnum].requirement));
7309 												break;
7310 											case UC_NIGHTSGRADE:
7311 												{
7312 													char grade = ('F' - (char)cond[condnum].requirement);
7313 													if (grade < 'A')
7314 														grade = 'A';
7315 													if (cond[condnum].extrainfo2)
7316 														beat = va("Get grade %c in %s, mare %d", grade, level, cond[condnum].extrainfo2);
7317 													else
7318 														beat = va("Get grade %c in %s", grade, level);
7319 												}
7320 											break;
7321 											default:
7322 												break;
7323 										}
7324 										Z_Free(title);
7325 									}
7326 								}
7327 								break;
7328 							case UC_TRIGGER:
7329 							case UC_EMBLEM:
7330 							case UC_CONDITIONSET:
7331 							default:
7332 								y -= 8; // Nope, not showing this.
7333 								break;
7334 						}
7335 						if (beat[0] != '!')
7336 						{
7337 							V_DrawString(currentMenu->x, y, 0, "\x1E");
7338 							V_DrawString(currentMenu->x+12, y, V_ALLOWLOWERCASE, beat);
7339 						}
7340 						previd = cond[condnum].id;
7341 					}
7342 				}
7343 			}
7344 			addy(12);
7345 		}
7346 		i = j;
7347 
7348 		/*V_DrawString(160, 8+(24*j), V_RETURN8, V_WordWrap(160, 292, 0, unlockables[i].objective));
7349 
7350 		if (unlockables[i].unlocked)
7351 			V_DrawString(308, 8+(24*j), V_YELLOWMAP, "Y");
7352 		else
7353 			V_DrawString(308, 8+(24*j), V_YELLOWMAP, "N");*/
7354 	}
7355 
7356 finishchecklist:
7357 	if ((checklist_cangodown = ((y - currentMenu->y) > (scrollareaheight*2)))) // haaaaaaacks.
7358 		V_DrawString(10, currentMenu->y+(scrollareaheight*2)+(skullAnimCounter/5), V_YELLOWMAP, "\x1B");
7359 }
7360 
7361 #define NUMHINTS 5
7362 
7363 static void M_EmblemHints(INT32 choice)
7364 {
7365 	INT32 i;
7366 	UINT32 local = 0;
7367 	emblem_t *emblem;
7368 	for (i = 0; i < numemblems; i++)
7369 	{
7370 		emblem = &emblemlocations[i];
7371 		if (emblem->level != gamemap || emblem->type > ET_SKIN)
7372 			continue;
7373 		if (++local > NUMHINTS*2)
7374 			break;
7375 	}
7376 
7377 	(void)choice;
7378 	SR_EmblemHintMenu[0].status = (local > NUMHINTS*2) ? (IT_STRING | IT_ARROWS) : (IT_DISABLED);
7379 	SR_EmblemHintMenu[1].status = (M_SecretUnlocked(SECRET_ITEMFINDER)) ? (IT_CVAR|IT_STRING) : (IT_SECRET);
7380 	hintpage = 1;
7381 	SR_EmblemHintDef.prevMenu = currentMenu;
7382 	M_SetupNextMenu(&SR_EmblemHintDef);
7383 	itemOn = 2; // always start on back.
7384 }
7385 
7386 static void M_DrawEmblemHints(void)
7387 {
7388 	INT32 i, j = 0, x, y, left_hints = NUMHINTS, pageflag = 0;
7389 	UINT32 collected = 0, totalemblems = 0, local = 0;
7390 	emblem_t *emblem;
7391 	const char *hint;
7392 
7393 	for (i = 0; i < numemblems; i++)
7394 	{
7395 		emblem = &emblemlocations[i];
7396 		if (emblem->level != gamemap || emblem->type > ET_SKIN)
7397 			continue;
7398 
7399 		local++;
7400 	}
7401 
7402 	x = (local > NUMHINTS ? 4 : 12);
7403 	y = 8;
7404 
7405 	if (local > NUMHINTS){
7406 		if (local > ((hintpage-1)*NUMHINTS*2) && local < ((hintpage)*NUMHINTS*2)){
7407 			if (NUMHINTS % 2 == 1)
7408 				left_hints = (local - ((hintpage-1)*NUMHINTS*2)  + 1) / 2;
7409 			else
7410 				left_hints = (local - ((hintpage-1)*NUMHINTS*2)) / 2;
7411 		}else{
7412 			left_hints = NUMHINTS;
7413 		}
7414 	}
7415 
7416 	if (local > NUMHINTS*2){
7417 		if (itemOn == 0){
7418 			pageflag = V_YELLOWMAP;
7419 		}
7420 		V_DrawString(currentMenu->x + 40, currentMenu->y + 10, pageflag, va("%d of %d",hintpage, local/(NUMHINTS*2) + 1));
7421 	}
7422 
7423 	// If there are more than 1 page's but less than 2 pages' worth of emblems on the last possible page,
7424 	// put half (rounded up) of the hints on the left, and half (rounded down) on the right
7425 
7426 
7427 	if (!local)
7428 		V_DrawCenteredString(160, 48, V_YELLOWMAP, "No hidden emblems on this map.");
7429 	else for (i = 0; i < numemblems; i++)
7430 	{
7431 		emblem = &emblemlocations[i];
7432 		if (emblem->level != gamemap || emblem->type > ET_SKIN)
7433 			continue;
7434 
7435 		totalemblems++;
7436 
7437 		if (totalemblems >= ((hintpage-1)*(NUMHINTS*2) + 1) && totalemblems < (hintpage*NUMHINTS*2)+1){
7438 
7439 			if (emblem->collected)
7440 			{
7441 				collected = V_GREENMAP;
7442 				V_DrawMappedPatch(x, y+4, 0, W_CachePatchName(M_GetEmblemPatch(emblem, false), PU_PATCH),
7443 					R_GetTranslationColormap(TC_DEFAULT, M_GetEmblemColor(emblem), GTC_CACHE));
7444 			}
7445 			else
7446 			{
7447 				collected = 0;
7448 				V_DrawScaledPatch(x, y+4, 0, W_CachePatchName("NEEDIT", PU_PATCH));
7449 			}
7450 
7451 			if (emblem->hint[0])
7452 				hint = emblem->hint;
7453 			else
7454 				hint = M_GetText("No hint available for this emblem.");
7455 			hint = V_WordWrap(40, BASEVIDWIDTH-12, 0, hint);
7456 			//always draw tiny if we have more than NUMHINTS*2, visually more appealing
7457 			if (local > NUMHINTS)
7458 				V_DrawThinString(x+28, y, V_RETURN8|V_ALLOWLOWERCASE|collected, hint);
7459 			else
7460 				V_DrawString(x+28, y, V_RETURN8|V_ALLOWLOWERCASE|collected, hint);
7461 
7462 			y += 28;
7463 
7464 			// If there are more than 1 page's but less than 2 pages' worth of emblems on the last possible page,
7465 			// put half (rounded up) of the hints on the left, and half (rounded down) on the right
7466 
7467 			if (++j == left_hints)
7468 			{
7469 				x = 4+(BASEVIDWIDTH/2);
7470 				y = 8;
7471 			}
7472 			else if (j >= NUMHINTS*2)
7473 				break;
7474 		}
7475 	}
7476 
7477 	M_DrawGenericMenu();
7478 }
7479 
7480 
7481 static void M_HandleEmblemHints(INT32 choice)
7482 {
7483 	INT32 i;
7484 	emblem_t *emblem;
7485 	UINT32 stageemblems = 0;
7486 
7487 	for (i = 0; i < numemblems; i++)
7488 	{
7489 		emblem = &emblemlocations[i];
7490 		if (emblem->level != gamemap || emblem->type > ET_SKIN)
7491 			continue;
7492 
7493 		stageemblems++;
7494 	}
7495 
7496 
7497 	if (choice == 0){
7498 		if (hintpage > 1){
7499 			hintpage--;
7500 		}
7501 	}else{
7502 		if (hintpage < ((stageemblems-1)/(NUMHINTS*2) + 1)){
7503 			hintpage++;
7504 		}
7505 	}
7506 
7507 }
7508 
7509 /*static void M_DrawSkyRoom(void)
7510 {
7511 	INT32 i, y = 0;
7512 
7513 	M_DrawGenericMenu();
7514 
7515 	for (i = 0; i < currentMenu->numitems; ++i)
7516 	{
7517 		if (currentMenu->menuitems[i].status == (IT_STRING|IT_KEYHANDLER))
7518 		{
7519 			y = currentMenu->menuitems[i].alphaKey;
7520 			break;
7521 		}
7522 	}
7523 	if (!y)
7524 		return;
7525 
7526 	V_DrawRightAlignedString(BASEVIDWIDTH - currentMenu->x, currentMenu->y + y, V_YELLOWMAP, cv_soundtest.string);
7527 	if (i == itemOn)
7528 	{
7529 		V_DrawCharacter(BASEVIDWIDTH - currentMenu->x - 10 - V_StringWidth(cv_soundtest.string, 0) - (skullAnimCounter/5), currentMenu->y + y,
7530 			'\x1C' | V_YELLOWMAP, false);
7531 		V_DrawCharacter(BASEVIDWIDTH - currentMenu->x + 2 + (skullAnimCounter/5), currentMenu->y + y,
7532 			'\x1D' | V_YELLOWMAP, false);
7533 	}
7534 	if (cv_soundtest.value)
7535 		V_DrawRightAlignedString(BASEVIDWIDTH - currentMenu->x, currentMenu->y + y + 8, V_YELLOWMAP, S_sfx[cv_soundtest.value].name);
7536 }*/
7537 
7538 static musicdef_t *curplaying = NULL;
7539 static INT32 st_sel = 0, st_cc = 0;
7540 static tic_t st_time = 0;
7541 static patch_t* st_radio[9];
7542 static patch_t* st_launchpad[4];
7543 
7544 static void M_CacheSoundTest(void)
7545 {
7546 	UINT8 i;
7547 	char buf[8];
7548 
7549 	STRBUFCPY(buf, "M_RADIOn");
7550 	for (i = 0; i < 9; i++)
7551 	{
7552 		buf[7] = (char)('0'+i);
7553 		st_radio[i] = W_CachePatchName(buf, PU_PATCH);
7554 	}
7555 
7556 	STRBUFCPY(buf, "M_LPADn");
7557 	for (i = 0; i < 4; i++)
7558 	{
7559 		buf[6] = (char)('0'+i);
7560 		st_launchpad[i] = W_CachePatchName(buf, PU_PATCH);
7561 	}
7562 }
7563 
7564 static void M_SoundTest(INT32 choice)
7565 {
7566 	INT32 ul = skyRoomMenuTranslations[choice-1];
7567 
7568 	soundtestpage = (UINT8)(unlockables[ul].variable);
7569 	if (!soundtestpage)
7570 		soundtestpage = 1;
7571 
7572 	if (!S_PrepareSoundTest())
7573 	{
7574 		M_StartMessage(M_GetText("No selectable tracks found.\n"),NULL,MM_NOTHING);
7575 		return;
7576 	}
7577 
7578 	M_CacheSoundTest();
7579 
7580 	curplaying = NULL;
7581 	st_time = 0;
7582 
7583 	st_sel = 0;
7584 
7585 	st_cc = cv_closedcaptioning.value; // hack;
7586 	cv_closedcaptioning.value = 1; // hack
7587 
7588 	M_SetupNextMenu(&SR_SoundTestDef);
7589 }
7590 
7591 static void M_DrawSoundTest(void)
7592 {
7593 	INT32 x, y, i;
7594 	fixed_t hscale = FRACUNIT/2, vscale = FRACUNIT/2, bounce = 0;
7595 	UINT8 frame[4] = {0, 0, -1, SKINCOLOR_RUBY};
7596 
7597 	// let's handle the ticker first. ideally we'd tick this somewhere else, BUT...
7598 	if (curplaying)
7599 	{
7600 		if (curplaying == &soundtestsfx)
7601 		{
7602 			if (cv_soundtest.value)
7603 			{
7604 				frame[1] = (2-st_time);
7605 				frame[2] = ((cv_soundtest.value - 1) % 9);
7606 				frame[3] += (((cv_soundtest.value - 1) / 9) % (FIRSTSUPERCOLOR - frame[3]));
7607 				if (st_time < 2)
7608 					st_time++;
7609 			}
7610 		}
7611 		else
7612 		{
7613 			if (curplaying->stoppingtics && st_time >= curplaying->stoppingtics)
7614 			{
7615 				curplaying = NULL;
7616 				st_time = 0;
7617 			}
7618 			else
7619 			{
7620 				fixed_t work, bpm = curplaying->bpm;
7621 				angle_t ang;
7622 				//bpm = FixedDiv((60*TICRATE)<<FRACBITS, bpm); -- bake this in on load
7623 
7624 				work = st_time<<FRACBITS;
7625 				work %= bpm;
7626 
7627 				if (st_time >= (FRACUNIT>>1)) // prevent overflow jump - takes about 15 minutes of loop on the same song to reach
7628 					st_time = (work>>FRACBITS);
7629 
7630 				work = FixedDiv(work*180, bpm);
7631 				frame[0] = 8-(work/(20<<FRACBITS));
7632 				if (frame[0] > 8) // VERY small likelihood for the above calculation to wrap, but it turns out it IS possible lmao
7633 					frame[0] = 0;
7634 				ang = (FixedAngle(work)>>ANGLETOFINESHIFT) & FINEMASK;
7635 				bounce = (FINESINE(ang) - FRACUNIT/2);
7636 				hscale -= bounce/16;
7637 				vscale += bounce/16;
7638 
7639 				st_time++;
7640 			}
7641 		}
7642 	}
7643 
7644 	x = 90<<FRACBITS;
7645 	y = (BASEVIDHEIGHT-32)<<FRACBITS;
7646 
7647 	V_DrawStretchyFixedPatch(x, y,
7648 		hscale, vscale,
7649 		0, st_radio[frame[0]], NULL);
7650 
7651 	V_DrawFixedPatch(x, y, FRACUNIT/2, 0, st_launchpad[0], NULL);
7652 
7653 	for (i = 0; i < 9; i++)
7654 	{
7655 		if (i == frame[2])
7656 		{
7657 			UINT8 *colmap = R_GetTranslationColormap(TC_RAINBOW, frame[3], GTC_CACHE);
7658 			V_DrawFixedPatch(x, y + (frame[1]<<FRACBITS), FRACUNIT/2, 0, st_launchpad[frame[1]+1], colmap);
7659 		}
7660 		else
7661 			V_DrawFixedPatch(x, y, FRACUNIT/2, 0, st_launchpad[1], NULL);
7662 
7663 		if ((i % 3) == 2)
7664 		{
7665 			x -= ((2*28) + 25)<<(FRACBITS-1);
7666 			y -= ((2*7) - 11)<<(FRACBITS-1);
7667 		}
7668 		else
7669 		{
7670 			x += 28<<(FRACBITS-1);
7671 			y += 7<<(FRACBITS-1);
7672 		}
7673 	}
7674 
7675 	y = (BASEVIDWIDTH-(vid.width/vid.dupx))/2;
7676 
7677 	V_DrawFill(y, 20, vid.width/vid.dupx, 24, 159);
7678 	{
7679 		static fixed_t st_scroll = -1;
7680 		const char* titl;
7681 		x = 16;
7682 		V_DrawString(x, 10, 0, "NOW PLAYING:");
7683 		if (curplaying)
7684 		{
7685 			if (curplaying->alttitle[0])
7686 				titl = va("%s - %s - ", curplaying->title, curplaying->alttitle);
7687 			else
7688 				titl = va("%s - ", curplaying->title);
7689 		}
7690 		else
7691 			titl = "None - ";
7692 
7693 		i = V_LevelNameWidth(titl);
7694 
7695 		if (++st_scroll >= i)
7696 			st_scroll %= i;
7697 
7698 		x -= st_scroll;
7699 
7700 		while (x < BASEVIDWIDTH-y)
7701 			x += i;
7702 		while (x > y)
7703 		{
7704 			x -= i;
7705 			V_DrawLevelTitle(x, 22, 0, titl);
7706 		}
7707 
7708 		if (curplaying)
7709 			V_DrawRightAlignedThinString(BASEVIDWIDTH-16, 46, V_ALLOWLOWERCASE, curplaying->authors);
7710 	}
7711 
7712 	V_DrawFill(165, 60, 140, 112, 159);
7713 
7714 	{
7715 		INT32 t, b, q, m = 112;
7716 
7717 		if (numsoundtestdefs <= 7)
7718 		{
7719 			t = 0;
7720 			b = numsoundtestdefs - 1;
7721 			i = 0;
7722 		}
7723 		else
7724 		{
7725 			q = m;
7726 			m = (5*m)/numsoundtestdefs;
7727 			if (st_sel < 3)
7728 			{
7729 				t = 0;
7730 				b = 6;
7731 				i = 0;
7732 			}
7733 			else if (st_sel >= numsoundtestdefs-4)
7734 			{
7735 				t = numsoundtestdefs - 7;
7736 				b = numsoundtestdefs - 1;
7737 				i = q-m;
7738 			}
7739 			else
7740 			{
7741 				t = st_sel - 3;
7742 				b = st_sel + 3;
7743 				i = (t * (q-m))/(numsoundtestdefs - 7);
7744 			}
7745 		}
7746 
7747 		V_DrawFill(165+140-1, 60 + i, 1, m, 0);
7748 
7749 		if (t != 0)
7750 			V_DrawString(165+140+4, 60+4 - (skullAnimCounter/5), V_YELLOWMAP, "\x1A");
7751 
7752 		if (b != numsoundtestdefs - 1)
7753 			V_DrawString(165+140+4, 60+112-12 + (skullAnimCounter/5), V_YELLOWMAP, "\x1B");
7754 
7755 		x = 169;
7756 		y = 64;
7757 
7758 		while (t <= b)
7759 		{
7760 			if (t == st_sel)
7761 				V_DrawFill(165, y-4, 140-1, 16, 155);
7762 			if (!soundtestdefs[t]->allowed)
7763 			{
7764 				V_DrawString(x, y, (t == st_sel ? V_YELLOWMAP : 0)|V_ALLOWLOWERCASE, "???");
7765 			}
7766 			else if (soundtestdefs[t] == &soundtestsfx)
7767 			{
7768 				const char *sfxstr = va("SFX %s", cv_soundtest.string);
7769 				V_DrawString(x, y, (t == st_sel ? V_YELLOWMAP : 0), sfxstr);
7770 				if (t == st_sel)
7771 				{
7772 					V_DrawCharacter(x - 10 - (skullAnimCounter/5), y,
7773 						'\x1C' | V_YELLOWMAP, false);
7774 					V_DrawCharacter(x + 2 + V_StringWidth(sfxstr, 0) + (skullAnimCounter/5), y,
7775 						'\x1D' | V_YELLOWMAP, false);
7776 				}
7777 
7778 				if (curplaying == soundtestdefs[t])
7779 				{
7780 					sfxstr = (cv_soundtest.value) ? S_sfx[cv_soundtest.value].name : "N/A";
7781 					i = V_StringWidth(sfxstr, 0);
7782 					V_DrawFill(165+140-9-i, y-4, i+8, 16, 150);
7783 					V_DrawRightAlignedString(165+140-5, y, V_YELLOWMAP, sfxstr);
7784 				}
7785 			}
7786 			else
7787 			{
7788 				V_DrawString(x, y, (t == st_sel ? V_YELLOWMAP : 0)|V_ALLOWLOWERCASE, soundtestdefs[t]->title);
7789 				if (curplaying == soundtestdefs[t])
7790 				{
7791 					V_DrawFill(165+140-9, y-4, 8, 16, 150);
7792 					//V_DrawCharacter(165+140-8, y, '\x19' | V_YELLOWMAP, false);
7793 					V_DrawFixedPatch((165+140-9)<<FRACBITS, (y<<FRACBITS)-(bounce*4), FRACUNIT, 0, hu_font['\x19'-HU_FONTSTART], V_GetStringColormap(V_YELLOWMAP));
7794 				}
7795 			}
7796 			t++;
7797 			y += 16;
7798 		}
7799 	}
7800 }
7801 
7802 static void M_HandleSoundTest(INT32 choice)
7803 {
7804 	boolean exitmenu = false; // exit to previous menu
7805 
7806 	switch (choice)
7807 	{
7808 		case KEY_DOWNARROW:
7809 			if (st_sel++ >= numsoundtestdefs-1)
7810 				st_sel = 0;
7811 			{
7812 				cv_closedcaptioning.value = st_cc; // hack
7813 				S_StartSound(NULL, sfx_menu1);
7814 				cv_closedcaptioning.value = 1; // hack
7815 			}
7816 			break;
7817 		case KEY_UPARROW:
7818 			if (!st_sel--)
7819 				st_sel = numsoundtestdefs-1;
7820 			{
7821 				cv_closedcaptioning.value = st_cc; // hack
7822 				S_StartSound(NULL, sfx_menu1);
7823 				cv_closedcaptioning.value = 1; // hack
7824 			}
7825 			break;
7826 		case KEY_PGDN:
7827 			if (st_sel < numsoundtestdefs-1)
7828 			{
7829 				st_sel += 3;
7830 				if (st_sel >= numsoundtestdefs-1)
7831 					st_sel = numsoundtestdefs-1;
7832 				cv_closedcaptioning.value = st_cc; // hack
7833 				S_StartSound(NULL, sfx_menu1);
7834 				cv_closedcaptioning.value = 1; // hack
7835 			}
7836 			break;
7837 		case KEY_PGUP:
7838 			if (st_sel)
7839 			{
7840 				st_sel -= 3;
7841 				if (st_sel < 0)
7842 					st_sel = 0;
7843 				cv_closedcaptioning.value = st_cc; // hack
7844 				S_StartSound(NULL, sfx_menu1);
7845 				cv_closedcaptioning.value = 1; // hack
7846 			}
7847 			break;
7848 		case KEY_BACKSPACE:
7849 			if (curplaying)
7850 			{
7851 				S_StopSounds();
7852 				S_StopMusic();
7853 				curplaying = NULL;
7854 				st_time = 0;
7855 				cv_closedcaptioning.value = st_cc; // hack
7856 				S_StartSound(NULL, sfx_skid);
7857 				cv_closedcaptioning.value = 1; // hack
7858 			}
7859 			break;
7860 		case KEY_ESCAPE:
7861 			exitmenu = true;
7862 			break;
7863 
7864 		case KEY_RIGHTARROW:
7865 			if (soundtestdefs[st_sel] == &soundtestsfx && soundtestdefs[st_sel]->allowed)
7866 			{
7867 				S_StopSounds();
7868 				S_StopMusic();
7869 				curplaying = soundtestdefs[st_sel];
7870 				st_time = 0;
7871 				CV_AddValue(&cv_soundtest, 1);
7872 			}
7873 			break;
7874 		case KEY_LEFTARROW:
7875 			if (soundtestdefs[st_sel] == &soundtestsfx && soundtestdefs[st_sel]->allowed)
7876 			{
7877 				S_StopSounds();
7878 				S_StopMusic();
7879 				curplaying = soundtestdefs[st_sel];
7880 				st_time = 0;
7881 				CV_AddValue(&cv_soundtest, -1);
7882 			}
7883 			break;
7884 		case KEY_ENTER:
7885 			S_StopSounds();
7886 			S_StopMusic();
7887 			st_time = 0;
7888 			if (soundtestdefs[st_sel]->allowed)
7889 			{
7890 				curplaying = soundtestdefs[st_sel];
7891 				if (curplaying == &soundtestsfx)
7892 				{
7893 					// S_StopMusic() -- is this necessary?
7894 					if (cv_soundtest.value)
7895 						S_StartSound(NULL, cv_soundtest.value);
7896 				}
7897 				else
7898 					S_ChangeMusicInternal(curplaying->name, !curplaying->stoppingtics);
7899 			}
7900 			else
7901 			{
7902 				curplaying = NULL;
7903 				S_StartSound(NULL, sfx_lose);
7904 			}
7905 			break;
7906 
7907 		default:
7908 			break;
7909 	}
7910 	if (exitmenu)
7911 	{
7912 		Z_Free(soundtestdefs);
7913 		soundtestdefs = NULL;
7914 
7915 		cv_closedcaptioning.value = st_cc; // undo hack
7916 
7917 		if (currentMenu->prevMenu)
7918 			M_SetupNextMenu(currentMenu->prevMenu);
7919 		else
7920 			M_ClearMenus(true);
7921 	}
7922 }
7923 
7924 // Entering secrets menu
7925 static void M_SecretsMenu(INT32 choice)
7926 {
7927 	INT32 i, j, ul;
7928 	UINT8 done[MAXUNLOCKABLES];
7929 	UINT16 curheight;
7930 
7931 	(void)choice;
7932 
7933 	// Clear all before starting
7934 	for (i = 1; i < MAXUNLOCKABLES+1; ++i)
7935 		SR_MainMenu[i].status = IT_DISABLED;
7936 
7937 	memset(skyRoomMenuTranslations, 0, sizeof(skyRoomMenuTranslations));
7938 	memset(done, 0, sizeof(done));
7939 
7940 	for (i = 1; i < MAXUNLOCKABLES+1; ++i)
7941 	{
7942 		curheight = UINT16_MAX;
7943 		ul = -1;
7944 
7945 		// Autosort unlockables
7946 		for (j = 0; j < MAXUNLOCKABLES; ++j)
7947 		{
7948 			if (!unlockables[j].height || done[j] || unlockables[j].type < 0)
7949 				continue;
7950 
7951 			if (unlockables[j].height < curheight)
7952 			{
7953 				curheight = unlockables[j].height;
7954 				ul = j;
7955 			}
7956 		}
7957 		if (ul < 0)
7958 			break;
7959 
7960 		done[ul] = true;
7961 
7962 		skyRoomMenuTranslations[i-1] = (UINT8)ul;
7963 		SR_MainMenu[i].text = unlockables[ul].name;
7964 		SR_MainMenu[i].alphaKey = (UINT16)unlockables[ul].height;
7965 
7966 		if (unlockables[ul].type == SECRET_HEADER)
7967 		{
7968 			SR_MainMenu[i].status = IT_HEADER;
7969 			continue;
7970 		}
7971 
7972 		SR_MainMenu[i].status = IT_SECRET;
7973 
7974 		if (unlockables[ul].unlocked)
7975 		{
7976 			switch (unlockables[ul].type)
7977 			{
7978 				case SECRET_LEVELSELECT:
7979 					SR_MainMenu[i].status = IT_STRING|IT_CALL;
7980 					SR_MainMenu[i].itemaction = M_CustomLevelSelect;
7981 					break;
7982 				case SECRET_WARP:
7983 					SR_MainMenu[i].status = IT_STRING|IT_CALL;
7984 					SR_MainMenu[i].itemaction = M_CustomWarp;
7985 					break;
7986 				case SECRET_CREDITS:
7987 					SR_MainMenu[i].status = IT_STRING|IT_CALL;
7988 					SR_MainMenu[i].itemaction = M_Credits;
7989 					break;
7990 				case SECRET_SOUNDTEST:
7991 					SR_MainMenu[i].status = IT_STRING|IT_CALL;
7992 					SR_MainMenu[i].itemaction = M_SoundTest;
7993 				default:
7994 					break;
7995 			}
7996 		}
7997 	}
7998 
7999 	M_SetupNextMenu(&SR_MainDef);
8000 }
8001 
8002 // ==================
8003 // NEW GAME FUNCTIONS
8004 // ==================
8005 
8006 INT32 ultimate_selectable = false;
8007 
8008 static void M_NewGame(void)
8009 {
8010 	fromlevelselect = false;
8011 
8012 	startmap = spstage_start;
8013 	CV_SetValue(&cv_newgametype, GT_COOP); // Graue 09-08-2004
8014 
8015 	M_SetupChoosePlayer(0);
8016 }
8017 
8018 static void M_CustomWarp(INT32 choice)
8019 {
8020 	INT32 ul = skyRoomMenuTranslations[choice-1];
8021 
8022 	startmap = (INT16)(unlockables[ul].variable);
8023 
8024 	M_SetupChoosePlayer(0);
8025 }
8026 
8027 static void M_Credits(INT32 choice)
8028 {
8029 	(void)choice;
8030 	cursaveslot = -1;
8031 	M_ClearMenus(true);
8032 	F_StartCredits();
8033 }
8034 
8035 static void M_CustomLevelSelect(INT32 choice)
8036 {
8037 	INT32 ul = skyRoomMenuTranslations[choice-1];
8038 
8039 	SR_LevelSelectDef.prevMenu = currentMenu;
8040 	levellistmode = LLM_LEVELSELECT;
8041 	maplistoption = (UINT8)(unlockables[ul].variable);
8042 
8043 	if (!M_PrepareLevelPlatter(-1, true))
8044 	{
8045 		M_StartMessage(M_GetText("No selectable levels found.\n"),NULL,MM_NOTHING);
8046 		return;
8047 	}
8048 
8049 	M_SetupNextMenu(&SR_LevelSelectDef);
8050 }
8051 
8052 // ==================
8053 // SINGLE PLAYER MENU
8054 // ==================
8055 
8056 static void M_SinglePlayerMenu(INT32 choice)
8057 {
8058 	(void)choice;
8059 
8060 
8061 	// Reset the item positions, to avoid them sinking farther down every time the menu is opened if one is unavailable
8062 	// Note that they're reset, not simply "not moved again", in case mid-game add-ons re-enable an option
8063 	SP_MainMenu[spstartgame]   .alphaKey = 76;
8064 	SP_MainMenu[sprecordattack].alphaKey = 84;
8065 	SP_MainMenu[spnightsmode]  .alphaKey = 92;
8066 	SP_MainMenu[spmarathon]    .alphaKey = 100;
8067 	//SP_MainMenu[sptutorial]  .alphaKey = 108; // Not needed
8068 	//SP_MainMenu[spstatistics].alphaKey = 116; // Not needed
8069 
8070 
8071 	levellistmode = LLM_RECORDATTACK;
8072 	if (M_GametypeHasLevels(-1))
8073 		SP_MainMenu[sprecordattack].status = (M_SecretUnlocked(SECRET_RECORDATTACK)) ? IT_CALL|IT_STRING : IT_SECRET;
8074 	else // If Record Attack is nonexistent in the current add-on...
8075 	{
8076 		SP_MainMenu[sprecordattack].status = IT_NOTHING|IT_DISABLED; // ...hide and disable the Record Attack option...
8077 		SP_MainMenu[spstartgame].alphaKey += 8; // ...and lower Start Game by 8 pixels to close the gap
8078 	}
8079 
8080 
8081 	levellistmode = LLM_NIGHTSATTACK;
8082 	if (M_GametypeHasLevels(-1))
8083 		SP_MainMenu[spnightsmode].status = (M_SecretUnlocked(SECRET_NIGHTSMODE)) ? IT_CALL|IT_STRING : IT_SECRET;
8084 	else // If NiGHTS Mode is nonexistent in the current add-on...
8085 	{
8086 		SP_MainMenu[spnightsmode].status = IT_NOTHING|IT_DISABLED; // ...hide and disable the NiGHTS Mode option...
8087 		// ...and lower the above options' display positions by 8 pixels to close the gap
8088 		SP_MainMenu[spstartgame]   .alphaKey += 8;
8089 		SP_MainMenu[sprecordattack].alphaKey += 8;
8090 	}
8091 
8092 
8093 	// If the FIRST stage immediately leads to the ending, or itself (which gets converted to the title screen in G_DoCompleted for marathonmode only), there's no point in having this option on the menu. You should use Record Attack in that circumstance, although if marathonnext is set this behaviour can be overridden if you make some weird mod that requires multiple playthroughs of the same map in sequence and has some in-level mechanism to break the cycle.
8094 	if (mapheaderinfo[spmarathon_start-1]
8095 		&& !mapheaderinfo[spmarathon_start-1]->marathonnext
8096 		&& (mapheaderinfo[spmarathon_start-1]->nextlevel == spmarathon_start
8097 			|| mapheaderinfo[spmarathon_start-1]->nextlevel >= 1100))
8098 	{
8099 		SP_MainMenu[spmarathon].status = IT_NOTHING|IT_DISABLED; // Hide and disable the Marathon Run option...
8100 		// ...and lower the above options' display positions by 8 pixels to close the gap
8101 		SP_MainMenu[spstartgame]   .alphaKey += 8;
8102 		SP_MainMenu[sprecordattack].alphaKey += 8;
8103 		SP_MainMenu[spnightsmode]  .alphaKey += 8;
8104 	}
8105 	else // Otherwise, if Marathon Run is allowed and Record Attack is unlocked, unlock Marathon Run!
8106 		SP_MainMenu[spmarathon].status = (M_SecretUnlocked(SECRET_RECORDATTACK)) ? IT_CALL|IT_STRING|IT_CALL_NOTMODIFIED : IT_SECRET;
8107 
8108 
8109 	if (tutorialmap) // If there's a tutorial available in the current add-on...
8110 		SP_MainMenu[sptutorial].status = IT_CALL | IT_STRING; // ...always unlock Tutorial
8111 	else // But if there's no tutorial available in the current add-on...
8112 	{
8113 		SP_MainMenu[sptutorial].status = IT_NOTHING|IT_DISABLED; // ...hide and disable the Tutorial option...
8114 		// ...and lower the above options' display positions by 8 pixels to close the gap
8115 		SP_MainMenu[spstartgame]   .alphaKey += 8;
8116 		SP_MainMenu[sprecordattack].alphaKey += 8;
8117 		SP_MainMenu[spnightsmode]  .alphaKey += 8;
8118 		SP_MainMenu[spmarathon]    .alphaKey += 8;
8119 	}
8120 
8121 
8122 	M_SetupNextMenu(&SP_MainDef);
8123 }
8124 
8125 static void M_LoadGameLevelSelect(INT32 choice)
8126 {
8127 	(void)choice;
8128 
8129 	SP_LevelSelectDef.prevMenu = currentMenu;
8130 	levellistmode = LLM_LEVELSELECT;
8131 	maplistoption = 1+2;
8132 
8133 	if (!M_PrepareLevelPlatter(-1, true))
8134 	{
8135 		M_StartMessage(M_GetText("No selectable levels found.\n"),NULL,MM_NOTHING);
8136 		return;
8137 	}
8138 
8139 	M_SetupNextMenu(&SP_LevelSelectDef);
8140 }
8141 
8142 void M_TutorialSaveControlResponse(INT32 ch)
8143 {
8144 	if (ch == 'y' || ch == KEY_ENTER)
8145 	{
8146 		G_CopyControls(gamecontrol, gamecontroldefault[tutorialgcs], gcl_tutorial_full, num_gcl_tutorial_full);
8147 		CV_Set(&cv_usemouse, cv_usemouse.defaultvalue);
8148 		CV_Set(&cv_alwaysfreelook, cv_alwaysfreelook.defaultvalue);
8149 		CV_Set(&cv_mousemove, cv_mousemove.defaultvalue);
8150 		CV_Set(&cv_analog[0], cv_analog[0].defaultvalue);
8151 		S_StartSound(NULL, sfx_itemup);
8152 	}
8153 	else
8154 		S_StartSound(NULL, sfx_menu1);
8155 }
8156 
8157 static void M_TutorialControlResponse(INT32 ch)
8158 {
8159 	if (ch != KEY_ESCAPE)
8160 	{
8161 		G_CopyControls(gamecontroldefault[gcs_custom], gamecontrol, NULL, 0); // using gcs_custom as temp storage for old controls
8162 		if (ch == 'y' || ch == KEY_ENTER)
8163 		{
8164 			tutorialgcs = gcs_fps;
8165 			tutorialusemouse = cv_usemouse.value;
8166 			tutorialfreelook = cv_alwaysfreelook.value;
8167 			tutorialmousemove = cv_mousemove.value;
8168 			tutorialanalog = cv_analog[0].value;
8169 
8170 			G_CopyControls(gamecontrol, gamecontroldefault[tutorialgcs], gcl_tutorial_full, num_gcl_tutorial_full);
8171 			CV_Set(&cv_usemouse, cv_usemouse.defaultvalue);
8172 			CV_Set(&cv_alwaysfreelook, cv_alwaysfreelook.defaultvalue);
8173 			CV_Set(&cv_mousemove, cv_mousemove.defaultvalue);
8174 			CV_Set(&cv_analog[0], cv_analog[0].defaultvalue);
8175 
8176 			//S_StartSound(NULL, sfx_itemup);
8177 		}
8178 		else
8179 		{
8180 			tutorialgcs = gcs_custom;
8181 			S_StartSound(NULL, sfx_menu1);
8182 		}
8183 		M_StartTutorial(INT32_MAX);
8184 	}
8185 	else
8186 		S_StartSound(NULL, sfx_menu1);
8187 
8188 	MessageDef.prevMenu = &SP_MainDef; // if FirstPrompt -> ControlsPrompt -> ESC, we would go to the main menu unless we force this
8189 }
8190 
8191 // Starts up the tutorial immediately (tbh I wasn't sure where else to put this)
8192 static void M_StartTutorial(INT32 choice)
8193 {
8194 	if (!tutorialmap)
8195 		return; // no map to go to, don't bother
8196 
8197 	if (choice != INT32_MAX && G_GetControlScheme(gamecontrol, gcl_tutorial_check, num_gcl_tutorial_check) != gcs_fps)
8198 	{
8199 		M_StartMessage("Do you want to try the \202recommended \202movement controls\x80?\n\nWe will set them just for this tutorial.\n\nPress 'Y' or 'Enter' to confirm\nPress 'N' or any key to keep \nyour current controls.\n",M_TutorialControlResponse,MM_YESNO);
8200 		return;
8201 	}
8202 	else if (choice != INT32_MAX)
8203 		tutorialgcs = gcs_custom;
8204 
8205 	CV_SetValue(&cv_tutorialprompt, 0); // first-time prompt
8206 
8207 	tutorialmode = true; // turn on tutorial mode
8208 
8209 	emeralds = 0;
8210 	memset(&luabanks, 0, sizeof(luabanks));
8211 	M_ClearMenus(true);
8212 	gamecomplete = 0;
8213 	cursaveslot = 0;
8214 	G_DeferedInitNew(false, G_BuildMapName(tutorialmap), 0, false, false);
8215 }
8216 
8217 // ==============
8218 // LOAD GAME MENU
8219 // ==============
8220 
8221 static INT32 saveSlotSelected = 1;
8222 static INT32 loadgamescroll = 0;
8223 static UINT8 loadgameoffset = 0;
8224 
8225 static void M_CacheLoadGameData(void)
8226 {
8227 	savselp[0] = W_CachePatchName("SAVEBACK", PU_PATCH);
8228 	savselp[1] = W_CachePatchName("SAVENONE", PU_PATCH);
8229 	savselp[2] = W_CachePatchName("ULTIMATE", PU_PATCH);
8230 
8231 	savselp[3] = W_CachePatchName("GAMEDONE", PU_PATCH);
8232 	savselp[4] = W_CachePatchName("BLACXLVL", PU_PATCH);
8233 	savselp[5] = W_CachePatchName("BLANKLVL", PU_PATCH);
8234 }
8235 
8236 static void M_DrawLoadGameData(void)
8237 {
8238 	INT32 i, prev_i = 1, savetodraw, x, y, hsep = 90;
8239 	skin_t *charskin = NULL;
8240 
8241 	if (vid.width != BASEVIDWIDTH*vid.dupx)
8242 		hsep = (hsep*vid.width)/(BASEVIDWIDTH*vid.dupx);
8243 
8244 	for (i = 2; prev_i; i = -(i + ((UINT32)i >> 31))) // draws from outwards in; 2, -2, 1, -1, 0
8245 	{
8246 		prev_i = i;
8247 		savetodraw = (saveSlotSelected + i + numsaves)%numsaves;
8248 		x = (BASEVIDWIDTH/2 - 42 + loadgamescroll) + (i*hsep);
8249 		y = 33 + 9;
8250 
8251 		{
8252 			INT32 diff = x - (BASEVIDWIDTH/2 - 42);
8253 			if (diff < 0)
8254 				diff = -diff;
8255 			diff = (42 - diff)/3 - loadgameoffset;
8256 			if (diff < 0)
8257 				diff = 0;
8258 			y -= diff;
8259 		}
8260 
8261 		if (savetodraw == 0)
8262 		{
8263 			V_DrawSmallScaledPatch(x, y, 0,
8264 				savselp[((ultimate_selectable) ? 2 : 1)]);
8265 			x += 2;
8266 			y += 1;
8267 			V_DrawString(x, y,
8268 				((savetodraw == saveSlotSelected) ? V_YELLOWMAP : 0),
8269 				"NO FILE");
8270 			if (savetodraw == saveSlotSelected)
8271 				V_DrawFill(x, y+9, 80, 1, yellowmap[3]);
8272 			y += 11;
8273 			V_DrawSmallScaledPatch(x, y, 0, savselp[4]);
8274 			M_DrawStaticBox(x, y, V_80TRANS, 80, 50);
8275 			y += 41;
8276 			if (ultimate_selectable)
8277 				V_DrawRightAlignedThinString(x + 79, y, V_REDMAP, "ULTIMATE.");
8278 			else
8279 				V_DrawRightAlignedThinString(x + 79, y, V_GRAYMAP, "DON'T SAVE!");
8280 
8281 			continue;
8282 		}
8283 
8284 		savetodraw--;
8285 
8286 		if (savegameinfo[savetodraw].lives > 0)
8287 			charskin = &skins[savegameinfo[savetodraw].skinnum];
8288 
8289 		// signpost background
8290 		{
8291 			UINT8 col;
8292 			if (savegameinfo[savetodraw].lives == -666)
8293 			{
8294 				V_DrawSmallScaledPatch(x+2, y+64, 0, savselp[5]);
8295 			}
8296 #ifdef PERFECTSAVE // disabled on request
8297 			else if ((savegameinfo[savetodraw].skinnum == 1)
8298 			&& (savegameinfo[savetodraw].lives == 99)
8299 			&& (savegameinfo[savetodraw].gamemap & 8192)
8300 			&& (savegameinfo[savetodraw].numgameovers == 0)
8301 			&& (savegameinfo[savetodraw].numemeralds == ((1<<7) - 1))) // perfect save
8302 			{
8303 				V_DrawFill(x+6, y+64, 72, 50, 134);
8304 				V_DrawFill(x+6, y+74, 72, 30, 201);
8305 				V_DrawFill(x+6, y+84, 72, 10, 1);
8306 			}
8307 #endif
8308 			else
8309 			{
8310 				if (savegameinfo[savetodraw].lives == -42)
8311 					col = 26;
8312 				else if (savegameinfo[savetodraw].botskin == 3) // & knuckles
8313 					col = 105;
8314 				else if (savegameinfo[savetodraw].botskin) // tailsbot or custom
8315 					col = 134;
8316 				else
8317 				{
8318 					if (charskin->prefoppositecolor)
8319 					{
8320 						col = charskin->prefoppositecolor;
8321 						col = skincolors[col].ramp[skincolors[skincolors[col].invcolor].invshade];
8322 					}
8323 					else
8324 					{
8325 						col = charskin->prefcolor;
8326 						col = skincolors[skincolors[col].invcolor].ramp[skincolors[col].invshade];
8327 					}
8328 				}
8329 
8330 				V_DrawFill(x+6, y+64, 72, 50, col);
8331 			}
8332 		}
8333 
8334 		V_DrawSmallScaledPatch(x, y, 0, savselp[0]);
8335 		x += 2;
8336 		y += 1;
8337 		V_DrawString(x, y,
8338 			((savetodraw == saveSlotSelected-1) ? V_YELLOWMAP : 0),
8339 			va("FILE %d", savetodraw+1));
8340 		if (savetodraw == saveSlotSelected-1)
8341 				V_DrawFill(x, y+9, 80, 1, yellowmap[3]);
8342 		y += 11;
8343 
8344 		// level image area
8345 		{
8346 			if ((savegameinfo[savetodraw].lives == -42)
8347 			|| (savegameinfo[savetodraw].lives == -666))
8348 			{
8349 				V_DrawFill(x, y, 80, 50, 31);
8350 				M_DrawStaticBox(x, y, V_80TRANS, 80, 50);
8351 			}
8352 			else
8353 			{
8354 				patch_t *patch;
8355 				if (savegameinfo[savetodraw].gamemap & 8192)
8356 					patch = savselp[3];
8357 				else
8358 				{
8359 					lumpnum_t lumpnum = W_CheckNumForName(va("%sP", G_BuildMapName((savegameinfo[savetodraw].gamemap) & 8191)));
8360 					if (lumpnum != LUMPERROR)
8361 						patch = W_CachePatchNum(lumpnum, PU_PATCH);
8362 					else
8363 						patch = savselp[5];
8364 				}
8365 				V_DrawSmallScaledPatch(x, y, 0, patch);
8366 			}
8367 
8368 			y += 41;
8369 
8370 			if (savegameinfo[savetodraw].lives == -42)
8371 				V_DrawRightAlignedThinString(x + 79, y, V_GRAYMAP, "NEW GAME");
8372 			else if (savegameinfo[savetodraw].lives == -666)
8373 				V_DrawRightAlignedThinString(x + 79, y, V_REDMAP, "CAN'T LOAD!");
8374 			else if (savegameinfo[savetodraw].gamemap & 8192)
8375 				V_DrawRightAlignedThinString(x + 79, y, V_GREENMAP, "CLEAR!");
8376 			else
8377 				V_DrawRightAlignedThinString(x + 79, y, V_YELLOWMAP, savegameinfo[savetodraw].levelname);
8378 		}
8379 
8380 		if (savegameinfo[savetodraw].lives == -42)
8381 		{
8382 			if (!useContinues)
8383 				V_DrawRightAlignedThinString(x + 80, y+1+60+16, V_GRAYMAP, "00000000");
8384 			continue;
8385 		}
8386 
8387 		if (savegameinfo[savetodraw].lives == -666)
8388 		{
8389 			if (!useContinues)
8390 				V_DrawRightAlignedThinString(x + 80, y+1+60+16, V_REDMAP, "????????");
8391 			continue;
8392 		}
8393 
8394 		y += 64;
8395 
8396 		// tiny emeralds
8397 		{
8398 			INT32 j, workx = x + 6;
8399 			for (j = 0; j < 7; ++j)
8400 			{
8401 				if (savegameinfo[savetodraw].numemeralds & (1 << j))
8402 					V_DrawScaledPatch(workx, y, 0, emeraldpics[1][j]);
8403 				workx += 10;
8404 			}
8405 		}
8406 
8407 		y -= 4;
8408 
8409 		// character heads, lives, and continues/score
8410 		{
8411 			spritedef_t *sprdef;
8412 			spriteframe_t *sprframe;
8413 			patch_t *patch;
8414 			UINT8 *colormap = NULL;
8415 
8416 			INT32 tempx = (x+40)<<FRACBITS, flip = 0;
8417 
8418 			// botskin first
8419 			if (savegameinfo[savetodraw].botskin)
8420 			{
8421 				skin_t *charbotskin = &skins[savegameinfo[savetodraw].botskin-1];
8422 				sprdef = &charbotskin->sprites[SPR2_SIGN];
8423 				if (!sprdef->numframes)
8424 					goto skipbot;
8425 				colormap = R_GetTranslationColormap(savegameinfo[savetodraw].botskin-1, charbotskin->prefcolor, GTC_CACHE);
8426 				sprframe = &sprdef->spriteframes[0];
8427 				patch = W_CachePatchNum(sprframe->lumppat[0], PU_PATCH);
8428 
8429 				V_DrawFixedPatch(
8430 					tempx + (18<<FRACBITS),
8431 					y<<FRACBITS,
8432 					charbotskin->highresscale,
8433 					0, patch, colormap);
8434 
8435 				tempx -= (20<<FRACBITS);
8436 				//flip = V_FLIP;
8437 			}
8438 skipbot:
8439 			// signpost image
8440 			if (!charskin) // shut up compiler
8441 				goto skipsign;
8442 			sprdef = &charskin->sprites[SPR2_SIGN];
8443 			colormap = R_GetTranslationColormap(savegameinfo[savetodraw].skinnum, charskin->prefcolor, GTC_CACHE);
8444 			if (!sprdef->numframes)
8445 				goto skipsign;
8446 			sprframe = &sprdef->spriteframes[0];
8447 			patch = W_CachePatchNum(sprframe->lumppat[0], PU_PATCH);
8448 
8449 			V_DrawFixedPatch(
8450 				tempx,
8451 				y<<FRACBITS,
8452 				charskin->highresscale,
8453 				flip, patch, colormap);
8454 
8455 skipsign:
8456 			y += 16;
8457 
8458 			tempx = x;
8459 			if (useContinues)
8460 			{
8461 				tempx += 10;
8462 				if (savegameinfo[savetodraw].lives != INFLIVES
8463 				&& savegameinfo[savetodraw].lives > 9)
8464 					tempx -= 4;
8465 			}
8466 
8467 			if (!charskin) // shut up compiler
8468 				goto skiplife;
8469 
8470 			// lives
8471 			sprdef = &charskin->sprites[SPR2_LIFE];
8472 			if (!sprdef->numframes)
8473 				goto skiplife;
8474 			sprframe = &sprdef->spriteframes[0];
8475 			patch = W_CachePatchNum(sprframe->lumppat[0], PU_PATCH);
8476 
8477 			V_DrawFixedPatch(
8478 				(tempx + 4)<<FRACBITS,
8479 				(y + 6)<<FRACBITS,
8480 				charskin->highresscale/2,
8481 				0, patch, colormap);
8482 skiplife:
8483 
8484 			patch = W_CachePatchName("STLIVEX", PU_PATCH);
8485 
8486 			V_DrawScaledPatch(tempx + 9, y + 2, 0, patch);
8487 			tempx += 16;
8488 			if (savegameinfo[savetodraw].lives == INFLIVES)
8489 				V_DrawCharacter(tempx, y + 1, '\x16', false);
8490 			else
8491 				V_DrawString(tempx, y, 0, va("%d", savegameinfo[savetodraw].lives));
8492 
8493 			if (!useContinues)
8494 			{
8495 				INT32 workingscorenum = savegameinfo[savetodraw].continuescore;
8496 				char workingscorestr[11] = " 000000000\0";
8497 				SINT8 j = 9;
8498 				// Change the above two lines if MAXSCORE ever changes from 8 digits long.
8499 				workingscorestr[0] = '\x86'; // done here instead of in initialiser 'cuz compiler complains
8500 				if (!workingscorenum)
8501 					j--; // just so ONE digit is not greyed out
8502 				else
8503 				{
8504 					while (workingscorenum)
8505 					{
8506 						workingscorestr[j--] = '0' + (workingscorenum % 10);
8507 						workingscorenum /= 10;
8508 					}
8509 				}
8510 				workingscorestr[j] = (savegameinfo[savetodraw].continuescore == MAXSCORE) ? '\x83' : '\x80';
8511 				V_DrawRightAlignedThinString(x + 80, y+1, 0, workingscorestr);
8512 			}
8513 			else
8514 			{
8515 				tempx = x + 47;
8516 				if (savegameinfo[savetodraw].continuescore > 9)
8517 					tempx -= 4;
8518 
8519 				// continues
8520 				if (savegameinfo[savetodraw].continuescore > 0)
8521 				{
8522 					V_DrawSmallScaledPatch(tempx, y, 0, W_CachePatchName("CONTSAVE", PU_PATCH));
8523 					V_DrawScaledPatch(tempx + 9, y + 2, 0, patch);
8524 					V_DrawString(tempx + 16, y, 0, va("%d", savegameinfo[savetodraw].continuescore));
8525 				}
8526 				else
8527 				{
8528 					V_DrawSmallScaledPatch(tempx, y, 0, W_CachePatchName("CONTNONE", PU_PATCH));
8529 					V_DrawScaledPatch(tempx + 9, y + 2, 0, W_CachePatchName("STNONEX", PU_PATCH));
8530 					V_DrawString(tempx + 16, y, V_GRAYMAP, "0");
8531 				}
8532 			}
8533 		}
8534 	}
8535 }
8536 
8537 static void M_DrawLoad(void)
8538 {
8539 	M_DrawMenuTitle();
8540 
8541 	if (loadgamescroll > 1 || loadgamescroll < -1)
8542 		loadgamescroll = 2*loadgamescroll/3;
8543 	else
8544 		loadgamescroll = 0;
8545 
8546 	if (loadgameoffset > 1)
8547 		loadgameoffset = 2*loadgameoffset/3;
8548 	else
8549 		loadgameoffset = 0;
8550 
8551 	M_DrawLoadGameData();
8552 }
8553 
8554 //
8555 // User wants to load this game
8556 //
8557 static void M_LoadSelect(INT32 choice)
8558 {
8559 	(void)choice;
8560 
8561 	if (saveSlotSelected == NOSAVESLOT) //last slot is play without saving
8562 	{
8563 		M_NewGame();
8564 		cursaveslot = 0;
8565 		return;
8566 	}
8567 
8568 	if (!FIL_ReadFileOK(va(savegamename, saveSlotSelected)))
8569 	{
8570 		// This slot is empty, so start a new game here.
8571 		M_NewGame();
8572 	}
8573 	else if (savegameinfo[saveSlotSelected-1].gamemap & 8192) // Completed
8574 		M_LoadGameLevelSelect(0);
8575 	else
8576 		G_LoadGame((UINT32)saveSlotSelected, 0);
8577 
8578 	cursaveslot = saveSlotSelected;
8579 }
8580 
8581 #define VERSIONSIZE 16
8582 #define BADSAVE { savegameinfo[slot].lives = -666; Z_Free(savebuffer); return; }
8583 #define CHECKPOS if (save_p >= end_p) BADSAVE
8584 // Reads the save file to list lives, level, player, etc.
8585 // Tails 05-29-2003
8586 static void M_ReadSavegameInfo(UINT32 slot)
8587 {
8588 	size_t length;
8589 	char savename[255];
8590 	UINT8 *savebuffer;
8591 	UINT8 *end_p; // buffer end point, don't read past here
8592 	UINT8 *save_p;
8593 	INT32 fake; // Dummy variable
8594 	char temp[sizeof(timeattackfolder)];
8595 	char vcheck[VERSIONSIZE];
8596 
8597 	sprintf(savename, savegamename, slot);
8598 
8599 	slot--;
8600 
8601 	length = FIL_ReadFile(savename, &savebuffer);
8602 	if (length == 0)
8603 	{
8604 		savegameinfo[slot].lives = -42;
8605 		return;
8606 	}
8607 
8608 	end_p = savebuffer + length;
8609 
8610 	// skip the description field
8611 	save_p = savebuffer;
8612 
8613 	// Version check
8614 	memset(vcheck, 0, sizeof (vcheck));
8615 	sprintf(vcheck, "version %d", VERSION);
8616 	if (strcmp((const char *)save_p, (const char *)vcheck)) BADSAVE
8617 	save_p += VERSIONSIZE;
8618 
8619 	// dearchive all the modifications
8620 	// P_UnArchiveMisc()
8621 
8622 	CHECKPOS
8623 	fake = READINT16(save_p);
8624 
8625 	if (((fake-1) & 8191) >= NUMMAPS) BADSAVE
8626 
8627 	if(!mapheaderinfo[(fake-1) & 8191])
8628 		savegameinfo[slot].levelname[0] = '\0';
8629 	else
8630 	{
8631 		strlcpy(savegameinfo[slot].levelname, mapheaderinfo[(fake-1) & 8191]->lvlttl, 17+1);
8632 
8633 		if (strlen(mapheaderinfo[(fake-1) & 8191]->lvlttl) >= 17)
8634 			strcpy(savegameinfo[slot].levelname+17-3, "...");
8635 	}
8636 
8637 	savegameinfo[slot].gamemap = fake;
8638 
8639 	CHECKPOS
8640 	savegameinfo[slot].numemeralds = READUINT16(save_p)-357; // emeralds
8641 
8642 	CHECKPOS
8643 	READSTRINGN(save_p, temp, sizeof(temp)); // mod it belongs to
8644 
8645 	if (strcmp(temp, timeattackfolder)) BADSAVE
8646 
8647 	// P_UnArchivePlayer()
8648 	CHECKPOS
8649 	fake = READUINT16(save_p);
8650 	savegameinfo[slot].skinnum = fake & ((1<<5) - 1);
8651 	if (savegameinfo[slot].skinnum >= numskins
8652 	|| !R_SkinUsable(-1, savegameinfo[slot].skinnum))
8653 		BADSAVE
8654 	savegameinfo[slot].botskin = fake >> 5;
8655 	if (savegameinfo[slot].botskin-1 >= numskins
8656 	|| !R_SkinUsable(-1, savegameinfo[slot].botskin-1))
8657 		BADSAVE
8658 
8659 	CHECKPOS
8660 	savegameinfo[slot].numgameovers = READUINT8(save_p); // numgameovers
8661 	CHECKPOS
8662 	savegameinfo[slot].lives = READSINT8(save_p); // lives
8663 	CHECKPOS
8664 	savegameinfo[slot].continuescore = READINT32(save_p); // score
8665 	CHECKPOS
8666 	fake = READINT32(save_p); // continues
8667 	if (useContinues)
8668 		savegameinfo[slot].continuescore = fake;
8669 
8670 	// File end marker check
8671 	CHECKPOS
8672 	switch (READUINT8(save_p))
8673 	{
8674 		case 0xb7:
8675 			{
8676 				UINT8 i, banksinuse;
8677 				CHECKPOS
8678 				banksinuse = READUINT8(save_p);
8679 				CHECKPOS
8680 				if (banksinuse > NUM_LUABANKS)
8681 					BADSAVE
8682 				for (i = 0; i < banksinuse; i++)
8683 				{
8684 					(void)READINT32(save_p);
8685 					CHECKPOS
8686 				}
8687 				if (READUINT8(save_p) != 0x1d)
8688 					BADSAVE
8689 			}
8690 		case 0x1d:
8691 			break;
8692 		default:
8693 			BADSAVE
8694 	}
8695 
8696 	// done
8697 	Z_Free(savebuffer);
8698 }
8699 #undef CHECKPOS
8700 #undef BADSAVE
8701 
8702 //
8703 // M_ReadSaveStrings
8704 //  read the strings from the savegame files
8705 //  and put it in savegamestrings global variable
8706 //
8707 static void M_ReadSaveStrings(void)
8708 {
8709 	FILE *handle;
8710 	SINT8 i;
8711 	char name[256];
8712 	boolean nofile[MAXSAVEGAMES-1];
8713 	SINT8 tolerance = 3; // empty slots at any time
8714 	UINT8 lastseen = 0;
8715 
8716 	loadgamescroll = 0;
8717 	loadgameoffset = 14;
8718 
8719 	for (i = 1; (i < MAXSAVEGAMES); i++) // slot 0 is no save
8720 	{
8721 		snprintf(name, sizeof name, savegamename, i);
8722 		name[sizeof name - 1] = '\0';
8723 
8724 		handle = fopen(name, "rb");
8725 		if ((nofile[i-1] = (handle == NULL)))
8726 			continue;
8727 		fclose(handle);
8728 		lastseen = i;
8729 	}
8730 
8731 	if (savegameinfo)
8732 		Z_Free(savegameinfo);
8733 	savegameinfo = NULL;
8734 
8735 	if (lastseen < saveSlotSelected)
8736 		lastseen = saveSlotSelected;
8737 
8738 	i = lastseen;
8739 
8740 	for (; (lastseen > 0 && tolerance); lastseen--)
8741 	{
8742 		if (nofile[lastseen-1])
8743 			tolerance--;
8744 	}
8745 
8746 	if ((i += tolerance+1) > MAXSAVEGAMES) // show 3 empty slots at minimum
8747 		i = MAXSAVEGAMES;
8748 
8749 	numsaves = i;
8750 	savegameinfo = Z_Realloc(savegameinfo, numsaves*sizeof(saveinfo_t), PU_STATIC, NULL);
8751 	if (!savegameinfo)
8752 		I_Error("Insufficient memory to prepare save platter");
8753 
8754 	for (; i > 0; i--)
8755 	{
8756 		if (nofile[i-1] == true)
8757 		{
8758 			savegameinfo[i-1].lives = -42;
8759 			continue;
8760 		}
8761 		M_ReadSavegameInfo(i);
8762 	}
8763 
8764 	M_CacheLoadGameData();
8765 }
8766 
8767 //
8768 // User wants to delete this game
8769 //
8770 static void M_SaveGameDeleteResponse(INT32 ch)
8771 {
8772 	char name[256];
8773 
8774 	if (ch != 'y' && ch != KEY_ENTER)
8775 		return;
8776 
8777 	// delete savegame
8778 	snprintf(name, sizeof name, savegamename, saveSlotSelected);
8779 	name[sizeof name - 1] = '\0';
8780 	remove(name);
8781 
8782 	BwehHehHe();
8783 	M_ReadSaveStrings(); // reload the menu
8784 }
8785 
8786 static void M_SaveGameUltimateResponse(INT32 ch)
8787 {
8788 	if (ch != 'y' && ch != KEY_ENTER)
8789 		return;
8790 
8791 	S_StartSound(NULL, sfx_menu1);
8792 	M_LoadSelect(saveSlotSelected);
8793 	SP_PlayerDef.prevMenu = MessageDef.prevMenu;
8794 	MessageDef.prevMenu = &SP_PlayerDef;
8795 }
8796 
8797 static void M_HandleLoadSave(INT32 choice)
8798 {
8799 	boolean exitmenu = false; // exit to previous menu
8800 
8801 	switch (choice)
8802 	{
8803 		case KEY_RIGHTARROW:
8804 			S_StartSound(NULL, sfx_s3kb7);
8805 			++saveSlotSelected;
8806 			if (saveSlotSelected >= numsaves)
8807 				saveSlotSelected -= numsaves;
8808 			loadgamescroll = 90;
8809 			break;
8810 
8811 		case KEY_LEFTARROW:
8812 			S_StartSound(NULL, sfx_s3kb7);
8813 			--saveSlotSelected;
8814 			if (saveSlotSelected < 0)
8815 				saveSlotSelected += numsaves;
8816 			loadgamescroll = -90;
8817 			break;
8818 
8819 		case KEY_ENTER:
8820 			if (ultimate_selectable && saveSlotSelected == NOSAVESLOT)
8821 			{
8822 				loadgamescroll = 0;
8823 				S_StartSound(NULL, sfx_skid);
8824 				M_StartMessage("Are you sure you want to play\n\x85ultimate mode\x80? It isn't remotely fair,\nand you don't even get an emblem for it.\n\n(Press 'Y' to confirm)\n",M_SaveGameUltimateResponse,MM_YESNO);
8825 			}
8826 			else if (saveSlotSelected != NOSAVESLOT && savegameinfo[saveSlotSelected-1].lives == -42 && !(!modifiedgame || savemoddata))
8827 			{
8828 				loadgamescroll = 0;
8829 				S_StartSound(NULL, sfx_skid);
8830 				M_StartMessage(M_GetText("This cannot be done in a modified game.\n\n(Press a key)\n"), NULL, MM_NOTHING);
8831 			}
8832 			else if (saveSlotSelected == NOSAVESLOT || savegameinfo[saveSlotSelected-1].lives != -666) // don't allow loading of "bad saves"
8833 			{
8834 				loadgamescroll = 0;
8835 				S_StartSound(NULL, sfx_menu1);
8836 				M_LoadSelect(saveSlotSelected);
8837 			}
8838 			else if (!loadgameoffset)
8839 			{
8840 				S_StartSound(NULL, sfx_lose);
8841 				loadgameoffset = 14;
8842 			}
8843 			break;
8844 
8845 		case KEY_ESCAPE:
8846 			exitmenu = true;
8847 			break;
8848 
8849 		case KEY_BACKSPACE:
8850 			// Don't allow people to 'delete' "Play without Saving."
8851 			// Nor allow people to 'delete' slots with no saves in them.
8852 			if (saveSlotSelected != NOSAVESLOT && savegameinfo[saveSlotSelected-1].lives != -42)
8853 			{
8854 				loadgamescroll = 0;
8855 				S_StartSound(NULL, sfx_skid);
8856 				M_StartMessage(va("Are you sure you want to delete\nsave file %d?\n\n(Press 'Y' to confirm)\n", saveSlotSelected),M_SaveGameDeleteResponse,MM_YESNO);
8857 			}
8858 			else if (!loadgameoffset)
8859 			{
8860 				if (saveSlotSelected == NOSAVESLOT && ultimate_selectable)
8861 				{
8862 					ultimate_selectable = false;
8863 					S_StartSound(NULL, sfx_strpst);
8864 				}
8865 				else
8866 					S_StartSound(NULL, sfx_lose);
8867 				loadgameoffset = 14;
8868 			}
8869 			break;
8870 	}
8871 	if (exitmenu)
8872 	{
8873 		// Is this a hack?
8874 		charseltimer = 0;
8875 		if (currentMenu->prevMenu)
8876 			M_SetupNextMenu(currentMenu->prevMenu);
8877 		else
8878 			M_ClearMenus(true);
8879 		Z_Free(savegameinfo);
8880 		savegameinfo = NULL;
8881 	}
8882 }
8883 
8884 static void M_FirstTimeResponse(INT32 ch)
8885 {
8886 	S_StartSound(NULL, sfx_menu1);
8887 
8888 	if (ch == KEY_ESCAPE)
8889 		return;
8890 
8891 	if (ch != 'y' && ch != KEY_ENTER)
8892 	{
8893 		CV_SetValue(&cv_tutorialprompt, 0);
8894 		M_ReadSaveStrings();
8895 		MessageDef.prevMenu = &SP_LoadDef; // calls M_SetupNextMenu
8896 	}
8897 	else
8898 	{
8899 		M_StartTutorial(0);
8900 		MessageDef.prevMenu = &MessageDef; // otherwise, the controls prompt won't fire
8901 	}
8902 }
8903 
8904 //
8905 // Selected from SRB2 menu
8906 //
8907 static void M_LoadGame(INT32 choice)
8908 {
8909 	(void)choice;
8910 
8911 	if (tutorialmap && cv_tutorialprompt.value)
8912 	{
8913 		M_StartMessage("Do you want to \x82play a brief Tutorial\x80?\n\nWe highly recommend this because \nthe controls are slightly different \nfrom other games.\n\nPress 'Y' or 'Enter' to go\nPress 'N' or any key to skip\n",
8914 			M_FirstTimeResponse, MM_YESNO);
8915 		return;
8916 	}
8917 
8918 	M_ReadSaveStrings();
8919 	M_SetupNextMenu(&SP_LoadDef);
8920 }
8921 
8922 //
8923 // Used by cheats to force the save menu to a specific spot.
8924 //
8925 void M_ForceSaveSlotSelected(INT32 sslot)
8926 {
8927 	loadgameoffset = 14;
8928 
8929 	// Already there? Whatever, then!
8930 	if (sslot == saveSlotSelected)
8931 		return;
8932 
8933 	loadgamescroll = 90;
8934 	if (saveSlotSelected <= numsaves/2)
8935 		loadgamescroll = -loadgamescroll;
8936 
8937 	saveSlotSelected = sslot;
8938 }
8939 
8940 // ================
8941 // CHARACTER SELECT
8942 // ================
8943 
8944 static void M_CacheCharacterSelectEntry(INT32 i, INT32 skinnum)
8945 {
8946 	if (!(description[i].picname[0]))
8947 	{
8948 		if (skins[skinnum].sprites[SPR2_XTRA].numframes > XTRA_CHARSEL)
8949 		{
8950 			spritedef_t *sprdef = &skins[skinnum].sprites[SPR2_XTRA];
8951 			spriteframe_t *sprframe = &sprdef->spriteframes[XTRA_CHARSEL];
8952 			description[i].charpic = W_CachePatchNum(sprframe->lumppat[0], PU_PATCH);
8953 		}
8954 		else
8955 			description[i].charpic = W_CachePatchName("MISSING", PU_PATCH);
8956 	}
8957 	else
8958 		description[i].charpic = W_CachePatchName(description[i].picname, PU_PATCH);
8959 
8960 	if (description[i].nametag[0])
8961 		description[i].namepic = W_CachePatchName(description[i].nametag, PU_PATCH);
8962 }
8963 
8964 static UINT8 M_SetupChoosePlayerDirect(INT32 choice)
8965 {
8966 	INT32 skinnum;
8967 	UINT8 i;
8968 	UINT8 firstvalid = 255, lastvalid = 255;
8969 	boolean allowed = false;
8970 	char *and;
8971 	(void)choice;
8972 
8973 	if (!mapheaderinfo[startmap-1] || mapheaderinfo[startmap-1]->forcecharacter[0] == '\0')
8974 	{
8975 		for (i = 0; i < MAXSKINS; i++) // Handle charsels, availability, and unlocks.
8976 		{
8977 			if (description[i].used) // If the character's disabled through SOC, there's nothing we can do for it.
8978 			{
8979 				and = strchr(description[i].skinname, '&');
8980 				if (and)
8981 				{
8982 					char firstskin[SKINNAMESIZE+1];
8983 					if (mapheaderinfo[startmap-1] && mapheaderinfo[startmap-1]->typeoflevel & TOL_NIGHTS) // skip tagteam characters for NiGHTS levels
8984 						continue;
8985 					strncpy(firstskin, description[i].skinname, (and - description[i].skinname));
8986 					firstskin[(and - description[i].skinname)] = '\0';
8987 					description[i].skinnum[0] = R_SkinAvailable(firstskin);
8988 					description[i].skinnum[1] = R_SkinAvailable(and+1);
8989 				}
8990 				else
8991 				{
8992 					description[i].skinnum[0] = R_SkinAvailable(description[i].skinname);
8993 					description[i].skinnum[1] = -1;
8994 				}
8995 				skinnum = description[i].skinnum[0];
8996 				if ((skinnum != -1) && (R_SkinUsable(-1, skinnum)))
8997 				{
8998 					// Handling order.
8999 					if (firstvalid == 255)
9000 						firstvalid = i;
9001 					else
9002 					{
9003 						description[i].prev = lastvalid;
9004 						description[lastvalid].next = i;
9005 					}
9006 					lastvalid = i;
9007 
9008 					if (i == char_on)
9009 						allowed = true;
9010 
9011 					M_CacheCharacterSelectEntry(i, skinnum);
9012 				}
9013 				// else -- Technically, character select icons without corresponding skins get bundled away behind this too. Sucks to be them.
9014 			}
9015 		}
9016 	}
9017 
9018 	if (firstvalid == lastvalid) // We're being forced into a specific character, so might as well just skip it.
9019 	{
9020 		return firstvalid;
9021 	}
9022 
9023 	// One last bit of order we can't do in the iteration above.
9024 	description[firstvalid].prev = lastvalid;
9025 	description[lastvalid].next = firstvalid;
9026 
9027 	if (!allowed)
9028 	{
9029 		char_on = firstvalid;
9030 		if (startchar > 0 && startchar < MAXSKINS)
9031 		{
9032 			INT16 workchar = startchar;
9033 			while (workchar--)
9034 				char_on = description[char_on].next;
9035 		}
9036 	}
9037 
9038 	return MAXSKINS;
9039 }
9040 
9041 static void M_SetupChoosePlayer(INT32 choice)
9042 {
9043 	UINT8 skinset = M_SetupChoosePlayerDirect(choice);
9044 	if (skinset != MAXSKINS)
9045 	{
9046 		M_ChoosePlayer(skinset);
9047 		return;
9048 	}
9049 
9050 	M_ChangeMenuMusic("_chsel", true);
9051 
9052 	/* the menus suck -James */
9053 	if (currentMenu == &SP_LoadDef)/* from save states */
9054 	{
9055 		SP_PlayerDef.menuid = MTREE3(MN_SP_MAIN, MN_SP_LOAD, MN_SP_PLAYER);
9056 	}
9057 	else/* from Secret level select */
9058 	{
9059 		SP_PlayerDef.menuid = MTREE2(MN_SR_MAIN, MN_SR_PLAYER);
9060 	}
9061 
9062 	SP_PlayerDef.prevMenu = currentMenu;
9063 	M_SetupNextMenu(&SP_PlayerDef);
9064 
9065 	// finish scrolling the menu
9066 	char_scroll = 0;
9067 	charseltimer = 0;
9068 
9069 	Z_Free(char_notes);
9070 	char_notes = V_WordWrap(0, 21*8, V_ALLOWLOWERCASE, description[char_on].notes);
9071 }
9072 
9073 //
9074 // M_HandleChoosePlayerMenu
9075 //
9076 // Reacts to your key inputs. Basically a mini menu thinker.
9077 //
9078 static void M_HandleChoosePlayerMenu(INT32 choice)
9079 {
9080 	boolean exitmenu = false;  // exit to previous menu
9081 	INT32 selectval;
9082 
9083 	if (keydown > 1)
9084 		return;
9085 
9086 	switch (choice)
9087 	{
9088 		case KEY_DOWNARROW:
9089 			if ((selectval = description[char_on].next) != char_on)
9090 			{
9091 				S_StartSound(NULL,sfx_s3kb7);
9092 				char_on = selectval;
9093 				char_scroll = -charscrollamt;
9094 				Z_Free(char_notes);
9095 				char_notes = V_WordWrap(0, 21*8, V_ALLOWLOWERCASE, description[char_on].notes);
9096 			}
9097 			else if (!char_scroll)
9098 			{
9099 				S_StartSound(NULL,sfx_s3kb7);
9100 				char_scroll = 16*FRACUNIT;
9101 			}
9102 			break;
9103 
9104 		case KEY_UPARROW:
9105 			if ((selectval = description[char_on].prev) != char_on)
9106 			{
9107 				S_StartSound(NULL,sfx_s3kb7);
9108 				char_on = selectval;
9109 				char_scroll = charscrollamt;
9110 				Z_Free(char_notes);
9111 				char_notes = V_WordWrap(0, 21*8, V_ALLOWLOWERCASE, description[char_on].notes);
9112 			}
9113 			else if (!char_scroll)
9114 			{
9115 				S_StartSound(NULL,sfx_s3kb7);
9116 				char_scroll = -16*FRACUNIT;
9117 			}
9118 			break;
9119 
9120 		case KEY_ENTER:
9121 			S_StartSound(NULL, sfx_menu1);
9122 			char_scroll = 0; // finish scrolling the menu
9123 			M_DrawSetupChoosePlayerMenu(); // draw the finally selected character one last time for the fadeout
9124 			// Is this a hack?
9125 			charseltimer = 0;
9126 			M_ChoosePlayer(char_on);
9127 			break;
9128 
9129 		case KEY_ESCAPE:
9130 			exitmenu = true;
9131 			break;
9132 
9133 		default:
9134 			break;
9135 	}
9136 
9137 	if (exitmenu)
9138 	{
9139 		// Is this a hack?
9140 		charseltimer = 0;
9141 		if (currentMenu->prevMenu)
9142 			M_SetupNextMenu(currentMenu->prevMenu);
9143 		else
9144 			M_ClearMenus(true);
9145 	}
9146 }
9147 
9148 // Draw the choose player setup menu, had some fun with player anim
9149 //define CHOOSEPLAYER_DRAWHEADER
9150 
9151 static void M_DrawSetupChoosePlayerMenu(void)
9152 {
9153 	const INT32 my = 16;
9154 
9155 	skin_t *charskin = &skins[0];
9156 	INT32 skinnum = 0;
9157 	UINT16 col;
9158 	UINT8 *colormap = NULL;
9159 	INT32 prev = -1, next = -1;
9160 
9161 	patch_t *charbg = W_CachePatchName("CHARBG", PU_PATCH);
9162 	patch_t *charfg = W_CachePatchName("CHARFG", PU_PATCH);
9163 	INT16 bgheight = charbg->height;
9164 	INT16 fgheight = charfg->height;
9165 	INT16 bgwidth = charbg->width;
9166 	INT16 fgwidth = charfg->width;
9167 	INT32 x, y;
9168 	INT32 w = (vid.width/vid.dupx);
9169 
9170 	if (abs(char_scroll) > FRACUNIT)
9171 		char_scroll -= (char_scroll>>2);
9172 	else // close enough.
9173 		char_scroll = 0; // just be exact now.
9174 
9175 	// Get prev character...
9176 	prev = description[char_on].prev;
9177 	// If there's more than one character available...
9178 	if (prev != char_on)
9179 		// Let's get the next character now.
9180 		next = description[char_on].next;
9181 	else
9182 		// No there isn't.
9183 		prev = -1;
9184 
9185 	// Find skin number from description[]
9186 	skinnum = description[char_on].skinnum[0];
9187 	charskin = &skins[skinnum];
9188 
9189 	// Use the opposite of the character's skincolor
9190 	col = description[char_on].oppositecolor;
9191 	if (!col)
9192 		col = skincolors[charskin->prefcolor].invcolor;
9193 
9194 	// Make the translation colormap
9195 	colormap = R_GetTranslationColormap(TC_DEFAULT, col, GTC_CACHE);
9196 
9197 	// Don't render the title map
9198 	hidetitlemap = true;
9199 	charseltimer++;
9200 
9201 	// Background and borders
9202 	V_DrawFill(0, 0, bgwidth, vid.height, V_SNAPTOTOP|colormap[101]);
9203 	{
9204 		INT32 sw = (BASEVIDWIDTH * vid.dupx);
9205 		INT32 bw = (vid.width - sw) / 2;
9206 		col = colormap[106];
9207 		if (bw)
9208 			V_DrawFill(0, 0, bw, vid.height, V_NOSCALESTART|col);
9209 	}
9210 
9211 	y = (charseltimer%32);
9212 	V_DrawMappedPatch(0, y-bgheight, V_SNAPTOTOP, charbg, colormap);
9213 	V_DrawMappedPatch(0, y, V_SNAPTOTOP, charbg, colormap);
9214 	V_DrawMappedPatch(0, y+bgheight, V_SNAPTOTOP, charbg, colormap);
9215 	V_DrawMappedPatch(0, -y, V_SNAPTOTOP, charfg, colormap);
9216 	V_DrawMappedPatch(0, -y+fgheight, V_SNAPTOTOP, charfg, colormap);
9217 	V_DrawFill(fgwidth, 0, vid.width, vid.height, V_SNAPTOTOP|colormap[106]);
9218 
9219 	// Character pictures
9220 	{
9221 		x = 8;
9222 		y = (my+16) - FixedInt(char_scroll);
9223 		V_DrawScaledPatch(x, y, 0, description[char_on].charpic);
9224 		if (prev != -1)
9225 			V_DrawScaledPatch(x, y - 144, 0, description[prev].charpic);
9226 		if (next != -1)
9227 			V_DrawScaledPatch(x, y + 144, 0, description[next].charpic);
9228 	}
9229 
9230 	// Character description
9231 	{
9232 		INT32 flags = V_ALLOWLOWERCASE|V_RETURN8;
9233 		x = 146;
9234 		y = my + 9;
9235 		V_DrawString(x, y, flags, char_notes);
9236 	}
9237 
9238 	// Name tags
9239 	{
9240 		INT32 ox, oxsh = FixedInt(FixedMul(BASEVIDWIDTH*FRACUNIT, FixedDiv(char_scroll, 128*FRACUNIT))), txsh;
9241 		patch_t *curpatch = NULL, *prevpatch = NULL, *nextpatch = NULL;
9242 		const char *curtext = NULL, *prevtext = NULL, *nexttext = NULL;
9243 		UINT16 curtextcolor = 0, prevtextcolor = 0, nexttextcolor = 0;
9244 		UINT16 curoutlinecolor = 0, prevoutlinecolor = 0, nextoutlinecolor = 0;
9245 
9246 		// Name tag
9247 		curtext = description[char_on].displayname;
9248 		curtextcolor = description[char_on].tagtextcolor;
9249 		curoutlinecolor = description[char_on].tagoutlinecolor;
9250 		if (curtext[0] == '\0')
9251 			curpatch = description[char_on].namepic;
9252 		if (!curtextcolor)
9253 			curtextcolor = charskin->prefcolor;
9254 		if (!curoutlinecolor)
9255 			curoutlinecolor = col = skincolors[charskin->prefcolor].invcolor;
9256 
9257 		txsh = oxsh;
9258 		ox = 8 + ((description[char_on].charpic)->width)/2;
9259 		y = my + 144;
9260 
9261 		// cur
9262 		{
9263 			x = ox - txsh;
9264 			if (curpatch)
9265 				x -= curpatch->width / 2;
9266 
9267 			if (curtext[0] != '\0')
9268 			{
9269 				V_DrawNameTag(
9270 					x, y, V_CENTERNAMETAG, FRACUNIT,
9271 					R_GetTranslationColormap(TC_DEFAULT, curtextcolor, GTC_CACHE),
9272 					R_GetTranslationColormap(TC_DEFAULT, curoutlinecolor, GTC_CACHE),
9273 					curtext
9274 				);
9275 			}
9276 			else if (curpatch)
9277 				V_DrawScaledPatch(x, y, 0, curpatch);
9278 		}
9279 
9280 		if (char_scroll)
9281 		{
9282 			// prev
9283 			if ((prev != -1) && char_scroll < 0)
9284 			{
9285 				prevtext = description[prev].displayname;
9286 				prevtextcolor = description[prev].tagtextcolor;
9287 				prevoutlinecolor = description[prev].tagoutlinecolor;
9288 				if (prevtext[0] == '\0')
9289 					prevpatch = description[prev].namepic;
9290 				charskin = &skins[description[prev].skinnum[0]];
9291 				if (!prevtextcolor)
9292 					prevtextcolor = charskin->prefcolor;
9293 				if (!prevoutlinecolor)
9294 					prevoutlinecolor = col = skincolors[charskin->prefcolor].invcolor;
9295 
9296 				x = (ox - txsh) - w;
9297 				if (prevpatch)
9298 					x -= prevpatch->width / 2;
9299 
9300 				if (prevtext[0] != '\0')
9301 				{
9302 					V_DrawNameTag(
9303 						x, y, V_CENTERNAMETAG, FRACUNIT,
9304 						R_GetTranslationColormap(TC_DEFAULT, prevtextcolor, GTC_CACHE),
9305 						R_GetTranslationColormap(TC_DEFAULT, prevoutlinecolor, GTC_CACHE),
9306 						prevtext
9307 					);
9308 				}
9309 				else if (prevpatch)
9310 					V_DrawScaledPatch(x, y, 0, prevpatch);
9311 			}
9312 			// next
9313 			else if ((next != -1) && char_scroll > 0)
9314 			{
9315 				nexttext = description[next].displayname;
9316 				nexttextcolor = description[next].tagtextcolor;
9317 				nextoutlinecolor = description[next].tagoutlinecolor;
9318 				if (nexttext[0] == '\0')
9319 					nextpatch = description[next].namepic;
9320 				charskin = &skins[description[next].skinnum[0]];
9321 				if (!nexttextcolor)
9322 					nexttextcolor = charskin->prefcolor;
9323 				if (!nextoutlinecolor)
9324 					nextoutlinecolor = col = skincolors[charskin->prefcolor].invcolor;
9325 
9326 				x = (ox - txsh) + w;
9327 				if (nextpatch)
9328 					x -= nextpatch->width / 2;
9329 
9330 				if (nexttext[0] != '\0')
9331 				{
9332 					V_DrawNameTag(
9333 						x, y, V_CENTERNAMETAG, FRACUNIT,
9334 						R_GetTranslationColormap(TC_DEFAULT, nexttextcolor, GTC_CACHE),
9335 						R_GetTranslationColormap(TC_DEFAULT, nextoutlinecolor, GTC_CACHE),
9336 						nexttext
9337 					);
9338 				}
9339 				else if (nextpatch)
9340 					V_DrawScaledPatch(x, y, 0, nextpatch);
9341 			}
9342 		}
9343 	}
9344 
9345 	// Alternative menu header
9346 #ifdef CHOOSEPLAYER_DRAWHEADER
9347 	{
9348 		patch_t *header = W_CachePatchName("M_PICKP", PU_PATCH);
9349 		INT32 xtitle = 146;
9350 		INT32 ytitle = (35 - header->height) / 2;
9351 		V_DrawFixedPatch(xtitle<<FRACBITS, ytitle<<FRACBITS, FRACUNIT/2, 0, header, NULL);
9352 	}
9353 #endif // CHOOSEPLAYER_DRAWHEADER
9354 
9355 	M_DrawMenuTitle();
9356 }
9357 
9358 // Chose the player you want to use Tails 03-02-2002
9359 static void M_ChoosePlayer(INT32 choice)
9360 {
9361 	boolean ultmode = (currentMenu == &SP_MarathonDef) ? (cv_dummymarathon.value == 2) : (ultimate_selectable && SP_PlayerDef.prevMenu == &SP_LoadDef && saveSlotSelected == NOSAVESLOT);
9362 	UINT8 skinnum;
9363 
9364 	// skip this if forcecharacter or no characters available
9365 	if (choice == 255)
9366 	{
9367 		skinnum = botskin = 0;
9368 		botingame = false;
9369 	}
9370 	// M_SetupChoosePlayer didn't call us directly, that means we've been properly set up.
9371 	else
9372 	{
9373 		skinnum = description[choice].skinnum[0];
9374 
9375 		if ((botingame = (description[choice].skinnum[1] != -1))) {
9376 			// this character has a second skin
9377 			botskin = (UINT8)(description[choice].skinnum[1]+1);
9378 			botcolor = skins[description[choice].skinnum[1]].prefcolor;
9379 		}
9380 		else
9381 			botskin = botcolor = 0;
9382 	}
9383 
9384 	M_ClearMenus(true);
9385 
9386 	if (!marathonmode && startmap != spstage_start)
9387 		cursaveslot = 0;
9388 
9389 	//lastmapsaved = 0;
9390 	gamecomplete = 0;
9391 
9392 	G_DeferedInitNew(ultmode, G_BuildMapName(startmap), skinnum, false, fromlevelselect);
9393 	COM_BufAddText("dummyconsvar 1\n"); // G_DeferedInitNew doesn't do this
9394 
9395 	if (levelselect.rows)
9396 		Z_Free(levelselect.rows);
9397 	levelselect.rows = NULL;
9398 
9399 	if (savegameinfo)
9400 		Z_Free(savegameinfo);
9401 	savegameinfo = NULL;
9402 }
9403 
9404 // ===============
9405 // STATISTICS MENU
9406 // ===============
9407 
9408 static INT32 statsLocation;
9409 static INT32 statsMax;
9410 static INT16 statsMapList[NUMMAPS+1];
9411 
9412 static void M_Statistics(INT32 choice)
9413 {
9414 	INT16 i, j = 0;
9415 
9416 	(void)choice;
9417 
9418 	memset(statsMapList, 0, sizeof(statsMapList));
9419 
9420 	for (i = 0; i < NUMMAPS; i++)
9421 	{
9422 		if (!mapheaderinfo[i] || mapheaderinfo[i]->lvlttl[0] == '\0')
9423 			continue;
9424 
9425 		if (!(mapheaderinfo[i]->typeoflevel & TOL_SP) || (mapheaderinfo[i]->menuflags & LF2_HIDEINSTATS))
9426 			continue;
9427 
9428 		if (!(mapvisited[i] & MV_MAX))
9429 			continue;
9430 
9431 		statsMapList[j++] = i;
9432 	}
9433 	statsMapList[j] = -1;
9434 	statsMax = j - 11 + numextraemblems;
9435 	statsLocation = 0;
9436 
9437 	if (statsMax < 0)
9438 		statsMax = 0;
9439 
9440 	M_SetupNextMenu(&SP_LevelStatsDef);
9441 }
9442 
9443 static void M_DrawStatsMaps(int location)
9444 {
9445 	INT32 y = 80, i = -1;
9446 	INT16 mnum;
9447 	extraemblem_t *exemblem;
9448 	boolean dotopname = true, dobottomarrow = (location < statsMax);
9449 
9450 	if (location)
9451 		V_DrawString(10, y-(skullAnimCounter/5), V_YELLOWMAP, "\x1A");
9452 
9453 	while (statsMapList[++i] != -1)
9454 	{
9455 		if (location)
9456 		{
9457 			--location;
9458 			continue;
9459 		}
9460 		else if (dotopname)
9461 		{
9462 			V_DrawString(20,  y, V_GREENMAP, "LEVEL NAME");
9463 			V_DrawString(248, y, V_GREENMAP, "EMBLEMS");
9464 			y += 8;
9465 			dotopname = false;
9466 		}
9467 
9468 		mnum = statsMapList[i];
9469 		M_DrawMapEmblems(mnum+1, 292, y);
9470 
9471 		if (mapheaderinfo[mnum]->actnum != 0)
9472 			V_DrawString(20, y, V_YELLOWMAP|V_ALLOWLOWERCASE, va("%s %d", mapheaderinfo[mnum]->lvlttl, mapheaderinfo[mnum]->actnum));
9473 		else
9474 			V_DrawString(20, y, V_YELLOWMAP|V_ALLOWLOWERCASE, mapheaderinfo[mnum]->lvlttl);
9475 
9476 		y += 8;
9477 
9478 		if (y >= BASEVIDHEIGHT-8)
9479 			goto bottomarrow;
9480 	}
9481 	if (dotopname && !location)
9482 	{
9483 		V_DrawString(20,  y, V_GREENMAP, "LEVEL NAME");
9484 		V_DrawString(248, y, V_GREENMAP, "EMBLEMS");
9485 		y += 8;
9486 	}
9487 	else if (location)
9488 		--location;
9489 
9490 	// Extra Emblems
9491 	for (i = -2; i < numextraemblems; ++i)
9492 	{
9493 		if (i == -1)
9494 		{
9495 			V_DrawString(20, y, V_GREENMAP, "EXTRA EMBLEMS");
9496 			if (location)
9497 			{
9498 				y += 8;
9499 				location++;
9500 			}
9501 		}
9502 		if (location)
9503 		{
9504 			--location;
9505 			continue;
9506 		}
9507 
9508 		if (i >= 0)
9509 		{
9510 			exemblem = &extraemblems[i];
9511 
9512 			if (exemblem->collected)
9513 				V_DrawSmallMappedPatch(292, y, 0, W_CachePatchName(M_GetExtraEmblemPatch(exemblem, false), PU_PATCH),
9514 				                       R_GetTranslationColormap(TC_DEFAULT, M_GetExtraEmblemColor(exemblem), GTC_CACHE));
9515 			else
9516 				V_DrawSmallScaledPatch(292, y, 0, W_CachePatchName("NEEDIT", PU_PATCH));
9517 
9518 			V_DrawString(20, y, V_YELLOWMAP|V_ALLOWLOWERCASE,
9519 				(!exemblem->collected && exemblem->showconditionset && !M_Achieved(exemblem->showconditionset))
9520 				? M_CreateSecretMenuOption(exemblem->description)
9521 				: exemblem->description);
9522 		}
9523 
9524 		y += 8;
9525 
9526 		if (y >= BASEVIDHEIGHT-8)
9527 			goto bottomarrow;
9528 	}
9529 bottomarrow:
9530 	if (dobottomarrow)
9531 		V_DrawString(10, y-8 + (skullAnimCounter/5), V_YELLOWMAP, "\x1B");
9532 }
9533 
9534 static void M_DrawLevelStats(void)
9535 {
9536 	char beststr[40];
9537 
9538 	tic_t besttime = 0;
9539 	UINT32 bestscore = 0;
9540 	UINT32 bestrings = 0;
9541 
9542 	INT32 i;
9543 	INT32 mapsunfinished = 0;
9544 	boolean bestunfinished[3] = {false, false, false};
9545 
9546 	M_DrawMenuTitle();
9547 
9548 	V_DrawString(20, 24, V_YELLOWMAP, "Total Play Time:");
9549 	V_DrawCenteredString(BASEVIDWIDTH/2, 32, 0, va("%i hours, %i minutes, %i seconds",
9550 	                         G_TicsToHours(totalplaytime),
9551 	                         G_TicsToMinutes(totalplaytime, false),
9552 	                         G_TicsToSeconds(totalplaytime)));
9553 
9554 	for (i = 0; i < NUMMAPS; i++)
9555 	{
9556 		boolean mapunfinished = false;
9557 
9558 		if (!mapheaderinfo[i] || !(mapheaderinfo[i]->menuflags & LF2_RECORDATTACK))
9559 			continue;
9560 
9561 		if (!mainrecords[i])
9562 		{
9563 			mapsunfinished++;
9564 			bestunfinished[0] = bestunfinished[1] = bestunfinished[2] = true;
9565 			continue;
9566 		}
9567 
9568 		if (mainrecords[i]->score > 0)
9569 			bestscore += mainrecords[i]->score;
9570 		else
9571 			mapunfinished = bestunfinished[0] = true;
9572 
9573 		if (mainrecords[i]->time > 0)
9574 			besttime += mainrecords[i]->time;
9575 		else
9576 			mapunfinished = bestunfinished[1] = true;
9577 
9578 		if (mainrecords[i]->rings > 0)
9579 			bestrings += mainrecords[i]->rings;
9580 		else
9581 			mapunfinished = bestunfinished[2] = true;
9582 
9583 		if (mapunfinished)
9584 			mapsunfinished++;
9585 	}
9586 
9587 	V_DrawString(20, 48, 0, "Combined records:");
9588 
9589 	if (mapsunfinished)
9590 		V_DrawString(20, 56, V_REDMAP, va("(%d unfinished)", mapsunfinished));
9591 	else
9592 		V_DrawString(20, 56, V_GREENMAP, "(complete)");
9593 
9594 	V_DrawString(36, 64, 0, va("x %d/%d", M_CountEmblems(), numemblems+numextraemblems));
9595 	V_DrawSmallScaledPatch(20, 64, 0, W_CachePatchName("EMBLICON", PU_PATCH));
9596 
9597 	sprintf(beststr, "%u", bestscore);
9598 	V_DrawString(BASEVIDWIDTH/2, 48, V_YELLOWMAP, "SCORE:");
9599 	V_DrawRightAlignedString(BASEVIDWIDTH-16, 48, (bestunfinished[0] ? V_REDMAP : 0), beststr);
9600 
9601 	sprintf(beststr, "%i:%02i:%02i.%02i", G_TicsToHours(besttime), G_TicsToMinutes(besttime, false), G_TicsToSeconds(besttime), G_TicsToCentiseconds(besttime));
9602 	V_DrawString(BASEVIDWIDTH/2, 56, V_YELLOWMAP, "TIME:");
9603 	V_DrawRightAlignedString(BASEVIDWIDTH-16, 56, (bestunfinished[1] ? V_REDMAP : 0), beststr);
9604 
9605 	sprintf(beststr, "%u", bestrings);
9606 	V_DrawString(BASEVIDWIDTH/2, 64, V_YELLOWMAP, "RINGS:");
9607 	V_DrawRightAlignedString(BASEVIDWIDTH-16, 64, (bestunfinished[2] ? V_REDMAP : 0), beststr);
9608 
9609 	M_DrawStatsMaps(statsLocation);
9610 }
9611 
9612 // Handle statistics.
9613 static void M_HandleLevelStats(INT32 choice)
9614 {
9615 	boolean exitmenu = false; // exit to previous menu
9616 
9617 	switch (choice)
9618 	{
9619 		case KEY_DOWNARROW:
9620 			S_StartSound(NULL, sfx_menu1);
9621 			if (statsLocation < statsMax)
9622 				++statsLocation;
9623 			break;
9624 
9625 		case KEY_UPARROW:
9626 			S_StartSound(NULL, sfx_menu1);
9627 			if (statsLocation)
9628 				--statsLocation;
9629 			break;
9630 
9631 		case KEY_PGDN:
9632 			S_StartSound(NULL, sfx_menu1);
9633 			statsLocation += (statsLocation+13 >= statsMax) ? statsMax-statsLocation : 13;
9634 			break;
9635 
9636 		case KEY_PGUP:
9637 			S_StartSound(NULL, sfx_menu1);
9638 			statsLocation -= (statsLocation < 13) ? statsLocation : 13;
9639 			break;
9640 
9641 		case KEY_ESCAPE:
9642 			exitmenu = true;
9643 			break;
9644 	}
9645 	if (exitmenu)
9646 	{
9647 		if (currentMenu->prevMenu)
9648 			M_SetupNextMenu(currentMenu->prevMenu);
9649 		else
9650 			M_ClearMenus(true);
9651 	}
9652 }
9653 
9654 // ===========
9655 // MODE ATTACK
9656 // ===========
9657 
9658 // Drawing function for Time Attack
9659 void M_DrawTimeAttackMenu(void)
9660 {
9661 	INT32 i, x, y, empatx, empaty, cursory = 0;
9662 	UINT16 dispstatus;
9663 	patch_t *PictureOfUrFace;	// my WHAT
9664 	patch_t *empatch;
9665 
9666 	M_SetMenuCurBackground("RECATKBG");
9667 
9668 	curbgxspeed = 0;
9669 	curbgyspeed = 18;
9670 
9671 	M_ChangeMenuMusic("_recat", true); // Eww, but needed for when user hits escape during demo playback
9672 
9673 	if (curbgcolor >= 0)
9674 		V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, curbgcolor);
9675 	else if (!curbghide || !titlemapinaction)
9676 	{
9677 		F_SkyScroll(curbgxspeed, curbgyspeed, curbgname);
9678 		// Draw and animate foreground
9679 		if (!strncmp("RECATKBG", curbgname, 8))
9680 			M_DrawRecordAttackForeground();
9681 	}
9682 	if (curfadevalue)
9683 		V_DrawFadeScreen(0xFF00, curfadevalue);
9684 
9685 	M_DrawMenuTitle();
9686 
9687 	// draw menu (everything else goes on top of it)
9688 	// Sadly we can't just use generic mode menus because we need some extra hacks
9689 	x = currentMenu->x;
9690 	y = currentMenu->y;
9691 
9692 	for (i = 0; i < currentMenu->numitems; ++i)
9693 	{
9694 		dispstatus = (currentMenu->menuitems[i].status & IT_DISPLAY);
9695 		if (dispstatus != IT_STRING && dispstatus != IT_WHITESTRING)
9696 			continue;
9697 
9698 		y = currentMenu->y+currentMenu->menuitems[i].alphaKey;
9699 		if (i == itemOn)
9700 			cursory = y;
9701 
9702 		V_DrawString(x, y, (dispstatus == IT_WHITESTRING) ? V_YELLOWMAP : 0 , currentMenu->menuitems[i].text);
9703 
9704 		// Cvar specific handling
9705 		if ((currentMenu->menuitems[i].status & IT_TYPE) == IT_CVAR)
9706 		{
9707 			consvar_t *cv = (consvar_t *)currentMenu->menuitems[i].itemaction;
9708 			INT32 soffset = 0;
9709 
9710 			// hack to keep the menu from overlapping the player icon
9711 			if (currentMenu != &SP_TimeAttackDef)
9712 				soffset = 80;
9713 
9714 			// Should see nothing but strings
9715 			V_DrawString(BASEVIDWIDTH - x - soffset - V_StringWidth(cv->string, 0), y, V_YELLOWMAP, cv->string);
9716 			if (i == itemOn)
9717 			{
9718 				V_DrawCharacter(BASEVIDWIDTH - x - soffset - 10 - V_StringWidth(cv->string, 0) - (skullAnimCounter/5), y,
9719 					'\x1C' | V_YELLOWMAP, false);
9720 				V_DrawCharacter(BASEVIDWIDTH - x - soffset + 2 + (skullAnimCounter/5), y,
9721 					'\x1D' | V_YELLOWMAP, false);
9722 			}
9723 		}
9724 	}
9725 
9726 	// DRAW THE SKULL CURSOR
9727 	V_DrawScaledPatch(currentMenu->x - 24, cursory, 0, W_CachePatchName("M_CURSOR", PU_PATCH));
9728 	V_DrawString(currentMenu->x, cursory, V_YELLOWMAP, currentMenu->menuitems[itemOn].text);
9729 
9730 	// Character face!
9731 	{
9732 		if (skins[cv_chooseskin.value-1].sprites[SPR2_XTRA].numframes > XTRA_CHARSEL)
9733 		{
9734 			spritedef_t *sprdef = &skins[cv_chooseskin.value-1].sprites[SPR2_XTRA];
9735 			spriteframe_t *sprframe = &sprdef->spriteframes[XTRA_CHARSEL];
9736 			PictureOfUrFace = W_CachePatchNum(sprframe->lumppat[0], PU_PATCH);
9737 		}
9738 		else
9739 			PictureOfUrFace = W_CachePatchName("MISSING", PU_PATCH);
9740 
9741 		if (PictureOfUrFace->width >= 256)
9742 			V_DrawTinyScaledPatch(224, 120, 0, PictureOfUrFace);
9743 		else
9744 			V_DrawSmallScaledPatch(224, 120, 0, PictureOfUrFace);
9745 	}
9746 
9747 	// Level record list
9748 	if (cv_nextmap.value)
9749 	{
9750 		emblem_t *em;
9751 		INT32 yHeight;
9752 		patch_t *PictureOfLevel;
9753 		lumpnum_t lumpnum;
9754 		char beststr[40];
9755 		char reqscore[40], reqtime[40], reqrings[40];
9756 
9757 		strcpy(reqscore, "\0");
9758 		strcpy(reqtime, "\0");
9759 		strcpy(reqrings, "\0");
9760 
9761 		M_DrawLevelPlatterHeader(32-lsheadingheight/2, cv_nextmap.string, true, false);
9762 
9763 		//  A 160x100 image of the level as entry MAPxxP
9764 		lumpnum = W_CheckNumForName(va("%sP", G_BuildMapName(cv_nextmap.value)));
9765 
9766 		if (lumpnum != LUMPERROR)
9767 			PictureOfLevel = W_CachePatchName(va("%sP", G_BuildMapName(cv_nextmap.value)), PU_PATCH);
9768 		else
9769 			PictureOfLevel = W_CachePatchName("BLANKLVL", PU_PATCH);
9770 
9771 		y = 32+lsheadingheight;
9772 		V_DrawSmallScaledPatch(216, y, 0, PictureOfLevel);
9773 
9774 
9775 		if (currentMenu == &SP_TimeAttackDef)
9776 		{
9777 			if (itemOn == talevel)
9778 			{
9779 				/* Draw arrows !! */
9780 				y = y + 25 - 4;
9781 				V_DrawCharacter(216 - 10 - (skullAnimCounter/5), y,
9782 						'\x1C' | V_YELLOWMAP, false);
9783 				V_DrawCharacter(216 + 80 + 2 + (skullAnimCounter/5), y,
9784 						'\x1D' | V_YELLOWMAP, false);
9785 			}
9786 			// Draw press ESC to exit string on main record attack menu
9787 			V_DrawString(104-72, 180, V_TRANSLUCENT, M_GetText("Press ESC to exit"));
9788 		}
9789 
9790 		em = M_GetLevelEmblems(cv_nextmap.value);
9791 		// Draw record emblems.
9792 		while (em)
9793 		{
9794 			switch (em->type)
9795 			{
9796 				case ET_SCORE:
9797 					yHeight = 33;
9798 					sprintf(reqscore, "(%u)", em->var);
9799 					break;
9800 				case ET_TIME:
9801 					yHeight = 53;
9802 					sprintf(reqtime, "(%i:%02i.%02i)", G_TicsToMinutes((tic_t)em->var, true),
9803 										G_TicsToSeconds((tic_t)em->var),
9804 										G_TicsToCentiseconds((tic_t)em->var));
9805 					break;
9806 				case ET_RINGS:
9807 					yHeight = 73;
9808 					sprintf(reqrings, "(%u)", em->var);
9809 					break;
9810 				default:
9811 					goto skipThisOne;
9812 			}
9813 
9814 			empatch = W_CachePatchName(M_GetEmblemPatch(em, true), PU_PATCH);
9815 
9816 			empatx = empatch->leftoffset / 2;
9817 			empaty = empatch->topoffset / 2;
9818 
9819 			if (em->collected)
9820 				V_DrawSmallMappedPatch(104+76+empatx, yHeight+lsheadingheight/2+empaty, 0, empatch,
9821 				                       R_GetTranslationColormap(TC_DEFAULT, M_GetEmblemColor(em), GTC_CACHE));
9822 			else
9823 				V_DrawSmallScaledPatch(104+76, yHeight+lsheadingheight/2, 0, W_CachePatchName("NEEDITL", PU_PATCH));
9824 
9825 			skipThisOne:
9826 			em = M_GetLevelEmblems(-1);
9827 		}
9828 
9829 		if (!mainrecords[cv_nextmap.value-1] || !mainrecords[cv_nextmap.value-1]->score)
9830 			sprintf(beststr, "(none)");
9831 		else
9832 			sprintf(beststr, "%u", mainrecords[cv_nextmap.value-1]->score);
9833 
9834 		V_DrawString(104-72, 33+lsheadingheight/2, V_YELLOWMAP, "SCORE:");
9835 		V_DrawRightAlignedString(104+64, 33+lsheadingheight/2, V_ALLOWLOWERCASE, beststr);
9836 		V_DrawRightAlignedString(104+72, 43+lsheadingheight/2, V_ALLOWLOWERCASE, reqscore);
9837 
9838 		if (!mainrecords[cv_nextmap.value-1] || !mainrecords[cv_nextmap.value-1]->time)
9839 			sprintf(beststr, "(none)");
9840 		else
9841 			sprintf(beststr, "%i:%02i.%02i", G_TicsToMinutes(mainrecords[cv_nextmap.value-1]->time, true),
9842 			                                 G_TicsToSeconds(mainrecords[cv_nextmap.value-1]->time),
9843 			                                 G_TicsToCentiseconds(mainrecords[cv_nextmap.value-1]->time));
9844 
9845 		V_DrawString(104-72, 53+lsheadingheight/2, V_YELLOWMAP, "TIME:");
9846 		V_DrawRightAlignedString(104+64, 53+lsheadingheight/2, V_ALLOWLOWERCASE, beststr);
9847 		V_DrawRightAlignedString(104+72, 63+lsheadingheight/2, V_ALLOWLOWERCASE, reqtime);
9848 
9849 		if (!mainrecords[cv_nextmap.value-1] || !mainrecords[cv_nextmap.value-1]->rings)
9850 			sprintf(beststr, "(none)");
9851 		else
9852 			sprintf(beststr, "%hu", mainrecords[cv_nextmap.value-1]->rings);
9853 
9854 		V_DrawString(104-72, 73+lsheadingheight/2, V_YELLOWMAP, "RINGS:");
9855 
9856 		V_DrawRightAlignedString(104+64, 73+lsheadingheight/2, V_ALLOWLOWERCASE|((mapvisited[cv_nextmap.value-1] & MV_PERFECTRA) ? V_YELLOWMAP : 0), beststr);
9857 
9858 		V_DrawRightAlignedString(104+72, 83+lsheadingheight/2, V_ALLOWLOWERCASE, reqrings);
9859 	}
9860 
9861 	// ALWAYS DRAW level and skin even when not on this menu!
9862 	if (currentMenu != &SP_TimeAttackDef)
9863 	{
9864 		consvar_t *ncv;
9865 
9866 		x = SP_TimeAttackDef.x;
9867 		y = SP_TimeAttackDef.y;
9868 
9869 		V_DrawString(x, y + SP_TimeAttackMenu[talevel].alphaKey, V_TRANSLUCENT, SP_TimeAttackMenu[talevel].text);
9870 
9871 		ncv = (consvar_t *)SP_TimeAttackMenu[taplayer].itemaction;
9872 		V_DrawString(x, y + SP_TimeAttackMenu[taplayer].alphaKey, V_TRANSLUCENT, SP_TimeAttackMenu[taplayer].text);
9873 		V_DrawString(BASEVIDWIDTH - x - V_StringWidth(ncv->string, 0), y + SP_TimeAttackMenu[taplayer].alphaKey, V_YELLOWMAP|V_TRANSLUCENT, ncv->string);
9874 	}
9875 }
9876 
9877 static void M_HandleTimeAttackLevelSelect(INT32 choice)
9878 {
9879 	switch (choice)
9880 	{
9881 		case KEY_DOWNARROW:
9882 			M_NextOpt();
9883 			break;
9884 		case KEY_UPARROW:
9885 			M_PrevOpt();
9886 			break;
9887 
9888 		case KEY_LEFTARROW:
9889 			CV_AddValue(&cv_nextmap, -1);
9890 			break;
9891 		case KEY_RIGHTARROW:
9892 			CV_AddValue(&cv_nextmap, 1);
9893 			break;
9894 
9895 		case KEY_ENTER:
9896 			if (levellistmode == LLM_NIGHTSATTACK)
9897 				M_NightsAttackLevelSelect(0);
9898 			else
9899 				M_TimeAttackLevelSelect(0);
9900 			break;
9901 
9902 		case KEY_ESCAPE:
9903 			noFurtherInput = true;
9904 			M_GoBack(0);
9905 			return;
9906 
9907 		default:
9908 			return;
9909 	}
9910 	S_StartSound(NULL, sfx_menu1);
9911 }
9912 
9913 static void M_TimeAttackLevelSelect(INT32 choice)
9914 {
9915 	(void)choice;
9916 	SP_TimeAttackLevelSelectDef.prevMenu = currentMenu;
9917 	M_SetupNextMenu(&SP_TimeAttackLevelSelectDef);
9918 }
9919 
9920 // Going to Time Attack menu...
9921 static void M_TimeAttack(INT32 choice)
9922 {
9923 	(void)choice;
9924 
9925 	SP_TimeAttackDef.prevMenu = &MainDef;
9926 	levellistmode = LLM_RECORDATTACK; // Don't be dependent on cv_newgametype
9927 
9928 	if (!M_PrepareLevelPlatter(-1, true))
9929 	{
9930 		M_StartMessage(M_GetText("No record-attackable levels found.\n"),NULL,MM_NOTHING);
9931 		return;
9932 	}
9933 
9934 	M_PatchSkinNameTable();
9935 
9936 	G_SetGamestate(GS_TIMEATTACK); // do this before M_SetupNextMenu so that menu meta state knows that we're switching
9937 	titlemapinaction = TITLEMAP_OFF; // Nope don't give us HOMs please
9938 	M_SetupNextMenu(&SP_TimeAttackDef);
9939 	if (!M_CanShowLevelInList(cv_nextmap.value-1, -1) && levelselect.rows[0].maplist[0])
9940 		CV_SetValue(&cv_nextmap, levelselect.rows[0].maplist[0]);
9941 	else
9942 		Nextmap_OnChange();
9943 
9944 	itemOn = tastart; // "Start" is selected.
9945 }
9946 
9947 // Drawing function for Nights Attack
9948 void M_DrawNightsAttackMenu(void)
9949 {
9950 	INT32 i, x, y, cursory = 0;
9951 	UINT16 dispstatus;
9952 
9953 	M_SetMenuCurBackground("NTSATKBG");
9954 
9955 	M_ChangeMenuMusic("_nitat", true); // Eww, but needed for when user hits escape during demo playback
9956 
9957 	M_DrawNightsAttackBackground();
9958 	if (curfadevalue)
9959 		V_DrawFadeScreen(0xFF00, curfadevalue);
9960 
9961 	M_DrawMenuTitle();
9962 
9963 	// draw menu (everything else goes on top of it)
9964 	// Sadly we can't just use generic mode menus because we need some extra hacks
9965 	x = currentMenu->x;
9966 	y = currentMenu->y;
9967 
9968 	for (i = 0; i < currentMenu->numitems; ++i)
9969 	{
9970 		dispstatus = (currentMenu->menuitems[i].status & IT_DISPLAY);
9971 		if (dispstatus != IT_STRING && dispstatus != IT_WHITESTRING)
9972 			continue;
9973 
9974 		y = currentMenu->y+currentMenu->menuitems[i].alphaKey;
9975 		if (i == itemOn)
9976 			cursory = y;
9977 
9978 		V_DrawString(x, y, (dispstatus == IT_WHITESTRING) ? V_YELLOWMAP : 0 , currentMenu->menuitems[i].text);
9979 
9980 		// Cvar specific handling
9981 		if ((currentMenu->menuitems[i].status & IT_TYPE) == IT_CVAR)
9982 		{
9983 			consvar_t *cv = (consvar_t *)currentMenu->menuitems[i].itemaction;
9984 			INT32 soffset = 0;
9985 
9986 			// hack to keep the menu from overlapping the overall grade icon
9987 			if (currentMenu != &SP_NightsAttackDef)
9988 				soffset = 80;
9989 
9990 			// Should see nothing but strings
9991 			V_DrawString(BASEVIDWIDTH - x - soffset - V_StringWidth(cv->string, 0), y, V_YELLOWMAP, cv->string);
9992 			if (i == itemOn)
9993 			{
9994 				V_DrawCharacter(BASEVIDWIDTH - x - soffset - 10 - V_StringWidth(cv->string, 0) - (skullAnimCounter/5), y,
9995 					'\x1C' | V_YELLOWMAP, false);
9996 				V_DrawCharacter(BASEVIDWIDTH - x - soffset + 2 + (skullAnimCounter/5), y,
9997 					'\x1D' | V_YELLOWMAP, false);
9998 			}
9999 		}
10000 	}
10001 
10002 	// DRAW THE SKULL CURSOR
10003 	V_DrawScaledPatch(currentMenu->x - 24, cursory, 0, W_CachePatchName("M_CURSOR", PU_PATCH));
10004 	V_DrawString(currentMenu->x, cursory, V_YELLOWMAP, currentMenu->menuitems[itemOn].text);
10005 
10006 	// Level record list
10007 	if (cv_nextmap.value)
10008 	{
10009 		emblem_t *em;
10010 		INT32 yHeight;
10011 		INT32 xpos;
10012 		patch_t *PictureOfLevel;
10013 		lumpnum_t lumpnum;
10014 		char beststr[40];
10015 
10016 		//UINT8 bestoverall	= G_GetBestNightsGrade(cv_nextmap.value, 0);
10017 		UINT8 bestgrade		= G_GetBestNightsGrade(cv_nextmap.value, cv_dummymares.value);
10018 		UINT32 bestscore	= G_GetBestNightsScore(cv_nextmap.value, cv_dummymares.value);
10019 		tic_t besttime		= G_GetBestNightsTime(cv_nextmap.value, cv_dummymares.value);
10020 
10021 		M_DrawLevelPlatterHeader(32-lsheadingheight/2, cv_nextmap.string, true, false);
10022 
10023 		//  A 160x100 image of the level as entry MAPxxP
10024 		lumpnum = W_CheckNumForName(va("%sP", G_BuildMapName(cv_nextmap.value)));
10025 
10026 		if (lumpnum != LUMPERROR)
10027 			PictureOfLevel = W_CachePatchName(va("%sP", G_BuildMapName(cv_nextmap.value)), PU_PATCH);
10028 		else
10029 			PictureOfLevel = W_CachePatchName("BLANKLVL", PU_PATCH);
10030 
10031 		y = 32+lsheadingheight;
10032 		V_DrawSmallScaledPatch(208, y, 0, PictureOfLevel);
10033 
10034 		// Draw press ESC to exit string on main nights attack menu
10035 		if (currentMenu == &SP_NightsAttackDef)
10036 		{
10037 			if (itemOn == nalevel)
10038 			{
10039 				/* Draw arrows !! */
10040 				y = y + 25 - 4;
10041 				V_DrawCharacter(208 - 10 - (skullAnimCounter/5), y,
10042 						'\x1C' | V_YELLOWMAP, false);
10043 				V_DrawCharacter(208 + 80 + 2 + (skullAnimCounter/5), y,
10044 						'\x1D' | V_YELLOWMAP, false);
10045 			}
10046 			// Draw press ESC to exit string on main record attack menu
10047 			V_DrawString(104-72, 180, V_TRANSLUCENT, M_GetText("Press ESC to exit"));
10048 		}
10049 
10050 		// Super Sonic
10051 		M_DrawNightsAttackSuperSonic();
10052 		//if (P_HasGrades(cv_nextmap.value, 0))
10053 		//	V_DrawScaledPatch(235 - (((ngradeletters[bestoverall])->width)*3)/2, 135, 0, ngradeletters[bestoverall]);
10054 
10055 		if (P_HasGrades(cv_nextmap.value, cv_dummymares.value))
10056 			{//make bigger again
10057 			V_DrawString(104 - 72, 48+lsheadingheight/2, V_YELLOWMAP, "BEST GRADE:");
10058 			V_DrawSmallScaledPatch(104 + 72 - (ngradeletters[bestgrade]->width/2),
10059 				48+lsheadingheight/2 + 8 - (ngradeletters[bestgrade]->height/2),
10060 				0, ngradeletters[bestgrade]);
10061 		}
10062 
10063 		if (!bestscore)
10064 			sprintf(beststr, "(none)");
10065 		else
10066 			sprintf(beststr, "%u", bestscore);
10067 
10068 		V_DrawString(104 - 72, 58+lsheadingheight/2, V_YELLOWMAP, "BEST SCORE:");
10069 		V_DrawRightAlignedString(104 + 72, 58+lsheadingheight/2, V_ALLOWLOWERCASE, beststr);
10070 
10071 		if (besttime == UINT32_MAX)
10072 			sprintf(beststr, "(none)");
10073 		else
10074 			sprintf(beststr, "%i:%02i.%02i", G_TicsToMinutes(besttime, true),
10075 																			 G_TicsToSeconds(besttime),
10076 																			 G_TicsToCentiseconds(besttime));
10077 
10078 		V_DrawString(104 - 72, 68+lsheadingheight/2, V_YELLOWMAP, "BEST TIME:");
10079 		V_DrawRightAlignedString(104 + 72, 68+lsheadingheight/2, V_ALLOWLOWERCASE, beststr);
10080 
10081 		if (cv_dummymares.value == 0) {
10082 			// Draw record emblems.
10083 			em = M_GetLevelEmblems(cv_nextmap.value);
10084 			while (em)
10085 			{
10086 				switch (em->type)
10087 				{
10088 					case ET_NGRADE:
10089 						xpos = 104+38;
10090 						yHeight = 48;
10091 						break;
10092 					case ET_NTIME:
10093 						xpos = 104+76;
10094 						yHeight = 68;
10095 						break;
10096 					default:
10097 						goto skipThisOne;
10098 				}
10099 
10100 				if (em->collected)
10101 					V_DrawSmallMappedPatch(xpos, yHeight+lsheadingheight/2, 0, W_CachePatchName(M_GetEmblemPatch(em, false), PU_PATCH),
10102 																 R_GetTranslationColormap(TC_DEFAULT, M_GetEmblemColor(em), GTC_CACHE));
10103 				else
10104 					V_DrawSmallScaledPatch(xpos, yHeight+lsheadingheight/2, 0, W_CachePatchName("NEEDIT", PU_PATCH));
10105 
10106 				skipThisOne:
10107 				em = M_GetLevelEmblems(-1);
10108 			}
10109 		}
10110 	}
10111 
10112 	// ALWAYS DRAW level even when not on this menu!
10113 	if (currentMenu != &SP_NightsAttackDef)
10114 		V_DrawString(SP_NightsAttackDef.x, SP_NightsAttackDef.y + SP_TimeAttackMenu[nalevel].alphaKey, V_TRANSLUCENT, SP_NightsAttackMenu[nalevel].text);
10115 }
10116 
10117 static void M_NightsAttackLevelSelect(INT32 choice)
10118 {
10119 	(void)choice;
10120 	SP_NightsAttackLevelSelectDef.prevMenu = currentMenu;
10121 	M_SetupNextMenu(&SP_NightsAttackLevelSelectDef);
10122 }
10123 
10124 // Going to Nights Attack menu...
10125 static void M_NightsAttack(INT32 choice)
10126 {
10127 	(void)choice;
10128 
10129 	SP_NightsAttackDef.prevMenu = &MainDef;
10130 	levellistmode = LLM_NIGHTSATTACK; // Don't be dependent on cv_newgametype
10131 
10132 	if (!M_PrepareLevelPlatter(-1, true))
10133 	{
10134 		M_StartMessage(M_GetText("No NiGHTS-attackable levels found.\n"),NULL,MM_NOTHING);
10135 		return;
10136 	}
10137 	// This is really just to make sure Sonic is the played character, just in case
10138 	M_PatchSkinNameTable();
10139 
10140 	ntssupersonic[0] = W_CachePatchName("NTSSONC1", PU_PATCH);
10141 	ntssupersonic[1] = W_CachePatchName("NTSSONC2", PU_PATCH);
10142 
10143 	G_SetGamestate(GS_TIMEATTACK); // do this before M_SetupNextMenu so that menu meta state knows that we're switching
10144 	titlemapinaction = TITLEMAP_OFF; // Nope don't give us HOMs please
10145 	M_SetupNextMenu(&SP_NightsAttackDef);
10146 	if (!M_CanShowLevelInList(cv_nextmap.value-1, -1) && levelselect.rows[0].maplist[0])
10147 		CV_SetValue(&cv_nextmap, levelselect.rows[0].maplist[0]);
10148 	else
10149 		Nextmap_OnChange();
10150 
10151 	itemOn = nastart; // "Start" is selected.
10152 }
10153 
10154 // Player has selected the "START" from the nights attack screen
10155 static void M_ChooseNightsAttack(INT32 choice)
10156 {
10157 	char *gpath;
10158 	const size_t glen = strlen("replay")+1+strlen(timeattackfolder)+1+strlen("MAPXX")+1;
10159 	char nameofdemo[256];
10160 	(void)choice;
10161 	emeralds = 0;
10162 	memset(&luabanks, 0, sizeof(luabanks));
10163 	M_ClearMenus(true);
10164 	modeattacking = ATTACKING_NIGHTS;
10165 
10166 	I_mkdir(va("%s"PATHSEP"replay", srb2home), 0755);
10167 	I_mkdir(va("%s"PATHSEP"replay"PATHSEP"%s", srb2home, timeattackfolder), 0755);
10168 
10169 	if ((gpath = malloc(glen)) == NULL)
10170 		I_Error("Out of memory for replay filepath\n");
10171 
10172 	sprintf(gpath,"replay"PATHSEP"%s"PATHSEP"%s", timeattackfolder, G_BuildMapName(cv_nextmap.value));
10173 	snprintf(nameofdemo, sizeof nameofdemo, "%s-%s-last", gpath, skins[cv_chooseskin.value-1].name);
10174 
10175 	if (!cv_autorecord.value)
10176 		remove(va("%s"PATHSEP"%s.lmp", srb2home, nameofdemo));
10177 	else
10178 		G_RecordDemo(nameofdemo);
10179 
10180 	G_DeferedInitNew(false, G_BuildMapName(cv_nextmap.value), (UINT8)(cv_chooseskin.value-1), false, false);
10181 }
10182 
10183 // Player has selected the "START" from the time attack screen
10184 static void M_ChooseTimeAttack(INT32 choice)
10185 {
10186 	char *gpath;
10187 	const size_t glen = strlen("replay")+1+strlen(timeattackfolder)+1+strlen("MAPXX")+1;
10188 	char nameofdemo[256];
10189 	(void)choice;
10190 	emeralds = 0;
10191 	memset(&luabanks, 0, sizeof(luabanks));
10192 	M_ClearMenus(true);
10193 	modeattacking = ATTACKING_RECORD;
10194 
10195 	I_mkdir(va("%s"PATHSEP"replay", srb2home), 0755);
10196 	I_mkdir(va("%s"PATHSEP"replay"PATHSEP"%s", srb2home, timeattackfolder), 0755);
10197 
10198 	if ((gpath = malloc(glen)) == NULL)
10199 		I_Error("Out of memory for replay filepath\n");
10200 
10201 	sprintf(gpath,"replay"PATHSEP"%s"PATHSEP"%s", timeattackfolder, G_BuildMapName(cv_nextmap.value));
10202 	snprintf(nameofdemo, sizeof nameofdemo, "%s-%s-last", gpath, skins[cv_chooseskin.value-1].name);
10203 
10204 	if (!cv_autorecord.value)
10205 		remove(va("%s"PATHSEP"%s.lmp", srb2home, nameofdemo));
10206 	else
10207 		G_RecordDemo(nameofdemo);
10208 
10209 	G_DeferedInitNew(false, G_BuildMapName(cv_nextmap.value), (UINT8)(cv_chooseskin.value-1), false, false);
10210 }
10211 
10212 // Player has selected the "REPLAY" from the time attack screen
10213 static void M_ReplayTimeAttack(INT32 choice)
10214 {
10215 	const char *which;
10216 	char *demoname;
10217 	M_ClearMenus(true);
10218 	modeattacking = ATTACKING_RECORD; // set modeattacking before G_DoPlayDemo so the map loader knows
10219 
10220 	if (currentMenu == &SP_ReplayDef)
10221 	{
10222 		switch(choice) {
10223 		default:
10224 		case 0: // best score
10225 			which = "score-best";
10226 			break;
10227 		case 1: // best time
10228 			which = "time-best";
10229 			break;
10230 		case 2: // best rings
10231 			which = "rings-best";
10232 			break;
10233 		case 3: // last
10234 			which = "last";
10235 			break;
10236 		case 4: // guest
10237 			// srb2/replay/main/map01-guest.lmp
10238 			G_DoPlayDemo(va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-guest.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value)));
10239 			return;
10240 		}
10241 		// srb2/replay/main/map01-sonic-time-best.lmp
10242 		G_DoPlayDemo(va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s-%s.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), skins[cv_chooseskin.value-1].name, which));
10243 	}
10244 	else if (currentMenu == &SP_NightsReplayDef)
10245 	{
10246 		switch(choice) {
10247 		default:
10248 		case 0: // best score
10249 			which = "score-best";
10250 			break;
10251 		case 1: // best time
10252 			which = "time-best";
10253 			break;
10254 		case 2: // last
10255 			which = "last";
10256 			break;
10257 		case 3: // guest
10258 			G_DoPlayDemo(va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-guest.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value)));
10259 			return;
10260 		}
10261 
10262 		demoname = va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s-%s.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), skins[cv_chooseskin.value-1].name, which);
10263 
10264 #ifdef OLDNREPLAYNAME // Check for old style named NiGHTS replay if a new style replay doesn't exist.
10265 		if (!FIL_FileExists(demoname))
10266 			demoname = va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), which);
10267 #endif
10268 
10269 		G_DoPlayDemo(demoname);
10270 	}
10271 }
10272 
10273 static void M_EraseGuest(INT32 choice)
10274 {
10275 	const char *rguest = va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-guest.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value));
10276 
10277 	if (choice == 'y' || choice == KEY_ENTER)
10278 	{
10279 		if (FIL_FileExists(rguest))
10280 			remove(rguest);
10281 	}
10282 	M_SetupNextMenu(currentMenu->prevMenu->prevMenu);
10283 	Nextmap_OnChange();
10284 	M_StartMessage(M_GetText("Guest replay data erased.\n"),NULL,MM_NOTHING);
10285 }
10286 
10287 static void M_OverwriteGuest(const char *which)
10288 {
10289 	char *rguest = Z_StrDup(va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-guest.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value)));
10290 	UINT8 *buf;
10291 	size_t len;
10292 	len = FIL_ReadFile(va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s-%s.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), skins[cv_chooseskin.value-1].name, which), &buf);
10293 
10294 	if (!len) {
10295 		return;
10296 	}
10297 	if (FIL_FileExists(rguest)) {
10298 		M_StopMessage(0);
10299 		remove(rguest);
10300 	}
10301 	FIL_WriteFile(rguest, buf, len);
10302 	Z_Free(rguest);
10303 	if (currentMenu == &SP_NightsGuestReplayDef)
10304 		M_SetupNextMenu(&SP_NightsAttackDef);
10305 	else
10306 		M_SetupNextMenu(&SP_TimeAttackDef);
10307 	Nextmap_OnChange();
10308 	M_StartMessage(M_GetText("Guest replay data saved.\n"),NULL,MM_NOTHING);
10309 }
10310 
10311 static void M_OverwriteGuest_Time(INT32 choice)
10312 {
10313 	(void)choice;
10314 	M_OverwriteGuest("time-best");
10315 }
10316 
10317 static void M_OverwriteGuest_Score(INT32 choice)
10318 {
10319 	(void)choice;
10320 	M_OverwriteGuest("score-best");
10321 }
10322 
10323 static void M_OverwriteGuest_Rings(INT32 choice)
10324 {
10325 	(void)choice;
10326 	M_OverwriteGuest("rings-best");
10327 }
10328 
10329 static void M_OverwriteGuest_Last(INT32 choice)
10330 {
10331 	(void)choice;
10332 	M_OverwriteGuest("last");
10333 }
10334 
10335 static void M_SetGuestReplay(INT32 choice)
10336 {
10337 	void (*which)(INT32);
10338 	if (currentMenu == &SP_NightsGuestReplayDef && choice >= 2)
10339 		choice++; // skip best rings
10340 	switch(choice)
10341 	{
10342 	case 0: // best score
10343 		which = M_OverwriteGuest_Score;
10344 		break;
10345 	case 1: // best time
10346 		which = M_OverwriteGuest_Time;
10347 		break;
10348 	case 2: // best rings
10349 		which = M_OverwriteGuest_Rings;
10350 		break;
10351 	case 3: // last
10352 		which = M_OverwriteGuest_Last;
10353 		break;
10354 	case 4: // guest
10355 	default:
10356 		M_StartMessage(M_GetText("Are you sure you want to\ndelete the guest replay data?\n\n(Press 'Y' to confirm)\n"),M_EraseGuest,MM_YESNO);
10357 		return;
10358 	}
10359 	if (FIL_FileExists(va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-guest.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value))))
10360 		M_StartMessage(M_GetText("Are you sure you want to\noverwrite the guest replay data?\n\n(Press 'Y' to confirm)\n"),which,MM_YESNO);
10361 	else
10362 		which(0);
10363 }
10364 
10365 void M_ModeAttackRetry(INT32 choice)
10366 {
10367 	(void)choice;
10368 	// todo -- maybe seperate this out and G_SetRetryFlag() here instead? is just calling this from the menu 100% safe?
10369 	G_CheckDemoStatus(); // Cancel recording
10370 	if (modeattacking == ATTACKING_RECORD)
10371 		M_ChooseTimeAttack(0);
10372 	else if (modeattacking == ATTACKING_NIGHTS)
10373 		M_ChooseNightsAttack(0);
10374 }
10375 
10376 static void M_ModeAttackEndGame(INT32 choice)
10377 {
10378 	(void)choice;
10379 	G_CheckDemoStatus(); // Cancel recording
10380 
10381 	if (gamestate == GS_LEVEL || gamestate == GS_INTERMISSION)
10382 		Command_ExitGame_f();
10383 
10384 	M_StartControlPanel();
10385 	switch(modeattacking)
10386 	{
10387 	default:
10388 	case ATTACKING_RECORD:
10389 		currentMenu = &SP_TimeAttackDef;
10390 		wipetypepost = menupres[MN_SP_TIMEATTACK].enterwipe;
10391 		break;
10392 	case ATTACKING_NIGHTS:
10393 		currentMenu = &SP_NightsAttackDef;
10394 		wipetypepost = menupres[MN_SP_NIGHTSATTACK].enterwipe;
10395 		break;
10396 	}
10397 	itemOn = currentMenu->lastOn;
10398 	G_SetGamestate(GS_TIMEATTACK);
10399 	modeattacking = ATTACKING_NONE;
10400 	M_ChangeMenuMusic("_title", true);
10401 	Nextmap_OnChange();
10402 }
10403 
10404 static void M_MarathonLiveEventBackup(INT32 choice)
10405 {
10406 	if (choice == 'y' || choice == KEY_ENTER)
10407 	{
10408 		marathonmode = MA_INIT;
10409 		G_LoadGame(MARATHONSLOT, 0);
10410 		cursaveslot = MARATHONSLOT;
10411 		if (!(marathonmode & MA_RUNNING))
10412 			marathonmode = 0;
10413 		return;
10414 	}
10415 
10416 	M_StopMessage(0);
10417 	stopstopmessage = true;
10418 
10419 	if (choice == KEY_DEL)
10420 	{
10421 		if (FIL_FileExists(liveeventbackup)) // just in case someone deleted it while we weren't looking.
10422 			remove(liveeventbackup);
10423 		BwehHehHe();
10424 		M_StartMessage("Live event backup erased.\n",M_Marathon,MM_NOTHING);
10425 		return;
10426 	}
10427 
10428 	M_Marathon(-1);
10429 }
10430 
10431 // Going to Marathon menu...
10432 static void M_Marathon(INT32 choice)
10433 {
10434 	UINT8 skinset;
10435 	INT32 mapnum = 0;
10436 
10437 	if (choice != -1 && FIL_FileExists(liveeventbackup))
10438 	{
10439 		M_StartMessage(\
10440 			"\x82Live event backup detected.\n\x80\
10441 			Do you want to resurrect the last run?\n\
10442 			(Fs in chat if we crashed on stream.)\n\
10443 			\n\
10444 			Press 'Y' or 'Enter' to resume,\n\
10445 			'Del' to delete, or any other\n\
10446 			key to continue to Marathon Run.",M_MarathonLiveEventBackup,MM_YESNO);
10447 		return;
10448 	}
10449 
10450 	fromlevelselect = false;
10451 
10452 	startmap = spmarathon_start;
10453 	CV_SetValue(&cv_newgametype, GT_COOP); // Graue 09-08-2004
10454 
10455 	skinset = M_SetupChoosePlayerDirect(-1);
10456 
10457 	SP_MarathonMenu[marathonplayer].status = (skinset == MAXSKINS) ? IT_KEYHANDLER|IT_STRING : IT_NOTHING|IT_DISABLED;
10458 
10459 	while (mapnum < NUMMAPS)
10460 	{
10461 		if (mapheaderinfo[mapnum])
10462 		{
10463 			if (mapheaderinfo[mapnum]->cutscenenum || mapheaderinfo[mapnum]->precutscenenum)
10464 				break;
10465 		}
10466 		mapnum++;
10467 	}
10468 
10469 	SP_MarathonMenu[marathoncutscenes].status = (mapnum < NUMMAPS) ? IT_CVAR|IT_STRING : IT_NOTHING|IT_DISABLED;
10470 
10471 	M_ChangeMenuMusic("spec8", true);
10472 
10473 	SP_MarathonDef.prevMenu = &MainDef;
10474 	G_SetGamestate(GS_TIMEATTACK); // do this before M_SetupNextMenu so that menu meta state knows that we're switching
10475 	titlemapinaction = TITLEMAP_OFF; // Nope don't give us HOMs please
10476 	M_SetupNextMenu(&SP_MarathonDef);
10477 	itemOn = marathonstart; // "Start" is selected.
10478 	recatkdrawtimer = 50-8;
10479 	char_scroll = 0;
10480 }
10481 
10482 static void M_HandleMarathonChoosePlayer(INT32 choice)
10483 {
10484 	INT32 selectval;
10485 
10486 	if (keydown > 1)
10487 		return;
10488 
10489 	switch (choice)
10490 	{
10491 		case KEY_DOWNARROW:
10492 			M_NextOpt();
10493 			break;
10494 		case KEY_UPARROW:
10495 			M_PrevOpt();
10496 			break;
10497 
10498 		case KEY_LEFTARROW:
10499 			if ((selectval = description[char_on].prev) == char_on)
10500 				return;
10501 			char_on = selectval;
10502 			break;
10503 		case KEY_RIGHTARROW:
10504 			if ((selectval = description[char_on].next) == char_on)
10505 				return;
10506 			char_on = selectval;
10507 			break;
10508 
10509 		case KEY_ESCAPE:
10510 			noFurtherInput = true;
10511 			M_GoBack(0);
10512 			return;
10513 
10514 		default:
10515 			return;
10516 	}
10517 	S_StartSound(NULL, sfx_menu1);
10518 }
10519 
10520 static void M_StartMarathon(INT32 choice)
10521 {
10522 	(void)choice;
10523 	marathontime = 0;
10524 	marathonmode = MA_RUNNING|MA_INIT;
10525 	cursaveslot = (cv_dummymarathon.value == 1) ? MARATHONSLOT : 0;
10526 	if (!cv_dummycutscenes.value)
10527 		marathonmode |= MA_NOCUTSCENES;
10528 	if (cv_dummyloadless.value)
10529 		marathonmode |= MA_INGAME;
10530 	M_ChoosePlayer(char_on);
10531 }
10532 
10533 // Drawing function for Marathon menu
10534 void M_DrawMarathon(void)
10535 {
10536 	INT32 i, x, y, cursory = 0, cnt, soffset = 0, w;
10537 	UINT16 dispstatus;
10538 	consvar_t *cv;
10539 	const char *cvstring;
10540 	char *work;
10541 	angle_t fa;
10542 	INT32 dupz = (vid.dupx < vid.dupy ? vid.dupx : vid.dupy), xspan = (vid.width/dupz), yspan = (vid.height/dupz), diffx = (xspan - BASEVIDWIDTH)/2, diffy = (yspan - BASEVIDHEIGHT)/2, maxy = BASEVIDHEIGHT + diffy;
10543 
10544 	curbgxspeed = 0;
10545 	curbgyspeed = 18;
10546 
10547 	M_ChangeMenuMusic("spec8", true); // Eww, but needed for when user hits escape during demo playback
10548 
10549 	V_DrawFill(-diffx, -diffy, diffx+(BASEVIDWIDTH-190)/2, yspan, 158);
10550 	V_DrawFill((BASEVIDWIDTH-190)/2, -diffy, 190, yspan, 31);
10551 	V_DrawFill((BASEVIDWIDTH+190)/2, -diffy, diffx+(BASEVIDWIDTH-190)/2, yspan, 158);
10552 	//M_DrawRecordAttackForeground();
10553 	if (curfadevalue)
10554 		V_DrawFadeScreen(0xFF00, curfadevalue);
10555 
10556 	x = (((BASEVIDWIDTH-82)/2)+11)<<FRACBITS;
10557 	y = (((BASEVIDHEIGHT-82)/2)+12-10)<<FRACBITS;
10558 
10559 	cnt = (36*(recatkdrawtimer<<FRACBITS))/TICRATE;
10560 	fa = (FixedAngle(cnt)>>ANGLETOFINESHIFT) & FINEMASK;
10561 	y -= (10*FINECOSINE(fa));
10562 
10563 	recatkdrawtimer++;
10564 
10565 	soffset = cnt = (recatkdrawtimer%50);
10566 	if (!useBlackRock)
10567 	{
10568 		if (cnt > 8)
10569 			cnt = 8;
10570 		V_DrawFixedPatch(x+(6<<FRACBITS), y, FRACUNIT/2, (cnt&~1)<<(V_ALPHASHIFT-1), W_CachePatchName("RECCLOCK", PU_PATCH), NULL);
10571 	}
10572 	else if (cnt > 8)
10573 	{
10574 		cnt = 8;
10575 		V_DrawFixedPatch(x, y, FRACUNIT, V_TRANSLUCENT, W_CachePatchName("ENDEGRK5", PU_PATCH), NULL);
10576 	}
10577 	else
10578 	{
10579 		V_DrawFixedPatch(x, y, FRACUNIT, cnt<<V_ALPHASHIFT, W_CachePatchName("ROID0000", PU_PATCH), NULL);
10580 		V_DrawFixedPatch(x, y, FRACUNIT, V_TRANSLUCENT, W_CachePatchName("ENDEGRK5", PU_PATCH), NULL);
10581 		V_DrawFixedPatch(x, y, FRACUNIT, cnt<<V_ALPHASHIFT, W_CachePatchName("ENDEGRK0", PU_PATCH), NULL);
10582 	}
10583 
10584 	{
10585 		UINT8 col;
10586 		i = 0;
10587 		w = (((8-cnt)+1)/3)+1;
10588 		w *= w;
10589 		cursory = 0;
10590 		while (i < cnt)
10591 		{
10592 			i++;
10593 			col = 158+((cnt-i)/3);
10594 			if (col >= 160)
10595 				col = 253;
10596 			V_DrawFill(((BASEVIDWIDTH-190)/2)-cursory-w, -diffy, w, yspan, col);
10597 			V_DrawFill(((BASEVIDWIDTH+190)/2)+cursory,   -diffy, w, yspan, col);
10598 			cursory += w;
10599 			w *= 2;
10600 		}
10601 	}
10602 
10603 	w = char_scroll + (((8-cnt)*(8-cnt))<<(FRACBITS-5));
10604 	if (soffset == 50-1)
10605 		w += FRACUNIT/2;
10606 
10607 	{
10608 		patch_t *fg = W_CachePatchName("RECATKFG", PU_PATCH);
10609 		INT32 trans = V_60TRANS+((cnt&~3)<<(V_ALPHASHIFT-2));
10610 		INT32 height = fg->height / 2;
10611 		char patchname[7] = "CEMGx0";
10612 
10613 		dupz = (w*7)/6; //(w*42*120)/(360*6); -- I don't know why this works but I'm not going to complain.
10614 		dupz = ((dupz>>FRACBITS) % height);
10615 		y = height/2;
10616 		while (y+dupz >= -diffy)
10617 			y -= height;
10618 		while (y-2-dupz < maxy)
10619 		{
10620 			V_DrawFixedPatch(((BASEVIDWIDTH-190)<<(FRACBITS-1)), (y-2-dupz)<<FRACBITS, FRACUNIT/2, trans, fg, NULL);
10621 			V_DrawFixedPatch(((BASEVIDWIDTH+190)<<(FRACBITS-1)), (y+dupz)<<FRACBITS, FRACUNIT/2, trans|V_FLIP, fg, NULL);
10622 			y += height;
10623 		}
10624 
10625 		trans = V_40TRANS+((cnt&~1)<<(V_ALPHASHIFT-1));
10626 
10627 		for (i = 0; i < 7; ++i)
10628 		{
10629 			fa = (FixedAngle(w)>>ANGLETOFINESHIFT) & FINEMASK;
10630 			x = (BASEVIDWIDTH<<(FRACBITS-1)) + (60*FINESINE(fa));
10631 			y = ((BASEVIDHEIGHT+16-20)<<(FRACBITS-1)) - (60*FINECOSINE(fa));
10632 			w += (360<<FRACBITS)/7;
10633 
10634 			patchname[4] = 'A'+(char)i;
10635 			V_DrawFixedPatch(x, y, FRACUNIT, trans, W_CachePatchName(patchname, PU_PATCH), NULL);
10636 		}
10637 
10638 		height = 18; // prevents the need for the next line
10639 		//dupz = (w*height)/18;
10640 		dupz = ((w>>FRACBITS) % height);
10641 		y = dupz+(height/4);
10642 		x = 105+dupz;
10643 		while (y >= -diffy)
10644 		{
10645 			x -= height;
10646 			y -= height;
10647 		}
10648 		while (y-dupz < maxy && x < (xspan/2))
10649 		{
10650 			V_DrawFill((BASEVIDWIDTH/2)-x-height, -diffy, height, diffy+y+height, 153);
10651 			V_DrawFill((BASEVIDWIDTH/2)+x, (maxy-y)-height, height, height+y, 153);
10652 			y += height;
10653 			x += height;
10654 		}
10655 	}
10656 
10657 	if (!soffset)
10658 	{
10659 		char_scroll += (360<<FRACBITS)/42; // like a clock, ticking at 42bpm!
10660 		if (char_scroll >= 360<<FRACBITS)
10661 			char_scroll -= 360<<FRACBITS;
10662 		if (recatkdrawtimer > (10*TICRATE))
10663 			recatkdrawtimer -= (10*TICRATE);
10664 	}
10665 
10666 	M_DrawMenuTitle();
10667 
10668 	// draw menu (everything else goes on top of it)
10669 	// Sadly we can't just use generic mode menus because we need some extra hacks
10670 	x = currentMenu->x;
10671 	y = currentMenu->y;
10672 
10673 	dispstatus = (currentMenu->menuitems[marathonplayer].status & IT_DISPLAY);
10674 	if (dispstatus == IT_STRING || dispstatus == IT_WHITESTRING)
10675 	{
10676 		soffset = 68;
10677 		if (description[char_on].charpic->width >= 256)
10678 			V_DrawTinyScaledPatch(224, 120, 0, description[char_on].charpic);
10679 		else
10680 			V_DrawSmallScaledPatch(224, 120, 0, description[char_on].charpic);
10681 	}
10682 	else
10683 		soffset = 0;
10684 
10685 	for (i = 0; i < currentMenu->numitems; ++i)
10686 	{
10687 		dispstatus = (currentMenu->menuitems[i].status & IT_DISPLAY);
10688 		if (dispstatus != IT_STRING && dispstatus != IT_WHITESTRING)
10689 			continue;
10690 
10691 		y = currentMenu->y+currentMenu->menuitems[i].alphaKey;
10692 		if (i == itemOn)
10693 			cursory = y;
10694 
10695 		V_DrawString(x, y, (dispstatus == IT_WHITESTRING) ? V_YELLOWMAP : 0 , currentMenu->menuitems[i].text);
10696 
10697 		cv = NULL;
10698 		cvstring = NULL;
10699 		work = NULL;
10700 		if ((currentMenu->menuitems[i].status & IT_TYPE) == IT_CVAR)
10701 		{
10702 			cv = (consvar_t *)currentMenu->menuitems[i].itemaction;
10703 			cvstring = cv->string;
10704 		}
10705 		else if (i == marathonplayer)
10706 		{
10707 			if (description[char_on].displayname[0])
10708 			{
10709 				work = Z_StrDup(description[char_on].displayname);
10710 				cnt = 0;
10711 				while (work[cnt])
10712 				{
10713 					if (work[cnt] == '\n')
10714 						work[cnt] = ' ';
10715 					cnt++;
10716 				}
10717 				cvstring = work;
10718 			}
10719 			else
10720 				cvstring = description[char_on].skinname;
10721 		}
10722 
10723 		// Cvar specific handling
10724 		if (cvstring)
10725 		{
10726 			INT32 flags = V_YELLOWMAP;
10727 			if (cv == &cv_dummymarathon && cv->value == 2) // ultimate_selectable
10728 				flags = V_REDMAP;
10729 
10730 			// Should see nothing but strings
10731 			if (cv == &cv_dummymarathon && cv->value == 1)
10732 			{
10733 				w = V_ThinStringWidth(cvstring, 0);
10734 				V_DrawThinString(BASEVIDWIDTH - x - soffset - w, y+1, flags, cvstring);
10735 			}
10736 			else
10737 			{
10738 				w = V_StringWidth(cvstring, 0);
10739 				V_DrawString(BASEVIDWIDTH - x - soffset - w, y, flags, cvstring);
10740 			}
10741 			if (i == itemOn)
10742 			{
10743 				V_DrawCharacter(BASEVIDWIDTH - x - soffset - 10 - w - (skullAnimCounter/5), y,
10744 					'\x1C' | V_YELLOWMAP, false);
10745 				V_DrawCharacter(BASEVIDWIDTH - x - soffset + 2 + (skullAnimCounter/5), y,
10746 					'\x1D' | V_YELLOWMAP, false);
10747 			}
10748 			if (work)
10749 				Z_Free(work);
10750 		}
10751 	}
10752 
10753 	// DRAW THE SKULL CURSOR
10754 	V_DrawScaledPatch(currentMenu->x - 24, cursory, 0, W_CachePatchName("M_CURSOR", PU_PATCH));
10755 	V_DrawString(currentMenu->x, cursory, V_YELLOWMAP, currentMenu->menuitems[itemOn].text);
10756 
10757 	// Draw press ESC to exit string on main record attack menu
10758 	V_DrawString(104-72, 180, V_TRANSLUCENT, M_GetText("Press ESC to exit"));
10759 }
10760 
10761 // ========
10762 // END GAME
10763 // ========
10764 
10765 static void M_ExitGameResponse(INT32 ch)
10766 {
10767 	if (ch != 'y' && ch != KEY_ENTER)
10768 		return;
10769 
10770 	//Command_ExitGame_f();
10771 	G_SetExitGameFlag();
10772 	M_ClearMenus(true);
10773 }
10774 
10775 static void M_EndGame(INT32 choice)
10776 {
10777 	(void)choice;
10778 	if (demoplayback || demorecording)
10779 		return;
10780 
10781 	if (!Playing())
10782 		return;
10783 
10784 	M_StartMessage(M_GetText("Are you sure you want to end the game?\n\n(Press 'Y' to confirm)\n"), M_ExitGameResponse, MM_YESNO);
10785 }
10786 
10787 //===========================================================================
10788 // Connect Menu
10789 //===========================================================================
10790 
10791 #define SERVERHEADERHEIGHT 44
10792 #define SERVERLINEHEIGHT 12
10793 
10794 #define S_LINEY(n) currentMenu->y + SERVERHEADERHEIGHT + (n * SERVERLINEHEIGHT)
10795 
10796 #ifndef NONET
10797 static UINT32 localservercount;
10798 
10799 static void M_HandleServerPage(INT32 choice)
10800 {
10801 	boolean exitmenu = false; // exit to previous menu
10802 
10803 	switch (choice)
10804 	{
10805 		case KEY_DOWNARROW:
10806 			M_NextOpt();
10807 			S_StartSound(NULL, sfx_menu1);
10808 			break;
10809 		case KEY_UPARROW:
10810 			M_PrevOpt();
10811 			S_StartSound(NULL, sfx_menu1);
10812 			break;
10813 		case KEY_BACKSPACE:
10814 		case KEY_ESCAPE:
10815 			exitmenu = true;
10816 			break;
10817 
10818 		case KEY_ENTER:
10819 		case KEY_RIGHTARROW:
10820 			S_StartSound(NULL, sfx_menu1);
10821 			if ((serverlistpage + 1) * SERVERS_PER_PAGE < serverlistcount)
10822 				serverlistpage++;
10823 			break;
10824 		case KEY_LEFTARROW:
10825 			S_StartSound(NULL, sfx_menu1);
10826 			if (serverlistpage > 0)
10827 				serverlistpage--;
10828 			break;
10829 
10830 		default:
10831 			break;
10832 	}
10833 	if (exitmenu)
10834 	{
10835 		if (currentMenu->prevMenu)
10836 			M_SetupNextMenu(currentMenu->prevMenu);
10837 		else
10838 			M_ClearMenus(true);
10839 	}
10840 }
10841 
10842 static void M_Connect(INT32 choice)
10843 {
10844 	// do not call menuexitfunc
10845 	M_ClearMenus(false);
10846 
10847 	COM_BufAddText(va("connect node %d\n", serverlist[choice-FIRSTSERVERLINE + serverlistpage * SERVERS_PER_PAGE].node));
10848 }
10849 
10850 static void M_Refresh(INT32 choice)
10851 {
10852 	(void)choice;
10853 
10854 	// Display a little "please wait" message.
10855 	M_DrawTextBox(52, BASEVIDHEIGHT/2-10, 25, 3);
10856 	V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT/2, 0, "Searching for servers...");
10857 	V_DrawCenteredString(BASEVIDWIDTH/2, (BASEVIDHEIGHT/2)+12, 0, "Please wait.");
10858 	I_OsPolling();
10859 	I_UpdateNoBlit();
10860 	if (rendermode == render_soft)
10861 		I_FinishUpdate(); // page flip or blit buffer
10862 
10863 	// note: this is the one case where 0 is a valid room number
10864 	// because it corresponds to "All"
10865 	CL_UpdateServerList(!(ms_RoomId < 0), ms_RoomId);
10866 
10867 	// first page of servers
10868 	serverlistpage = 0;
10869 }
10870 
10871 static INT32 menuRoomIndex = 0;
10872 
10873 static void M_DrawRoomMenu(void)
10874 {
10875 	static int frame = -12;
10876 	int dot_frame;
10877 	char text[4];
10878 
10879 	const char *rmotd;
10880 	const char *waiting_message;
10881 
10882 	int dots;
10883 
10884 	if (m_waiting_mode)
10885 	{
10886 		dot_frame = frame / 4;
10887 		dots = dot_frame + 3;
10888 
10889 		strcpy(text, "   ");
10890 
10891 		if (dots > 0)
10892 		{
10893 			if (dot_frame < 0)
10894 				dot_frame = 0;
10895 
10896 			strncpy(&text[dot_frame], "...", min(dots, 3 - dot_frame));
10897 		}
10898 
10899 		if (++frame == 12)
10900 			frame = -12;
10901 
10902 		currentMenu->menuitems[0].text = text;
10903 	}
10904 
10905 	// use generic drawer for cursor, items and title
10906 	M_DrawGenericMenu();
10907 
10908 	V_DrawString(currentMenu->x - 16, currentMenu->y, V_YELLOWMAP, M_GetText("Select a room"));
10909 
10910 	if (m_waiting_mode == M_NOT_WAITING)
10911 	{
10912 		M_DrawTextBox(144, 24, 20, 20);
10913 
10914 		if (itemOn == 0)
10915 			rmotd = M_GetText("Don't connect to the Master Server.");
10916 		else
10917 			rmotd = room_list[itemOn-1].motd;
10918 
10919 		rmotd = V_WordWrap(0, 20*8, 0, rmotd);
10920 		V_DrawString(144+8, 32, V_ALLOWLOWERCASE|V_RETURN8, rmotd);
10921 	}
10922 
10923 	if (m_waiting_mode)
10924 	{
10925 		// Display a little "please wait" message.
10926 		M_DrawTextBox(52, BASEVIDHEIGHT/2-10, 25, 3);
10927 		if (m_waiting_mode == M_WAITING_VERSION)
10928 			waiting_message = "Checking for updates...";
10929 		else
10930 			waiting_message = "Fetching room info...";
10931 		V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT/2, 0, waiting_message);
10932 		V_DrawCenteredString(BASEVIDWIDTH/2, (BASEVIDHEIGHT/2)+12, 0, "Please wait.");
10933 	}
10934 }
10935 
10936 static void M_DrawConnectMenu(void)
10937 {
10938 	UINT16 i;
10939 	char *gt;
10940 	INT32 numPages = (serverlistcount+(SERVERS_PER_PAGE-1))/SERVERS_PER_PAGE;
10941 
10942 	for (i = FIRSTSERVERLINE; i < min(localservercount, SERVERS_PER_PAGE)+FIRSTSERVERLINE; i++)
10943 		MP_ConnectMenu[i].status = IT_STRING | IT_SPACE;
10944 
10945 	if (!numPages)
10946 		numPages = 1;
10947 
10948 	// Room name
10949 	if (ms_RoomId < 0)
10950 		V_DrawRightAlignedString(BASEVIDWIDTH - currentMenu->x, currentMenu->y + MP_ConnectMenu[mp_connect_room].alphaKey,
10951 		                         V_YELLOWMAP, (itemOn == mp_connect_room) ? "<Select to change>" : "<Unlisted Mode>");
10952 	else
10953 		V_DrawRightAlignedString(BASEVIDWIDTH - currentMenu->x, currentMenu->y + MP_ConnectMenu[mp_connect_room].alphaKey,
10954 		                         V_YELLOWMAP, room_list[menuRoomIndex].name);
10955 
10956 	// Page num
10957 	V_DrawRightAlignedString(BASEVIDWIDTH - currentMenu->x, currentMenu->y + MP_ConnectMenu[mp_connect_page].alphaKey,
10958 	                         V_YELLOWMAP, va("%u of %d", serverlistpage+1, numPages));
10959 
10960 	// Horizontal line!
10961 	V_DrawFill(1, currentMenu->y+40, 318, 1, 0);
10962 
10963 	if (serverlistcount <= 0)
10964 		V_DrawString(currentMenu->x,currentMenu->y+SERVERHEADERHEIGHT, 0, "No servers found");
10965 	else
10966 	for (i = 0; i < min(serverlistcount - serverlistpage * SERVERS_PER_PAGE, SERVERS_PER_PAGE); i++)
10967 	{
10968 		INT32 slindex = i + serverlistpage * SERVERS_PER_PAGE;
10969 		UINT32 globalflags = (serverlist[slindex].info.refusereason ? V_TRANSLUCENT : 0)
10970 			|((itemOn == FIRSTSERVERLINE+i) ? V_YELLOWMAP : 0)|V_ALLOWLOWERCASE;
10971 
10972 		V_DrawString(currentMenu->x, S_LINEY(i), globalflags, serverlist[slindex].info.servername);
10973 
10974 		// Don't use color flags intentionally, the global yellow color will auto override the text color code
10975 		if (serverlist[slindex].info.modifiedgame)
10976 			V_DrawSmallString(currentMenu->x+202, S_LINEY(i)+8, globalflags, "\x85" "Mod");
10977 		if (serverlist[slindex].info.cheatsenabled)
10978 			V_DrawSmallString(currentMenu->x+222, S_LINEY(i)+8, globalflags, "\x83" "Cheats");
10979 
10980 		V_DrawSmallString(currentMenu->x, S_LINEY(i)+8, globalflags,
10981 		                     va("Ping: %u", (UINT32)LONG(serverlist[slindex].info.time)));
10982 
10983 		gt = serverlist[slindex].info.gametypename;
10984 
10985 		V_DrawSmallString(currentMenu->x+46,S_LINEY(i)+8, globalflags,
10986 		                         va("Players: %02d/%02d", serverlist[slindex].info.numberofplayer, serverlist[slindex].info.maxplayer));
10987 
10988 		if (strlen(gt) > 11)
10989 			gt = va("Gametype: %.11s...", gt);
10990 		else
10991 			gt = va("Gametype: %s", gt);
10992 
10993 		V_DrawSmallString(currentMenu->x+112, S_LINEY(i)+8, globalflags, gt);
10994 
10995 		MP_ConnectMenu[i+FIRSTSERVERLINE].status = IT_STRING | IT_CALL;
10996 	}
10997 
10998 	localservercount = serverlistcount;
10999 
11000 	M_DrawGenericMenu();
11001 
11002 	if (m_waiting_mode)
11003 	{
11004 		// Display a little "please wait" message.
11005 		M_DrawTextBox(52, BASEVIDHEIGHT/2-10, 25, 3);
11006 		V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT/2, 0, "Searching for servers...");
11007 		V_DrawCenteredString(BASEVIDWIDTH/2, (BASEVIDHEIGHT/2)+12, 0, "Please wait.");
11008 	}
11009 }
11010 
11011 static boolean M_CancelConnect(void)
11012 {
11013 	D_CloseConnection();
11014 	return true;
11015 }
11016 
11017 // Ascending order, not descending.
11018 // The casts are safe as long as the caller doesn't do anything stupid.
11019 #define SERVER_LIST_ENTRY_COMPARATOR(key) \
11020 static int ServerListEntryComparator_##key(const void *entry1, const void *entry2) \
11021 { \
11022 	const serverelem_t *sa = (const serverelem_t*)entry1, *sb = (const serverelem_t*)entry2; \
11023 	if (sa->info.key != sb->info.key) \
11024 		return sa->info.key - sb->info.key; \
11025 	return strcmp(sa->info.servername, sb->info.servername); \
11026 }
11027 
11028 // This does descending instead of ascending.
11029 #define SERVER_LIST_ENTRY_COMPARATOR_REVERSE(key) \
11030 static int ServerListEntryComparator_##key##_reverse(const void *entry1, const void *entry2) \
11031 { \
11032 	const serverelem_t *sa = (const serverelem_t*)entry1, *sb = (const serverelem_t*)entry2; \
11033 	if (sb->info.key != sa->info.key) \
11034 		return sb->info.key - sa->info.key; \
11035 	return strcmp(sb->info.servername, sa->info.servername); \
11036 }
11037 
11038 SERVER_LIST_ENTRY_COMPARATOR(time)
11039 SERVER_LIST_ENTRY_COMPARATOR(numberofplayer)
11040 SERVER_LIST_ENTRY_COMPARATOR_REVERSE(numberofplayer)
11041 SERVER_LIST_ENTRY_COMPARATOR_REVERSE(maxplayer)
11042 
11043 static int ServerListEntryComparator_gametypename(const void *entry1, const void *entry2)
11044 {
11045 	const serverelem_t *sa = (const serverelem_t*)entry1, *sb = (const serverelem_t*)entry2;
11046 	int c;
11047 	if (( c = strcasecmp(sa->info.gametypename, sb->info.gametypename) ))
11048 		return c;
11049 	return strcmp(sa->info.servername, sb->info.servername); \
11050 }
11051 
11052 // Special one for modified state.
11053 static int ServerListEntryComparator_modified(const void *entry1, const void *entry2)
11054 {
11055 	const serverelem_t *sa = (const serverelem_t*)entry1, *sb = (const serverelem_t*)entry2;
11056 
11057 	// Modified acts as 2 points, cheats act as one point.
11058 	int modstate_a = (sa->info.cheatsenabled ? 1 : 0) | (sa->info.modifiedgame ? 2 : 0);
11059 	int modstate_b = (sb->info.cheatsenabled ? 1 : 0) | (sb->info.modifiedgame ? 2 : 0);
11060 
11061 	if (modstate_a != modstate_b)
11062 		return modstate_a - modstate_b;
11063 
11064 	// Default to strcmp.
11065 	return strcmp(sa->info.servername, sb->info.servername);
11066 }
11067 #endif
11068 
11069 void M_SortServerList(void)
11070 {
11071 #ifndef NONET
11072 	switch(cv_serversort.value)
11073 	{
11074 	case 0:		// Ping.
11075 		qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_time);
11076 		break;
11077 	case 1:		// Modified state.
11078 		qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_modified);
11079 		break;
11080 	case 2:		// Most players.
11081 		qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_numberofplayer_reverse);
11082 		break;
11083 	case 3:		// Least players.
11084 		qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_numberofplayer);
11085 		break;
11086 	case 4:		// Max players.
11087 		qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_maxplayer_reverse);
11088 		break;
11089 	case 5:		// Gametype.
11090 		qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_gametypename);
11091 		break;
11092 	}
11093 #endif
11094 }
11095 
11096 #ifndef NONET
11097 #ifdef UPDATE_ALERT
11098 static boolean M_CheckMODVersion(int id)
11099 {
11100 	char updatestring[500];
11101 	const char *updatecheck = GetMODVersion(id);
11102 	if(updatecheck)
11103 	{
11104 		sprintf(updatestring, UPDATE_ALERT_STRING, VERSIONSTRING, updatecheck);
11105 		M_StartMessage(updatestring, NULL, MM_NOTHING);
11106 		return false;
11107 	} else
11108 		return true;
11109 }
11110 #endif/*UPDATE_ALERT*/
11111 
11112 #if defined (MASTERSERVER) && defined (HAVE_THREADS)
11113 static void
11114 Check_new_version_thread (int *id)
11115 {
11116 	int hosting;
11117 	int okay;
11118 
11119 	okay = 0;
11120 
11121 #ifdef UPDATE_ALERT
11122 	if (M_CheckMODVersion(*id))
11123 #endif
11124 	{
11125 		I_lock_mutex(&ms_QueryId_mutex);
11126 		{
11127 			okay = ( *id == ms_QueryId );
11128 		}
11129 		I_unlock_mutex(ms_QueryId_mutex);
11130 
11131 		if (okay)
11132 		{
11133 			I_lock_mutex(&m_menu_mutex);
11134 			{
11135 				m_waiting_mode = M_WAITING_ROOMS;
11136 				hosting = ( currentMenu->prevMenu == &MP_ServerDef );
11137 			}
11138 			I_unlock_mutex(m_menu_mutex);
11139 
11140 			GetRoomsList(hosting, *id);
11141 		}
11142 	}
11143 #ifdef UPDATE_ALERT
11144 	else
11145 	{
11146 		I_lock_mutex(&ms_QueryId_mutex);
11147 		{
11148 			okay = ( *id == ms_QueryId );
11149 		}
11150 		I_unlock_mutex(ms_QueryId_mutex);
11151 	}
11152 #endif
11153 
11154 	if (okay)
11155 	{
11156 		I_lock_mutex(&m_menu_mutex);
11157 		{
11158 			if (m_waiting_mode)
11159 			{
11160 				m_waiting_mode = M_NOT_WAITING;
11161 				MP_RoomMenu[0].text = "<Offline Mode>";
11162 			}
11163 		}
11164 		I_unlock_mutex(m_menu_mutex);
11165 	}
11166 
11167 	free(id);
11168 }
11169 #endif/*defined (MASTERSERVER) && defined (HAVE_THREADS)*/
11170 
11171 static void M_ConnectMenu(INT32 choice)
11172 {
11173 	(void)choice;
11174 	// modified game check: no longer handled
11175 	// we don't request a restart unless the filelist differs
11176 
11177 	// first page of servers
11178 	serverlistpage = 0;
11179 	if (ms_RoomId < 0)
11180 	{
11181 		M_RoomMenu(0); // Select a room instead of staring at an empty list
11182 		// This prevents us from returning to the modified game alert.
11183 		currentMenu->prevMenu = &MP_MainDef;
11184 	}
11185 	else
11186 		M_SetupNextMenu(&MP_ConnectDef);
11187 	itemOn = 0;
11188 	M_Refresh(0);
11189 }
11190 
11191 static void M_ConnectMenuModChecks(INT32 choice)
11192 {
11193 	(void)choice;
11194 	// okay never mind we want to COMMUNICATE to the player pre-emptively instead of letting them try and then get confused when it doesn't work
11195 
11196 	if (modifiedgame)
11197 	{
11198 		M_StartMessage(M_GetText("You have add-ons loaded.\nYou won't be able to join netgames!\n\nTo play online, restart the game\nand don't load any addons.\nSRB2 will automatically add\neverything you need when you join.\n\n(Press a key)\n"),M_ConnectMenu,MM_EVENTHANDLER);
11199 		return;
11200 	}
11201 
11202 	M_ConnectMenu(-1);
11203 }
11204 
11205 UINT32 roomIds[NUM_LIST_ROOMS];
11206 
11207 static void M_RoomMenu(INT32 choice)
11208 {
11209 	INT32 i;
11210 #if defined (MASTERSERVER) && defined (HAVE_THREADS)
11211 	int *id;
11212 #endif
11213 
11214 	(void)choice;
11215 
11216 	// Display a little "please wait" message.
11217 	M_DrawTextBox(52, BASEVIDHEIGHT/2-10, 25, 3);
11218 	V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT/2, 0, "Fetching room info...");
11219 	V_DrawCenteredString(BASEVIDWIDTH/2, (BASEVIDHEIGHT/2)+12, 0, "Please wait.");
11220 	I_OsPolling();
11221 	I_UpdateNoBlit();
11222 	if (rendermode == render_soft)
11223 		I_FinishUpdate(); // page flip or blit buffer
11224 
11225 	for (i = 1; i < NUM_LIST_ROOMS+1; ++i)
11226 		MP_RoomMenu[i].status = IT_DISABLED;
11227 	memset(roomIds, 0, sizeof(roomIds));
11228 
11229 	MP_RoomDef.prevMenu = currentMenu;
11230 	M_SetupNextMenu(&MP_RoomDef);
11231 
11232 #ifdef MASTERSERVER
11233 #ifdef HAVE_THREADS
11234 #ifdef UPDATE_ALERT
11235 	m_waiting_mode = M_WAITING_VERSION;
11236 #else/*UPDATE_ALERT*/
11237 	m_waiting_mode = M_WAITING_ROOMS;
11238 #endif/*UPDATE_ALERT*/
11239 
11240 	MP_RoomMenu[0].text = "";
11241 
11242 	id = malloc(sizeof *id);
11243 
11244 	I_lock_mutex(&ms_QueryId_mutex);
11245 	{
11246 		*id = ms_QueryId;
11247 	}
11248 	I_unlock_mutex(ms_QueryId_mutex);
11249 
11250 	I_spawn_thread("check-new-version",
11251 			(I_thread_fn)Check_new_version_thread, id);
11252 #else/*HAVE_THREADS*/
11253 #ifdef UPDATE_ALERT
11254 	if (M_CheckMODVersion(0))
11255 #endif/*UPDATE_ALERT*/
11256 	{
11257 		GetRoomsList(currentMenu->prevMenu == &MP_ServerDef, 0);
11258 	}
11259 #endif/*HAVE_THREADS*/
11260 #endif/*MASTERSERVER*/
11261 }
11262 
11263 static void M_ChooseRoom(INT32 choice)
11264 {
11265 #if defined (MASTERSERVER) && defined (HAVE_THREADS)
11266 	I_lock_mutex(&ms_QueryId_mutex);
11267 	{
11268 		ms_QueryId++;
11269 	}
11270 	I_unlock_mutex(ms_QueryId_mutex);
11271 #endif
11272 
11273 	if (choice == 0)
11274 		ms_RoomId = -1;
11275 	else
11276 	{
11277 		ms_RoomId = roomIds[choice-1];
11278 		menuRoomIndex = choice - 1;
11279 	}
11280 
11281 	serverlistpage = 0;
11282 	/*
11283 	We were on the Multiplayer menu? That means that we must have been trying to
11284 	view the server browser, but we hadn't selected a room yet. So we need to go
11285 	to the browser next, not back there.
11286 	*/
11287 	if (currentMenu->prevMenu == &MP_MainDef)
11288 		M_SetupNextMenu(&MP_ConnectDef);
11289 	else
11290 		M_SetupNextMenu(currentMenu->prevMenu);
11291 
11292 	if (currentMenu == &MP_ConnectDef)
11293 		M_Refresh(0);
11294 }
11295 #endif //NONET
11296 
11297 //===========================================================================
11298 // Start Server Menu
11299 //===========================================================================
11300 
11301 static void M_StartServer(INT32 choice)
11302 {
11303 	boolean StartSplitScreenGame = (currentMenu == &MP_SplitServerDef);
11304 
11305 	(void)choice;
11306 	if (!StartSplitScreenGame)
11307 		netgame = true;
11308 
11309 	multiplayer = true;
11310 
11311 	// Still need to reset devmode
11312 	cv_debug = 0;
11313 
11314 	if (demoplayback)
11315 		G_StopDemo();
11316 	if (metalrecording)
11317 		G_StopMetalDemo();
11318 
11319 	if (!StartSplitScreenGame)
11320 	{
11321 		D_MapChange(cv_nextmap.value, cv_newgametype.value, false, 1, 1, false, false);
11322 		COM_BufAddText("dummyconsvar 1\n");
11323 	}
11324 	else // split screen
11325 	{
11326 		paused = false;
11327 		SV_StartSinglePlayerServer();
11328 		if (!splitscreen)
11329 		{
11330 			splitscreen = true;
11331 			SplitScreen_OnChange();
11332 		}
11333 		D_MapChange(cv_nextmap.value, cv_newgametype.value, false, 1, 1, false, false);
11334 	}
11335 
11336 	M_ClearMenus(true);
11337 }
11338 
11339 static void M_DrawServerMenu(void)
11340 {
11341 	M_DrawGenericMenu();
11342 
11343 #ifndef NONET
11344 	// Room name
11345 	if (currentMenu == &MP_ServerDef)
11346 	{
11347 		M_DrawLevelPlatterHeader(currentMenu->y - lsheadingheight/2, "Server settings", true, false);
11348 		if (ms_RoomId < 0)
11349 			V_DrawRightAlignedString(BASEVIDWIDTH - currentMenu->x, currentMenu->y + MP_ServerMenu[mp_server_room].alphaKey,
11350 			                         V_YELLOWMAP, (itemOn == mp_server_room) ? "<Select to change>" : "<Unlisted Mode>");
11351 		else
11352 			V_DrawRightAlignedString(BASEVIDWIDTH - currentMenu->x, currentMenu->y + MP_ServerMenu[mp_server_room].alphaKey,
11353 			                         V_YELLOWMAP, room_list[menuRoomIndex].name);
11354 	}
11355 #endif
11356 
11357 	if (cv_nextmap.value)
11358 	{
11359 #ifndef NONET
11360 #define imgheight MP_ServerMenu[mp_server_levelgt].alphaKey
11361 #else
11362 #define imgheight 100
11363 #endif
11364 		patch_t *PictureOfLevel;
11365 		lumpnum_t lumpnum;
11366 		char headerstr[40];
11367 
11368 		sprintf(headerstr, "%s - %s", cv_newgametype.string, cv_nextmap.string);
11369 
11370 		M_DrawLevelPlatterHeader(currentMenu->y + imgheight - 10 - lsheadingheight/2, (const char *)headerstr, true, false);
11371 
11372 		//  A 160x100 image of the level as entry MAPxxP
11373 		lumpnum = W_CheckNumForName(va("%sP", G_BuildMapName(cv_nextmap.value)));
11374 
11375 		if (lumpnum != LUMPERROR)
11376 			PictureOfLevel = W_CachePatchName(va("%sP", G_BuildMapName(cv_nextmap.value)), PU_PATCH);
11377 		else
11378 			PictureOfLevel = W_CachePatchName("BLANKLVL", PU_PATCH);
11379 
11380 		V_DrawSmallScaledPatch(319 - (currentMenu->x + (PictureOfLevel->width / 2)), currentMenu->y + imgheight, 0, PictureOfLevel);
11381 	}
11382 }
11383 
11384 static void M_MapChange(INT32 choice)
11385 {
11386 	(void)choice;
11387 
11388 	MISC_ChangeLevelDef.prevMenu = currentMenu;
11389 	levellistmode = LLM_CREATESERVER;
11390 
11391 	if (Playing() && !(M_CanShowLevelOnPlatter(cv_nextmap.value-1, cv_newgametype.value)) && (M_CanShowLevelOnPlatter(gamemap-1, cv_newgametype.value)))
11392 		CV_SetValue(&cv_nextmap, gamemap);
11393 
11394 	if (!M_PrepareLevelPlatter(cv_newgametype.value, (currentMenu == &MPauseDef)))
11395 	{
11396 		M_StartMessage(M_GetText("No selectable levels found.\n"),NULL,MM_NOTHING);
11397 		return;
11398 	}
11399 
11400 	M_SetupNextMenu(&MISC_ChangeLevelDef);
11401 }
11402 
11403 static void M_StartSplitServerMenu(INT32 choice)
11404 {
11405 	(void)choice;
11406 	levellistmode = LLM_CREATESERVER;
11407 	Newgametype_OnChange();
11408 	M_SetupNextMenu(&MP_SplitServerDef);
11409 }
11410 
11411 static void M_ServerOptions(INT32 choice)
11412 {
11413 	(void)choice;
11414 
11415 #ifndef NONET
11416 	if ((splitscreen && !netgame) || currentMenu == &MP_SplitServerDef)
11417 	{
11418 		OP_ServerOptionsMenu[ 1].status = IT_GRAYEDOUT; // Server name
11419 		OP_ServerOptionsMenu[ 2].status = IT_GRAYEDOUT; // Max players
11420 		OP_ServerOptionsMenu[ 3].status = IT_GRAYEDOUT; // Allow add-on downloading
11421 		OP_ServerOptionsMenu[ 4].status = IT_GRAYEDOUT; // Allow players to join
11422 		OP_ServerOptionsMenu[36].status = IT_GRAYEDOUT; // Master server
11423 		OP_ServerOptionsMenu[37].status = IT_GRAYEDOUT; // Minimum delay between joins
11424 		OP_ServerOptionsMenu[38].status = IT_GRAYEDOUT; // Attempts to resynchronise
11425 	}
11426 	else
11427 	{
11428 		OP_ServerOptionsMenu[ 1].status = IT_STRING | IT_CVAR | IT_CV_STRING;
11429 		OP_ServerOptionsMenu[ 2].status = IT_STRING | IT_CVAR;
11430 		OP_ServerOptionsMenu[ 3].status = IT_STRING | IT_CVAR;
11431 		OP_ServerOptionsMenu[ 4].status = IT_STRING | IT_CVAR;
11432 		OP_ServerOptionsMenu[36].status = (netgame
11433 			? IT_GRAYEDOUT
11434 			: (IT_STRING | IT_CVAR | IT_CV_STRING));
11435 		OP_ServerOptionsMenu[37].status = IT_STRING | IT_CVAR;
11436 		OP_ServerOptionsMenu[38].status = IT_STRING | IT_CVAR;
11437 	}
11438 #endif
11439 
11440 	/* Disable fading because of different menu head. */
11441 	if (currentMenu == &OP_MainDef)/* from Options menu */
11442 		OP_ServerOptionsDef.menuid = MTREE2(MN_OP_MAIN, MN_OP_SERVER);
11443 	else/* from Multiplayer menu */
11444 		OP_ServerOptionsDef.menuid = MTREE2(MN_MP_MAIN, MN_MP_SERVER_OPTIONS);
11445 
11446 	OP_ServerOptionsDef.prevMenu = currentMenu;
11447 	M_SetupNextMenu(&OP_ServerOptionsDef);
11448 }
11449 
11450 #ifndef NONET
11451 static void M_StartServerMenu(INT32 choice)
11452 {
11453 	(void)choice;
11454 	ms_RoomId = -1;
11455 	levellistmode = LLM_CREATESERVER;
11456 	Newgametype_OnChange();
11457 	M_SetupNextMenu(&MP_ServerDef);
11458 	itemOn = 1;
11459 }
11460 
11461 // ==============
11462 // CONNECT VIA IP
11463 // ==============
11464 
11465 static char setupm_ip[28];
11466 
11467 // Draw the funky Connect IP menu. Tails 11-19-2002
11468 // So much work for such a little thing!
11469 static void M_DrawMPMainMenu(void)
11470 {
11471 	INT32 x = currentMenu->x;
11472 	INT32 y = currentMenu->y;
11473 
11474 	// use generic drawer for cursor, items and title
11475 	M_DrawGenericMenu();
11476 
11477 	V_DrawRightAlignedString(BASEVIDWIDTH-x, y+66,
11478 		((itemOn == 4) ? V_YELLOWMAP : 0), va("(2-%d players)", MAXPLAYERS));
11479 
11480 	V_DrawRightAlignedString(BASEVIDWIDTH-x, y+76,
11481 		((itemOn == 5) ? V_YELLOWMAP : 0), "(2 players)");
11482 
11483 	V_DrawRightAlignedString(BASEVIDWIDTH-x, y+116,
11484 		((itemOn == 8) ? V_YELLOWMAP : 0), "(splitscreen)");
11485 
11486 	y += 22;
11487 
11488 	V_DrawFill(x+5, y+4+5, /*16*8 + 6,*/ BASEVIDWIDTH - 2*(x+5), 8+6, 159);
11489 
11490 	// draw name string
11491 	V_DrawString(x+8,y+12, V_ALLOWLOWERCASE, setupm_ip);
11492 
11493 	// draw text cursor for name
11494 	if (itemOn == 2 //0
11495 	    && skullAnimCounter < 4)   //blink cursor
11496 		V_DrawCharacter(x+8+V_StringWidth(setupm_ip, V_ALLOWLOWERCASE),y+12,'_',false);
11497 }
11498 
11499 // Tails 11-19-2002
11500 static void M_ConnectIP(INT32 choice)
11501 {
11502 	(void)choice;
11503 
11504 	if (*setupm_ip == 0)
11505 	{
11506 		M_StartMessage("You must specify an IP address.\n", NULL, MM_NOTHING);
11507 		return;
11508 	}
11509 
11510 	M_ClearMenus(true);
11511 
11512 	COM_BufAddText(va("connect \"%s\"\n", setupm_ip));
11513 
11514 	// A little "please wait" message.
11515 	M_DrawTextBox(56, BASEVIDHEIGHT/2-12, 24, 2);
11516 	V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT/2, 0, "Connecting to server...");
11517 	I_OsPolling();
11518 	I_UpdateNoBlit();
11519 	if (rendermode == render_soft)
11520 		I_FinishUpdate(); // page flip or blit buffer
11521 }
11522 
11523 // Tails 11-19-2002
11524 static void M_HandleConnectIP(INT32 choice)
11525 {
11526 	size_t l;
11527 	boolean exitmenu = false;  // exit to previous menu and send name change
11528 
11529 	switch (choice)
11530 	{
11531 		case KEY_DOWNARROW:
11532 			M_NextOpt();
11533 			S_StartSound(NULL,sfx_menu1); // Tails
11534 			break;
11535 
11536 		case KEY_UPARROW:
11537 			M_PrevOpt();
11538 			S_StartSound(NULL,sfx_menu1); // Tails
11539 			break;
11540 
11541 		case KEY_ENTER:
11542 			S_StartSound(NULL,sfx_menu1); // Tails
11543 			M_ConnectIP(1);
11544 			break;
11545 
11546 		case KEY_ESCAPE:
11547 			exitmenu = true;
11548 			break;
11549 
11550 		case KEY_BACKSPACE:
11551 			if ((l = strlen(setupm_ip)) != 0)
11552 			{
11553 				S_StartSound(NULL,sfx_menu1); // Tails
11554 				setupm_ip[l-1] = 0;
11555 			}
11556 			break;
11557 
11558 		case KEY_DEL:
11559 			if (setupm_ip[0] && !shiftdown) // Shift+Delete is used for something else.
11560 			{
11561 				S_StartSound(NULL,sfx_menu1); // Tails
11562 				setupm_ip[0] = 0;
11563 			}
11564 			if (!shiftdown) // Shift+Delete is used for something else.
11565 				break;
11566 
11567 			/* FALLTHRU */
11568 		default:
11569 			l = strlen(setupm_ip);
11570 
11571 			if ( ctrldown ) {
11572 				switch (choice) {
11573 					case 'v':
11574 					case 'V': // ctrl+v, pasting
11575 					{
11576 						const char *paste = I_ClipboardPaste();
11577 
11578 						if (paste != NULL) {
11579 							strncat(setupm_ip, paste, 28-1 - l); // Concat the ip field with clipboard
11580 							if (strlen(paste) != 0) // Don't play sound if nothing was pasted
11581 								S_StartSound(NULL,sfx_menu1); // Tails
11582 						}
11583 
11584 						break;
11585 					}
11586 					case KEY_INS:
11587 					case 'c':
11588 					case 'C': // ctrl+c, ctrl+insert, copying
11589 						I_ClipboardCopy(setupm_ip, l);
11590 						S_StartSound(NULL,sfx_menu1); // Tails
11591 						break;
11592 
11593 					case 'x':
11594 					case 'X': // ctrl+x, cutting
11595 						I_ClipboardCopy(setupm_ip, l);
11596 						S_StartSound(NULL,sfx_menu1); // Tails
11597 						setupm_ip[0] = 0;
11598 						break;
11599 
11600 					default: // otherwise do nothing
11601 						break;
11602 				}
11603 				break; // don't check for typed keys
11604 			}
11605 
11606 			if ( shiftdown ) {
11607 				switch (choice) {
11608 					case KEY_INS: // shift+insert, pasting
11609 						{
11610 							const char *paste = I_ClipboardPaste();
11611 
11612 							if (paste != NULL) {
11613 								strncat(setupm_ip, paste, 28-1 - l); // Concat the ip field with clipboard
11614 								if (strlen(paste) != 0) // Don't play sound if nothing was pasted
11615 									S_StartSound(NULL,sfx_menu1); // Tails
11616 							}
11617 
11618 							break;
11619 						}
11620 					case KEY_DEL: // shift+delete, cutting
11621 						I_ClipboardCopy(setupm_ip, l);
11622 						S_StartSound(NULL,sfx_menu1); // Tails
11623 						setupm_ip[0] = 0;
11624 						break;
11625 					default: // otherwise do nothing.
11626 						break;
11627 				}
11628 			}
11629 
11630 			if (l >= 28-1)
11631 				break;
11632 
11633 			// Rudimentary number and period enforcing - also allows letters so hostnames can be used instead
11634 			if ((choice >= '-' && choice <= ':') || (choice >= 'A' && choice <= 'Z') || (choice >= 'a' && choice <= 'z'))
11635 			{
11636 				S_StartSound(NULL,sfx_menu1); // Tails
11637 				setupm_ip[l] = (char)choice;
11638 				setupm_ip[l+1] = 0;
11639 			}
11640 			else if (choice >= 199 && choice <= 211 && choice != 202 && choice != 206) //numpad too!
11641 			{
11642 				char keypad_translation[] = {'7','8','9','-','4','5','6','+','1','2','3','0','.'};
11643 				choice = keypad_translation[choice - 199];
11644 				S_StartSound(NULL,sfx_menu1); // Tails
11645 				setupm_ip[l] = (char)choice;
11646 				setupm_ip[l+1] = 0;
11647 			}
11648 
11649 			break;
11650 	}
11651 
11652 	if (exitmenu)
11653 	{
11654 		currentMenu->lastOn = itemOn;
11655 		if (currentMenu->prevMenu)
11656 			M_SetupNextMenu (currentMenu->prevMenu);
11657 		else
11658 			M_ClearMenus(true);
11659 	}
11660 }
11661 #endif //!NONET
11662 
11663 // ========================
11664 // MULTIPLAYER PLAYER SETUP
11665 // ========================
11666 // Tails 03-02-2002
11667 
11668 static UINT8      multi_tics;
11669 static UINT8      multi_frame;
11670 static UINT8      multi_spr2;
11671 
11672 // this is set before entering the MultiPlayer setup menu,
11673 // for either player 1 or 2
11674 static char         setupm_name[MAXPLAYERNAME+1];
11675 static player_t    *setupm_player;
11676 static consvar_t   *setupm_cvskin;
11677 static consvar_t   *setupm_cvcolor;
11678 static consvar_t   *setupm_cvname;
11679 static consvar_t   *setupm_cvdefaultskin;
11680 static consvar_t   *setupm_cvdefaultcolor;
11681 static INT32        setupm_fakeskin;
11682 static menucolor_t *setupm_fakecolor;
11683 
11684 static void M_DrawSetupMultiPlayerMenu(void)
11685 {
11686 	INT32 x, y, cursory = 0, flags = 0;
11687 	spritedef_t *sprdef;
11688 	spriteframe_t *sprframe;
11689 	patch_t *patch;
11690 	UINT8 *colormap;
11691 
11692 	x = MP_PlayerSetupDef.x;
11693 	y = MP_PlayerSetupDef.y;
11694 
11695 	// use generic drawer for cursor, items and title
11696 	//M_DrawGenericMenu();
11697 
11698 	// draw title (or big pic)
11699 	M_DrawMenuTitle();
11700 
11701 	M_DrawLevelPlatterHeader(y - (lsheadingheight - 12), "Name", true, false);
11702 	if (itemOn == 0)
11703 		cursory = y;
11704 	y += 11;
11705 
11706 	// draw name string
11707 	V_DrawFill(x, y, 282/*(MAXPLAYERNAME+1)*8+6*/, 14, 159);
11708 	V_DrawString(x + 8, y + 3, V_ALLOWLOWERCASE, setupm_name);
11709 	if (skullAnimCounter < 4 && itemOn == 0)
11710 		V_DrawCharacter(x + 8 + V_StringWidth(setupm_name, V_ALLOWLOWERCASE), y + 3,
11711 			'_' | 0x80, false);
11712 
11713 	y += 20;
11714 
11715 	M_DrawLevelPlatterHeader(y - (lsheadingheight - 12), "Character", true, false);
11716 	if (itemOn == 1)
11717 		cursory = y;
11718 
11719 	// draw skin string
11720 	V_DrawRightAlignedString(BASEVIDWIDTH - x, y,
11721 	             ((MP_PlayerSetupMenu[1].status & IT_TYPE) == IT_SPACE ? V_TRANSLUCENT : 0)|(itemOn == 1 ? V_YELLOWMAP : 0)|V_ALLOWLOWERCASE,
11722 	             skins[setupm_fakeskin].realname);
11723 
11724 	if (itemOn == 1 && (MP_PlayerSetupMenu[1].status & IT_TYPE) != IT_SPACE)
11725 	{
11726 		V_DrawCharacter(BASEVIDWIDTH - x - 10 - V_StringWidth(skins[setupm_fakeskin].realname, V_ALLOWLOWERCASE) - (skullAnimCounter/5), y,
11727 			'\x1C' | V_YELLOWMAP, false);
11728 		V_DrawCharacter(BASEVIDWIDTH - x + 2 + (skullAnimCounter/5), y,
11729 			'\x1D' | V_YELLOWMAP, false);
11730 	}
11731 
11732 	x = BASEVIDWIDTH/2;
11733 	y += 11;
11734 
11735 	// anim the player in the box
11736 	if (--multi_tics <= 0)
11737 	{
11738 		multi_frame++;
11739 		multi_tics = 4;
11740 	}
11741 
11742 #define charw 74
11743 
11744 	// draw box around character
11745 	V_DrawFill(x-(charw/2), y, charw, 84, 159);
11746 
11747 	sprdef = &skins[setupm_fakeskin].sprites[multi_spr2];
11748 
11749 	if (!setupm_fakecolor->color || !sprdef->numframes) // should never happen but hey, who knows
11750 		goto faildraw;
11751 
11752 	// ok, draw player sprite for sure now
11753 	colormap = R_GetTranslationColormap(setupm_fakeskin, setupm_fakecolor->color, GTC_CACHE);
11754 
11755 	if (multi_frame >= sprdef->numframes)
11756 		multi_frame = 0;
11757 
11758 	sprframe = &sprdef->spriteframes[multi_frame];
11759 	patch = W_CachePatchNum(sprframe->lumppat[0], PU_PATCH);
11760 	if (sprframe->flip & 1) // Only for first sprite
11761 		flags |= V_FLIP; // This sprite is left/right flipped!
11762 
11763 #define chary (y+64)
11764 
11765 	V_DrawFixedPatch(
11766 		x<<FRACBITS,
11767 		chary<<FRACBITS,
11768 		FixedDiv(skins[setupm_fakeskin].highresscale, skins[setupm_fakeskin].shieldscale),
11769 		flags, patch, colormap);
11770 
11771 	goto colordraw;
11772 
11773 faildraw:
11774 	sprdef = &sprites[SPR_UNKN];
11775 	if (!sprdef->numframes) // No frames ??
11776 		return; // Can't render!
11777 
11778 	sprframe = &sprdef->spriteframes[0];
11779 	patch = W_CachePatchNum(sprframe->lumppat[0], PU_PATCH);
11780 	if (sprframe->flip & 1) // Only for first sprite
11781 		flags |= V_FLIP; // This sprite is left/right flipped!
11782 
11783 	V_DrawScaledPatch(x, chary, flags, patch);
11784 
11785 #undef chary
11786 
11787 colordraw:
11788 	x = MP_PlayerSetupDef.x;
11789 	y += 75;
11790 
11791 	M_DrawLevelPlatterHeader(y - (lsheadingheight - 12), "Color", true, false);
11792 	if (itemOn == 2)
11793 		cursory = y;
11794 
11795 	// draw color string
11796 	V_DrawRightAlignedString(BASEVIDWIDTH - x, y,
11797 	             ((MP_PlayerSetupMenu[2].status & IT_TYPE) == IT_SPACE ? V_TRANSLUCENT : 0)|(itemOn == 2 ? V_YELLOWMAP : 0)|V_ALLOWLOWERCASE,
11798 	             skincolors[setupm_fakecolor->color].name);
11799 
11800 	if (itemOn == 2 && (MP_PlayerSetupMenu[2].status & IT_TYPE) != IT_SPACE)
11801 	{
11802 		V_DrawCharacter(BASEVIDWIDTH - x - 10 - V_StringWidth(skincolors[setupm_fakecolor->color].name, V_ALLOWLOWERCASE) - (skullAnimCounter/5), y,
11803 			'\x1C' | V_YELLOWMAP, false);
11804 		V_DrawCharacter(BASEVIDWIDTH - x + 2 + (skullAnimCounter/5), y,
11805 			'\x1D' | V_YELLOWMAP, false);
11806 	}
11807 
11808 	y += 11;
11809 
11810 #define indexwidth 8
11811 	{
11812 		const INT32 numcolors = (282-charw)/(2*indexwidth); // Number of colors per side
11813 		INT32 w = indexwidth; // Width of a singular color block
11814 		menucolor_t *mc = setupm_fakecolor->prev; // Last accessed color
11815 		UINT8 h;
11816 		INT16 i;
11817 
11818 		// Draw color in the middle
11819 		x += numcolors*w;
11820 		for (h = 0; h < 16; h++)
11821 			V_DrawFill(x, y+h, charw, 1, skincolors[setupm_fakecolor->color].ramp[h]);
11822 
11823 		//Draw colors from middle to left
11824 		for (i=0; i<numcolors; i++) {
11825 			x -= w;
11826 			// Find accessible color before this one
11827 			while (!skincolors[mc->color].accessible)
11828 				mc = mc->prev;
11829 			for (h = 0; h < 16; h++)
11830 				V_DrawFill(x, y+h, w, 1, skincolors[mc->color].ramp[h]);
11831 			mc = mc->prev;
11832 		}
11833 
11834 		// Draw colors from middle to right
11835 		mc = setupm_fakecolor->next;
11836 		x += numcolors*w + charw;
11837 		for (i=0; i<numcolors; i++) {
11838 			// Find accessible color after this one
11839 			while (!skincolors[mc->color].accessible)
11840 				mc = mc->next;
11841 			for (h = 0; h < 16; h++)
11842 				V_DrawFill(x, y+h, w, 1, skincolors[mc->color].ramp[h]);
11843 			x += w;
11844 			mc = mc->next;
11845 		}
11846 	}
11847 #undef charw
11848 #undef indexwidth
11849 
11850 	x = MP_PlayerSetupDef.x;
11851 	y += 20;
11852 
11853 	V_DrawString(x, y,
11854 		((R_SkinAvailable(setupm_cvdefaultskin->string) != setupm_fakeskin
11855 		|| setupm_cvdefaultcolor->value != setupm_fakecolor->color)
11856 			? 0
11857 			: V_TRANSLUCENT)
11858 		| ((itemOn == 3) ? V_YELLOWMAP : 0),
11859 		"Save as default");
11860 	if (itemOn == 3)
11861 		cursory = y;
11862 
11863 	V_DrawScaledPatch(x - 17, cursory, 0,
11864 		W_CachePatchName("M_CURSOR", PU_PATCH));
11865 }
11866 
11867 // Handle 1P/2P MP Setup
11868 static void M_HandleSetupMultiPlayer(INT32 choice)
11869 {
11870 	size_t   l;
11871 	INT32 prev_setupm_fakeskin;
11872 	boolean  exitmenu = false;  // exit to previous menu and send name change
11873 
11874 	switch (choice)
11875 	{
11876 		case KEY_DOWNARROW:
11877 			M_NextOpt();
11878 			S_StartSound(NULL,sfx_menu1); // Tails
11879 			break;
11880 
11881 		case KEY_UPARROW:
11882 			M_PrevOpt();
11883 			S_StartSound(NULL,sfx_menu1); // Tails
11884 			break;
11885 
11886 		case KEY_LEFTARROW:
11887 			if (itemOn == 1)       //player skin
11888 			{
11889 				S_StartSound(NULL,sfx_menu1); // Tails
11890 				prev_setupm_fakeskin = setupm_fakeskin;
11891 				do
11892 				{
11893 					setupm_fakeskin--;
11894 					if (setupm_fakeskin < 0)
11895 						setupm_fakeskin = numskins-1;
11896 				}
11897 				while ((prev_setupm_fakeskin != setupm_fakeskin) && !(R_SkinUsable(-1, setupm_fakeskin)));
11898 				multi_spr2 = P_GetSkinSprite2(&skins[setupm_fakeskin], SPR2_WALK, NULL);
11899 			}
11900 			else if (itemOn == 2) // player color
11901 			{
11902 				S_StartSound(NULL,sfx_menu1); // Tails
11903 				setupm_fakecolor = setupm_fakecolor->prev;
11904 			}
11905 			break;
11906 
11907 		case KEY_ENTER:
11908 			if (itemOn == 3
11909 			&& (R_SkinAvailable(setupm_cvdefaultskin->string) != setupm_fakeskin
11910 			|| setupm_cvdefaultcolor->value != setupm_fakecolor->color))
11911 			{
11912 				S_StartSound(NULL,sfx_strpst);
11913 				// you know what? always putting these in the buffer won't hurt anything.
11914 				COM_BufAddText (va("%s \"%s\"\n",setupm_cvdefaultskin->name,skins[setupm_fakeskin].name));
11915 				COM_BufAddText (va("%s %d\n",setupm_cvdefaultcolor->name,setupm_fakecolor->color));
11916 				break;
11917 			}
11918 			/* FALLTHRU */
11919 		case KEY_RIGHTARROW:
11920 			if (itemOn == 1)       //player skin
11921 			{
11922 				S_StartSound(NULL,sfx_menu1); // Tails
11923 				prev_setupm_fakeskin = setupm_fakeskin;
11924 				do
11925 				{
11926 					setupm_fakeskin++;
11927 					if (setupm_fakeskin > numskins-1)
11928 						setupm_fakeskin = 0;
11929 				}
11930 				while ((prev_setupm_fakeskin != setupm_fakeskin) && !(R_SkinUsable(-1, setupm_fakeskin)));
11931 				multi_spr2 = P_GetSkinSprite2(&skins[setupm_fakeskin], SPR2_WALK, NULL);
11932 			}
11933 			else if (itemOn == 2) // player color
11934 			{
11935 				S_StartSound(NULL,sfx_menu1); // Tails
11936 				setupm_fakecolor = setupm_fakecolor->next;
11937 			}
11938 			break;
11939 
11940 		case KEY_ESCAPE:
11941 			exitmenu = true;
11942 			break;
11943 
11944 		case KEY_BACKSPACE:
11945 			if (itemOn == 0 && (l = strlen(setupm_name))!=0)
11946 			{
11947 				S_StartSound(NULL,sfx_menu1); // Tails
11948 				setupm_name[l-1] = 0;
11949 			}
11950 			else if (itemOn == 2)
11951 			{
11952 				UINT16 col = skins[setupm_fakeskin].prefcolor;
11953 				if ((setupm_fakecolor->color != col) && skincolors[col].accessible)
11954 				{
11955 					S_StartSound(NULL,sfx_menu1); // Tails
11956 					for (setupm_fakecolor=menucolorhead;;setupm_fakecolor=setupm_fakecolor->next)
11957 						if (setupm_fakecolor->color == col || setupm_fakecolor == menucolortail)
11958 							break;
11959 				}
11960 			}
11961 			break;
11962 			break;
11963 
11964 		case KEY_DEL:
11965 			if (itemOn == 0 && (l = strlen(setupm_name))!=0)
11966 			{
11967 				S_StartSound(NULL,sfx_menu1); // Tails
11968 				setupm_name[0] = 0;
11969 			}
11970 			break;
11971 
11972 		default:
11973 			if (itemOn != 0 || choice < 32 || choice > 127)
11974 				break;
11975 			S_StartSound(NULL,sfx_menu1); // Tails
11976 			l = strlen(setupm_name);
11977 			if (l < MAXPLAYERNAME)
11978 			{
11979 				setupm_name[l] = (char)choice;
11980 				setupm_name[l+1] = 0;
11981 			}
11982 			break;
11983 	}
11984 
11985 	// check color
11986 	if (itemOn == 2 && !skincolors[setupm_fakecolor->color].accessible) {
11987 		if (choice == KEY_LEFTARROW)
11988 			while (!skincolors[setupm_fakecolor->color].accessible)
11989 				setupm_fakecolor = setupm_fakecolor->prev;
11990 		else if (choice == KEY_RIGHTARROW || choice == KEY_ENTER)
11991 			while (!skincolors[setupm_fakecolor->color].accessible)
11992 				setupm_fakecolor = setupm_fakecolor->next;
11993 	}
11994 
11995 	if (exitmenu)
11996 	{
11997 		if (currentMenu->prevMenu)
11998 			M_SetupNextMenu (currentMenu->prevMenu);
11999 		else
12000 			M_ClearMenus(true);
12001 	}
12002 }
12003 
12004 // start the multiplayer setup menu
12005 static void M_SetupMultiPlayer(INT32 choice)
12006 {
12007 	(void)choice;
12008 
12009 	multi_frame = 0;
12010 	multi_tics = 4;
12011 	strcpy(setupm_name, cv_playername.string);
12012 
12013 	// set for player 1
12014 	setupm_player = &players[consoleplayer];
12015 	setupm_cvskin = &cv_skin;
12016 	setupm_cvcolor = &cv_playercolor;
12017 	setupm_cvname = &cv_playername;
12018 	setupm_cvdefaultskin = &cv_defaultskin;
12019 	setupm_cvdefaultcolor = &cv_defaultplayercolor;
12020 
12021 	// For whatever reason this doesn't work right if you just use ->value
12022 	setupm_fakeskin = R_SkinAvailable(setupm_cvskin->string);
12023 	if (setupm_fakeskin == -1)
12024 		setupm_fakeskin = 0;
12025 
12026 	for (setupm_fakecolor=menucolorhead;;setupm_fakecolor=setupm_fakecolor->next)
12027 		if (setupm_fakecolor->color == setupm_cvcolor->value || setupm_fakecolor == menucolortail)
12028 			break;
12029 
12030 	// disable skin changes if we can't actually change skins
12031 	if (!CanChangeSkin(consoleplayer))
12032 		MP_PlayerSetupMenu[1].status = (IT_GRAYEDOUT);
12033 	else
12034 		MP_PlayerSetupMenu[1].status = (IT_KEYHANDLER|IT_STRING);
12035 
12036 	// ditto with colour
12037 	if (Playing() && G_GametypeHasTeams())
12038 		MP_PlayerSetupMenu[2].status = (IT_GRAYEDOUT);
12039 	else
12040 		MP_PlayerSetupMenu[2].status = (IT_KEYHANDLER|IT_STRING);
12041 
12042 	multi_spr2 = P_GetSkinSprite2(&skins[setupm_fakeskin], SPR2_WALK, NULL);
12043 
12044 	MP_PlayerSetupDef.prevMenu = currentMenu;
12045 	M_SetupNextMenu(&MP_PlayerSetupDef);
12046 }
12047 
12048 // start the multiplayer setup menu, for secondary player (splitscreen mode)
12049 static void M_SetupMultiPlayer2(INT32 choice)
12050 {
12051 	(void)choice;
12052 
12053 	multi_frame = 0;
12054 	multi_tics = 4;
12055 	strcpy (setupm_name, cv_playername2.string);
12056 
12057 	// set for splitscreen secondary player
12058 	setupm_player = &players[secondarydisplayplayer];
12059 	setupm_cvskin = &cv_skin2;
12060 	setupm_cvcolor = &cv_playercolor2;
12061 	setupm_cvname = &cv_playername2;
12062 	setupm_cvdefaultskin = &cv_defaultskin2;
12063 	setupm_cvdefaultcolor = &cv_defaultplayercolor2;
12064 
12065 	// For whatever reason this doesn't work right if you just use ->value
12066 	setupm_fakeskin = R_SkinAvailable(setupm_cvskin->string);
12067 	if (setupm_fakeskin == -1)
12068 		setupm_fakeskin = 0;
12069 
12070 	for (setupm_fakecolor=menucolorhead;;setupm_fakecolor=setupm_fakecolor->next)
12071 		if (setupm_fakecolor->color == setupm_cvcolor->value || setupm_fakecolor == menucolortail)
12072 			break;
12073 
12074 	// disable skin changes if we can't actually change skins
12075 	if (splitscreen && !CanChangeSkin(secondarydisplayplayer))
12076 		MP_PlayerSetupMenu[1].status = (IT_GRAYEDOUT);
12077 	else
12078 		MP_PlayerSetupMenu[1].status = (IT_KEYHANDLER | IT_STRING);
12079 
12080 	// ditto with colour
12081 	if (Playing() && G_GametypeHasTeams())
12082 		MP_PlayerSetupMenu[2].status = (IT_GRAYEDOUT);
12083 	else
12084 		MP_PlayerSetupMenu[2].status = (IT_KEYHANDLER|IT_STRING);
12085 
12086 	multi_spr2 = P_GetSkinSprite2(&skins[setupm_fakeskin], SPR2_WALK, NULL);
12087 
12088 	MP_PlayerSetupDef.prevMenu = currentMenu;
12089 	M_SetupNextMenu(&MP_PlayerSetupDef);
12090 }
12091 
12092 static boolean M_QuitMultiPlayerMenu(void)
12093 {
12094 	size_t l;
12095 	// send name if changed
12096 	if (strcmp(setupm_name, setupm_cvname->string))
12097 	{
12098 		// remove trailing whitespaces
12099 		for (l= strlen(setupm_name)-1;
12100 		    (signed)l >= 0 && setupm_name[l] ==' '; l--)
12101 			setupm_name[l] =0;
12102 		COM_BufAddText (va("%s \"%s\"\n",setupm_cvname->name,setupm_name));
12103 	}
12104 	COM_BufAddText (va("%s \"%s\"\n",setupm_cvskin->name,skins[setupm_fakeskin].name));
12105 	// send color if changed
12106 	if (setupm_fakecolor->color != setupm_cvcolor->value)
12107 		COM_BufAddText (va("%s %d\n",setupm_cvcolor->name,setupm_fakecolor->color));
12108 	return true;
12109 }
12110 
12111 void M_AddMenuColor(UINT16 color) {
12112 	menucolor_t *c;
12113 
12114 	if (color >= numskincolors) {
12115 		CONS_Printf("M_AddMenuColor: color %d does not exist.",color);
12116 		return;
12117 	}
12118 
12119 	c = (menucolor_t *)malloc(sizeof(menucolor_t));
12120 	c->color = color;
12121 	if (menucolorhead == NULL) {
12122 		c->next = c;
12123 		c->prev = c;
12124 		menucolorhead = c;
12125 		menucolortail = c;
12126 	} else {
12127 		c->next = menucolorhead;
12128 		c->prev = menucolortail;
12129 		menucolortail->next = c;
12130 		menucolorhead->prev = c;
12131 		menucolortail = c;
12132 	}
12133 }
12134 
12135 void M_MoveColorBefore(UINT16 color, UINT16 targ) {
12136 	menucolor_t *look, *c = NULL, *t = NULL;
12137 
12138 	if (color == targ)
12139 		return;
12140 	if (color >= numskincolors) {
12141 		CONS_Printf("M_MoveColorBefore: color %d does not exist.",color);
12142 		return;
12143 	}
12144 	if (targ >= numskincolors) {
12145 		CONS_Printf("M_MoveColorBefore: target color %d does not exist.",targ);
12146 		return;
12147 	}
12148 
12149 	for (look=menucolorhead;;look=look->next) {
12150 		if (look->color == color)
12151 			c = look;
12152 		else if (look->color == targ)
12153 			t = look;
12154 		if (c != NULL && t != NULL)
12155 			break;
12156 		if (look==menucolortail)
12157 			return;
12158 	}
12159 
12160 	if (c == t->prev)
12161 		return;
12162 
12163 	if (t==menucolorhead)
12164 		menucolorhead = c;
12165 	if (c==menucolortail)
12166 		menucolortail = c->prev;
12167 
12168 	c->prev->next = c->next;
12169 	c->next->prev = c->prev;
12170 
12171 	c->prev = t->prev;
12172 	c->next = t;
12173 	t->prev->next = c;
12174 	t->prev = c;
12175 }
12176 
12177 void M_MoveColorAfter(UINT16 color, UINT16 targ) {
12178 	menucolor_t *look, *c = NULL, *t = NULL;
12179 
12180 	if (color == targ)
12181 		return;
12182 	if (color >= numskincolors) {
12183 		CONS_Printf("M_MoveColorAfter: color %d does not exist.\n",color);
12184 		return;
12185 	}
12186 	if (targ >= numskincolors) {
12187 		CONS_Printf("M_MoveColorAfter: target color %d does not exist.\n",targ);
12188 		return;
12189 	}
12190 
12191 	for (look=menucolorhead;;look=look->next) {
12192 		if (look->color == color)
12193 			c = look;
12194 		else if (look->color == targ)
12195 			t = look;
12196 		if (c != NULL && t != NULL)
12197 			break;
12198 		if (look==menucolortail)
12199 			return;
12200 	}
12201 
12202 	if (t == c->prev)
12203 		return;
12204 
12205 	if (t==menucolortail)
12206 		menucolortail = c;
12207 	else if (c==menucolortail)
12208 		menucolortail = c->prev;
12209 
12210 	c->prev->next = c->next;
12211 	c->next->prev = c->prev;
12212 
12213 	c->next = t->next;
12214 	c->prev = t;
12215 	t->next->prev = c;
12216 	t->next = c;
12217 }
12218 
12219 UINT16 M_GetColorBefore(UINT16 color) {
12220 	menucolor_t *look;
12221 
12222 	if (color >= numskincolors) {
12223 		CONS_Printf("M_GetColorBefore: color %d does not exist.\n",color);
12224 		return 0;
12225 	}
12226 
12227 	for (look=menucolorhead;;look=look->next) {
12228 		if (look->color == color)
12229 			return look->prev->color;
12230 		if (look==menucolortail)
12231 			return 0;
12232 	}
12233 }
12234 
12235 UINT16 M_GetColorAfter(UINT16 color) {
12236 	menucolor_t *look;
12237 
12238 	if (color >= numskincolors) {
12239 		CONS_Printf("M_GetColorAfter: color %d does not exist.\n",color);
12240 		return 0;
12241 	}
12242 
12243 	for (look=menucolorhead;;look=look->next) {
12244 		if (look->color == color)
12245 			return look->next->color;
12246 		if (look==menucolortail)
12247 			return 0;
12248 	}
12249 }
12250 
12251 void M_InitPlayerSetupColors(void) {
12252 	UINT8 i;
12253 	numskincolors = SKINCOLOR_FIRSTFREESLOT;
12254 	menucolorhead = menucolortail = NULL;
12255 	for (i=0; i<numskincolors; i++)
12256 		M_AddMenuColor(i);
12257 }
12258 
12259 void M_FreePlayerSetupColors(void) {
12260 	menucolor_t *look = menucolorhead, *tmp;
12261 
12262 	if (menucolorhead==NULL)
12263 		return;
12264 
12265 	while (true) {
12266 		if (look != menucolortail) {
12267 			tmp = look;
12268 			look = look->next;
12269 			free(tmp);
12270 		} else {
12271 			free(look);
12272 			return;
12273 		}
12274 	}
12275 
12276 	menucolorhead = menucolortail = NULL;
12277 }
12278 
12279 // =================
12280 // DATA OPTIONS MENU
12281 // =================
12282 static UINT8 erasecontext = 0;
12283 
12284 static void M_EraseDataResponse(INT32 ch)
12285 {
12286 	if (ch != 'y' && ch != KEY_ENTER)
12287 		return;
12288 
12289 	// Delete the data
12290 	if (erasecontext != 1)
12291 		G_ClearRecords();
12292 	if (erasecontext != 0)
12293 		M_ClearSecrets();
12294 	if (erasecontext == 2)
12295 	{
12296 		totalplaytime = 0;
12297 		F_StartIntro();
12298 	}
12299 	BwehHehHe();
12300 	M_ClearMenus(true);
12301 }
12302 
12303 static void M_EraseData(INT32 choice)
12304 {
12305 	const char *eschoice, *esstr = M_GetText("Are you sure you want to erase\n%s?\n\n(Press 'Y' to confirm)\n");
12306 
12307 	erasecontext = (UINT8)choice;
12308 
12309 	if (choice == 0)
12310 		eschoice = M_GetText("Record Attack data");
12311 	else if (choice == 1)
12312 		eschoice = M_GetText("Extras data");
12313 	else
12314 		eschoice = M_GetText("ALL game data");
12315 
12316 	M_StartMessage(va(esstr, eschoice),M_EraseDataResponse,MM_YESNO);
12317 }
12318 
12319 static void M_ScreenshotOptions(INT32 choice)
12320 {
12321 	(void)choice;
12322 	Screenshot_option_Onchange();
12323 	Moviemode_mode_Onchange();
12324 
12325 	M_SetupScreenshotMenu();
12326 	M_SetupNextMenu(&OP_ScreenshotOptionsDef);
12327 }
12328 
12329 static void M_SetupScreenshotMenu(void)
12330 {
12331 	menuitem_t *item = &OP_ScreenshotOptionsMenu[op_screenshot_colorprofile];
12332 
12333 #ifdef HWRENDER
12334 	// Hide some options based on render mode
12335 	if (rendermode == render_opengl)
12336 	{
12337 		item->status = IT_GRAYEDOUT;
12338 		if ((currentMenu == &OP_ScreenshotOptionsDef) && (itemOn == op_screenshot_colorprofile)) // Can't select that
12339 			itemOn = op_screenshot_storagelocation;
12340 	}
12341 	else
12342 #endif
12343 		item->status = (IT_STRING | IT_CVAR);
12344 }
12345 
12346 // =============
12347 // JOYSTICK MENU
12348 // =============
12349 
12350 // Start the controls menu, setting it up for either the console player,
12351 // or the secondary splitscreen player
12352 
12353 static void M_DrawJoystick(void)
12354 {
12355 	INT32 i, compareval2, compareval;
12356 
12357 	// draw title (or big pic)
12358 	M_DrawMenuTitle();
12359 
12360 	for (i = 0; i <= MAX_JOYSTICKS; i++) // See MAX_JOYSTICKS
12361 	{
12362 		M_DrawTextBox(OP_JoystickSetDef.x-8, OP_JoystickSetDef.y+LINEHEIGHT*i-12, 28, 1);
12363 		//M_DrawSaveLoadBorder(OP_JoystickSetDef.x+4, OP_JoystickSetDef.y+1+LINEHEIGHT*i);
12364 
12365 #ifdef JOYSTICK_HOTPLUG
12366 		if (atoi(cv_usejoystick2.string) > I_NumJoys())
12367 			compareval2 = atoi(cv_usejoystick2.string);
12368 		else
12369 			compareval2 = cv_usejoystick2.value;
12370 
12371 		if (atoi(cv_usejoystick.string) > I_NumJoys())
12372 			compareval = atoi(cv_usejoystick.string);
12373 		else
12374 			compareval = cv_usejoystick.value;
12375 #else
12376 		compareval2 = cv_usejoystick2.value;
12377 		compareval = cv_usejoystick.value;
12378 #endif
12379 
12380 		if ((setupcontrols_secondaryplayer && (i == compareval2))
12381 			|| (!setupcontrols_secondaryplayer && (i == compareval)))
12382 			V_DrawString(OP_JoystickSetDef.x, OP_JoystickSetDef.y+LINEHEIGHT*i-4,V_GREENMAP,joystickInfo[i]);
12383 		else
12384 			V_DrawString(OP_JoystickSetDef.x, OP_JoystickSetDef.y+LINEHEIGHT*i-4,0,joystickInfo[i]);
12385 
12386 		if (i == itemOn)
12387 		{
12388 			V_DrawScaledPatch(currentMenu->x - 24, OP_JoystickSetDef.y+LINEHEIGHT*i-4, 0,
12389 				W_CachePatchName("M_CURSOR", PU_PATCH));
12390 		}
12391 	}
12392 }
12393 
12394 void M_SetupJoystickMenu(INT32 choice)
12395 {
12396 	INT32 i = 0;
12397 	const char *joyNA = "Unavailable";
12398 	INT32 n = I_NumJoys();
12399 	(void)choice;
12400 
12401 	strcpy(joystickInfo[i], "None");
12402 
12403 	for (i = 1; i <= MAX_JOYSTICKS; i++)
12404 	{
12405 		if (i <= n && (I_GetJoyName(i)) != NULL)
12406 			strncpy(joystickInfo[i], I_GetJoyName(i), 28);
12407 		else
12408 			strcpy(joystickInfo[i], joyNA);
12409 
12410 #ifdef JOYSTICK_HOTPLUG
12411 		// We use cv_usejoystick.string as the USER-SET var
12412 		// and cv_usejoystick.value as the INTERNAL var
12413 		//
12414 		// In practice, if cv_usejoystick.string == 0, this overrides
12415 		// cv_usejoystick.value and always disables
12416 		//
12417 		// Update cv_usejoystick.string here so that the user can
12418 		// properly change this value.
12419 		if (i == cv_usejoystick.value)
12420 			CV_SetValue(&cv_usejoystick, i);
12421 		if (i == cv_usejoystick2.value)
12422 			CV_SetValue(&cv_usejoystick2, i);
12423 #endif
12424 	}
12425 
12426 	M_SetupNextMenu(&OP_JoystickSetDef);
12427 }
12428 
12429 static void M_Setup1PJoystickMenu(INT32 choice)
12430 {
12431 	setupcontrols_secondaryplayer = false;
12432 	OP_JoystickSetDef.prevMenu = &OP_Joystick1Def;
12433 	OP_JoystickSetDef.menuid &= ~(((1 << MENUBITS) - 1) << MENUBITS);
12434 	OP_JoystickSetDef.menuid &= ~(((1 << MENUBITS) - 1) << (MENUBITS*2));
12435 	OP_JoystickSetDef.menuid |= MN_OP_P1CONTROLS << MENUBITS;
12436 	OP_JoystickSetDef.menuid |= MN_OP_P1JOYSTICK << (MENUBITS*2);
12437 	M_SetupJoystickMenu(choice);
12438 }
12439 
12440 static void M_Setup2PJoystickMenu(INT32 choice)
12441 {
12442 	setupcontrols_secondaryplayer = true;
12443 	OP_JoystickSetDef.prevMenu = &OP_Joystick2Def;
12444 	OP_JoystickSetDef.menuid &= ~(((1 << MENUBITS) - 1) << MENUBITS);
12445 	OP_JoystickSetDef.menuid &= ~(((1 << MENUBITS) - 1) << (MENUBITS*2));
12446 	OP_JoystickSetDef.menuid |= MN_OP_P2CONTROLS << MENUBITS;
12447 	OP_JoystickSetDef.menuid |= MN_OP_P2JOYSTICK << (MENUBITS*2);
12448 	M_SetupJoystickMenu(choice);
12449 }
12450 
12451 static void M_AssignJoystick(INT32 choice)
12452 {
12453 #ifdef JOYSTICK_HOTPLUG
12454 	INT32 oldchoice, oldstringchoice;
12455 	INT32 numjoys = I_NumJoys();
12456 
12457 	if (setupcontrols_secondaryplayer)
12458 	{
12459 		oldchoice = oldstringchoice = atoi(cv_usejoystick2.string) > numjoys ? atoi(cv_usejoystick2.string) : cv_usejoystick2.value;
12460 		CV_SetValue(&cv_usejoystick2, choice);
12461 
12462 		// Just in case last-minute changes were made to cv_usejoystick.value,
12463 		// update the string too
12464 		// But don't do this if we're intentionally setting higher than numjoys
12465 		if (choice <= numjoys)
12466 		{
12467 			CV_SetValue(&cv_usejoystick2, cv_usejoystick2.value);
12468 
12469 			// reset this so the comparison is valid
12470 			if (oldchoice > numjoys)
12471 				oldchoice = cv_usejoystick2.value;
12472 
12473 			if (oldchoice != choice)
12474 			{
12475 				if (choice && oldstringchoice > numjoys) // if we did not select "None", we likely selected a used device
12476 					CV_SetValue(&cv_usejoystick2, (oldstringchoice > numjoys ? oldstringchoice : oldchoice));
12477 
12478 				if (oldstringchoice ==
12479 					(atoi(cv_usejoystick2.string) > numjoys ? atoi(cv_usejoystick2.string) : cv_usejoystick2.value))
12480 					M_StartMessage("This gamepad is used by another\n"
12481 					               "player. Reset the gamepad\n"
12482 					               "for that player first.\n\n"
12483 					               "(Press a key)\n", NULL, MM_NOTHING);
12484 			}
12485 		}
12486 	}
12487 	else
12488 	{
12489 		oldchoice = oldstringchoice = atoi(cv_usejoystick.string) > numjoys ? atoi(cv_usejoystick.string) : cv_usejoystick.value;
12490 		CV_SetValue(&cv_usejoystick, choice);
12491 
12492 		// Just in case last-minute changes were made to cv_usejoystick.value,
12493 		// update the string too
12494 		// But don't do this if we're intentionally setting higher than numjoys
12495 		if (choice <= numjoys)
12496 		{
12497 			CV_SetValue(&cv_usejoystick, cv_usejoystick.value);
12498 
12499 			// reset this so the comparison is valid
12500 			if (oldchoice > numjoys)
12501 				oldchoice = cv_usejoystick.value;
12502 
12503 			if (oldchoice != choice)
12504 			{
12505 				if (choice && oldstringchoice > numjoys) // if we did not select "None", we likely selected a used device
12506 					CV_SetValue(&cv_usejoystick, (oldstringchoice > numjoys ? oldstringchoice : oldchoice));
12507 
12508 				if (oldstringchoice ==
12509 					(atoi(cv_usejoystick.string) > numjoys ? atoi(cv_usejoystick.string) : cv_usejoystick.value))
12510 					M_StartMessage("This gamepad is used by another\n"
12511 					               "player. Reset the gamepad\n"
12512 					               "for that player first.\n\n"
12513 					               "(Press a key)\n", NULL, MM_NOTHING);
12514 			}
12515 		}
12516 	}
12517 #else
12518 	if (setupcontrols_secondaryplayer)
12519 		CV_SetValue(&cv_usejoystick2, choice);
12520 	else
12521 		CV_SetValue(&cv_usejoystick, choice);
12522 #endif
12523 }
12524 
12525 // =============
12526 // CONTROLS MENU
12527 // =============
12528 
12529 static void M_Setup1PControlsMenu(INT32 choice)
12530 {
12531 	(void)choice;
12532 	setupcontrols_secondaryplayer = false;
12533 	setupcontrols = gamecontrol;        // was called from main Options (for console player, then)
12534 	currentMenu->lastOn = itemOn;
12535 
12536 	// Unhide the nine non-P2 controls and their headers
12537 	//OP_ChangeControlsMenu[18+0].status = IT_HEADER;
12538 	//OP_ChangeControlsMenu[18+1].status = IT_SPACE;
12539 	// ...
12540 	OP_ChangeControlsMenu[18+2].status = IT_CALL|IT_STRING2;
12541 	OP_ChangeControlsMenu[18+3].status = IT_CALL|IT_STRING2;
12542 	OP_ChangeControlsMenu[18+4].status = IT_CALL|IT_STRING2;
12543 	OP_ChangeControlsMenu[18+5].status = IT_CALL|IT_STRING2;
12544 	OP_ChangeControlsMenu[18+6].status = IT_CALL|IT_STRING2;
12545 	//OP_ChangeControlsMenu[18+7].status = IT_CALL|IT_STRING2;
12546 	OP_ChangeControlsMenu[18+8].status = IT_CALL|IT_STRING2;
12547 	// ...
12548 	OP_ChangeControlsMenu[27+0].status = IT_HEADER;
12549 	OP_ChangeControlsMenu[27+1].status = IT_SPACE;
12550 	// ...
12551 	OP_ChangeControlsMenu[27+2].status = IT_CALL|IT_STRING2;
12552 	OP_ChangeControlsMenu[27+3].status = IT_CALL|IT_STRING2;
12553 
12554 	OP_ChangeControlsDef.prevMenu = &OP_P1ControlsDef;
12555 	OP_ChangeControlsDef.menuid &= ~(((1 << MENUBITS) - 1) << MENUBITS); // remove second level
12556 	OP_ChangeControlsDef.menuid |= MN_OP_P1CONTROLS << MENUBITS; // combine second level
12557 	M_SetupNextMenu(&OP_ChangeControlsDef);
12558 }
12559 
12560 static void M_Setup2PControlsMenu(INT32 choice)
12561 {
12562 	(void)choice;
12563 	setupcontrols_secondaryplayer = true;
12564 	setupcontrols = gamecontrolbis;
12565 	currentMenu->lastOn = itemOn;
12566 
12567 	// Hide the nine non-P2 controls and their headers
12568 	//OP_ChangeControlsMenu[18+0].status = IT_GRAYEDOUT2;
12569 	//OP_ChangeControlsMenu[18+1].status = IT_GRAYEDOUT2;
12570 	// ...
12571 	OP_ChangeControlsMenu[18+2].status = IT_GRAYEDOUT2;
12572 	OP_ChangeControlsMenu[18+3].status = IT_GRAYEDOUT2;
12573 	OP_ChangeControlsMenu[18+4].status = IT_GRAYEDOUT2;
12574 	OP_ChangeControlsMenu[18+5].status = IT_GRAYEDOUT2;
12575 	OP_ChangeControlsMenu[18+6].status = IT_GRAYEDOUT2;
12576 	//OP_ChangeControlsMenu[18+7].status = IT_GRAYEDOUT2;
12577 	OP_ChangeControlsMenu[18+8].status = IT_GRAYEDOUT2;
12578 	// ...
12579 	OP_ChangeControlsMenu[27+0].status = IT_GRAYEDOUT2;
12580 	OP_ChangeControlsMenu[27+1].status = IT_GRAYEDOUT2;
12581 	// ...
12582 	OP_ChangeControlsMenu[27+2].status = IT_GRAYEDOUT2;
12583 	OP_ChangeControlsMenu[27+3].status = IT_GRAYEDOUT2;
12584 
12585 	OP_ChangeControlsDef.prevMenu = &OP_P2ControlsDef;
12586 	OP_ChangeControlsDef.menuid &= ~(((1 << MENUBITS) - 1) << MENUBITS); // remove second level
12587 	OP_ChangeControlsDef.menuid |= MN_OP_P2CONTROLS << MENUBITS; // combine second level
12588 	M_SetupNextMenu(&OP_ChangeControlsDef);
12589 }
12590 
12591 #define controlheight 18
12592 
12593 // Draws the Customise Controls menu
12594 static void M_DrawControl(void)
12595 {
12596 	char     tmp[50];
12597 	INT32    x, y, i, max, cursory = 0, iter;
12598 	INT32    keys[2];
12599 
12600 	x = currentMenu->x;
12601 	y = currentMenu->y;
12602 
12603 	/*i = itemOn - (controlheight/2);
12604 	if (i < 0)
12605 		i = 0;
12606 	*/
12607 
12608 	iter = (controlheight/2);
12609 	for (i = itemOn; ((iter || currentMenu->menuitems[i].status == IT_GRAYEDOUT2) && i > 0); i--)
12610 	{
12611 		if (currentMenu->menuitems[i].status != IT_GRAYEDOUT2)
12612 			iter--;
12613 	}
12614 	if (currentMenu->menuitems[i].status == IT_GRAYEDOUT2)
12615 		i--;
12616 
12617 	iter += (controlheight/2);
12618 	for (max = itemOn; (iter && max < currentMenu->numitems); max++)
12619 	{
12620 		if (currentMenu->menuitems[max].status != IT_GRAYEDOUT2)
12621 			iter--;
12622 	}
12623 
12624 	if (iter)
12625 	{
12626 		iter += (controlheight/2);
12627 		for (i = itemOn; ((iter || currentMenu->menuitems[i].status == IT_GRAYEDOUT2) && i > 0); i--)
12628 		{
12629 			if (currentMenu->menuitems[i].status != IT_GRAYEDOUT2)
12630 				iter--;
12631 		}
12632 	}
12633 
12634 	/*max = i + controlheight;
12635 	if (max > currentMenu->numitems)
12636 	{
12637 		max = currentMenu->numitems;
12638 		if (max < controlheight)
12639 			i = 0;
12640 		else
12641 			i = max - controlheight;
12642 	}*/
12643 
12644 	// draw title (or big pic)
12645 	M_DrawMenuTitle();
12646 
12647 	if (tutorialmode && tutorialgcs)
12648 	{
12649 		if ((gametic / TICRATE) % 2)
12650 			M_CentreText(30, "\202EXIT THE TUTORIAL TO CHANGE THE CONTROLS");
12651 		else
12652 			M_CentreText(30, "EXIT THE TUTORIAL TO CHANGE THE CONTROLS");
12653 	}
12654 	else
12655 		M_CentreText(30,
12656 		    (setupcontrols_secondaryplayer ? "SET CONTROLS FOR SECONDARY PLAYER" :
12657 		                                     "PRESS ENTER TO CHANGE, BACKSPACE TO CLEAR"));
12658 
12659 	if (i)
12660 		V_DrawString(currentMenu->x - 16, y-(skullAnimCounter/5), V_YELLOWMAP, "\x1A"); // up arrow
12661 	if (max != currentMenu->numitems)
12662 		V_DrawString(currentMenu->x - 16, y+(SMALLLINEHEIGHT*(controlheight-1))+(skullAnimCounter/5), V_YELLOWMAP, "\x1B"); // down arrow
12663 
12664 	for (; i < max; i++)
12665 	{
12666 		if (currentMenu->menuitems[i].status == IT_GRAYEDOUT2)
12667 			continue;
12668 
12669 		if (i == itemOn)
12670 			cursory = y;
12671 
12672 		if (currentMenu->menuitems[i].status == IT_CONTROL)
12673 		{
12674 			V_DrawString(x, y, ((i == itemOn) ? V_YELLOWMAP : 0), currentMenu->menuitems[i].text);
12675 			keys[0] = setupcontrols[currentMenu->menuitems[i].alphaKey][0];
12676 			keys[1] = setupcontrols[currentMenu->menuitems[i].alphaKey][1];
12677 
12678 			tmp[0] ='\0';
12679 			if (keys[0] == KEY_NULL && keys[1] == KEY_NULL)
12680 			{
12681 				strcpy(tmp, "---");
12682 			}
12683 			else
12684 			{
12685 				if (keys[0] != KEY_NULL)
12686 					strcat (tmp, G_KeynumToString (keys[0]));
12687 
12688 				if (keys[0] != KEY_NULL && keys[1] != KEY_NULL)
12689 					strcat(tmp," or ");
12690 
12691 				if (keys[1] != KEY_NULL)
12692 					strcat (tmp, G_KeynumToString (keys[1]));
12693 
12694 
12695 			}
12696 			V_DrawRightAlignedString(BASEVIDWIDTH-currentMenu->x, y, V_YELLOWMAP, tmp);
12697 		}
12698 		/*else if (currentMenu->menuitems[i].status == IT_GRAYEDOUT2)
12699 			V_DrawString(x, y, V_TRANSLUCENT, currentMenu->menuitems[i].text);*/
12700 		else if ((currentMenu->menuitems[i].status == IT_HEADER) && (i != max-1))
12701 			M_DrawLevelPlatterHeader(y, currentMenu->menuitems[i].text, true, false);
12702 
12703 		y += SMALLLINEHEIGHT;
12704 	}
12705 
12706 	V_DrawScaledPatch(currentMenu->x - 20, cursory, 0,
12707 		W_CachePatchName("M_CURSOR", PU_PATCH));
12708 }
12709 
12710 #undef controlbuffer
12711 
12712 static INT32 controltochange;
12713 static char controltochangetext[33];
12714 
12715 static void M_ChangecontrolResponse(event_t *ev)
12716 {
12717 	INT32        control;
12718 	INT32        found;
12719 	INT32        ch = ev->data1;
12720 
12721 	// ESCAPE cancels; dummy out PAUSE
12722 	if (ch != KEY_ESCAPE && ch != KEY_PAUSE)
12723 	{
12724 
12725 		switch (ev->type)
12726 		{
12727 			// ignore mouse/joy movements, just get buttons
12728 			case ev_mouse:
12729 			case ev_mouse2:
12730 			case ev_joystick:
12731 			case ev_joystick2:
12732 				ch = KEY_NULL;      // no key
12733 			break;
12734 
12735 			// keypad arrows are converted for the menu in cursor arrows
12736 			// so use the event instead of ch
12737 			case ev_keydown:
12738 				ch = ev->data1;
12739 			break;
12740 
12741 			default:
12742 			break;
12743 		}
12744 
12745 		control = controltochange;
12746 
12747 		// check if we already entered this key
12748 		found = -1;
12749 		if (setupcontrols[control][0] ==ch)
12750 			found = 0;
12751 		else if (setupcontrols[control][1] ==ch)
12752 			found = 1;
12753 		if (found >= 0)
12754 		{
12755 			// replace mouse and joy clicks by double clicks
12756 			if (ch >= KEY_MOUSE1 && ch <= KEY_MOUSE1+MOUSEBUTTONS)
12757 				setupcontrols[control][found] = ch-KEY_MOUSE1+KEY_DBLMOUSE1;
12758 			else if (ch >= KEY_JOY1 && ch <= KEY_JOY1+JOYBUTTONS)
12759 				setupcontrols[control][found] = ch-KEY_JOY1+KEY_DBLJOY1;
12760 			else if (ch >= KEY_2MOUSE1 && ch <= KEY_2MOUSE1+MOUSEBUTTONS)
12761 				setupcontrols[control][found] = ch-KEY_2MOUSE1+KEY_DBL2MOUSE1;
12762 			else if (ch >= KEY_2JOY1 && ch <= KEY_2JOY1+JOYBUTTONS)
12763 				setupcontrols[control][found] = ch-KEY_2JOY1+KEY_DBL2JOY1;
12764 		}
12765 		else
12766 		{
12767 			// check if change key1 or key2, or replace the two by the new
12768 			found = 0;
12769 			if (setupcontrols[control][0] == KEY_NULL)
12770 				found++;
12771 			if (setupcontrols[control][1] == KEY_NULL)
12772 				found++;
12773 			if (found == 2)
12774 			{
12775 				found = 0;
12776 				setupcontrols[control][1] = KEY_NULL;  //replace key 1,clear key2
12777 			}
12778 			(void)G_CheckDoubleUsage(ch, true);
12779 			setupcontrols[control][found] = ch;
12780 		}
12781 		S_StartSound(NULL, sfx_strpst);
12782 	}
12783 	else if (ch == KEY_PAUSE)
12784 	{
12785 		// This buffer assumes a 125-character message plus a 32-character control name (per controltochangetext buffer size)
12786 		static char tmp[158];
12787 		menu_t *prev = currentMenu->prevMenu;
12788 
12789 		if (controltochange == gc_pause)
12790 			sprintf(tmp, M_GetText("The \x82Pause Key \x80is enabled, but \nit cannot be used to retry runs \nduring Record Attack. \n\nHit another key for\n%s\nESC for Cancel"),
12791 				controltochangetext);
12792 		else
12793 			sprintf(tmp, M_GetText("The \x82Pause Key \x80is enabled, but \nit is not configurable. \n\nHit another key for\n%s\nESC for Cancel"),
12794 				controltochangetext);
12795 
12796 		M_StartMessage(tmp, M_ChangecontrolResponse, MM_EVENTHANDLER);
12797 		currentMenu->prevMenu = prev;
12798 
12799 		S_StartSound(NULL, sfx_s3k42);
12800 		return;
12801 	}
12802 	else
12803 		S_StartSound(NULL, sfx_skid);
12804 
12805 	M_StopMessage(0);
12806 }
12807 
12808 static void M_ChangeControl(INT32 choice)
12809 {
12810 	// This buffer assumes a 35-character message (per below) plus a max control name limit of 32 chars (per controltochangetext)
12811 	// If you change the below message, then change the size of this buffer!
12812 	static char tmp[68];
12813 
12814 	if (tutorialmode && tutorialgcs) // don't allow control changes if temp control override is active
12815 		return;
12816 
12817 	controltochange = currentMenu->menuitems[choice].alphaKey;
12818 	sprintf(tmp, M_GetText("Hit the new key for\n%s\nESC for Cancel"),
12819 		currentMenu->menuitems[choice].text);
12820 	strlcpy(controltochangetext, currentMenu->menuitems[choice].text, 33);
12821 
12822 	M_StartMessage(tmp, M_ChangecontrolResponse, MM_EVENTHANDLER);
12823 }
12824 
12825 static void M_Setup1PPlaystyleMenu(INT32 choice)
12826 {
12827 	(void)choice;
12828 
12829 	playstyle_activeplayer = 0;
12830 	OP_PlaystyleDef.prevMenu = &OP_P1ControlsDef;
12831 	M_SetupNextMenu(&OP_PlaystyleDef);
12832 }
12833 
12834 static void M_Setup2PPlaystyleMenu(INT32 choice)
12835 {
12836 	(void)choice;
12837 
12838 	playstyle_activeplayer = 1;
12839 	OP_PlaystyleDef.prevMenu = &OP_P2ControlsDef;
12840 	M_SetupNextMenu(&OP_PlaystyleDef);
12841 }
12842 
12843 static void M_DrawPlaystyleMenu(void)
12844 {
12845 	size_t i;
12846 
12847 	for (i = 0; i < 4; i++)
12848 	{
12849 		if (i != 3)
12850 			V_DrawCenteredString((i+1)*BASEVIDWIDTH/4, 20, (i == playstyle_currentchoice) ? V_YELLOWMAP : 0, PlaystyleNames[i]);
12851 
12852 		if (i == playstyle_currentchoice)
12853 		{
12854 			V_DrawScaledPatch((i+1)*BASEVIDWIDTH/4 - 8, 10, 0, W_CachePatchName("M_CURSOR", PU_CACHE));
12855 			V_DrawString(30, 50, V_ALLOWLOWERCASE, PlaystyleDesc[i]);
12856 		}
12857 	}
12858 }
12859 
12860 static void M_HandlePlaystyleMenu(INT32 choice)
12861 {
12862 	switch (choice)
12863 	{
12864 	case KEY_ESCAPE:
12865 	case KEY_BACKSPACE:
12866 		M_SetupNextMenu(currentMenu->prevMenu);
12867 		break;
12868 
12869 	case KEY_ENTER:
12870 		S_StartSound(NULL, sfx_menu1);
12871 		CV_SetValue((playstyle_activeplayer ? &cv_directionchar[1] : &cv_directionchar[0]), playstyle_currentchoice ? 1 : 0);
12872 		CV_SetValue((playstyle_activeplayer ? &cv_useranalog[1] : &cv_useranalog[0]), playstyle_currentchoice/2);
12873 
12874 		if (playstyle_activeplayer)
12875 			CV_UpdateCam2Dist();
12876 		else
12877 			CV_UpdateCamDist();
12878 
12879 		M_SetupNextMenu(currentMenu->prevMenu);
12880 		break;
12881 
12882 	case KEY_LEFTARROW:
12883 		S_StartSound(NULL, sfx_menu1);
12884 		playstyle_currentchoice = (playstyle_currentchoice+2)%3;
12885 		break;
12886 
12887 	case KEY_RIGHTARROW:
12888 		S_StartSound(NULL, sfx_menu1);
12889 		playstyle_currentchoice = (playstyle_currentchoice+1)%3;
12890 		break;
12891 	}
12892 }
12893 
12894 static void M_DrawCameraOptionsMenu(void)
12895 {
12896 	M_DrawGenericScrollMenu();
12897 
12898 	if (gamestate == GS_LEVEL && (paused || P_AutoPause()))
12899 	{
12900 		if (currentMenu == &OP_Camera2OptionsDef && splitscreen && camera2.chase)
12901 			P_MoveChaseCamera(&players[secondarydisplayplayer], &camera2, false);
12902 		if (currentMenu == &OP_CameraOptionsDef && camera.chase)
12903 			P_MoveChaseCamera(&players[displayplayer], &camera, false);
12904 	}
12905 }
12906 
12907 // ===============
12908 // VIDEO MODE MENU
12909 // ===============
12910 
12911 //added : 30-01-98:
12912 #define MAXCOLUMNMODES   12     //max modes displayed in one column
12913 #define MAXMODEDESCS     (MAXCOLUMNMODES*3)
12914 
12915 static modedesc_t modedescs[MAXMODEDESCS];
12916 
12917 static void M_VideoModeMenu(INT32 choice)
12918 {
12919 	INT32 i, j, vdup, nummodes, width, height;
12920 	const char *desc;
12921 
12922 	(void)choice;
12923 
12924 	memset(modedescs, 0, sizeof(modedescs));
12925 
12926 #if (defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL)
12927 	VID_PrepareModeList(); // FIXME: hack
12928 #endif
12929 	vidm_nummodes = 0;
12930 	vidm_selected = 0;
12931 	nummodes = VID_NumModes();
12932 
12933 #ifdef _WINDOWS
12934 	// clean that later: skip windowed mode 0, video modes menu only shows FULL SCREEN modes
12935 	if (nummodes <= NUMSPECIALMODES)
12936 		i = 0; // unless we have nothing
12937 	else
12938 		i = NUMSPECIALMODES;
12939 #else
12940 	// DOS does not skip mode 0, because mode 0 is ALWAYS present
12941 	i = 0;
12942 #endif
12943 	for (; i < nummodes && vidm_nummodes < MAXMODEDESCS; i++)
12944 	{
12945 		desc = VID_GetModeName(i);
12946 		if (desc)
12947 		{
12948 			vdup = 0;
12949 
12950 			// when a resolution exists both under VGA and VESA, keep the
12951 			// VESA mode, which is always a higher modenum
12952 			for (j = 0; j < vidm_nummodes; j++)
12953 			{
12954 				if (!strcmp(modedescs[j].desc, desc))
12955 				{
12956 					// mode(0): 320x200 is always standard VGA, not vesa
12957 					if (modedescs[j].modenum)
12958 					{
12959 						modedescs[j].modenum = i;
12960 						vdup = 1;
12961 
12962 						if (i == vid.modenum)
12963 							vidm_selected = j;
12964 					}
12965 					else
12966 						vdup = 1;
12967 
12968 					break;
12969 				}
12970 			}
12971 
12972 			if (!vdup)
12973 			{
12974 				modedescs[vidm_nummodes].modenum = i;
12975 				modedescs[vidm_nummodes].desc = desc;
12976 
12977 				if (i == vid.modenum)
12978 					vidm_selected = vidm_nummodes;
12979 
12980 				// Pull out the width and height
12981 				sscanf(desc, "%u%*c%u", &width, &height);
12982 
12983 				// Show multiples of 320x200 as green.
12984 				if (SCR_IsAspectCorrect(width, height))
12985 					modedescs[vidm_nummodes].goodratio = 1;
12986 
12987 				vidm_nummodes++;
12988 			}
12989 		}
12990 	}
12991 
12992 	vidm_column_size = (vidm_nummodes+2) / 3;
12993 
12994 	M_SetupNextMenu(&OP_VideoModeDef);
12995 }
12996 
12997 static void M_DrawMainVideoMenu(void)
12998 {
12999 	M_DrawGenericScrollMenu();
13000 	if (itemOn < 8) // where it starts to go offscreen; change this number if you change the layout of the video menu
13001 	{
13002 		INT32 y = currentMenu->y+currentMenu->menuitems[1].alphaKey*2;
13003 		if (itemOn == 7)
13004 			y -= 10;
13005 		V_DrawRightAlignedString(BASEVIDWIDTH - currentMenu->x, y,
13006 		(SCR_IsAspectCorrect(vid.width, vid.height) ? V_GREENMAP : V_YELLOWMAP),
13007 			va("%dx%d", vid.width, vid.height));
13008 	}
13009 }
13010 
13011 // Draw the video modes list, a-la-Quake
13012 static void M_DrawVideoMode(void)
13013 {
13014 	INT32 i, j, row, col;
13015 
13016 	// draw title
13017 	M_DrawMenuTitle();
13018 
13019 	V_DrawCenteredString(BASEVIDWIDTH/2, OP_VideoModeDef.y,
13020 		V_YELLOWMAP, "Choose mode, reselect to change default");
13021 
13022 	row = 41;
13023 	col = OP_VideoModeDef.y + 14;
13024 	for (i = 0; i < vidm_nummodes; i++)
13025 	{
13026 		if (i == vidm_selected)
13027 			V_DrawString(row, col, V_YELLOWMAP, modedescs[i].desc);
13028 		// Show multiples of 320x200 as green.
13029 		else
13030 			V_DrawString(row, col, (modedescs[i].goodratio) ? V_GREENMAP : 0, modedescs[i].desc);
13031 
13032 		col += 8;
13033 		if ((i % vidm_column_size) == (vidm_column_size-1))
13034 		{
13035 			row += 7*13;
13036 			col = OP_VideoModeDef.y + 14;
13037 		}
13038 	}
13039 
13040 	if (vidm_testingmode > 0)
13041 	{
13042 		INT32 testtime = (vidm_testingmode/TICRATE) + 1;
13043 
13044 		M_CentreText(OP_VideoModeDef.y + 116,
13045 			va("Previewing mode %c%dx%d",
13046 				(SCR_IsAspectCorrect(vid.width, vid.height)) ? 0x83 : 0x80,
13047 				vid.width, vid.height));
13048 		M_CentreText(OP_VideoModeDef.y + 138,
13049 			"Press ENTER again to keep this mode");
13050 		M_CentreText(OP_VideoModeDef.y + 150,
13051 			va("Wait %d second%s", testtime, (testtime > 1) ? "s" : ""));
13052 		M_CentreText(OP_VideoModeDef.y + 158,
13053 			"or press ESC to return");
13054 
13055 	}
13056 	else
13057 	{
13058 		M_CentreText(OP_VideoModeDef.y + 116,
13059 			va("Current mode is %c%dx%d",
13060 				(SCR_IsAspectCorrect(vid.width, vid.height)) ? 0x83 : 0x80,
13061 				vid.width, vid.height));
13062 		M_CentreText(OP_VideoModeDef.y + 124,
13063 			va("Default mode is %c%dx%d",
13064 				(SCR_IsAspectCorrect(cv_scr_width.value, cv_scr_height.value)) ? 0x83 : 0x80,
13065 				cv_scr_width.value, cv_scr_height.value));
13066 
13067 		V_DrawCenteredString(BASEVIDWIDTH/2, OP_VideoModeDef.y + 138,
13068 			V_GREENMAP, "Green modes are recommended.");
13069 		V_DrawCenteredString(BASEVIDWIDTH/2, OP_VideoModeDef.y + 146,
13070 			V_YELLOWMAP, "Other modes may have visual errors.");
13071 		V_DrawCenteredString(BASEVIDWIDTH/2, OP_VideoModeDef.y + 158,
13072 			V_YELLOWMAP, "Larger modes may have performance issues.");
13073 	}
13074 
13075 	// Draw the cursor for the VidMode menu
13076 	i = 41 - 10 + ((vidm_selected / vidm_column_size)*7*13);
13077 	j = OP_VideoModeDef.y + 14 + ((vidm_selected % vidm_column_size)*8);
13078 
13079 	V_DrawScaledPatch(i - 8, j, 0,
13080 		W_CachePatchName("M_CURSOR", PU_PATCH));
13081 }
13082 
13083 // Just M_DrawGenericScrollMenu but showing a backing behind the headers.
13084 static void M_DrawColorMenu(void)
13085 {
13086 	INT32 x, y, i, max, tempcentery, cursory = 0;
13087 
13088 	// DRAW MENU
13089 	x = currentMenu->x;
13090 	y = currentMenu->y;
13091 
13092 	V_DrawFill(19       , y-4, 47, 1,  35);
13093 	V_DrawFill(19+(  47), y-4, 47, 1,  73);
13094 	V_DrawFill(19+(2*47), y-4, 47, 1, 112);
13095 	V_DrawFill(19+(3*47), y-4, 47, 1, 255);
13096 	V_DrawFill(19+(4*47), y-4, 47, 1, 152);
13097 	V_DrawFill(19+(5*47), y-4, 46, 1, 181);
13098 
13099 	V_DrawFill(300, y-4, 1, 1, 26);
13100 	V_DrawFill( 19, y-3, 282, 1, 26);
13101 
13102 	if ((currentMenu->menuitems[itemOn].alphaKey*2 - currentMenu->menuitems[0].alphaKey*2) <= scrollareaheight)
13103 		tempcentery = currentMenu->y - currentMenu->menuitems[0].alphaKey*2;
13104 	else if ((currentMenu->menuitems[currentMenu->numitems-1].alphaKey*2 - currentMenu->menuitems[itemOn].alphaKey*2) <= scrollareaheight)
13105 		tempcentery = currentMenu->y - currentMenu->menuitems[currentMenu->numitems-1].alphaKey*2 + 2*scrollareaheight;
13106 	else
13107 		tempcentery = currentMenu->y - currentMenu->menuitems[itemOn].alphaKey*2 + scrollareaheight;
13108 
13109 	for (i = 0; i < currentMenu->numitems; i++)
13110 	{
13111 		if (currentMenu->menuitems[i].status != IT_DISABLED && currentMenu->menuitems[i].alphaKey*2 + tempcentery >= currentMenu->y)
13112 			break;
13113 	}
13114 
13115 	for (max = currentMenu->numitems; max > 0; max--)
13116 	{
13117 		if (currentMenu->menuitems[max].status != IT_DISABLED && currentMenu->menuitems[max-1].alphaKey*2 + tempcentery <= (currentMenu->y + 2*scrollareaheight))
13118 			break;
13119 	}
13120 
13121 	if (i)
13122 		V_DrawString(currentMenu->x - 20, currentMenu->y - (skullAnimCounter/5), V_YELLOWMAP, "\x1A"); // up arrow
13123 	if (max != currentMenu->numitems)
13124 		V_DrawString(currentMenu->x - 20, currentMenu->y + 2*scrollareaheight + (skullAnimCounter/5), V_YELLOWMAP, "\x1B"); // down arrow
13125 
13126 	// draw title (or big pic)
13127 	M_DrawMenuTitle();
13128 
13129 	for (; i < max; i++)
13130 	{
13131 		y = currentMenu->menuitems[i].alphaKey*2 + tempcentery;
13132 		if (i == itemOn)
13133 			cursory = y;
13134 		switch (currentMenu->menuitems[i].status & IT_DISPLAY)
13135 		{
13136 			case IT_PATCH:
13137 			case IT_DYBIGSPACE:
13138 			case IT_BIGSLIDER:
13139 			case IT_STRING2:
13140 			case IT_DYLITLSPACE:
13141 			case IT_GRAYPATCH:
13142 			case IT_TRANSTEXT2:
13143 				// unsupported
13144 				break;
13145 			case IT_NOTHING:
13146 				break;
13147 			case IT_STRING:
13148 			case IT_WHITESTRING:
13149 				if (i != itemOn && (currentMenu->menuitems[i].status & IT_DISPLAY)==IT_STRING)
13150 					V_DrawString(x, y, 0, currentMenu->menuitems[i].text);
13151 				else
13152 					V_DrawString(x, y, V_YELLOWMAP, currentMenu->menuitems[i].text);
13153 
13154 				// Cvar specific handling
13155 				switch (currentMenu->menuitems[i].status & IT_TYPE)
13156 					case IT_CVAR:
13157 					{
13158 						consvar_t *cv = (consvar_t *)currentMenu->menuitems[i].itemaction;
13159 						switch (currentMenu->menuitems[i].status & IT_CVARTYPE)
13160 						{
13161 							case IT_CV_SLIDER:
13162 								M_DrawSlider(x, y, cv, (i == itemOn));
13163 							case IT_CV_NOPRINT: // color use this
13164 							case IT_CV_INVISSLIDER: // monitor toggles use this
13165 								break;
13166 							case IT_CV_STRING:
13167 								if (y + 12 > (currentMenu->y + 2*scrollareaheight))
13168 									break;
13169 								M_DrawTextBox(x, y + 4, MAXSTRINGLENGTH, 1);
13170 								V_DrawString(x + 8, y + 12, V_ALLOWLOWERCASE, cv->string);
13171 								if (skullAnimCounter < 4 && i == itemOn)
13172 									V_DrawCharacter(x + 8 + V_StringWidth(cv->string, 0), y + 12,
13173 										'_' | 0x80, false);
13174 								y += 16;
13175 								break;
13176 							default:
13177 								V_DrawRightAlignedString(BASEVIDWIDTH - x, y,
13178 									((cv->flags & CV_CHEAT) && !CV_IsSetToDefault(cv) ? V_REDMAP : V_YELLOWMAP), cv->string);
13179 								if (i == itemOn)
13180 								{
13181 									V_DrawCharacter(BASEVIDWIDTH - x - 10 - V_StringWidth(cv->string, 0) - (skullAnimCounter/5), y,
13182 											'\x1C' | V_YELLOWMAP, false);
13183 									V_DrawCharacter(BASEVIDWIDTH - x + 2 + (skullAnimCounter/5), y,
13184 											'\x1D' | V_YELLOWMAP, false);
13185 								}
13186 								break;
13187 						}
13188 						break;
13189 					}
13190 					break;
13191 			case IT_TRANSTEXT:
13192 				V_DrawString(x, y, V_TRANSLUCENT, currentMenu->menuitems[i].text);
13193 				break;
13194 			case IT_QUESTIONMARKS:
13195 				V_DrawString(x, y, V_TRANSLUCENT|V_OLDSPACING, M_CreateSecretMenuOption(currentMenu->menuitems[i].text));
13196 				break;
13197 			case IT_HEADERTEXT:
13198 				//V_DrawString(x-16, y, V_YELLOWMAP, currentMenu->menuitems[i].text);
13199 				V_DrawFill(19, y, 281, 9, currentMenu->menuitems[i+1].alphaKey);
13200 				V_DrawFill(300, y, 1, 9, 26);
13201 				M_DrawLevelPlatterHeader(y - (lsheadingheight - 12), currentMenu->menuitems[i].text, false, false);
13202 				break;
13203 		}
13204 	}
13205 
13206 	// DRAW THE SKULL CURSOR
13207 	V_DrawScaledPatch(currentMenu->x - 24, cursory, 0,
13208 		W_CachePatchName("M_CURSOR", PU_PATCH));
13209 }
13210 
13211 // special menuitem key handler for video mode list
13212 static void M_HandleVideoMode(INT32 ch)
13213 {
13214 	if (vidm_testingmode > 0) switch (ch)
13215 	{
13216 		// change back to the previous mode quickly
13217 		case KEY_ESCAPE:
13218 			setmodeneeded = vidm_previousmode + 1;
13219 			vidm_testingmode = 0;
13220 			break;
13221 
13222 		case KEY_ENTER:
13223 			S_StartSound(NULL, sfx_menu1);
13224 			vidm_testingmode = 0; // stop testing
13225 	}
13226 
13227 	else switch (ch)
13228 	{
13229 		case KEY_DOWNARROW:
13230 			S_StartSound(NULL, sfx_menu1);
13231 			if (++vidm_selected >= vidm_nummodes)
13232 				vidm_selected = 0;
13233 			break;
13234 
13235 		case KEY_UPARROW:
13236 			S_StartSound(NULL, sfx_menu1);
13237 			if (--vidm_selected < 0)
13238 				vidm_selected = vidm_nummodes - 1;
13239 			break;
13240 
13241 		case KEY_LEFTARROW:
13242 			S_StartSound(NULL, sfx_menu1);
13243 			vidm_selected -= vidm_column_size;
13244 			if (vidm_selected < 0)
13245 				vidm_selected = (vidm_column_size*3) + vidm_selected;
13246 			if (vidm_selected >= vidm_nummodes)
13247 				vidm_selected = vidm_nummodes - 1;
13248 			break;
13249 
13250 		case KEY_RIGHTARROW:
13251 			S_StartSound(NULL, sfx_menu1);
13252 			vidm_selected += vidm_column_size;
13253 			if (vidm_selected >= (vidm_column_size*3))
13254 				vidm_selected %= vidm_column_size;
13255 			if (vidm_selected >= vidm_nummodes)
13256 				vidm_selected = vidm_nummodes - 1;
13257 			break;
13258 
13259 		case KEY_ENTER:
13260 			S_StartSound(NULL, sfx_menu1);
13261 			if (vid.modenum == modedescs[vidm_selected].modenum)
13262 				SCR_SetDefaultMode();
13263 			else
13264 			{
13265 				vidm_testingmode = 15*TICRATE;
13266 				vidm_previousmode = vid.modenum;
13267 				if (!setmodeneeded) // in case the previous setmode was not finished
13268 					setmodeneeded = modedescs[vidm_selected].modenum + 1;
13269 			}
13270 			break;
13271 
13272 		case KEY_ESCAPE: // this one same as M_Responder
13273 			if (currentMenu->prevMenu)
13274 				M_SetupNextMenu(currentMenu->prevMenu);
13275 			else
13276 				M_ClearMenus(true);
13277 			break;
13278 
13279 		default:
13280 			break;
13281 	}
13282 }
13283 
13284 static void M_DrawScreenshotMenu(void)
13285 {
13286 	M_DrawGenericScrollMenu();
13287 #ifdef HWRENDER
13288 	if ((rendermode == render_opengl) && (itemOn < 7)) // where it starts to go offscreen; change this number if you change the layout of the screenshot menu
13289 	{
13290 		INT32 y = currentMenu->y+currentMenu->menuitems[op_screenshot_colorprofile].alphaKey*2;
13291 		if (itemOn == 6)
13292 			y -= 10;
13293 		V_DrawRightAlignedString(BASEVIDWIDTH - currentMenu->x, y, V_REDMAP, "Yes");
13294 	}
13295 #endif
13296 }
13297 
13298 // ===============
13299 // Monitor Toggles
13300 // ===============
13301 static void M_DrawMonitorToggles(void)
13302 {
13303 	INT32 i, y;
13304 	INT32 sum = 0;
13305 	consvar_t *cv;
13306 	boolean cheating = false;
13307 
13308 	M_DrawGenericMenu();
13309 
13310 	// Assumes all are cvar type.
13311 	for (i = 0; i < currentMenu->numitems; ++i)
13312 	{
13313 		if (!(currentMenu->menuitems[i].status & IT_CVAR) || !(cv = (consvar_t *)currentMenu->menuitems[i].itemaction))
13314 			continue;
13315 		sum += cv->value;
13316 
13317 		if (!CV_IsSetToDefault(cv))
13318 			cheating = true;
13319 	}
13320 
13321 	for (i = 0; i < currentMenu->numitems; ++i)
13322 	{
13323 		if (!(currentMenu->menuitems[i].status & IT_CVAR) || !(cv = (consvar_t *)currentMenu->menuitems[i].itemaction))
13324 			continue;
13325 		y = currentMenu->y + currentMenu->menuitems[i].alphaKey;
13326 
13327 		M_DrawSlider(currentMenu->x + 20, y, cv, (i == itemOn));
13328 
13329 		if (!cv->value)
13330 			V_DrawRightAlignedString(312, y, V_OLDSPACING|((i == itemOn) ? V_YELLOWMAP : 0), "None");
13331 		else
13332 			V_DrawRightAlignedString(312, y, V_OLDSPACING|((i == itemOn) ? V_YELLOWMAP : 0), va("%3d%%", (cv->value*100)/sum));
13333 	}
13334 
13335 	if (cheating)
13336 		V_DrawCenteredString(BASEVIDWIDTH/2, currentMenu->y, V_REDMAP, "* MODIFIED, CHEATS ENABLED *");
13337 }
13338 
13339 // =========
13340 // Quit Game
13341 // =========
13342 static INT32 quitsounds[] =
13343 {
13344 	// holy shit we're changing things up!
13345 	sfx_itemup, // Tails 11-09-99
13346 	sfx_jump, // Tails 11-09-99
13347 	sfx_skid, // Inu 04-03-13
13348 	sfx_spring, // Tails 11-09-99
13349 	sfx_pop,
13350 	sfx_spdpad, // Inu 04-03-13
13351 	sfx_wdjump, // Inu 04-03-13
13352 	sfx_mswarp, // Inu 04-03-13
13353 	sfx_splash, // Tails 11-09-99
13354 	sfx_floush, // Tails 11-09-99
13355 	sfx_gloop, // Tails 11-09-99
13356 	sfx_s3k66, // Inu 04-03-13
13357 	sfx_s3k6a, // Inu 04-03-13
13358 	sfx_s3k73, // Inu 04-03-13
13359 	sfx_chchng // Tails 11-09-99
13360 };
13361 
13362 void M_QuitResponse(INT32 ch)
13363 {
13364 	tic_t ptime;
13365 	INT32 mrand;
13366 
13367 	if (ch != 'y' && ch != KEY_ENTER)
13368 		return;
13369 	LUAh_GameQuit(true);
13370 	if (!(netgame || cv_debug))
13371 	{
13372 		S_ResetCaptions();
13373 		marathonmode = 0;
13374 
13375 		mrand = M_RandomKey(sizeof(quitsounds)/sizeof(INT32));
13376 		if (quitsounds[mrand]) S_StartSound(NULL, quitsounds[mrand]);
13377 
13378 		//added : 12-02-98: do that instead of I_WaitVbl which does not work
13379 		ptime = I_GetTime() + NEWTICRATE*2; // Shortened the quit time, used to be 2 seconds Tails 03-26-2001
13380 		while (ptime > I_GetTime())
13381 		{
13382 			V_DrawScaledPatch(0, 0, 0, W_CachePatchName("GAMEQUIT", PU_PATCH)); // Demo 3 Quit Screen Tails 06-16-2001
13383 			I_FinishUpdate(); // Update the screen with the image Tails 06-19-2001
13384 			I_Sleep();
13385 		}
13386 	}
13387 	I_Quit();
13388 }
13389 
13390 static void M_QuitSRB2(INT32 choice)
13391 {
13392 	// We pick index 0 which is language sensitive, or one at random,
13393 	// between 1 and maximum number.
13394 	(void)choice;
13395 	M_StartMessage(quitmsg[M_RandomKey(NUM_QUITMESSAGES)], M_QuitResponse, MM_YESNO);
13396 }
13397