diff --git a/src/m_menu.c b/src/m_menu.c new file mode 100644 index 000000000..3a5be46a9 --- /dev/null +++ b/src/m_menu.c @@ -0,0 +1,11579 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 1993-1996 by id Software, Inc. +// Copyright (C) 1998-2000 by DooM Legacy Team. +// Copyright (C) 2011-2016 by Matthew "Inuyasha" Walsh. +// Copyright (C) 1999-2018 by Sonic Team Junior. +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- +/// \file m_menu.c +/// \brief XMOD's extremely revamped menu system. + +#ifdef __GNUC__ +#include +#endif + +#include "m_menu.h" + +#include "doomdef.h" +#include "d_main.h" +#include "d_netcmd.h" +#include "console.h" +#include "r_local.h" +#include "hu_stuff.h" +#include "g_game.h" +#include "g_input.h" +#include "m_argv.h" + +// Data. +#include "sounds.h" +#include "s_sound.h" +#include "i_system.h" +#include "i_threads.h" + +// Addfile +#include "filesrch.h" + +#include "v_video.h" +#include "i_video.h" +#include "keys.h" +#include "z_zone.h" +#include "w_wad.h" +#include "p_local.h" +#include "p_setup.h" +#include "f_finale.h" + +#ifdef HWRENDER +#include "hardware/hw_main.h" +#endif + +#include "d_net.h" +#include "mserv.h" +#include "m_misc.h" +#include "m_anigif.h" +#include "byteptr.h" +#include "st_stuff.h" +#include "i_sound.h" +#include "k_hud.h" // SRB2kart +#include "k_kart.h" // KartItemCVars +#include "k_pwrlv.h" +#include "d_player.h" // KITEM_ constants +#include "k_color.h" +#include "k_grandprix.h" + +#include "i_joy.h" // for joystick menu controls + +// Condition Sets +#include "m_cond.h" + +// And just some randomness for the exits. +#include "m_random.h" + +#if defined(HAVE_SDL) +#include "SDL.h" +#if SDL_VERSION_ATLEAST(2,0,0) +#include "sdl/sdlmain.h" // JOYSTICK_HOTPLUG +#endif +#endif + +#ifdef PC_DOS +#include // for snprintf +int snprintf(char *str, size_t n, const char *fmt, ...); +//int vsnprintf(char *str, size_t n, const char *fmt, va_list ap); +#endif + +#ifdef HAVE_DISCORDRPC +//#include "discord_rpc.h" +#include "discord.h" +#endif + +#define SKULLXOFF -32 +#define LINEHEIGHT 16 +#define STRINGHEIGHT 8 +#define FONTBHEIGHT 20 +#define SMALLLINEHEIGHT 8 +#define SLIDER_RANGE 10 +#define SLIDER_WIDTH (8*SLIDER_RANGE+6) +#define SERVERS_PER_PAGE 11 + +#if defined (NONET) || defined (TESTERS) +#define NOMENUHOST +#endif + +typedef enum +{ + QUITMSG = 0, + QUITMSG1, + QUITMSG2, + QUITMSG3, + QUITMSG4, + QUITMSG5, + QUITMSG6, + QUITMSG7, + + QUIT2MSG, + QUIT2MSG1, + QUIT2MSG2, + QUIT2MSG3, + QUIT2MSG4, + QUIT2MSG5, + QUIT2MSG6, + + QUIT3MSG, + QUIT3MSG1, + QUIT3MSG2, + QUIT3MSG3, + QUIT3MSG4, + QUIT3MSG5, + QUIT3MSG6, + NUM_QUITMESSAGES +} text_enum; + +#ifdef HAVE_THREADS +I_mutex m_menu_mutex; +#endif + +M_waiting_mode_t m_waiting_mode = M_NOT_WAITING; + +const char *quitmsg[NUM_QUITMESSAGES]; + +// Stuff for customizing the player select screen Tails 09-22-2003 +description_t description[MAXSKINS]; + +//static char *char_notes = NULL; +//static fixed_t char_scroll = 0; + +boolean menuactive = false; +boolean fromlevelselect = false; + +typedef enum +{ + LLM_CREATESERVER, + LLM_LEVELSELECT, + LLM_TIMEATTACK, + LLM_BREAKTHECAPSULES +} levellist_mode_t; + +levellist_mode_t levellistmode = LLM_CREATESERVER; +UINT8 maplistoption = 0; + +static char joystickInfo[8][29]; +#ifndef NONET +static UINT32 serverlistpage; +#endif + +//static saveinfo_t savegameinfo[MAXSAVEGAMES]; // Extra info about the save games. + +INT16 startmap; // Mario, NiGHTS, or just a plain old normal game? + +static INT16 itemOn = 1; // menu item skull is on, Hack by Tails 09-18-2002 +static INT16 skullAnimCounter = 10; // skull animation counter +static tic_t followertimer = 0; // Used for smooth follower floating + +static UINT8 setupcontrolplayer; +static INT32 (*setupcontrols)[2]; // pointer to the gamecontrols of the player being edited + +// shhh... what am I doing... nooooo! +static INT32 vidm_testingmode = 0; +static INT32 vidm_previousmode; +static INT32 vidm_selected = 0; +static INT32 vidm_nummodes; +static INT32 vidm_column_size; + +// +// PROTOTYPES +// + +static void M_StopMessage(INT32 choice); + +#ifndef NONET +static void M_HandleServerPage(INT32 choice); +#endif + +// Prototyping is fun, innit? +// ========================================================================== +// NEEDED FUNCTION PROTOTYPES GO HERE +// ========================================================================== + +void M_SetWaitingMode(int mode); +int M_GetWaitingMode(void); + +// the haxor message menu +menu_t MessageDef; + +#ifdef HAVE_DISCORDRPC +menu_t MISC_DiscordRequestsDef; +static void M_HandleDiscordRequests(INT32 choice); +static void M_DrawDiscordRequests(void); +#endif + +menu_t SPauseDef; + +#define lsheadingheight 16 + +// Sky Room +//static void M_CustomLevelSelect(INT32 choice); +//static void M_CustomWarp(INT32 choice); +FUNCNORETURN static ATTRNORETURN void M_UltimateCheat(INT32 choice); +//static void M_LoadGameLevelSelect(INT32 choice); +static void M_GetAllEmeralds(INT32 choice); +static void M_DestroyRobots(INT32 choice); +//static void M_LevelSelectWarp(INT32 choice); +static void M_Credits(INT32 choice); +static void M_PandorasBox(INT32 choice); +static void M_EmblemHints(INT32 choice); +static char *M_GetConditionString(condition_t cond); +menu_t SR_MainDef, SR_UnlockChecklistDef; + +// Misc. Main Menu +#ifndef TESTERS +static void M_SinglePlayerMenu(INT32 choice); +#endif +static void M_Options(INT32 choice); +static void M_Manual(INT32 choice); +static void M_SelectableClearMenus(INT32 choice); +static void M_Retry(INT32 choice); +static void M_EndGame(INT32 choice); +static void M_MapChange(INT32 choice); +static void M_ChangeLevel(INT32 choice); +static void M_ConfirmSpectate(INT32 choice); +static void M_ConfirmEnterGame(INT32 choice); +static void M_ConfirmTeamScramble(INT32 choice); +static void M_ConfirmTeamChange(INT32 choice); +static void M_ConfirmSpectateChange(INT32 choice); +//static void M_SecretsMenu(INT32 choice); +//static void M_SetupChoosePlayer(INT32 choice); +static void M_QuitSRB2(INT32 choice); +menu_t SP_MainDef, MP_MainDef, OP_MainDef; +menu_t MISC_ScrambleTeamDef, MISC_ChangeTeamDef, MISC_ChangeSpectateDef; + +// Single Player +static void M_GrandPrixTemp(INT32 choice); +static void M_StartGrandPrix(INT32 choice); +static void M_TimeAttack(INT32 choice); +static boolean M_QuitTimeAttackMenu(void); +static void M_BreakTheCapsules(INT32 choice); +static void M_Statistics(INT32 choice); +static void M_HandleStaffReplay(INT32 choice); +static void M_ReplayTimeAttack(INT32 choice); +static void M_ChooseTimeAttack(INT32 choice); +//static void M_ChooseNightsAttack(INT32 choice); +static void M_ModeAttackEndGame(INT32 choice); +static void M_SetGuestReplay(INT32 choice); +//static void M_ChoosePlayer(INT32 choice); +menu_t SP_LevelStatsDef; +static menu_t SP_GrandPrixTempDef; +static menu_t SP_TimeAttackDef, SP_ReplayDef, SP_GuestReplayDef, SP_GhostDef; +//static menu_t SP_NightsAttackDef, SP_NightsReplayDef, SP_NightsGuestReplayDef, SP_NightsGhostDef; + +// Multiplayer +#ifndef NONET +#ifndef TESTERS +static void M_StartServerMenu(INT32 choice); +#endif +static void M_ConnectMenu(INT32 choice); +static void M_ConnectMenuModChecks(INT32 choice); +static void M_Refresh(INT32 choice); +static void M_Connect(INT32 choice); +#endif +#ifndef TESTERS +static void M_StartOfflineServerMenu(INT32 choice); +#endif +static void M_StartServer(INT32 choice); +static void M_SetupMultiPlayer(INT32 choice); +static void M_SetupMultiPlayer2(INT32 choice); +static void M_SetupMultiPlayer3(INT32 choice); +static void M_SetupMultiPlayer4(INT32 choice); +static void M_SetupMultiHandler(INT32 choice); + +// Options +// Split into multiple parts due to size +// Controls +menu_t OP_ControlsDef, OP_AllControlsDef; +menu_t OP_MouseOptionsDef; +menu_t OP_Joystick1Def, OP_Joystick2Def, OP_Joystick3Def, OP_Joystick4Def; +static void M_VideoModeMenu(INT32 choice); +static void M_Setup1PControlsMenu(INT32 choice); +static void M_Setup2PControlsMenu(INT32 choice); +static void M_Setup3PControlsMenu(INT32 choice); +static void M_Setup4PControlsMenu(INT32 choice); + +static void M_Setup1PJoystickMenu(INT32 choice); +static void M_Setup2PJoystickMenu(INT32 choice); +static void M_Setup3PJoystickMenu(INT32 choice); +static void M_Setup4PJoystickMenu(INT32 choice); + +static void M_AssignJoystick(INT32 choice); +static void M_ChangeControl(INT32 choice); +static void M_ResetControls(INT32 choice); + +// Video & Sound +menu_t OP_VideoOptionsDef, OP_VideoModeDef; +#ifdef HWRENDER +static void M_OpenGLOptionsMenu(void); +menu_t OP_OpenGLOptionsDef; +#endif +menu_t OP_SoundOptionsDef; +//static void M_RestartAudio(void); + +//Misc +menu_t OP_DataOptionsDef, OP_ScreenshotOptionsDef, OP_EraseDataDef; +#ifdef HAVE_DISCORDRPC +menu_t OP_DiscordOptionsDef; +#endif +menu_t OP_HUDOptionsDef, OP_ChatOptionsDef; +menu_t OP_GameOptionsDef, OP_ServerOptionsDef; +#ifndef NONET +menu_t OP_AdvServerOptionsDef; +#endif +//menu_t OP_NetgameOptionsDef, OP_GametypeOptionsDef; +menu_t OP_MonitorToggleDef; +static void M_ScreenshotOptions(INT32 choice); +static void M_EraseData(INT32 choice); + +static void M_Addons(INT32 choice); +static void M_AddonsOptions(INT32 choice); +static patch_t *addonsp[NUM_EXT+5]; + +#define numaddonsshown 4 + +// Replay hut +menu_t MISC_ReplayHutDef; +menu_t MISC_ReplayOptionsDef; +static void M_HandleReplayHutList(INT32 choice); +static void M_DrawReplayHut(void); +static void M_DrawReplayStartMenu(void); +static boolean M_QuitReplayHut(void); +static void M_HutStartReplay(INT32 choice); + +static void M_DrawPlaybackMenu(void); +static void M_PlaybackRewind(INT32 choice); +static void M_PlaybackPause(INT32 choice); +static void M_PlaybackFastForward(INT32 choice); +static void M_PlaybackAdvance(INT32 choice); +static void M_PlaybackSetViews(INT32 choice); +static void M_PlaybackAdjustView(INT32 choice); +static void M_PlaybackToggleFreecam(INT32 choice); +static void M_PlaybackQuit(INT32 choice); + +static UINT8 playback_enterheld = 0; // horrid hack to prevent holding the button from being extremely fucked + +// Drawing functions +static void M_DrawGenericMenu(void); +static void M_DrawGenericBackgroundMenu(void); +static void M_DrawCenteredMenu(void); +static void M_DrawAddons(void); +static void M_DrawSkyRoom(void); +static void M_DrawChecklist(void); +static void M_DrawEmblemHints(void); +static void M_DrawPauseMenu(void); +static void M_DrawLevelSelectOnly(boolean leftfade, boolean rightfade); +static void M_DrawServerMenu(void); +static void M_DrawImageDef(void); +//static void M_DrawLoad(void); +static void M_DrawLevelStats(void); +static void M_DrawTimeAttackMenu(void); +//static void M_DrawNightsAttackMenu(void); +//static void M_DrawSetupChoosePlayerMenu(void); +static void M_DrawControl(void); +static void M_DrawVideoMenu(void); +static void M_DrawHUDOptions(void); +static void M_DrawVideoMode(void); +static void M_DrawMonitorToggles(void); +static void M_DrawMPMainMenu(void); +#ifndef NONET +static void M_DrawConnectMenu(void); +#endif +static void M_DrawJoystick(void); +static void M_DrawSetupMultiPlayerMenu(void); + +// Handling functions +#ifndef NONET +static boolean M_CancelConnect(void); +#endif +static boolean M_ExitPandorasBox(void); +static boolean M_QuitMultiPlayerMenu(void); +static void M_HandleAddons(INT32 choice); +static void M_HandleSoundTest(INT32 choice); +static void M_HandleImageDef(INT32 choice); +//static void M_HandleLoadSave(INT32 choice); +static void M_HandleLevelStats(INT32 choice); +#ifndef NONET +static void M_HandleConnectIP(INT32 choice); +#endif +static void M_HandleSetupMultiPlayer(INT32 choice); +static void M_HandleVideoMode(INT32 choice); +static void M_HandleMonitorToggles(INT32 choice); + +// Consvar onchange functions +static void Newgametype_OnChange(void); +static void Dummymenuplayer_OnChange(void); +//static void Dummymares_OnChange(void); +static void Dummystaff_OnChange(void); + +// ========================================================================== +// CONSOLE VARIABLES AND THEIR POSSIBLE VALUES GO HERE. +// ========================================================================== + +consvar_t cv_showfocuslost = CVAR_INIT ("showfocuslost", "Yes", CV_SAVE, CV_YesNo, NULL); + +static CV_PossibleValue_t map_cons_t[] = { + {0,"MIN"}, + {NUMMAPS, "MAX"}, + {0, NULL} +}; +consvar_t cv_nextmap = CVAR_INIT ("nextmap", "1", CV_HIDEN|CV_CALL, map_cons_t, Nextmap_OnChange); + +static CV_PossibleValue_t skins_cons_t[MAXSKINS+1] = {{1, DEFAULTSKIN}}; +consvar_t cv_chooseskin = CVAR_INIT ("chooseskin", DEFAULTSKIN, CV_HIDEN|CV_CALL, skins_cons_t, Nextmap_OnChange); + +// This gametype list is integral for many different reasons. +// When you add gametypes here, don't forget to update them in dehacked.c and doomstat.h! +CV_PossibleValue_t gametype_cons_t[NUMGAMETYPES+1]; + +consvar_t cv_newgametype = CVAR_INIT ("newgametype", "Race", CV_HIDEN|CV_CALL, gametype_cons_t, Newgametype_OnChange); + +static CV_PossibleValue_t serversort_cons_t[] = { + {0,"Ping"}, + {1,"Modified State"}, + {2,"Most Players"}, + {3,"Least Players"}, + {4,"Max Player Slots"}, + {5,"Gametype"}, + {0,NULL} +}; +consvar_t cv_serversort = CVAR_INIT ("serversort", "Ping", CV_CALL, serversort_cons_t, M_SortServerList); + +// first time memory +consvar_t cv_tutorialprompt = CVAR_INIT ("tutorialprompt", "On", CV_SAVE, CV_OnOff, NULL); + +// autorecord demos for time attack +static consvar_t cv_autorecord = CVAR_INIT ("autorecord", "Yes", 0, CV_YesNo, NULL); + +CV_PossibleValue_t ghost_cons_t[] = {{0, "Hide"}, {1, "Show Character"}, {2, "Show All"}, {0, NULL}}; +CV_PossibleValue_t ghost2_cons_t[] = {{0, "Hide"}, {1, "Show"}, {0, NULL}}; + +consvar_t cv_ghost_besttime = CVAR_INIT ("ghost_besttime", "Show All", CV_SAVE, ghost_cons_t, NULL); +consvar_t cv_ghost_bestlap = CVAR_INIT ("ghost_bestlap", "Show All", CV_SAVE, ghost_cons_t, NULL); +consvar_t cv_ghost_last = CVAR_INIT ("ghost_last", "Show All", CV_SAVE, ghost_cons_t, NULL); +consvar_t cv_ghost_guest = CVAR_INIT ("ghost_guest", "Show", CV_SAVE, ghost2_cons_t, NULL); +consvar_t cv_ghost_staff = CVAR_INIT ("ghost_staff", "Show", CV_SAVE, ghost2_cons_t, NULL); + +//Console variables used solely in the menu system. +//todo: add a way to use non-console variables in the menu +// or make these consvars legitimate like color or skin. +static void Splitplayers_OnChange(void); +CV_PossibleValue_t splitplayers_cons_t[] = {{1, "One"}, {2, "Two"}, {3, "Three"}, {4, "Four"}, {0, NULL}}; +consvar_t cv_splitplayers = CVAR_INIT ("splitplayers", "One", CV_CALL, splitplayers_cons_t, Splitplayers_OnChange); + +static CV_PossibleValue_t dummymenuplayer_cons_t[] = {{0, "NOPE"}, {1, "P1"}, {2, "P2"}, {3, "P3"}, {4, "P4"}, {0, NULL}}; +static CV_PossibleValue_t dummyteam_cons_t[] = {{0, "Spectator"}, {1, "Red"}, {2, "Blue"}, {0, NULL}}; +static CV_PossibleValue_t dummyspectate_cons_t[] = {{0, "Spectator"}, {1, "Playing"}, {0, NULL}}; +static CV_PossibleValue_t dummyscramble_cons_t[] = {{0, "Random"}, {1, "Points"}, {0, NULL}}; +static CV_PossibleValue_t ringlimit_cons_t[] = {{-20, "MIN"}, {20, "MAX"}, {0, NULL}}; +static CV_PossibleValue_t liveslimit_cons_t[] = {{-1, "MIN"}, {9, "MAX"}, {0, NULL}}; +/*static CV_PossibleValue_t dummymares_cons_t[] = { + {-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} +};*/ +static CV_PossibleValue_t dummystaff_cons_t[] = {{0, "MIN"}, {100, "MAX"}, {0, NULL}}; + +static consvar_t cv_dummymenuplayer = CVAR_INIT ("dummymenuplayer", "P1", CV_HIDEN|CV_CALL, dummymenuplayer_cons_t, Dummymenuplayer_OnChange); +static consvar_t cv_dummyteam = CVAR_INIT ("dummyteam", "Spectator", CV_HIDEN, dummyteam_cons_t, NULL); +static consvar_t cv_dummyspectate = CVAR_INIT ("dummyspectate", "Spectator", CV_HIDEN, dummyspectate_cons_t, NULL); +static consvar_t cv_dummyscramble = CVAR_INIT ("dummyscramble", "Random", CV_HIDEN, dummyscramble_cons_t, NULL); +static consvar_t cv_dummyrings = CVAR_INIT ("dummyrings", "0", CV_HIDEN, ringlimit_cons_t, NULL); +static consvar_t cv_dummylives = CVAR_INIT ("dummylives", "0", CV_HIDEN, liveslimit_cons_t, NULL); +static consvar_t cv_dummystaff = CVAR_INIT ("dummystaff", "0", CV_HIDEN|CV_CALL, dummystaff_cons_t, Dummystaff_OnChange); + +static CV_PossibleValue_t dummygpdifficulty_cons_t[] = {{0, "Easy"}, {1, "Normal"}, {2, "Hard"}, {3, "Master"}, {0, NULL}}; +static CV_PossibleValue_t dummygpcup_cons_t[50] = {{1, "TEMP"}}; // A REALLY BIG NUMBER, SINCE THIS IS TEMP UNTIL NEW MENUS + +static consvar_t cv_dummygpdifficulty = CVAR_INIT ("dummygpdifficulty", "Normal", CV_HIDEN, dummygpdifficulty_cons_t, NULL); +static consvar_t cv_dummygpencore = CVAR_INIT ("dummygpencore", "Off", CV_HIDEN, CV_OnOff, NULL); +static consvar_t cv_dummygpcup = CVAR_INIT ("dummygpcup", "TEMP", CV_HIDEN, dummygpcup_cons_t, NULL); + +// ========================================================================== +// ORGANIZATION START. +// ========================================================================== +// Note: Never should we be jumping from one category of menu options to another +// without first going to the Main Menu. +// Note: Ignore the above if you're working with the Pause menu. +// Note: (Prefix)_MainMenu should be the target of all Main Menu options that +// point to submenus. + +// --------- +// Main Menu +// --------- +static menuitem_t MainMenu[] = +{ + {IT_SUBMENU|IT_STRING, NULL, "Extras", &SR_MainDef, 76}, +#ifdef TESTERS + {IT_GRAYEDOUT, NULL, "1 Player", NULL, 84}, +#else + {IT_CALL |IT_STRING, NULL, "1 Player", M_SinglePlayerMenu, 84}, +#endif + {IT_SUBMENU|IT_STRING, NULL, "Multiplayer", &MP_MainDef, 92}, + {IT_CALL |IT_STRING, NULL, "Options", M_Options, 100}, + /* I don't think is useful at all... */ + {IT_CALL |IT_STRING, NULL, "Addons", M_Addons, 108}, + {IT_CALL |IT_STRING, NULL, "Quit Game", M_QuitSRB2, 116}, +}; + +typedef enum +{ + secrets = 0, + singleplr, + multiplr, + options, + addons, + quitdoom +} main_e; + +static menuitem_t MISC_AddonsMenu[] = +{ + {IT_KEYHANDLER | IT_NOTHING, NULL, "", M_HandleAddons, 0}, // dummy menuitem for the control func +}; + +static menuitem_t MISC_ReplayHutMenu[] = +{ + {IT_KEYHANDLER|IT_NOTHING, NULL, "", M_HandleReplayHutList, 0}, // Dummy menuitem for the replay list + {IT_NOTHING, NULL, "", NULL, 0}, // Dummy for handling wrapping to the top of the menu.. +}; + +static menuitem_t MISC_ReplayStartMenu[] = +{ + {IT_CALL |IT_STRING, NULL, "Load Addons and Watch", M_HutStartReplay, 0}, + {IT_CALL |IT_STRING, NULL, "Watch Without Addons", M_HutStartReplay, 10}, + {IT_CALL |IT_STRING, NULL, "Watch Replay", M_HutStartReplay, 10}, + {IT_SUBMENU |IT_STRING, NULL, "Back", &MISC_ReplayHutDef, 30}, +}; + +static menuitem_t MISC_ReplayOptionsMenu[] = +{ + {IT_CVAR|IT_STRING, NULL, "Record Replays", &cv_recordmultiplayerdemos, 0}, + {IT_CVAR|IT_STRING, NULL, "Sync Check Interval", &cv_netdemosyncquality, 10}, +}; + +static tic_t playback_last_menu_interaction_leveltime = 0; +static menuitem_t PlaybackMenu[] = +{ + {IT_CALL | IT_STRING, "M_PHIDE", "Hide Menu (Esc)", M_SelectableClearMenus, 0}, + + {IT_CALL | IT_STRING, "M_PREW", "Rewind ([)", M_PlaybackRewind, 20}, + {IT_CALL | IT_STRING, "M_PPAUSE", "Pause (\\)", M_PlaybackPause, 36}, + {IT_CALL | IT_STRING, "M_PFFWD", "Fast-Forward (])", M_PlaybackFastForward, 52}, + {IT_CALL | IT_STRING, "M_PSTEPB", "Backup Frame ([)", M_PlaybackRewind, 20}, + {IT_CALL | IT_STRING, "M_PRESUM", "Resume", M_PlaybackPause, 36}, + {IT_CALL | IT_STRING, "M_PFADV", "Advance Frame (])", M_PlaybackAdvance, 52}, + + {IT_ARROWS | IT_STRING, "M_PVIEWS", "View Count (- and =)", M_PlaybackSetViews, 72}, + {IT_ARROWS | IT_STRING, "M_PNVIEW", "Viewpoint (1)", M_PlaybackAdjustView, 88}, + {IT_ARROWS | IT_STRING, "M_PNVIEW", "Viewpoint 2 (2)", M_PlaybackAdjustView, 104}, + {IT_ARROWS | IT_STRING, "M_PNVIEW", "Viewpoint 3 (3)", M_PlaybackAdjustView, 120}, + {IT_ARROWS | IT_STRING, "M_PNVIEW", "Viewpoint 4 (4)", M_PlaybackAdjustView, 136}, + + {IT_CALL | IT_STRING, "M_PVIEWS", "Toggle Free Camera (')", M_PlaybackToggleFreecam, 156}, + {IT_CALL | IT_STRING, "M_PEXIT", "Stop Playback", M_PlaybackQuit, 172}, +}; +typedef enum +{ + playback_hide, + playback_rewind, + playback_pause, + playback_fastforward, + playback_backframe, + playback_resume, + playback_advanceframe, + playback_viewcount, + playback_view1, + playback_view2, + playback_view3, + playback_view4, + playback_freecamera, + //playback_moreoptions, + playback_quit +} playback_e; + +// --------------------------------- +// Pause Menu Mode Attacking Edition +// --------------------------------- +static menuitem_t MAPauseMenu[] = +{ + {IT_CALL | IT_STRING, NULL, "Continue", M_SelectableClearMenus,48}, + {IT_CALL | IT_STRING, NULL, "Retry", M_ModeAttackRetry, 56}, + {IT_CALL | IT_STRING, NULL, "Abort", M_ModeAttackEndGame, 64}, +}; + +typedef enum +{ + mapause_continue, + mapause_retry, + mapause_abort +} mapause_e; + +// --------------------- +// Pause Menu MP Edition +// --------------------- +static menuitem_t MPauseMenu[] = +{ + {IT_STRING | IT_CALL, NULL, "Addons...", M_Addons, 8}, + {IT_STRING | IT_SUBMENU, NULL, "Scramble Teams...", &MISC_ScrambleTeamDef, 16}, + {IT_STRING | IT_CALL, NULL, "Switch Map..." , M_MapChange, 24}, + +#ifdef HAVE_DISCORDRPC + {IT_STRING | IT_SUBMENU, NULL, "Ask To Join Requests...", &MISC_DiscordRequestsDef, 24}, +#endif + + {IT_CALL | IT_STRING, NULL, "Continue", M_SelectableClearMenus, 40}, + {IT_CALL | IT_STRING, NULL, "P1 Setup...", M_SetupMultiPlayer, 48}, // splitscreen + {IT_CALL | IT_STRING, NULL, "P2 Setup...", M_SetupMultiPlayer2, 56}, // splitscreen + {IT_CALL | IT_STRING, NULL, "P3 Setup...", M_SetupMultiPlayer3, 64}, // splitscreen + {IT_CALL | IT_STRING, NULL, "P4 Setup...", M_SetupMultiPlayer4, 72}, // splitscreen + + {IT_STRING | IT_CALL, NULL, "Spectate", M_ConfirmSpectate, 48}, // alone + {IT_STRING | IT_CALL, NULL, "Enter Game", M_ConfirmEnterGame, 48}, // alone + {IT_STRING | IT_CALL, NULL, "Cancel Join", M_ConfirmSpectate, 48}, // alone + {IT_STRING | IT_SUBMENU, NULL, "Switch Team...", &MISC_ChangeTeamDef, 48}, + {IT_STRING | IT_SUBMENU, NULL, "Enter/Spectate...", &MISC_ChangeSpectateDef,48}, + {IT_CALL | IT_STRING, NULL, "Player Setup...", M_SetupMultiPlayer, 56}, // alone + {IT_CALL | IT_STRING, NULL, "Options", M_Options, 64}, + + {IT_CALL | IT_STRING, NULL, "Return to Title", M_EndGame, 80}, + {IT_CALL | IT_STRING, NULL, "Quit Game", M_QuitSRB2, 88}, +}; + +typedef enum +{ + mpause_addons = 0, + mpause_scramble, + mpause_switchmap, +#ifdef HAVE_DISCORDRPC + mpause_discordrequests, +#endif + + mpause_continue, + mpause_psetupsplit, + mpause_psetupsplit2, + mpause_psetupsplit3, + mpause_psetupsplit4, + + mpause_spectate, + mpause_entergame, + mpause_canceljoin, + mpause_switchteam, + mpause_switchspectate, + mpause_psetup, + mpause_options, + + mpause_title, + mpause_quit +} mpause_e; + +// --------------------- +// Pause Menu SP Edition +// --------------------- +static menuitem_t SPauseMenu[] = +{ + // Pandora's Box will be shifted up if both options are available + {IT_CALL | IT_STRING, NULL, "Pandora's Box...", M_PandorasBox, 16}, + {IT_CALL | IT_STRING, NULL, "Medal Hints...", M_EmblemHints, 24}, + //{IT_CALL | IT_STRING, NULL, "Level Select...", M_LoadGameLevelSelect, 32}, + + {IT_CALL | IT_STRING, NULL, "Continue", M_SelectableClearMenus,48}, + {IT_CALL | IT_STRING, NULL, "Retry", M_Retry, 56}, + {IT_CALL | IT_STRING, NULL, "Options", M_Options, 64}, + + {IT_CALL | IT_STRING, NULL, "Return to Title", M_EndGame, 80}, + {IT_CALL | IT_STRING, NULL, "Quit Game", M_QuitSRB2, 88}, +}; + +typedef enum +{ + spause_pandora = 0, + spause_hints, + //spause_levelselect, + + spause_continue, + spause_retry, + spause_options, + spause_title, + spause_quit +} spause_e; + +#ifdef HAVE_DISCORDRPC +static menuitem_t MISC_DiscordRequestsMenu[] = +{ + {IT_KEYHANDLER|IT_NOTHING, NULL, "", M_HandleDiscordRequests, 0}, +}; +#endif + +// ----------------- +// Misc menu options +// ----------------- +// Prefix: MISC_ +static menuitem_t MISC_ScrambleTeamMenu[] = +{ + {IT_STRING|IT_CVAR, NULL, "Scramble Method", &cv_dummyscramble, 30}, + {IT_WHITESTRING|IT_CALL, NULL, "Confirm", M_ConfirmTeamScramble, 90}, +}; + +static menuitem_t MISC_ChangeTeamMenu[] = +{ + {IT_STRING|IT_CVAR, NULL, "Player", &cv_dummymenuplayer, 30}, + {IT_STRING|IT_CVAR, NULL, "Team", &cv_dummyteam, 40}, + {IT_WHITESTRING|IT_CALL, NULL, "Confirm", M_ConfirmTeamChange, 90}, +}; + +static menuitem_t MISC_ChangeSpectateMenu[] = +{ + {IT_STRING|IT_CVAR, NULL, "Player", &cv_dummymenuplayer, 30}, + {IT_STRING|IT_CVAR, NULL, "Status", &cv_dummyspectate, 40}, + {IT_WHITESTRING|IT_CALL, NULL, "Confirm", M_ConfirmSpectateChange, 90}, +}; + +static menuitem_t MISC_ChangeLevelMenu[] = +{ + {IT_STRING|IT_CVAR, NULL, "Game Type", &cv_newgametype, 68}, + {IT_STRING|IT_CVAR, NULL, "Level", &cv_nextmap, 78}, + {IT_WHITESTRING|IT_CALL, NULL, "Change Level", M_ChangeLevel, 130}, +}; + +static menuitem_t MISC_HelpMenu[] = +{ + {IT_KEYHANDLER | IT_NOTHING, NULL, "MANUAL00", M_HandleImageDef, 0}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "MANUAL01", M_HandleImageDef, 1}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "MANUAL02", M_HandleImageDef, 1}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "MANUAL03", M_HandleImageDef, 1}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "MANUAL04", M_HandleImageDef, 1}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "MANUAL05", M_HandleImageDef, 1}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "MANUAL06", M_HandleImageDef, 1}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "MANUAL07", M_HandleImageDef, 1}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "MANUAL08", M_HandleImageDef, 1}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "MANUAL09", M_HandleImageDef, 1}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "MANUAL10", M_HandleImageDef, 1}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "MANUAL11", M_HandleImageDef, 1}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "MANUAL12", M_HandleImageDef, 1}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "MANUAL99", M_HandleImageDef, 0}, +}; + +// -------------------------------- +// Sky Room and all of its submenus +// -------------------------------- +// Prefix: SR_ + +// Pause Menu Pandora's Box Options +static menuitem_t SR_PandorasBox[] = +{ + {IT_STRING | IT_CVAR, NULL, "Rings", &cv_dummyrings, 20}, + {IT_STRING | IT_CVAR, NULL, "Lives", &cv_dummylives, 30}, + + {IT_STRING | IT_CVAR, NULL, "Gravity", &cv_gravity, 60}, + + {IT_STRING | IT_CALL, NULL, "Get All Emeralds", M_GetAllEmeralds, 90}, + {IT_STRING | IT_CALL, NULL, "Destroy All Robots", M_DestroyRobots, 100}, + + {IT_STRING | IT_CALL, NULL, "Ultimate Cheat", M_UltimateCheat, 130}, +}; + +// Sky Room Custom Unlocks +static menuitem_t SR_MainMenu[] = +{ +#ifndef TESTERS + {IT_STRING|IT_SUBMENU, NULL, "Unlockables", &SR_UnlockChecklistDef, 100}, +#endif + {IT_CALL|IT_STRING|IT_CALL_NOTMODIFIED, NULL, "Statistics", M_Statistics, 108}, + {IT_CALL|IT_STRING, NULL, "Replay Hut", M_ReplayHut, 116}, + {IT_DISABLED, NULL, "", NULL, 0}, // Custom1 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom2 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom3 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom4 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom5 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom6 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom7 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom8 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom9 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom10 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom11 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom12 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom13 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom14 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom15 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom16 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom17 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom18 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom19 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom20 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom21 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom22 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom23 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom24 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom25 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom26 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom27 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom28 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom29 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom30 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom31 + {IT_DISABLED, NULL, "", NULL, 0}, // Custom32 + +}; + +/*static menuitem_t SR_LevelSelectMenu[] = +{ + {IT_STRING|IT_CVAR, NULL, "Level", &cv_nextmap, 78}, + {IT_WHITESTRING|IT_CALL, NULL, "Start", M_LevelSelectWarp, 130}, +};*/ + +static menuitem_t SR_UnlockChecklistMenu[] = +{ + {IT_SUBMENU | IT_STRING, NULL, "NEXT", &MainDef, 192}, +}; + +static menuitem_t SR_EmblemHintMenu[] = +{ + {IT_STRING|IT_CVAR, NULL, "Medal Radar", &cv_itemfinder, 10}, + {IT_WHITESTRING|IT_SUBMENU, NULL, "Back", &SPauseDef, 20} +}; + +// -------------------------------- +// 1 Player and all of its submenus +// -------------------------------- +// Prefix: SP_ + +// Single Player Main +static menuitem_t SP_MainMenu[] = +{ + {IT_STRING|IT_CALL, NULL, "Grand Prix", M_GrandPrixTemp, 92}, + {IT_SECRET, NULL, "Time Attack", M_TimeAttack, 100}, + {IT_SECRET, NULL, "Break the Capsules", M_BreakTheCapsules, 108}, +}; + +enum +{ + spgrandprix, + sptimeattack, + spbreakthecapsules +}; + +// Single Player Load Game +static menuitem_t SP_GrandPrixPlaceholderMenu[] = +{ + {IT_STRING|IT_CVAR, NULL, "Character", &cv_chooseskin, 10}, + {IT_STRING|IT_CVAR, NULL, "Color", &cv_playercolor[0], 20}, + + {IT_STRING|IT_CVAR, NULL, "Difficulty", &cv_dummygpdifficulty, 40}, + {IT_STRING|IT_CVAR, NULL, "Encore Mode", &cv_dummygpencore, 50}, + + {IT_STRING|IT_CVAR, NULL, "Cup", &cv_dummygpcup, 70}, + {IT_STRING|IT_CALL, NULL, "Start", M_StartGrandPrix, 80}, +}; + +// Single Player Time Attack +static menuitem_t SP_TimeAttackMenu[] = +{ + {IT_STRING|IT_CVAR|IT_CV_STRING, NULL, "Name", &cv_playername[0], 0}, + {IT_STRING|IT_CVAR, NULL, "Character", &cv_chooseskin, 13}, + {IT_STRING|IT_CVAR, NULL, "Color", &cv_playercolor[0], 26}, + {IT_STRING|IT_CVAR, NULL, "Level", &cv_nextmap, 78}, + + {IT_DISABLED, NULL, "Guest...", &SP_GuestReplayDef, 98}, + {IT_DISABLED, NULL, "Replay...", &SP_ReplayDef, 108}, + {IT_WHITESTRING|IT_SUBMENU, NULL, "Ghosts...", &SP_GhostDef, 118}, + {IT_WHITESTRING|IT_CALL|IT_CALL_NOTMODIFIED, NULL, "Start", M_ChooseTimeAttack, 130}, +}; + +enum +{ + taname, + taplayer, + tacolor, + talevel, + + taguest, + tareplay, + taghost, + tastart +}; + +static menuitem_t SP_ReplayMenu[] = +{ + {IT_WHITESTRING|IT_CALL, NULL, "Replay Best Time", M_ReplayTimeAttack, 90}, + {IT_WHITESTRING|IT_CALL, NULL, "Replay Best Lap", M_ReplayTimeAttack, 98}, + + {IT_WHITESTRING|IT_CALL, NULL, "Replay Last", M_ReplayTimeAttack, 106}, + {IT_WHITESTRING|IT_CALL, NULL, "Replay Guest", M_ReplayTimeAttack, 114}, + {IT_WHITESTRING|IT_KEYHANDLER, NULL, "Replay Staff",M_HandleStaffReplay,122}, + + {IT_WHITESTRING|IT_SUBMENU, NULL, "Back", &SP_TimeAttackDef, 130} +}; + +/*static menuitem_t SP_NightsReplayMenu[] = +{ + {IT_WHITESTRING|IT_CALL, NULL, "Replay Best Score", M_ReplayTimeAttack, 0}, + {IT_WHITESTRING|IT_CALL, NULL, "Replay Best Time", M_ReplayTimeAttack,16}, + + {IT_WHITESTRING|IT_CALL, NULL, "Replay Last", M_ReplayTimeAttack,21}, + {IT_WHITESTRING|IT_CALL, NULL, "Replay Guest", M_ReplayTimeAttack,29}, + {IT_WHITESTRING|IT_KEYHANDLER, NULL, "Replay Staff",M_HandleStaffReplay,37}, + + {IT_WHITESTRING|IT_SUBMENU, NULL, "Back", &SP_NightsAttackDef, 50} +};*/ + +static menuitem_t SP_GuestReplayMenu[] = +{ + {IT_WHITESTRING|IT_CALL, NULL, "Save Best Time as Guest", M_SetGuestReplay, 94}, + {IT_WHITESTRING|IT_CALL, NULL, "Save Best Lap as Guest", M_SetGuestReplay,102}, + {IT_WHITESTRING|IT_CALL, NULL, "Save Last as Guest", M_SetGuestReplay,110}, + + {IT_WHITESTRING|IT_CALL, NULL, "Delete Guest Replay", M_SetGuestReplay,120}, + + {IT_WHITESTRING|IT_SUBMENU, NULL, "Back", &SP_TimeAttackDef, 130} +}; + +/*static menuitem_t SP_NightsGuestReplayMenu[] = +{ + {IT_WHITESTRING|IT_CALL, NULL, "Save Best Score as Guest", M_SetGuestReplay, 8}, + {IT_WHITESTRING|IT_CALL, NULL, "Save Best Time as Guest", M_SetGuestReplay,16}, + {IT_WHITESTRING|IT_CALL, NULL, "Save Last as Guest", M_SetGuestReplay,24}, + + {IT_WHITESTRING|IT_CALL, NULL, "Delete Guest Replay", M_SetGuestReplay,37}, + + {IT_WHITESTRING|IT_SUBMENU, NULL, "Back", &SP_NightsAttackDef, 50} +};*/ + +static menuitem_t SP_GhostMenu[] = +{ + {IT_STRING|IT_CVAR, NULL, "Best Time", &cv_ghost_besttime, 88}, + {IT_STRING|IT_CVAR, NULL, "Best Lap", &cv_ghost_bestlap, 96}, + {IT_STRING|IT_CVAR, NULL, "Last", &cv_ghost_last, 104}, + {IT_DISABLED, NULL, "Guest", &cv_ghost_guest, 112}, + {IT_DISABLED, NULL, "Staff Attack",&cv_ghost_staff, 120}, + + {IT_WHITESTRING|IT_SUBMENU, NULL, "Back", &SP_TimeAttackDef, 130} +}; + +/*static menuitem_t SP_NightsGhostMenu[] = +{ + {IT_STRING|IT_CVAR, NULL, "Best Score", &cv_ghost_bestscore, 0}, + {IT_STRING|IT_CVAR, NULL, "Best Time", &cv_ghost_besttime, 8}, + {IT_STRING|IT_CVAR, NULL, "Last", &cv_ghost_last, 16}, + + {IT_STRING|IT_CVAR, NULL, "Guest", &cv_ghost_guest, 29}, + {IT_STRING|IT_CVAR, NULL, "Staff Attack",&cv_ghost_staff, 37}, + + {IT_WHITESTRING|IT_SUBMENU, NULL, "Back", &SP_NightsAttackDef, 50} +};*/ + +// Single Player Nights Attack +/*static menuitem_t SP_NightsAttackMenu[] = +{ + {IT_STRING|IT_CVAR, NULL, "Level", &cv_nextmap, 44}, + {IT_STRING|IT_CVAR, NULL, "Show Records For", &cv_dummymares, 54}, + + {IT_DISABLED, NULL, "Guest Option...", &SP_NightsGuestReplayDef, 108}, + {IT_DISABLED, NULL, "Replay...", &SP_NightsReplayDef, 118}, + {IT_DISABLED, NULL, "Ghosts...", &SP_NightsGhostDef, 128}, + {IT_WHITESTRING|IT_CALL|IT_CALL_NOTMODIFIED, NULL, "Start", M_ChooseNightsAttack, 138}, +};*/ + +enum +{ + nalevel, + narecords, + + naguest, + nareplay, + naghost, + nastart +}; + +// Statistics +static menuitem_t SP_LevelStatsMenu[] = +{ + {IT_KEYHANDLER | IT_NOTHING, NULL, "", M_HandleLevelStats, '\0'}, // dummy menuitem for the control func +}; + +// A rare case. +// External files modify this menu, so we can't call it static. +// And I'm too lazy to go through and rename it everywhere. ARRGH! +#define M_ChoosePlayer NULL +menuitem_t PlayerMenu[MAXSKINS]; + +// ----------------------------------- +// Multiplayer and all of its submenus +// ----------------------------------- +// Prefix: MP_ + +static menuitem_t MP_MainMenu[] = +{ + {IT_HEADER, NULL, "Players", NULL, 0}, + {IT_STRING|IT_CVAR, NULL, "Number of local players", &cv_splitplayers, 10}, + + {IT_STRING|IT_KEYHANDLER,NULL, "Player setup...", M_SetupMultiHandler,18}, + + {IT_HEADER, NULL, "Host a game", NULL, 100-24}, +#ifndef NOMENUHOST + {IT_STRING|IT_CALL, NULL, "Internet/LAN...", M_StartServerMenu, 110-24}, +#else + {IT_GRAYEDOUT, NULL, "Internet/LAN...", NULL, 110-24}, +#endif +#ifdef TESTERS + {IT_GRAYEDOUT, NULL, "Offline...", NULL, 118-24}, +#else + {IT_STRING|IT_CALL, NULL, "Offline...", M_StartOfflineServerMenu, 118-24}, +#endif + + {IT_HEADER, NULL, "Join a game", NULL, 132-24}, +#ifndef NONET + {IT_STRING|IT_CALL, NULL, "Internet server browser...",M_ConnectMenuModChecks, 142-24}, + {IT_STRING|IT_KEYHANDLER, NULL, "Specify IPv4 address:", M_HandleConnectIP, 150-24}, +#else + {IT_GRAYEDOUT, NULL, "Internet server browser...",NULL, 142-24}, + {IT_GRAYEDOUT, NULL, "Specify IPv4 address:", NULL, 150-24}, +#endif + //{IT_HEADER, NULL, "Player setup", NULL, 80}, + //{IT_STRING|IT_CALL, NULL, "Name, character, color...", M_SetupMultiPlayer, 90}, +}; + +#ifndef NONET + +static menuitem_t MP_ServerMenu[] = +{ + {IT_STRING|IT_CVAR, NULL, "Max. Player Count", &cv_maxplayers, 10}, + {IT_STRING|IT_CVAR, NULL, "Advertise", &cv_advertise, 20}, + {IT_STRING|IT_CVAR|IT_CV_STRING, NULL, "Server Name", &cv_servername, 30}, + + {IT_STRING|IT_CVAR, NULL, "Game Type", &cv_newgametype, 68}, + {IT_STRING|IT_CVAR, NULL, "Level", &cv_nextmap, 78}, + + {IT_WHITESTRING|IT_CALL, NULL, "Start", M_StartServer, 130}, +}; + +#endif + +// Separated offline and normal servers. +static menuitem_t MP_OfflineServerMenu[] = +{ + {IT_STRING|IT_CVAR, NULL, "Game Type", &cv_newgametype, 68}, + {IT_STRING|IT_CVAR, NULL, "Level", &cv_nextmap, 78}, + + {IT_WHITESTRING|IT_CALL, NULL, "Start", M_StartServer, 130}, +}; + +static menuitem_t MP_PlayerSetupMenu[] = +{ + {IT_KEYHANDLER | IT_STRING, NULL, "Name", M_HandleSetupMultiPlayer, 0}, + {IT_KEYHANDLER | IT_STRING, NULL, "Character", M_HandleSetupMultiPlayer, 16}, // Tails 01-18-2001 + {IT_KEYHANDLER | IT_STRING, NULL, "Follower", M_HandleSetupMultiPlayer, 26}, + {IT_KEYHANDLER | IT_STRING, NULL, "Color", M_HandleSetupMultiPlayer, 152}, +}; + +#ifndef NONET +static menuitem_t MP_ConnectMenu[] = +{ + {IT_STRING | IT_CVAR, NULL, "Sort By", &cv_serversort, 4}, + {IT_STRING | IT_KEYHANDLER, NULL, "Page", M_HandleServerPage, 12}, + {IT_STRING | IT_CALL, NULL, "Refresh", M_Refresh, 20}, + + {IT_STRING | IT_SPACE, NULL, "", M_Connect, 36}, + {IT_STRING | IT_SPACE, NULL, "", M_Connect, 48}, + {IT_STRING | IT_SPACE, NULL, "", M_Connect, 60}, + {IT_STRING | IT_SPACE, NULL, "", M_Connect, 72}, + {IT_STRING | IT_SPACE, NULL, "", M_Connect, 84}, + {IT_STRING | IT_SPACE, NULL, "", M_Connect, 96}, + {IT_STRING | IT_SPACE, NULL, "", M_Connect, 108}, + {IT_STRING | IT_SPACE, NULL, "", M_Connect, 120}, + {IT_STRING | IT_SPACE, NULL, "", M_Connect, 132}, + {IT_STRING | IT_SPACE, NULL, "", M_Connect, 144}, + {IT_STRING | IT_SPACE, NULL, "", M_Connect, 156}, +}; + +enum +{ + mp_connect_sort, + mp_connect_page, + mp_connect_refresh, + FIRSTSERVERLINE +}; +#endif + +// ------------------------------------ +// Options and most (?) of its submenus +// ------------------------------------ +// Prefix: OP_ +static menuitem_t OP_MainMenu[] = +{ + {IT_SUBMENU|IT_STRING, NULL, "Control Setup...", &OP_ControlsDef, 10}, + + {IT_SUBMENU|IT_STRING, NULL, "Video Options...", &OP_VideoOptionsDef, 30}, + {IT_SUBMENU|IT_STRING, NULL, "Sound Options...", &OP_SoundOptionsDef, 40}, + + {IT_SUBMENU|IT_STRING, NULL, "HUD Options...", &OP_HUDOptionsDef, 60}, + {IT_SUBMENU|IT_STRING, NULL, "Gameplay Options...", &OP_GameOptionsDef, 70}, + {IT_SUBMENU|IT_STRING, NULL, "Server Options...", &OP_ServerOptionsDef, 80}, + + {IT_SUBMENU|IT_STRING, NULL, "Data Options...", &OP_DataOptionsDef, 100}, + + {IT_CALL|IT_STRING, NULL, "Tricks & Secrets (F1)", M_Manual, 120}, + {IT_CALL|IT_STRING, NULL, "Play Credits", M_Credits, 130}, +}; + +static menuitem_t OP_ControlsMenu[] = +{ + {IT_CALL | IT_STRING, NULL, "Player 1 Controls...", M_Setup1PControlsMenu, 10}, + {IT_CALL | IT_STRING, NULL, "Player 2 Controls...", M_Setup2PControlsMenu, 20}, + + {IT_CALL | IT_STRING, NULL, "Player 3 Controls...", &M_Setup3PControlsMenu, 30}, + {IT_CALL | IT_STRING, NULL, "Player 4 Controls...", &M_Setup4PControlsMenu, 40}, + + {IT_STRING | IT_CVAR, NULL, "Controls per key", &cv_controlperkey, 60}, +}; + +static menuitem_t OP_AllControlsMenu[] = +{ + {IT_SUBMENU|IT_STRING, NULL, "Gamepad Options...", &OP_Joystick1Def, 0}, + {IT_CALL|IT_STRING, NULL, "Reset to defaults", M_ResetControls, 8}, + //{IT_SPACE, NULL, NULL, NULL, 0}, + {IT_HEADER, NULL, "Gameplay Controls", NULL, 0}, + {IT_SPACE, NULL, NULL, NULL, 0}, + {IT_CONTROL, NULL, "Accelerate", M_ChangeControl, gc_accelerate }, + {IT_CONTROL, NULL, "Turn Left", M_ChangeControl, gc_turnleft }, + {IT_CONTROL, NULL, "Turn Right", M_ChangeControl, gc_turnright }, + {IT_CONTROL, NULL, "Drift", M_ChangeControl, gc_drift }, + {IT_CONTROL, NULL, "Brake", M_ChangeControl, gc_brake }, + {IT_CONTROL, NULL, "Spindash", M_ChangeControl, gc_spindash }, + {IT_CONTROL, NULL, "Use/Throw Item", M_ChangeControl, gc_fire }, + {IT_CONTROL, NULL, "Aim Forward", M_ChangeControl, gc_aimforward }, + {IT_CONTROL, NULL, "Aim Backward", M_ChangeControl, gc_aimbackward}, + {IT_CONTROL, NULL, "Look Backward", M_ChangeControl, gc_lookback }, + {IT_HEADER, NULL, "Miscelleanous Controls", NULL, 0}, + {IT_SPACE, NULL, NULL, NULL, 0}, + {IT_CONTROL, NULL, "Chat", M_ChangeControl, gc_talkkey }, + //{IT_CONTROL, NULL, "Team Chat", M_ChangeControl, gc_teamkey }, + {IT_CONTROL, NULL, "Show Rankings", M_ChangeControl, gc_scores }, + {IT_CONTROL, NULL, "Change Viewpoint", M_ChangeControl, gc_viewpoint }, + {IT_CONTROL, NULL, "Reset Camera", M_ChangeControl, gc_camreset }, + {IT_CONTROL, NULL, "Toggle First-Person", M_ChangeControl, gc_camtoggle }, + {IT_CONTROL, NULL, "Pause", M_ChangeControl, gc_pause }, + {IT_CONTROL, NULL, "Screenshot", M_ChangeControl, gc_screenshot }, + {IT_CONTROL, NULL, "Toggle GIF Recording", M_ChangeControl, gc_recordgif }, + {IT_CONTROL, NULL, "Open/Close Menu (ESC)", M_ChangeControl, gc_systemmenu }, + {IT_CONTROL, NULL, "Developer Console", M_ChangeControl, gc_console }, + {IT_HEADER, NULL, "Spectator Controls", NULL, 0}, + {IT_SPACE, NULL, NULL, NULL, 0}, + {IT_CONTROL, NULL, "Become Spectator", M_ChangeControl, gc_spectate }, + {IT_CONTROL, NULL, "Look Up", M_ChangeControl, gc_lookup }, + {IT_CONTROL, NULL, "Look Down", M_ChangeControl, gc_lookdown }, + {IT_CONTROL, NULL, "Center View", M_ChangeControl, gc_centerview }, + {IT_HEADER, NULL, "Custom Lua Actions", NULL, 0}, + {IT_SPACE, NULL, NULL, NULL, 0}, + {IT_CONTROL, NULL, "Custom Action 1", M_ChangeControl, gc_custom1 }, + {IT_CONTROL, NULL, "Custom Action 2", M_ChangeControl, gc_custom2 }, + {IT_CONTROL, NULL, "Custom Action 3", M_ChangeControl, gc_custom3 }, +}; + +static menuitem_t OP_Joystick1Menu[] = +{ + {IT_STRING | IT_CALL, NULL, "Select Gamepad..." , M_Setup1PJoystickMenu, 10}, + {IT_STRING | IT_CVAR, NULL, "Aim Forward/Back" , &cv_aimaxis[0] , 30}, + {IT_STRING | IT_CVAR, NULL, "Turn Left/Right" , &cv_turnaxis[0] , 40}, + {IT_STRING | IT_CVAR, NULL, "Accelerate" , &cv_moveaxis[0] , 50}, + {IT_STRING | IT_CVAR, NULL, "Brake" , &cv_brakeaxis[0] , 60}, + {IT_STRING | IT_CVAR, NULL, "Drift" , &cv_driftaxis[0] , 70}, + {IT_STRING | IT_CVAR, NULL, "Use Item" , &cv_fireaxis[0] , 80}, + {IT_STRING | IT_CVAR, NULL, "Look Up/Down" , &cv_lookaxis[0] , 90}, +}; + +static menuitem_t OP_Joystick2Menu[] = +{ + {IT_STRING | IT_CALL, NULL, "Select Gamepad..." , M_Setup2PJoystickMenu, 10}, + {IT_STRING | IT_CVAR, NULL, "Aim Forward/Back" , &cv_aimaxis[1] , 30}, + {IT_STRING | IT_CVAR, NULL, "Turn Left/Right" , &cv_turnaxis[1] , 40}, + {IT_STRING | IT_CVAR, NULL, "Accelerate" , &cv_moveaxis[1] , 50}, + {IT_STRING | IT_CVAR, NULL, "Brake" , &cv_brakeaxis[1] , 60}, + {IT_STRING | IT_CVAR, NULL, "Drift" , &cv_driftaxis[1] , 70}, + {IT_STRING | IT_CVAR, NULL, "Use Item" , &cv_fireaxis[1] , 80}, + {IT_STRING | IT_CVAR, NULL, "Look Up/Down" , &cv_lookaxis[1] , 90}, +}; + +static menuitem_t OP_Joystick3Menu[] = +{ + {IT_STRING | IT_CALL, NULL, "Select Gamepad..." , M_Setup3PJoystickMenu, 10}, + {IT_STRING | IT_CVAR, NULL, "Aim Forward/Back" , &cv_aimaxis[2] , 30}, + {IT_STRING | IT_CVAR, NULL, "Turn Left/Right" , &cv_turnaxis[2] , 40}, + {IT_STRING | IT_CVAR, NULL, "Accelerate" , &cv_moveaxis[2] , 50}, + {IT_STRING | IT_CVAR, NULL, "Brake" , &cv_brakeaxis[2] , 60}, + {IT_STRING | IT_CVAR, NULL, "Drift" , &cv_driftaxis[2] , 70}, + {IT_STRING | IT_CVAR, NULL, "Use Item" , &cv_fireaxis[2] , 80}, + {IT_STRING | IT_CVAR, NULL, "Look Up/Down" , &cv_lookaxis[2] , 90}, +}; + +static menuitem_t OP_Joystick4Menu[] = +{ + {IT_STRING | IT_CALL, NULL, "Select Gamepad..." , M_Setup4PJoystickMenu, 10}, + {IT_STRING | IT_CVAR, NULL, "Aim Forward/Back" , &cv_aimaxis[3] , 30}, + {IT_STRING | IT_CVAR, NULL, "Turn Left/Right" , &cv_turnaxis[3] , 40}, + {IT_STRING | IT_CVAR, NULL, "Accelerate" , &cv_moveaxis[3] , 50}, + {IT_STRING | IT_CVAR, NULL, "Brake" , &cv_brakeaxis[3] , 60}, + {IT_STRING | IT_CVAR, NULL, "Drift" , &cv_driftaxis[3] , 70}, + {IT_STRING | IT_CVAR, NULL, "Use Item" , &cv_fireaxis[3] , 80}, + {IT_STRING | IT_CVAR, NULL, "Look Up/Down" , &cv_lookaxis[3] , 90}, +}; + +static menuitem_t OP_JoystickSetMenu[] = +{ + {IT_CALL | IT_NOTHING, "None", NULL, M_AssignJoystick, LINEHEIGHT+5}, + {IT_CALL | IT_NOTHING, "", NULL, M_AssignJoystick, (LINEHEIGHT*2)+5}, + {IT_CALL | IT_NOTHING, "", NULL, M_AssignJoystick, (LINEHEIGHT*3)+5}, + {IT_CALL | IT_NOTHING, "", NULL, M_AssignJoystick, (LINEHEIGHT*4)+5}, + {IT_CALL | IT_NOTHING, "", NULL, M_AssignJoystick, (LINEHEIGHT*5)+5}, + {IT_CALL | IT_NOTHING, "", NULL, M_AssignJoystick, (LINEHEIGHT*6)+5}, + {IT_CALL | IT_NOTHING, "", NULL, M_AssignJoystick, (LINEHEIGHT*7)+5}, + {IT_CALL | IT_NOTHING, "", NULL, M_AssignJoystick, (LINEHEIGHT*8)+5}, +}; + +/* +static menuitem_t OP_MouseOptionsMenu[] = +{ + {IT_STRING | IT_CVAR, NULL, "Use Mouse", &cv_usemouse, 10}, + + + {IT_STRING | IT_CVAR, NULL, "First-Person MouseLook", &cv_alwaysfreelook, 30}, + {IT_STRING | IT_CVAR, NULL, "Third-Person MouseLook", &cv_chasefreelook, 40}, + {IT_STRING | IT_CVAR, NULL, "Mouse Move", &cv_mousemove, 50}, + {IT_STRING | IT_CVAR, NULL, "Invert Mouse", &cv_invertmouse, 60}, + {IT_STRING | IT_CVAR | IT_CV_SLIDER, + NULL, "Mouse X Speed", &cv_mousesens, 70}, + {IT_STRING | IT_CVAR | IT_CV_SLIDER, + NULL, "Mouse Y Speed", &cv_mouseysens, 80}, +}; +*/ + +static menuitem_t OP_VideoOptionsMenu[] = +{ + {IT_STRING | IT_CALL, NULL, "Set Resolution...", M_VideoModeMenu, 10}, +#if (defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL) + {IT_STRING|IT_CVAR, NULL, "Fullscreen", &cv_fullscreen, 20}, +#endif +#ifdef HWRENDER + {IT_STRING | IT_CVAR, NULL, "Renderer", &cv_renderer, 30}, +#else + {IT_TRANSTEXT | IT_PAIR, "Renderer", "Software", &cv_renderer, 30}, +#endif + {IT_STRING | IT_CVAR | IT_CV_SLIDER, + NULL, "Gamma", &cv_globalgamma, 50}, + + {IT_STRING | IT_CVAR, NULL, "Show FPS", &cv_ticrate, 60}, + {IT_STRING | IT_CVAR, NULL, "Vertical Sync", &cv_vidwait, 70}, + + {IT_STRING | IT_CVAR, NULL, "Draw Distance", &cv_drawdist, 90}, + {IT_STRING | IT_CVAR, NULL, "Weather Draw Distance",&cv_drawdist_precip, 100}, + {IT_STRING | IT_CVAR, NULL, "Skyboxes", &cv_skybox, 110}, + +#ifdef HWRENDER + {IT_CALL | IT_STRING, NULL, "OpenGL Options...", &M_OpenGLOptionsMenu, 140}, +#endif +}; + +enum +{ + op_video_res = 0, +#if (defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL) + op_video_fullscreen, +#endif + op_video_gamma, + op_video_dd, + op_video_wdd, + //op_video_wd, + op_video_skybox, + op_video_fps, + op_video_vsync, +#ifdef HWRENDER + op_video_ogl, +#endif +}; + +static menuitem_t OP_VideoModeMenu[] = +{ + {IT_KEYHANDLER | IT_NOTHING, NULL, "", M_HandleVideoMode, '\0'}, // dummy menuitem for the control func +}; + +#ifdef HWRENDER +static menuitem_t OP_OpenGLOptionsMenu[] = +{ + {IT_STRING | IT_CVAR, NULL, "3D Models", &cv_glmodels, 10}, + {IT_STRING|IT_CVAR, NULL, "Shaders", &cv_glshaders, 20}, + + {IT_STRING|IT_CVAR, NULL, "Texture Quality", &cv_scr_depth, 40}, + {IT_STRING|IT_CVAR, NULL, "Texture Filter", &cv_glfiltermode, 50}, + {IT_STRING|IT_CVAR, NULL, "Anisotropic", &cv_glanisotropicmode, 60}, + + {IT_STRING|IT_CVAR, NULL, "Wall Contrast Style", &cv_glfakecontrast, 80}, + {IT_STRING|IT_CVAR, NULL, "Sprite Billboarding", &cv_glspritebillboarding, 90}, + {IT_STRING|IT_CVAR, NULL, "Software Perspective", &cv_glshearing, 100}, +}; +#endif + +static menuitem_t OP_SoundOptionsMenu[] = +{ + {IT_STRING|IT_CVAR, NULL, "SFX", &cv_gamesounds, 10}, + {IT_STRING|IT_CVAR|IT_CV_SLIDER, + NULL, "SFX Volume", &cv_soundvolume, 18}, + + {IT_STRING|IT_CVAR, NULL, "Music", &cv_gamedigimusic, 30}, + {IT_STRING|IT_CVAR|IT_CV_SLIDER, + NULL, "Music Volume", &cv_digmusicvolume, 38}, + + //{IT_STRING|IT_CALL, NULL, "Restart Audio System", M_RestartAudio, 50}, + + {IT_STRING|IT_CVAR, NULL, "Reverse L/R Channels", &stereoreverse, 50}, + {IT_STRING|IT_CVAR, NULL, "Surround Sound", &surround, 60}, + + {IT_STRING|IT_CVAR, NULL, "Chat Notifications", &cv_chatnotifications, 75}, + {IT_STRING|IT_CVAR, NULL, "Character voices", &cv_kartvoices, 85}, + {IT_STRING|IT_CVAR, NULL, "Powerup Warning", &cv_kartinvinsfx, 95}, + + {IT_KEYHANDLER|IT_STRING, NULL, "Sound Test", M_HandleSoundTest, 110}, + + {IT_STRING|IT_CVAR, NULL, "Play Music While Unfocused", &cv_playmusicifunfocused, 125}, + {IT_STRING|IT_CVAR, NULL, "Play SFX While Unfocused", &cv_playsoundifunfocused, 135}, +}; + +static menuitem_t OP_DataOptionsMenu[] = +{ + + {IT_STRING | IT_CALL, NULL, "Screenshot Options...", M_ScreenshotOptions, 10}, + {IT_STRING | IT_CALL, NULL, "Addon Options...", M_AddonsOptions, 20}, + {IT_STRING | IT_SUBMENU, NULL, "Replay Options...", &MISC_ReplayOptionsDef, 30}, +#ifdef HAVE_DISCORDRPC + {IT_STRING | IT_SUBMENU, NULL, "Discord Options...", &OP_DiscordOptionsDef, 40}, + + {IT_STRING | IT_SUBMENU, NULL, "Erase Data...", &OP_EraseDataDef, 60}, +#else + {IT_STRING | IT_SUBMENU, NULL, "Erase Data...", &OP_EraseDataDef, 50}, +#endif +}; + +static menuitem_t OP_ScreenshotOptionsMenu[] = +{ + {IT_STRING|IT_CVAR, NULL, "Storage Location", &cv_screenshot_option, 10}, + {IT_STRING|IT_CVAR|IT_CV_STRING, NULL, "Custom Folder", &cv_screenshot_folder, 20}, + + {IT_HEADER, NULL, "Screenshots (F8)", NULL, 50}, + {IT_STRING|IT_CVAR, NULL, "Memory Level", &cv_zlib_memory, 60}, + {IT_STRING|IT_CVAR, NULL, "Compression Level", &cv_zlib_level, 70}, + {IT_STRING|IT_CVAR, NULL, "Strategy", &cv_zlib_strategy, 80}, + {IT_STRING|IT_CVAR, NULL, "Window Size", &cv_zlib_window_bits, 90}, + + {IT_HEADER, NULL, "Movie Mode (F9)", NULL, 105}, + {IT_STRING|IT_CVAR, NULL, "Capture Mode", &cv_moviemode, 115}, + + {IT_STRING|IT_CVAR, NULL, "Region Optimizing", &cv_gif_optimize, 125}, + {IT_STRING|IT_CVAR, NULL, "Downscaling", &cv_gif_downscale, 135}, + + {IT_STRING|IT_CVAR, NULL, "Memory Level", &cv_zlib_memorya, 125}, + {IT_STRING|IT_CVAR, NULL, "Compression Level", &cv_zlib_levela, 135}, + {IT_STRING|IT_CVAR, NULL, "Strategy", &cv_zlib_strategya, 145}, + {IT_STRING|IT_CVAR, NULL, "Window Size", &cv_zlib_window_bitsa, 155}, +}; + +enum +{ + op_screenshot_folder = 1, + op_screenshot_capture = 8, + op_screenshot_gif_start = 9, + op_screenshot_gif_end = 10, + op_screenshot_apng_start = 11, + op_screenshot_apng_end = 14, +}; + +static menuitem_t OP_EraseDataMenu[] = +{ + {IT_STRING | IT_CALL, NULL, "Erase Record Data", M_EraseData, 10}, + {IT_STRING | IT_CALL, NULL, "Erase Unlockable Data", M_EraseData, 20}, + + {IT_STRING | IT_CALL, NULL, "\x85" "Erase ALL Data", M_EraseData, 40}, +}; + +static menuitem_t OP_AddonsOptionsMenu[] = +{ + {IT_HEADER, NULL, "Menu", NULL, 0}, + {IT_STRING|IT_CVAR, NULL, "Location", &cv_addons_option, 10}, + {IT_STRING|IT_CVAR|IT_CV_STRING, NULL, "Custom Folder", &cv_addons_folder, 20}, + {IT_STRING|IT_CVAR, NULL, "Identify addons via", &cv_addons_md5, 48}, + {IT_STRING|IT_CVAR, NULL, "Show unsupported file types", &cv_addons_showall, 58}, + + {IT_HEADER, NULL, "Search", NULL, 76}, + {IT_STRING|IT_CVAR, NULL, "Matching", &cv_addons_search_type, 86}, + {IT_STRING|IT_CVAR, NULL, "Case-sensitive", &cv_addons_search_case, 96}, +}; + +enum +{ + op_addons_folder = 2, +}; + +#ifdef HAVE_DISCORDRPC +static menuitem_t OP_DiscordOptionsMenu[] = +{ + {IT_STRING | IT_CVAR, NULL, "Rich Presence", &cv_discordrp, 10}, + + {IT_HEADER, NULL, "Rich Presence Settings", NULL, 30}, + {IT_STRING | IT_CVAR, NULL, "Streamer Mode", &cv_discordstreamer, 40}, + + {IT_STRING | IT_CVAR, NULL, "Allow Ask To Join", &cv_discordasks, 60}, + {IT_STRING | IT_CVAR, NULL, "Allow Invites", &cv_discordinvites, 70}, +}; +#endif + +static menuitem_t OP_HUDOptionsMenu[] = +{ + {IT_STRING | IT_CVAR, NULL, "Show HUD (F3)", &cv_showhud, 20}, + {IT_STRING | IT_CVAR | IT_CV_SLIDER, + NULL, "HUD Visibility", &cv_translucenthud, 30}, + + {IT_STRING | IT_SUBMENU, NULL, "Online HUD options...",&OP_ChatOptionsDef, 45}, + {IT_STRING | IT_CVAR, NULL, "Background Glass", &cons_backcolor, 55}, + + {IT_STRING | IT_CVAR | IT_CV_SLIDER, + NULL, "Minimap Visibility", &cv_kartminimap, 70}, + {IT_STRING | IT_CVAR, NULL, "Speedometer Display", &cv_kartspeedometer, 80}, + {IT_STRING | IT_CVAR, NULL, "Show \"CHECK\"", &cv_kartcheck, 90}, + + {IT_STRING | IT_CVAR, NULL, "Menu Highlights", &cons_menuhighlight, 105}, + // highlight info - (GOOD HIGHLIGHT, WARNING HIGHLIGHT) - 105 (see M_DrawHUDOptions) + + {IT_STRING | IT_CVAR, NULL, "Console Text Size", &cv_constextsize, 130}, + + {IT_STRING | IT_CVAR, NULL, "Show \"FOCUS LOST\"", &cv_showfocuslost, 145}, +}; + +// Ok it's still called chatoptions but we'll put ping display in here to be clean +static menuitem_t OP_ChatOptionsMenu[] = +{ + // will ANYONE who doesn't know how to use the console want to touch this one? + {IT_STRING | IT_CVAR, NULL, "Chat Mode", &cv_consolechat, 10}, // nonetheless... + + {IT_STRING | IT_CVAR | IT_CV_SLIDER, + NULL, "Chat Box Width", &cv_chatwidth, 25}, + {IT_STRING | IT_CVAR | IT_CV_SLIDER, + NULL, "Chat Box Height", &cv_chatheight, 35}, + + {IT_STRING | IT_CVAR, NULL, "Chat Background Tint", &cv_chatbacktint, 50}, + {IT_STRING | IT_CVAR, NULL, "Message Fadeout Time", &cv_chattime, 60}, + {IT_STRING | IT_CVAR, NULL, "Spam Protection", &cv_chatspamprotection, 70}, + + {IT_STRING | IT_CVAR, NULL, "Local ping display", &cv_showping, 90}, // shows ping next to framerate if we want to. +}; + +static menuitem_t OP_GameOptionsMenu[] = +{ + {IT_STRING | IT_SUBMENU, NULL, "Random Item Toggles...", &OP_MonitorToggleDef, 10}, + + {IT_STRING | IT_CVAR, NULL, "Game Speed", &cv_kartspeed, 30}, + {IT_STRING | IT_CVAR, NULL, "Frantic Items", &cv_kartfrantic, 40}, + {IT_SECRET, NULL, "Encore Mode", &cv_kartencore, 50}, + + {IT_STRING | IT_CVAR, NULL, "Number of Laps", &cv_basenumlaps, 70}, + {IT_STRING | IT_CVAR, NULL, "Exit Countdown Timer", &cv_countdowntime, 80}, + + {IT_STRING | IT_CVAR, NULL, "Time Limit", &cv_timelimit, 100}, + {IT_STRING | IT_CVAR, NULL, "Starting Bumpers", &cv_kartbumpers, 110}, + {IT_STRING | IT_CVAR, NULL, "Karma Comeback", &cv_kartcomeback, 120}, + + {IT_STRING | IT_CVAR, NULL, "Track Power Levels", &cv_kartusepwrlv, 140}, +}; + +static menuitem_t OP_ServerOptionsMenu[] = +{ +#ifndef NONET + {IT_STRING | IT_CVAR | IT_CV_STRING, + NULL, "Server Name", &cv_servername, 10}, +#endif + + {IT_STRING | IT_CVAR, NULL, "Intermission Timer", &cv_inttime, 40}, + {IT_STRING | IT_CVAR, NULL, "Map Progression", &cv_advancemap, 50}, + {IT_STRING | IT_CVAR, NULL, "Voting Timer", &cv_votetime, 60}, + {IT_STRING | IT_CVAR, NULL, "Voting Rule Changes", &cv_kartvoterulechanges, 70}, + +#ifndef NONET + {IT_STRING | IT_CVAR, NULL, "Max. Player Count", &cv_maxplayers, 90}, + {IT_STRING | IT_CVAR, NULL, "Allow Players to Join", &cv_allownewplayer, 100}, + {IT_STRING | IT_CVAR, NULL, "Allow Addon Downloading", &cv_downloading, 110}, + {IT_STRING | IT_CVAR, NULL, "Pause Permission", &cv_pause, 120}, + {IT_STRING | IT_CVAR, NULL, "Mute All Chat", &cv_mute, 130}, + + {IT_SUBMENU|IT_STRING, NULL, "Advanced Options...", &OP_AdvServerOptionsDef,150}, +#endif +}; + +#ifndef NONET +static menuitem_t OP_AdvServerOptionsMenu[] = +{ + {IT_STRING | IT_CVAR | IT_CV_STRING, + NULL, "Server Browser Address", &cv_masterserver, 10}, + + {IT_STRING | IT_CVAR, NULL, "Attempts to resynchronise", &cv_resynchattempts, 40}, + {IT_STRING | IT_CVAR, NULL, "Ping limit (ms)", &cv_maxping, 50}, + {IT_STRING | IT_CVAR, NULL, "Ping timeout (s)", &cv_pingtimeout, 60}, + {IT_STRING | IT_CVAR, NULL, "Connection timeout (tics)", &cv_nettimeout, 70}, + {IT_STRING | IT_CVAR, NULL, "Join timeout (tics)", &cv_jointimeout, 80}, + + {IT_STRING | IT_CVAR, NULL, "Max. file transfer send (KB)", &cv_maxsend, 100}, + {IT_STRING | IT_CVAR, NULL, "File transfer packet rate", &cv_downloadspeed, 110}, + + {IT_STRING | IT_CVAR, NULL, "Log join addresses", &cv_showjoinaddress, 130}, + {IT_STRING | IT_CVAR, NULL, "Log resyncs", &cv_blamecfail, 140}, + {IT_STRING | IT_CVAR, NULL, "Log file transfers", &cv_noticedownload, 150}, +}; +#endif + +/*static menuitem_t OP_NetgameOptionsMenu[] = +{ + {IT_STRING | IT_CVAR, NULL, "Time Limit", &cv_timelimit, 10}, + {IT_STRING | IT_CVAR, NULL, "Point Limit", &cv_pointlimit, 18}, + + {IT_STRING | IT_CVAR, NULL, "Frantic Items", &cv_kartfrantic, 34}, + + {IT_STRING | IT_CVAR, NULL, "Item Respawn", &cv_itemrespawn, 50}, + {IT_STRING | IT_CVAR, NULL, "Item Respawn Delay", &cv_itemrespawntime, 58}, + + {IT_STRING | IT_CVAR, NULL, "Player Respawn Delay", &cv_respawntime, 74}, + + {IT_STRING | IT_CVAR, NULL, "Force Skin #", &cv_forceskin, 90}, + {IT_STRING | IT_CVAR, NULL, "Restrict Skin Changes", &cv_restrictskinchange, 98}, + + //{IT_STRING | IT_CVAR, NULL, "Autobalance Teams", &cv_autobalance, 114}, + //{IT_STRING | IT_CVAR, NULL, "Scramble Teams on Map Change", &cv_scrambleonchange, 122}, +};*/ + +/*static menuitem_t OP_GametypeOptionsMenu[] = +{ + {IT_HEADER, NULL, "RACE", NULL, 2}, + {IT_STRING | IT_CVAR, NULL, "Game Speed", &cv_kartspeed, 10}, + {IT_STRING | IT_CVAR, NULL, "Encore Mode", &cv_kartencore, 18}, + {IT_STRING | IT_CVAR, NULL, "Number of Laps", &cv_numlaps, 26}, + {IT_STRING | IT_CVAR, NULL, "Use Map Lap Counts", &cv_usemapnumlaps, 34}, + + {IT_HEADER, NULL, "BATTLE", NULL, 50}, + {IT_STRING | IT_CVAR, NULL, "Starting Bumpers", &cv_kartbumpers, 58}, + {IT_STRING | IT_CVAR, NULL, "Karma Comeback", &cv_kartcomeback, 66}, +};*/ + +//#define ITEMTOGGLEBOTTOMRIGHT + +static menuitem_t OP_MonitorToggleMenu[] = +{ + // Mostly handled by the drawing function. + // Instead of using this for dumb monitors, lets use the new item bools we have :V + {IT_KEYHANDLER | IT_NOTHING, NULL, "Sneakers", M_HandleMonitorToggles, KITEM_SNEAKER}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Sneakers x3", M_HandleMonitorToggles, KRITEM_TRIPLESNEAKER}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Rocket Sneakers", M_HandleMonitorToggles, KITEM_ROCKETSNEAKER}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Toggle All", M_HandleMonitorToggles, 0}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Bananas", M_HandleMonitorToggles, KITEM_BANANA}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Bananas x3", M_HandleMonitorToggles, KRITEM_TRIPLEBANANA}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Bananas x10", M_HandleMonitorToggles, KRITEM_TENFOLDBANANA}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Eggman Monitors", M_HandleMonitorToggles, KITEM_EGGMAN}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Orbinauts", M_HandleMonitorToggles, KITEM_ORBINAUT}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Orbinauts x3", M_HandleMonitorToggles, KRITEM_TRIPLEORBINAUT}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Orbinauts x4", M_HandleMonitorToggles, KRITEM_QUADORBINAUT}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Mines", M_HandleMonitorToggles, KITEM_MINE}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Jawz", M_HandleMonitorToggles, KITEM_JAWZ}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Jawz x2", M_HandleMonitorToggles, KRITEM_DUALJAWZ}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Ballhogs", M_HandleMonitorToggles, KITEM_BALLHOG}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Self-Propelled Bombs", M_HandleMonitorToggles, KITEM_SPB}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Invinciblity", M_HandleMonitorToggles, KITEM_INVINCIBILITY}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Grow", M_HandleMonitorToggles, KITEM_GROW}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Shrink", M_HandleMonitorToggles, KITEM_SHRINK}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Thunder Shields", M_HandleMonitorToggles, KITEM_THUNDERSHIELD}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Hyudoros", M_HandleMonitorToggles, KITEM_HYUDORO}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Pogo Springs", M_HandleMonitorToggles, KITEM_POGOSPRING}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Super Rings", M_HandleMonitorToggles, KITEM_SUPERRING}, + {IT_KEYHANDLER | IT_NOTHING, NULL, "Kitchen Sinks", M_HandleMonitorToggles, KITEM_KITCHENSINK}, +#ifdef ITEMTOGGLEBOTTOMRIGHT + {IT_KEYHANDLER | IT_NOTHING, NULL, "---", M_HandleMonitorToggles, 255}, +#endif +}; + +// ========================================================================== +// ALL MENU DEFINITIONS GO HERE +// ========================================================================== + +// Main Menu and related +menu_t MainDef = CENTERMENUSTYLE(MN_NONE, NULL, MainMenu, NULL, 72); + +menu_t MISC_AddonsDef = +{ + MN_NONE, + NULL, + sizeof (MISC_AddonsMenu)/sizeof (menuitem_t), + &OP_DataOptionsDef, + MISC_AddonsMenu, + M_DrawAddons, + 50, 28, + 0, + NULL +}; + +menu_t MISC_ReplayHutDef = +{ + MN_NONE, + NULL, + sizeof (MISC_ReplayHutMenu)/sizeof (menuitem_t), + NULL, + MISC_ReplayHutMenu, + M_DrawReplayHut, + 30, 80, + 0, + M_QuitReplayHut +}; + +menu_t MISC_ReplayOptionsDef = +{ + MN_NONE, + "M_REPOPT", + sizeof (MISC_ReplayOptionsMenu)/sizeof (menuitem_t), + &OP_DataOptionsDef, + MISC_ReplayOptionsMenu, + M_DrawGenericMenu, + 27, 40, + 0, + NULL +}; + +menu_t MISC_ReplayStartDef = +{ + MN_NONE, + NULL, + sizeof (MISC_ReplayStartMenu)/sizeof (menuitem_t), + &MISC_ReplayHutDef, + MISC_ReplayStartMenu, + M_DrawReplayStartMenu, + 30, 90, + 0, + NULL +}; + +menu_t PlaybackMenuDef = { + MN_NONE, + NULL, + sizeof (PlaybackMenu)/sizeof (menuitem_t), + NULL, + PlaybackMenu, + M_DrawPlaybackMenu, + //BASEVIDWIDTH/2 - 94, 2, + BASEVIDWIDTH/2 - 88, 2, + 0, + NULL +}; + +menu_t MAPauseDef = PAUSEMENUSTYLE(MAPauseMenu, 40, 72); +menu_t SPauseDef = PAUSEMENUSTYLE(SPauseMenu, 40, 72); +menu_t MPauseDef = PAUSEMENUSTYLE(MPauseMenu, 40, 72); + +#ifdef HAVE_DISCORDRPC +menu_t MISC_DiscordRequestsDef = { + MN_NONE, + NULL, + sizeof (MISC_DiscordRequestsMenu)/sizeof (menuitem_t), + &MPauseDef, + MISC_DiscordRequestsMenu, + M_DrawDiscordRequests, + 0, 0, + 0, + NULL +}; +#endif + +// Misc Main Menu +menu_t MISC_ScrambleTeamDef = DEFAULTMENUSTYLE(MN_NONE, NULL, MISC_ScrambleTeamMenu, &MPauseDef, 27, 40); +menu_t MISC_ChangeTeamDef = DEFAULTMENUSTYLE(MN_NONE, NULL, MISC_ChangeTeamMenu, &MPauseDef, 27, 40); +menu_t MISC_ChangeSpectateDef = DEFAULTMENUSTYLE(MN_NONE, NULL, MISC_ChangeSpectateMenu, &MPauseDef, 27, 40); +menu_t MISC_ChangeLevelDef = MAPICONMENUSTYLE(NULL, MISC_ChangeLevelMenu, &MPauseDef); +menu_t MISC_HelpDef = IMAGEDEF(MISC_HelpMenu); + +// +// M_GetGametypeColor +// +// Pretty and consistent ^u^ +// See also G_GetGametypeColor. +// + +static INT32 highlightflags, recommendedflags, warningflags; + +inline static void M_GetGametypeColor(void) +{ + INT16 gt; + + warningflags = V_REDMAP; + recommendedflags = V_GREENMAP; + + if (cons_menuhighlight.value) + { + highlightflags = cons_menuhighlight.value; + if (highlightflags == V_REDMAP) + { + warningflags = V_ORANGEMAP; + return; + } + if (highlightflags == V_GREENMAP) + { + recommendedflags = V_SKYMAP; + return; + } + return; + } + + warningflags = V_REDMAP; + recommendedflags = V_GREENMAP; + + if (modeattacking // == ATTACKING_TIME + || gamestate == GS_TIMEATTACK) + { + highlightflags = V_ORANGEMAP; + return; + } + + if (currentMenu->drawroutine == M_DrawServerMenu) + gt = cv_newgametype.value; + else if (!Playing()) + { + highlightflags = V_YELLOWMAP; + return; + } + else + gt = gametype; + + if (gt == GT_BATTLE) + { + highlightflags = V_REDMAP; + warningflags = V_ORANGEMAP; + return; + } + if (gt == GT_RACE) + { + highlightflags = V_SKYMAP; + return; + } + + highlightflags = V_YELLOWMAP; // FALLBACK +} + +// excuse me but I'm extremely lazy: +INT32 HU_GetHighlightColor(void) +{ + M_GetGametypeColor(); // update flag colour reguardless of the menu being opened or not. + return highlightflags; +} + +// Sky Room +menu_t SR_PandoraDef = +{ + MN_NONE, + "M_PANDRA", + sizeof (SR_PandorasBox)/sizeof (menuitem_t), + &SPauseDef, + SR_PandorasBox, + M_DrawGenericMenu, + 60, 40, + 0, + M_ExitPandorasBox +}; +menu_t SR_MainDef = CENTERMENUSTYLE(MN_NONE, NULL, SR_MainMenu, &MainDef, 72); + +//menu_t SR_LevelSelectDef = MAPICONMENUSTYLE(NULL, SR_LevelSelectMenu, &SR_MainDef); + +menu_t SR_UnlockChecklistDef = +{ + MN_NONE, + NULL, + 1, + &SR_MainDef, + SR_UnlockChecklistMenu, + M_DrawChecklist, + 280, 185, + 0, + NULL +}; +menu_t SR_EmblemHintDef = +{ + MN_NONE, + NULL, + sizeof (SR_EmblemHintMenu)/sizeof (menuitem_t), + &SPauseDef, + SR_EmblemHintMenu, + M_DrawEmblemHints, + 60, 150, + 0, + NULL +}; + +// Single Player +menu_t SP_MainDef = CENTERMENUSTYLE(MN_NONE, NULL, SP_MainMenu, &MainDef, 72); +/*menu_t SP_LoadDef = +{ + MN_NONE, + "M_PICKG", + 1, + &SP_MainDef, + SP_LoadGameMenu, + M_DrawLoad, + 68, 46, + 0, + NULL +}; +menu_t SP_LevelSelectDef = MAPICONMENUSTYLE(NULL, SP_LevelSelectMenu, &SP_LoadDef);*/ + +menu_t SP_LevelStatsDef = +{ + MN_NONE, + "M_STATS", + 1, + &SR_MainDef, + SP_LevelStatsMenu, + M_DrawLevelStats, + 280, 185, + 0, + NULL +}; + +static menu_t SP_GrandPrixTempDef = DEFAULTMENUSTYLE(MN_NONE, NULL, SP_GrandPrixPlaceholderMenu, &MainDef, 60, 30); + +static menu_t SP_TimeAttackDef = +{ + MN_NONE, + "M_ATTACK", + sizeof (SP_TimeAttackMenu)/sizeof (menuitem_t), + &MainDef, // Doesn't matter. + SP_TimeAttackMenu, + M_DrawTimeAttackMenu, + 34, 40, + 0, + M_QuitTimeAttackMenu +}; +static menu_t SP_ReplayDef = +{ + MN_NONE, + "M_ATTACK", + sizeof(SP_ReplayMenu)/sizeof(menuitem_t), + &SP_TimeAttackDef, + SP_ReplayMenu, + M_DrawTimeAttackMenu, + 34, 40, + 0, + NULL +}; +static menu_t SP_GuestReplayDef = +{ + MN_NONE, + "M_ATTACK", + sizeof(SP_GuestReplayMenu)/sizeof(menuitem_t), + &SP_TimeAttackDef, + SP_GuestReplayMenu, + M_DrawTimeAttackMenu, + 34, 40, + 0, + NULL +}; +static menu_t SP_GhostDef = +{ + MN_NONE, + "M_ATTACK", + sizeof(SP_GhostMenu)/sizeof(menuitem_t), + &SP_TimeAttackDef, + SP_GhostMenu, + M_DrawTimeAttackMenu, + 34, 40, + 0, + NULL +}; + +/*menu_t SP_PlayerDef = +{ + MN_NONE, + "M_PICKP", + sizeof (PlayerMenu)/sizeof (menuitem_t),//player_end, + &SP_MainDef, + PlayerMenu, + M_DrawSetupChoosePlayerMenu, + 24, 32, + 0, + NULL +};*/ + +// Multiplayer +menu_t MP_MainDef = +{ + MN_NONE, + "M_MULTI", + sizeof (MP_MainMenu)/sizeof (menuitem_t), + &MainDef, + MP_MainMenu, + M_DrawMPMainMenu, + 42, 30, + 0, +#ifndef NONET + M_CancelConnect +#else + NULL +#endif +}; + +menu_t MP_OfflineServerDef = MAPICONMENUSTYLE("M_MULTI", MP_OfflineServerMenu, &MP_MainDef); + +#ifndef NONET +menu_t MP_ServerDef = MAPICONMENUSTYLE("M_MULTI", MP_ServerMenu, &MP_MainDef); + +menu_t MP_ConnectDef = +{ + MN_NONE, + "M_MULTI", + sizeof (MP_ConnectMenu)/sizeof (menuitem_t), + &MP_MainDef, + MP_ConnectMenu, + M_DrawConnectMenu, + 27,24, + 0, + M_CancelConnect +}; +#endif +menu_t MP_PlayerSetupDef = +{ + MN_NONE, + NULL, //"M_SPLAYR" + sizeof (MP_PlayerSetupMenu)/sizeof (menuitem_t), + &MP_MainDef, + MP_PlayerSetupMenu, + M_DrawSetupMultiPlayerMenu, + 36, 14, + 0, + M_QuitMultiPlayerMenu +}; + +// Options +menu_t OP_MainDef = +{ + MN_NONE, + "M_OPTTTL", + sizeof (OP_MainMenu)/sizeof (menuitem_t), + &MainDef, + OP_MainMenu, + M_DrawGenericMenu, + 60, 30, + 0, + NULL +}; + +menu_t OP_ControlsDef = DEFAULTMENUSTYLE(MN_NONE, "M_CONTRO", OP_ControlsMenu, &OP_MainDef, 60, 30); +menu_t OP_AllControlsDef = CONTROLMENUSTYLE(MN_NONE, OP_AllControlsMenu, &OP_ControlsDef); +menu_t OP_Joystick1Def = DEFAULTMENUSTYLE(MN_NONE, "M_CONTRO", OP_Joystick1Menu, &OP_AllControlsDef, 60, 30); +menu_t OP_Joystick2Def = DEFAULTMENUSTYLE(MN_NONE, "M_CONTRO", OP_Joystick2Menu, &OP_AllControlsDef, 60, 30); +menu_t OP_Joystick3Def = DEFAULTMENUSTYLE(MN_NONE, "M_CONTRO", OP_Joystick3Menu, &OP_AllControlsDef, 60, 30); +menu_t OP_Joystick4Def = DEFAULTMENUSTYLE(MN_NONE, "M_CONTRO", OP_Joystick4Menu, &OP_AllControlsDef, 60, 30); +menu_t OP_JoystickSetDef = +{ + MN_NONE, + "M_CONTRO", + sizeof (OP_JoystickSetMenu)/sizeof (menuitem_t), + &OP_Joystick1Def, + OP_JoystickSetMenu, + M_DrawJoystick, + 50, 40, + 0, + NULL +}; + +menu_t OP_VideoOptionsDef = +{ + MN_NONE, + "M_VIDEO", + sizeof(OP_VideoOptionsMenu)/sizeof(menuitem_t), + &OP_MainDef, + OP_VideoOptionsMenu, + M_DrawVideoMenu, + 30, 30, + 0, + NULL +}; + +menu_t OP_VideoModeDef = +{ + MN_NONE, + "M_VIDEO", + 1, + &OP_VideoOptionsDef, + OP_VideoModeMenu, + M_DrawVideoMode, + 48, 26, + 0, + NULL +}; + +menu_t OP_SoundOptionsDef = +{ + MN_NONE, + "M_SOUND", + sizeof (OP_SoundOptionsMenu)/sizeof (menuitem_t), + &OP_MainDef, + OP_SoundOptionsMenu, + M_DrawSkyRoom, + 30, 30, + 0, + NULL +}; + +menu_t OP_HUDOptionsDef = +{ + MN_NONE, + "M_HUD", + sizeof (OP_HUDOptionsMenu)/sizeof (menuitem_t), + &OP_MainDef, + OP_HUDOptionsMenu, + M_DrawHUDOptions, + 30, 30, + 0, + NULL +}; + +menu_t OP_ChatOptionsDef = DEFAULTMENUSTYLE(MN_NONE, "M_HUD", OP_ChatOptionsMenu, &OP_HUDOptionsDef, 30, 30); + +menu_t OP_GameOptionsDef = DEFAULTMENUSTYLE(MN_NONE, "M_GAME", OP_GameOptionsMenu, &OP_MainDef, 30, 30); +menu_t OP_ServerOptionsDef = DEFAULTMENUSTYLE(MN_NONE, "M_SERVER", OP_ServerOptionsMenu, &OP_MainDef, 24, 30); +#ifndef NONET +menu_t OP_AdvServerOptionsDef = DEFAULTMENUSTYLE(MN_NONE, "M_SERVER", OP_AdvServerOptionsMenu, &OP_ServerOptionsDef, 24, 30); +#endif + +//menu_t OP_NetgameOptionsDef = DEFAULTMENUSTYLE(MN_NONE, "M_SERVER", OP_NetgameOptionsMenu, &OP_ServerOptionsDef, 30, 30); +//menu_t OP_GametypeOptionsDef = DEFAULTMENUSTYLE(MN_NONE, "M_SERVER", OP_GametypeOptionsMenu, &OP_ServerOptionsDef, 30, 30); +//menu_t OP_ChatOptionsDef = DEFAULTMENUSTYLE(MN_NONE, "M_GAME", OP_ChatOptionsMenu, &OP_GameOptionsDef, 30, 30); +menu_t OP_MonitorToggleDef = +{ + MN_NONE, + "M_GAME", + sizeof (OP_MonitorToggleMenu)/sizeof (menuitem_t), + &OP_GameOptionsDef, + OP_MonitorToggleMenu, + M_DrawMonitorToggles, + 47, 30, + 0, + NULL +}; + +#ifdef HWRENDER +static void M_OpenGLOptionsMenu(void) +{ + if (rendermode == render_opengl) + M_SetupNextMenu(&OP_OpenGLOptionsDef); + else + M_StartMessage(M_GetText("You must be in OpenGL mode\nto access this menu.\n\n(Press a key)\n"), NULL, MM_NOTHING); +} + +menu_t OP_OpenGLOptionsDef = DEFAULTMENUSTYLE(MN_NONE, "M_VIDEO", OP_OpenGLOptionsMenu, &OP_VideoOptionsDef, 30, 30); +#endif +menu_t OP_DataOptionsDef = DEFAULTMENUSTYLE(MN_NONE, "M_DATA", OP_DataOptionsMenu, &OP_MainDef, 60, 30); +menu_t OP_ScreenshotOptionsDef = DEFAULTMENUSTYLE(MN_NONE, "M_SCSHOT", OP_ScreenshotOptionsMenu, &OP_DataOptionsDef, 30, 30); +menu_t OP_AddonsOptionsDef = DEFAULTMENUSTYLE(MN_NONE, "M_ADDONS", OP_AddonsOptionsMenu, &OP_DataOptionsDef, 30, 30); +#ifdef HAVE_DISCORDRPC +menu_t OP_DiscordOptionsDef = DEFAULTMENUSTYLE(MN_NONE, NULL, OP_DiscordOptionsMenu, &OP_DataOptionsDef, 30, 30); +#endif +menu_t OP_EraseDataDef = DEFAULTMENUSTYLE(MN_NONE, "M_DATA", OP_EraseDataMenu, &OP_DataOptionsDef, 30, 30); + +// ========================================================================== +// CVAR ONCHANGE EVENTS GO HERE +// ========================================================================== +// (there's only a couple anyway) + +// Prototypes +static INT32 M_FindFirstMap(INT32 gtype); +static INT32 M_GetFirstLevelInList(void); + +// Nextmap. Used for Time Attack. +void Nextmap_OnChange(void) +{ + char *leveltitle; + UINT8 active; + + // Update the string in the consvar. + Z_Free(cv_nextmap.zstring); + leveltitle = G_BuildMapTitle(cv_nextmap.value); + cv_nextmap.string = cv_nextmap.zstring = leveltitle ? leveltitle : Z_StrDup(G_BuildMapName(cv_nextmap.value)); + + if (currentMenu == &SP_TimeAttackDef) + { + // see also p_setup.c's P_LoadRecordGhosts + const size_t glen = strlen(srb2home)+1+strlen("media")+1+strlen("replay")+1+strlen(timeattackfolder)+1+strlen("MAPXX")+1; + char *gpath = malloc(glen); + INT32 i; + + if (!gpath) + return; + + sprintf(gpath,"%s"PATHSEP"media"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value)); + + CV_StealthSetValue(&cv_dummystaff, 0); + + active = 0; + SP_TimeAttackMenu[taguest].status = IT_DISABLED; + SP_TimeAttackMenu[tareplay].status = IT_DISABLED; + //SP_TimeAttackMenu[taghost].status = IT_DISABLED; + + // Check if file exists, if not, disable REPLAY option + for (i = 0; i < 4; i++) + { + SP_ReplayMenu[i].status = IT_DISABLED; + SP_GuestReplayMenu[i].status = IT_DISABLED; + } + SP_ReplayMenu[4].status = IT_DISABLED; + + SP_GhostMenu[3].status = IT_DISABLED; + SP_GhostMenu[4].status = IT_DISABLED; + + if (FIL_FileExists(va("%s-%s-time-best.lmp", gpath, cv_chooseskin.string))) { + SP_ReplayMenu[0].status = IT_WHITESTRING|IT_CALL; + SP_GuestReplayMenu[0].status = IT_WHITESTRING|IT_CALL; + active |= 3; + } + + if (levellistmode != LLM_BREAKTHECAPSULES) { + if (FIL_FileExists(va("%s-%s-lap-best.lmp", gpath, cv_chooseskin.string))) { + SP_ReplayMenu[1].status = IT_WHITESTRING|IT_CALL; + SP_GuestReplayMenu[1].status = IT_WHITESTRING|IT_CALL; + active |= 3; + } + } + + if (FIL_FileExists(va("%s-%s-last.lmp", gpath, cv_chooseskin.string))) { + SP_ReplayMenu[2].status = IT_WHITESTRING|IT_CALL; + SP_GuestReplayMenu[2].status = IT_WHITESTRING|IT_CALL; + active |= 3; + } + + if (FIL_FileExists(va("%s-guest.lmp", gpath))) + { + SP_ReplayMenu[3].status = IT_WHITESTRING|IT_CALL; + SP_GuestReplayMenu[3].status = IT_WHITESTRING|IT_CALL; + SP_GhostMenu[3].status = IT_STRING|IT_CVAR; + active |= 3; + } + + CV_SetValue(&cv_dummystaff, 1); + if (cv_dummystaff.value) + { + SP_ReplayMenu[4].status = IT_WHITESTRING|IT_KEYHANDLER; + SP_GhostMenu[4].status = IT_STRING|IT_CVAR; + CV_StealthSetValue(&cv_dummystaff, 1); + active |= 1; + } + + if (active) { + if (active & 1) + SP_TimeAttackMenu[tareplay].status = IT_WHITESTRING|IT_SUBMENU; + if (active & 2) + SP_TimeAttackMenu[taguest].status = IT_WHITESTRING|IT_SUBMENU; + } + else if (itemOn == tareplay) // Reset lastOn so replay isn't still selected when not available. + { + currentMenu->lastOn = itemOn; + itemOn = tastart; + } + + if (mapheaderinfo[cv_nextmap.value-1] && mapheaderinfo[cv_nextmap.value-1]->forcecharacter[0] != '\0') + CV_Set(&cv_chooseskin, mapheaderinfo[cv_nextmap.value-1]->forcecharacter); + + free(gpath); + } +} + +static void Dummymenuplayer_OnChange(void) +{ + if (cv_dummymenuplayer.value < 1) + CV_StealthSetValue(&cv_dummymenuplayer, splitscreen+1); + else if (cv_dummymenuplayer.value > splitscreen+1) + CV_StealthSetValue(&cv_dummymenuplayer, 1); +} + +/*static void Dummymares_OnChange(void) +{ + if (!nightsrecords[cv_nextmap.value-1]) + { + CV_StealthSetValue(&cv_dummymares, 0); + return; + } + else + { + UINT8 mares = nightsrecords[cv_nextmap.value-1]->nummares; + + if (cv_dummymares.value < 0) + CV_StealthSetValue(&cv_dummymares, mares); + else if (cv_dummymares.value > mares) + CV_StealthSetValue(&cv_dummymares, 0); + } +}*/ + +char dummystaffname[22]; + +static void Dummystaff_OnChange(void) +{ + lumpnum_t l; + + dummystaffname[0] = '\0'; + + if ((l = W_CheckNumForName(va("%sS01",G_BuildMapName(cv_nextmap.value)))) == LUMPERROR) + { + CV_StealthSetValue(&cv_dummystaff, 0); + return; + } + else + { + char *temp = dummystaffname; + UINT8 numstaff = 1; + while (numstaff < 99 && (l = W_CheckNumForName(va("%sS%02u",G_BuildMapName(cv_nextmap.value),numstaff+1))) != LUMPERROR) + numstaff++; + + if (cv_dummystaff.value < 1) + CV_StealthSetValue(&cv_dummystaff, numstaff); + else if (cv_dummystaff.value > numstaff) + CV_StealthSetValue(&cv_dummystaff, 1); + + if ((l = W_CheckNumForName(va("%sS%02u",G_BuildMapName(cv_nextmap.value), cv_dummystaff.value))) == LUMPERROR) + return; // shouldn't happen but might as well check... + + G_UpdateStaffGhostName(l); + + while (*temp) + temp++; + + sprintf(temp, " - %d", cv_dummystaff.value); + } +} + +// Newgametype. Used for gametype changes. +static void Newgametype_OnChange(void) +{ + if (menuactive && cv_nextmap.value) + { + INT32 gt = cv_newgametype.value; + + if (!mapheaderinfo[cv_nextmap.value-1]) + P_AllocMapHeader((INT16)(cv_nextmap.value-1)); + + if (gt >= 0 && gt < gametypecount && !(mapheaderinfo[cv_nextmap.value-1]->typeoflevel & gametypetol[gt])) + CV_SetValue(&cv_nextmap, M_FindFirstMap(gt)); + } +} + +void Screenshot_option_Onchange(void) +{ + OP_ScreenshotOptionsMenu[op_screenshot_folder].status = + (cv_screenshot_option.value == 3 ? IT_CVAR|IT_STRING|IT_CV_STRING : IT_DISABLED); +} + +void Moviemode_mode_Onchange(void) +{ + INT32 i, cstart, cend; + for (i = op_screenshot_gif_start; i <= op_screenshot_apng_end; ++i) + OP_ScreenshotOptionsMenu[i].status = IT_DISABLED; + + switch (cv_moviemode.value) + { + case MM_GIF: + cstart = op_screenshot_gif_start; + cend = op_screenshot_gif_end; + break; + case MM_APNG: + cstart = op_screenshot_apng_start; + cend = op_screenshot_apng_end; + break; + default: + return; + } + for (i = cstart; i <= cend; ++i) + OP_ScreenshotOptionsMenu[i].status = IT_STRING|IT_CVAR; +} + +void Addons_option_Onchange(void) +{ + OP_AddonsOptionsMenu[op_addons_folder].status = + (cv_addons_option.value == 3 ? IT_CVAR|IT_STRING|IT_CV_STRING : IT_DISABLED); +} + +void Moviemode_option_Onchange(void) +{ + ; +} + +// ========================================================================== +// END ORGANIZATION STUFF. +// ========================================================================== + +// current menudef +menu_t *currentMenu = &MainDef; + +// ========================================================================= +// MENU PRESENTATION PARAMETER HANDLING (BACKGROUNDS) +// ========================================================================= + +// menu IDs are equal to current/prevMenu in most cases, except MN_SPECIAL when we don't want to operate on Message, Pause, etc. +UINT32 prevMenuId = 0; +UINT32 activeMenuId = 0; + +menupres_t menupres[NUMMENUTYPES]; + +void M_InitMenuPresTables(void) +{ + INT32 i; + + // Called in d_main before SOC can get to the tables + // Set menupres defaults + for (i = 0; i < NUMMENUTYPES; i++) + { + // so-called "undefined" + menupres[i].fadestrength = -1; + menupres[i].hidetitlepics = -1; // inherits global hidetitlepics + menupres[i].ttmode = TTMODE_NONE; + menupres[i].ttscale = UINT8_MAX; + menupres[i].ttname[0] = 0; + menupres[i].ttx = INT16_MAX; + menupres[i].tty = INT16_MAX; + menupres[i].ttloop = INT16_MAX; + menupres[i].tttics = UINT16_MAX; + menupres[i].enterwipe = -1; + menupres[i].exitwipe = -1; + menupres[i].bgcolor = -1; + menupres[i].titlescrollxspeed = INT32_MAX; + menupres[i].titlescrollyspeed = INT32_MAX; + menupres[i].bghide = true; + // default true + menupres[i].enterbubble = true; + menupres[i].exitbubble = true; + + if (i != MN_MAIN) + { + menupres[i].muslooping = true; + } + if (i == MN_SP_TIMEATTACK) + strncpy(menupres[i].musname, "_recat", 7); + else if (i == MN_SP_NIGHTSATTACK) + strncpy(menupres[i].musname, "_nitat", 7); + else if (i == MN_SP_MARATHON) + strncpy(menupres[i].musname, "spec8", 6); + else if (i == MN_SP_PLAYER || i == MN_SR_PLAYER) + strncpy(menupres[i].musname, "_chsel", 7); + else if (i == MN_SR_SOUNDTEST) + { + *menupres[i].musname = '\0'; + menupres[i].musstop = true; + } + } +} + +// ========================================================================= +// BASIC MENU HANDLING +// ========================================================================= + +static void M_ChangeCvar(INT32 choice) +{ + consvar_t *cv = (consvar_t *)currentMenu->menuitems[itemOn].itemaction; + + if (choice == -1) + { + if (cv == &cv_playercolor[0]) + { + SINT8 skinno = R_SkinAvailable(cv_chooseskin.string); + if (skinno != -1) + CV_SetValue(cv,skins[skinno].prefcolor); + return; + } + CV_Set(cv,cv->defaultvalue); + return; + } + + choice = (choice<<1) - 1; + + if (((currentMenu->menuitems[itemOn].status & IT_CVARTYPE) == IT_CV_SLIDER) + ||((currentMenu->menuitems[itemOn].status & IT_CVARTYPE) == IT_CV_INVISSLIDER) + ||((currentMenu->menuitems[itemOn].status & IT_CVARTYPE) == IT_CV_NOMOD)) + { + if (cv == &cv_digmusicvolume || cv == &cv_soundvolume) + { + choice *= 5; + } + + CV_SetValue(cv,cv->value+choice); + } + else if (cv->flags & CV_FLOAT) + { + char s[20]; + sprintf(s,"%f",FIXED_TO_FLOAT(cv->value)+(choice)*(1.0f/16.0f)); + CV_Set(cv,s); + } + else + { +#ifndef NONET + if (cv == &cv_nettimeout || cv == &cv_jointimeout) + choice *= (TICRATE/7); + else if (cv == &cv_maxsend) + choice *= 512; + else if (cv == &cv_maxping) + choice *= 50; +#endif + CV_AddValue(cv,choice); + } +} + +static boolean M_ChangeStringCvar(INT32 choice) +{ + consvar_t *cv = (consvar_t *)currentMenu->menuitems[itemOn].itemaction; + char buf[MAXSTRINGLENGTH]; + size_t len; + + if (shiftdown && choice >= 32 && choice <= 127) + choice = shiftxform[choice]; + + switch (choice) + { + case KEY_BACKSPACE: + len = strlen(cv->string); + if (len > 0) + { + S_StartSound(NULL,sfx_menu1); // Tails + M_Memcpy(buf, cv->string, len); + buf[len-1] = 0; + CV_Set(cv, buf); + } + return true; + case KEY_DEL: + if (cv->string[0]) + { + S_StartSound(NULL,sfx_menu1); // Tails + CV_Set(cv, ""); + } + return true; + default: + if (choice >= 32 && choice <= 127) + { + len = strlen(cv->string); + if (len < MAXSTRINGLENGTH - 1) + { + S_StartSound(NULL,sfx_menu1); // Tails + M_Memcpy(buf, cv->string, len); + buf[len++] = (char)choice; + buf[len] = 0; + CV_Set(cv, buf); + } + return true; + } + break; + } + return false; +} + +static void M_NextOpt(void) +{ + INT16 oldItemOn = itemOn; // prevent infinite loop + + do + { + if (itemOn + 1 > currentMenu->numitems - 1) + itemOn = 0; + else + itemOn++; + } while (oldItemOn != itemOn && (currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_SPACE); +} + +static void M_PrevOpt(void) +{ + INT16 oldItemOn = itemOn; // prevent infinite loop + + do + { + if (!itemOn) + itemOn = currentMenu->numitems - 1; + else + itemOn--; + } while (oldItemOn != itemOn && (currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_SPACE); +} + +// lock out further input in a tic when important buttons are pressed +// (in other words -- stop bullshit happening by mashing buttons in fades) +static boolean noFurtherInput = false; + +static void Command_Manual_f(void) +{ + if (modeattacking) + return; + M_StartControlPanel(); + M_Manual(INT32_MAX); + itemOn = 0; +} + +// +// M_Responder +// +boolean M_Responder(event_t *ev) +{ + INT32 ch = -1; +// INT32 i; + static tic_t joywait = 0, mousewait = 0; + static INT32 pjoyx = 0, pjoyy = 0; + static INT32 pmousex = 0, pmousey = 0; + static INT32 lastx = 0, lasty = 0; + void (*routine)(INT32 choice); // for some casting problem + + if (dedicated || (demo.playback && demo.title) + || gamestate == GS_INTRO || gamestate == GS_CUTSCENE || gamestate == GS_GAMEEND + || gamestate == GS_CREDITS || gamestate == GS_EVALUATION) + return false; + + if (noFurtherInput) + { + // Ignore input after enter/escape/other buttons + // (but still allow shift keyup so caps doesn't get stuck) + return false; + } + else if (ev->type == ev_keydown) + { + ch = ev->data1; + + // added 5-2-98 remap virtual keys (mouse & joystick buttons) + switch (ch) + { + case KEY_MOUSE1: + //case KEY_JOY1: + //case KEY_JOY1 + 2: + ch = KEY_ENTER; + break; + /*case KEY_JOY1 + 3: // Brake can function as 'n' for message boxes now. + ch = 'n'; + break;*/ + case KEY_MOUSE1 + 1: + //case KEY_JOY1 + 1: + ch = KEY_BACKSPACE; + break; + case KEY_HAT1: + ch = KEY_UPARROW; + break; + case KEY_HAT1 + 1: + ch = KEY_DOWNARROW; + break; + case KEY_HAT1 + 2: + ch = KEY_LEFTARROW; + break; + case KEY_HAT1 + 3: + ch = KEY_RIGHTARROW; + break; + } + } + else if (menuactive) + { + if (ev->type == ev_joystick && ev->data1 == 0 && joywait < I_GetTime()) + { + const INT32 jdeadzone = ((JOYAXISRANGE-1) * cv_deadzone[0].value) >> FRACBITS; + if (ev->data3 != INT32_MAX) + { + if (Joystick[0].bGamepadStyle || abs(ev->data3) > jdeadzone) + { + if (ev->data3 < 0 && pjoyy >= 0) + { + ch = KEY_UPARROW; + joywait = I_GetTime() + NEWTICRATE/7; + } + else if (ev->data3 > 0 && pjoyy <= 0) + { + ch = KEY_DOWNARROW; + joywait = I_GetTime() + NEWTICRATE/7; + } + pjoyy = ev->data3; + } + else + pjoyy = 0; + } + + if (ev->data2 != INT32_MAX) + { + if (Joystick[0].bGamepadStyle || abs(ev->data2) > jdeadzone) + { + if (ev->data2 < 0 && pjoyx >= 0) + { + ch = KEY_LEFTARROW; + joywait = I_GetTime() + NEWTICRATE/17; + } + else if (ev->data2 > 0 && pjoyx <= 0) + { + ch = KEY_RIGHTARROW; + joywait = I_GetTime() + NEWTICRATE/17; + } + pjoyx = ev->data2; + } + else + pjoyx = 0; + } + } + else if (ev->type == ev_mouse && mousewait < I_GetTime()) + { + pmousey += ev->data3; + if (pmousey < lasty-30) + { + ch = KEY_DOWNARROW; + mousewait = I_GetTime() + NEWTICRATE/7; + pmousey = lasty -= 30; + } + else if (pmousey > lasty + 30) + { + ch = KEY_UPARROW; + mousewait = I_GetTime() + NEWTICRATE/7; + pmousey = lasty += 30; + } + + pmousex += ev->data2; + if (pmousex < lastx - 30) + { + ch = KEY_LEFTARROW; + mousewait = I_GetTime() + NEWTICRATE/7; + pmousex = lastx -= 30; + } + else if (pmousex > lastx+30) + { + ch = KEY_RIGHTARROW; + mousewait = I_GetTime() + NEWTICRATE/7; + pmousex = lastx += 30; + } + } + } + + if (ch == -1) + return false; + else if (ch == gamecontrol[0][gc_systemmenu][0] || ch == gamecontrol[0][gc_systemmenu][1]) // allow remappable ESC key + ch = KEY_ESCAPE; + else if ((ch == gamecontrol[0][gc_accelerate][0] || ch == gamecontrol[0][gc_accelerate][1]) && ch >= KEY_MOUSE1) + ch = KEY_ENTER; + + // F-Keys + if (!menuactive) + { + noFurtherInput = true; + + switch (ch) + { + case KEY_F1: // Help key + Command_Manual_f(); + return true; + + case KEY_F2: // Empty + return true; + + case KEY_F3: // Toggle HUD + CV_SetValue(&cv_showhud, !cv_showhud.value); + return true; + + case KEY_F4: // Sound Volume + if (modeattacking) + return true; + M_StartControlPanel(); + M_Options(0); + currentMenu = &OP_SoundOptionsDef; + itemOn = 0; + return true; + +#ifndef DC + case KEY_F5: // Video Mode + if (modeattacking) + return true; + M_StartControlPanel(); + M_Options(0); + M_VideoModeMenu(0); + return true; +#endif + + case KEY_F6: // Empty + return true; + + case KEY_F7: // Options + if (modeattacking) + return true; + M_StartControlPanel(); + M_Options(0); + M_SetupNextMenu(&OP_MainDef); + return true; + + // Screenshots on F8 now handled elsewhere + // Same with Moviemode on F9 + + case KEY_F10: // Quit SRB2 + M_QuitSRB2(0); + return true; + + case KEY_F11: // Fullscreen + CV_AddValue(&cv_fullscreen, 1); + return true; + + // Spymode on F12 handled in game logic + + case KEY_ESCAPE: // Pop up menu + if (chat_on) + { + HU_clearChatChars(); + chat_on = false; + } + else + M_StartControlPanel(); + return true; + } + noFurtherInput = false; // turns out we didn't care + return false; + } + + if ((ch == gamecontrol[0][gc_brake][0] || ch == gamecontrol[0][gc_brake][1]) && ch >= KEY_MOUSE1) // do this here, otherwise brake opens the menu mid-game + ch = KEY_ESCAPE; + + routine = currentMenu->menuitems[itemOn].itemaction; + + // Handle menuitems which need a specific key handling + if (routine && (currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_KEYHANDLER) + { + if (shiftdown && ch >= 32 && ch <= 127) + ch = shiftxform[ch]; + routine(ch); + return true; + } + + if (currentMenu->menuitems[itemOn].status == IT_MSGHANDLER) + { + if (currentMenu->menuitems[itemOn].alphaKey != MM_EVENTHANDLER) + { + if (ch == ' ' || ch == 'n' || ch == 'y' || ch == KEY_ESCAPE || ch == KEY_ENTER) + { + if (routine) + routine(ch); + M_StopMessage(0); + noFurtherInput = true; + return true; + } + return true; + } + else + { + // dirty hack: for customising controls, I want only buttons/keys, not moves + if (ev->type == ev_mouse + || ev->type == ev_joystick + || ev->type == ev_joystick2 + || ev->type == ev_joystick3 + || ev->type == ev_joystick4) + return true; + if (routine) + { + void (*otherroutine)(event_t *sev) = currentMenu->menuitems[itemOn].itemaction; + otherroutine(ev); //Alam: what a hack + } + return true; + } + } + + // BP: one of the more big hack i have never made + if (routine && (currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_CVAR) + { + if ((currentMenu->menuitems[itemOn].status & IT_CVARTYPE) == IT_CV_STRING) + { + + if (shiftdown && ch >= 32 && ch <= 127) + ch = shiftxform[ch]; + if (M_ChangeStringCvar(ch)) + return true; + else + routine = NULL; + } + else + routine = M_ChangeCvar; + } + + if (currentMenu == &PlaybackMenuDef && !con_destlines) + { + playback_last_menu_interaction_leveltime = leveltime; + // Flip left/right with up/down for the playback menu, since it's a horizontal icon row. + switch (ch) + { + case KEY_LEFTARROW: ch = KEY_UPARROW; break; + case KEY_UPARROW: ch = KEY_RIGHTARROW; break; + case KEY_RIGHTARROW: ch = KEY_DOWNARROW; break; + case KEY_DOWNARROW: ch = KEY_LEFTARROW; break; + + // arbitrary keyboard shortcuts because fuck you + + case '\'': // toggle freecam + M_PlaybackToggleFreecam(0); + break; + + case ']': // ffw / advance frame (depends on if paused or not) + if (paused) + M_PlaybackAdvance(0); + else + M_PlaybackFastForward(0); + break; + + case '[': // rewind /backupframe, uses the same function + M_PlaybackRewind(0); + break; + + case '\\': // pause + M_PlaybackPause(0); + break; + + // viewpoints, an annoyance (tm) + case '-': // viewpoint minus + M_PlaybackSetViews(-1); // yeah lol. + break; + + case '=': // viewpoint plus + M_PlaybackSetViews(1); // yeah lol. + break; + + // switch viewpoints: + case '1': // viewpoint for p1 (also f12) + // maximum laziness: + if (!demo.freecam) + G_AdjustView(1, 1, true); + break; + case '2': // viewpoint for p2 + if (!demo.freecam) + G_AdjustView(2, 1, true); + break; + case '3': // viewpoint for p3 + if (!demo.freecam) + G_AdjustView(3, 1, true); + break; + case '4': // viewpoint for p4 + if (!demo.freecam) + G_AdjustView(4, 1, true); + break; + + default: break; + } + } + + // Keys usable within menu + switch (ch) + { + case KEY_DOWNARROW: + M_NextOpt(); + S_StartSound(NULL, sfx_menu1); + /*if (currentMenu == &SP_PlayerDef) + { + Z_Free(char_notes); + char_notes = NULL; + }*/ + return true; + + case KEY_UPARROW: + M_PrevOpt(); + S_StartSound(NULL, sfx_menu1); + /*if (currentMenu == &SP_PlayerDef) + { + Z_Free(char_notes); + char_notes = NULL; + }*/ + return true; + + case KEY_LEFTARROW: + if (routine && ((currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_ARROWS + || (currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_CVAR)) + { + if (currentMenu != &OP_SoundOptionsDef || itemOn > 3) + S_StartSound(NULL, sfx_menu1); + routine(0); + } + return true; + + case KEY_RIGHTARROW: + if (routine && ((currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_ARROWS + || (currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_CVAR)) + { + if (currentMenu != &OP_SoundOptionsDef || itemOn > 3) + S_StartSound(NULL, sfx_menu1); + routine(1); + } + return true; + + case KEY_ENTER: + noFurtherInput = true; + currentMenu->lastOn = itemOn; + + if (currentMenu == &PlaybackMenuDef) + { + boolean held = (boolean)playback_enterheld; + if (held) + return true; + playback_enterheld = 3; + } + + if (routine) + { + if (((currentMenu->menuitems[itemOn].status & IT_TYPE)==IT_CALL + || (currentMenu->menuitems[itemOn].status & IT_TYPE)==IT_SUBMENU) + && (currentMenu->menuitems[itemOn].status & IT_CALLTYPE)) + { + if (((currentMenu->menuitems[itemOn].status & IT_CALLTYPE) & IT_CALL_NOTMODIFIED) && majormods) + { + S_StartSound(NULL, sfx_menu1); + M_StartMessage(M_GetText("This cannot be done with complex addons\nor in a cheated game.\n\n(Press a key)\n"), NULL, MM_NOTHING); + return true; + } + } + S_StartSound(NULL, sfx_menu1); + switch (currentMenu->menuitems[itemOn].status & IT_TYPE) + { + case IT_CVAR: + case IT_ARROWS: + routine(1); // right arrow + break; + case IT_CALL: + routine(itemOn); + break; + case IT_SUBMENU: + currentMenu->lastOn = itemOn; + M_SetupNextMenu((menu_t *)currentMenu->menuitems[itemOn].itemaction); + break; + } + } + return true; + + case KEY_ESCAPE: + //case KEY_JOY1 + 2: + noFurtherInput = true; + currentMenu->lastOn = itemOn; + if (currentMenu->prevMenu) + { + //If we entered the game search menu, but didn't enter a game, + //make sure the game doesn't still think we're in a netgame. + if (!Playing() && netgame && multiplayer) + { + netgame = false; + multiplayer = false; + } + + if (currentMenu == &SP_TimeAttackDef) //|| currentMenu == &SP_NightsAttackDef + { + // D_StartTitle does its own wipe, since GS_TIMEATTACK is now a complete gamestate. + menuactive = false; + D_StartTitle(); + } + else + M_SetupNextMenu(currentMenu->prevMenu); + } + else + M_ClearMenus(true); + + return true; + + case KEY_BACKSPACE: + if ((currentMenu->menuitems[itemOn].status) == IT_CONTROL) + { + // detach any keys associated with the game control + G_ClearControlKeys(setupcontrols, currentMenu->menuitems[itemOn].alphaKey); + S_StartSound(NULL, sfx_shldls); + return true; + } + + if (routine && ((currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_ARROWS + || (currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_CVAR)) + { + consvar_t *cv = (consvar_t *)currentMenu->menuitems[itemOn].itemaction; + + if (cv == &cv_chooseskin + || cv == &cv_dummystaff + || cv == &cv_nextmap + || cv == &cv_newgametype) + return true; + + if (currentMenu != &OP_SoundOptionsDef || itemOn > 3) + S_StartSound(NULL, sfx_menu1); + routine(-1); + return true; + } + + // Why _does_ backspace go back anyway? + //currentMenu->lastOn = itemOn; + //if (currentMenu->prevMenu) + // M_SetupNextMenu(currentMenu->prevMenu); + return false; + + default: + break; + } + + return true; +} + +// special responder for demos +boolean M_DemoResponder(event_t *ev) +{ + + INT32 ch = -1; // cur event data + boolean eatinput = false; // :omnom: + + //should be accounted for beforehand but just to be safe... + if (!demo.playback || demo.title) + return false; + + if (noFurtherInput) + { + // Ignore input after enter/escape/other buttons + // (but still allow shift keyup so caps doesn't get stuck) + return false; + } + else if (ev->type == ev_keydown && !con_destlines) // not while the console is on please + { + ch = ev->data1; + // since this is ONLY for demos, there isn't MUCH for us to do. + // mirrored from m_responder + + switch (ch) + { + // arbitrary keyboard shortcuts because fuck you + + case '\'': // toggle freecam + M_PlaybackToggleFreecam(0); + eatinput = true; + break; + + case ']': // ffw / advance frame (depends on if paused or not) + if (paused) + M_PlaybackAdvance(0); + else + M_PlaybackFastForward(0); + eatinput = true; + break; + + case '[': // rewind /backupframe, uses the same function + M_PlaybackRewind(0); + break; + + case '\\': // pause + M_PlaybackPause(0); + eatinput = true; + break; + + // viewpoints, an annoyance (tm) + case '-': // viewpoint minus + M_PlaybackSetViews(-1); // yeah lol. + eatinput = true; + break; + + case '=': // viewpoint plus + M_PlaybackSetViews(1); // yeah lol. + eatinput = true; + break; + + // switch viewpoints: + case '1': // viewpoint for p1 (also f12) + // maximum laziness: + if (!demo.freecam) + G_AdjustView(1, 1, true); + break; + case '2': // viewpoint for p2 + if (!demo.freecam) + G_AdjustView(2, 1, true); + break; + case '3': // viewpoint for p3 + if (!demo.freecam) + G_AdjustView(3, 1, true); + break; + case '4': // viewpoint for p4 + if (!demo.freecam) + G_AdjustView(4, 1, true); + break; + + default: break; + } + + } + return eatinput; +} + + +// +// M_Drawer +// Called after the view has been rendered, +// but before it has been blitted. +// +void M_Drawer(void) +{ + if (currentMenu == &MessageDef) + menuactive = true; + + if (menuactive) + { + // now that's more readable with a faded background (yeah like Quake...) + if (!WipeInAction && currentMenu != &PlaybackMenuDef) // Replay playback has its own background + V_DrawFadeScreen(0xFF00, 16); + + if (currentMenu->drawroutine) + { + M_GetGametypeColor(); + currentMenu->drawroutine(); // call current menu Draw routine + } + + // Draw version down in corner + // ... but only in the MAIN MENU. I'm a picky bastard. + if (currentMenu == &MainDef) + { + if (customversionstring[0] != '\0') + { + V_DrawThinString(vid.dupx, vid.height - 20*vid.dupy, V_NOSCALESTART|V_TRANSLUCENT, "Mod version:"); + V_DrawThinString(vid.dupx, vid.height - 10*vid.dupy, V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, customversionstring); + } + else + { +#ifdef DEVELOP // Development -- show revision / branch info + V_DrawThinString(vid.dupx, vid.height - 20*vid.dupy, V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, compbranch); + V_DrawThinString(vid.dupx, vid.height - 10*vid.dupy, V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, comprevision); +#else // Regular build + V_DrawThinString(vid.dupx, vid.height - 10*vid.dupy, V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, va("%s", VERSIONSTRING)); +#endif + } + } + } + + // focus lost notification goes on top of everything, even the former everything + if (window_notinfocus && cv_showfocuslost.value) + { + M_DrawTextBox((BASEVIDWIDTH/2) - (60), (BASEVIDHEIGHT/2) - (16), 13, 2); + if (gamestate == GS_LEVEL && (P_AutoPause() || paused)) + V_DrawCenteredString(BASEVIDWIDTH/2, (BASEVIDHEIGHT/2) - (4), highlightflags, "Game Paused"); + else + V_DrawCenteredString(BASEVIDWIDTH/2, (BASEVIDHEIGHT/2) - (4), highlightflags, "Focus Lost"); + } +} + +// +// M_StartControlPanel +// +void M_StartControlPanel(void) +{ + // intro might call this repeatedly + if (menuactive) + { + CON_ToggleOff(); // move away console + return; + } + + menuactive = true; + + if (demo.playback) + { + currentMenu = &PlaybackMenuDef; + playback_last_menu_interaction_leveltime = leveltime; + } + else if (!Playing()) + { + // Secret menu! + //MainMenu[secrets].status = (M_AnySecretUnlocked()) ? (IT_STRING | IT_CALL) : (IT_DISABLED); + + currentMenu = &MainDef; +#ifdef TESTERS + itemOn = multiplr; +#else + itemOn = singleplr; +#endif + } + else if (modeattacking) + { + currentMenu = &MAPauseDef; + itemOn = mapause_continue; + } + else if (!(netgame || multiplayer)) // Single Player + { + if (gamestate != GS_LEVEL /*|| ultimatemode*/) // intermission, so gray out stuff. + { + SPauseMenu[spause_pandora].status = (M_SecretUnlocked(SECRET_PANDORA)) ? (IT_GRAYEDOUT) : (IT_DISABLED); + SPauseMenu[spause_retry].status = IT_GRAYEDOUT; + } + else + { + //INT32 numlives = 2; + + SPauseMenu[spause_pandora].status = (M_SecretUnlocked(SECRET_PANDORA)) ? (IT_STRING | IT_CALL) : (IT_DISABLED); + + /*if (&players[consoleplayer]) + { + numlives = players[consoleplayer].lives; + if (players[consoleplayer].playerstate != PST_LIVE) + ++numlives; + } + + // The list of things that can disable retrying is (was?) a little too complex + // for me to want to use the short if statement syntax + if (numlives <= 1 || G_IsSpecialStage(gamemap)) + SPauseMenu[spause_retry].status = (IT_GRAYEDOUT); + else*/ + SPauseMenu[spause_retry].status = (IT_STRING | IT_CALL); + } + + // We can always use level select though. :33 + //SPauseMenu[spause_levelselect].status = (gamecomplete) ? (IT_STRING | IT_CALL) : (IT_DISABLED); + + // And emblem hints. + SPauseMenu[spause_hints].status = (M_SecretUnlocked(SECRET_EMBLEMHINTS)) ? (IT_STRING | IT_CALL) : (IT_DISABLED); + + // Shift up Pandora's Box if both pandora and levelselect are active + /*if (SPauseMenu[spause_pandora].status != (IT_DISABLED) + && SPauseMenu[spause_levelselect].status != (IT_DISABLED)) + SPauseMenu[spause_pandora].alphaKey = 24; + else + SPauseMenu[spause_pandora].alphaKey = 32;*/ + + currentMenu = &SPauseDef; + itemOn = spause_continue; + } + else // multiplayer + { + MPauseMenu[mpause_switchmap].status = IT_DISABLED; + MPauseMenu[mpause_addons].status = IT_DISABLED; + MPauseMenu[mpause_scramble].status = IT_DISABLED; + MPauseMenu[mpause_psetupsplit].status = IT_DISABLED; + MPauseMenu[mpause_psetupsplit2].status = IT_DISABLED; + MPauseMenu[mpause_psetupsplit3].status = IT_DISABLED; + MPauseMenu[mpause_psetupsplit4].status = IT_DISABLED; + MPauseMenu[mpause_spectate].status = IT_DISABLED; + MPauseMenu[mpause_entergame].status = IT_DISABLED; + MPauseMenu[mpause_canceljoin].status = IT_DISABLED; + MPauseMenu[mpause_switchteam].status = IT_DISABLED; + MPauseMenu[mpause_switchspectate].status = IT_DISABLED; + MPauseMenu[mpause_psetup].status = IT_DISABLED; + MISC_ChangeTeamMenu[0].status = IT_DISABLED; + MISC_ChangeSpectateMenu[0].status = IT_DISABLED; + + // Reset these in case splitscreen messes things up + MPauseMenu[mpause_addons].alphaKey = 8; + MPauseMenu[mpause_scramble].alphaKey = 8; + MPauseMenu[mpause_switchmap].alphaKey = 24; + + MPauseMenu[mpause_switchteam].alphaKey = 48; + MPauseMenu[mpause_switchspectate].alphaKey = 48; + MPauseMenu[mpause_options].alphaKey = 64; + MPauseMenu[mpause_title].alphaKey = 80; + MPauseMenu[mpause_quit].alphaKey = 88; + + Dummymenuplayer_OnChange(); + + if ((server || IsPlayerAdmin(consoleplayer))) + { + MPauseMenu[mpause_switchmap].status = IT_STRING | IT_CALL; + MPauseMenu[mpause_addons].status = IT_STRING | IT_CALL; + if (G_GametypeHasTeams()) + MPauseMenu[mpause_scramble].status = IT_STRING | IT_SUBMENU; + } + + if (splitscreen) + { + MPauseMenu[mpause_psetupsplit].status = MPauseMenu[mpause_psetupsplit2].status = IT_STRING | IT_CALL; + MISC_ChangeTeamMenu[0].status = MISC_ChangeSpectateMenu[0].status = IT_STRING|IT_CVAR; + + if (netgame) + { + if (G_GametypeHasTeams()) + { + MPauseMenu[mpause_switchteam].status = IT_STRING | IT_SUBMENU; + MPauseMenu[mpause_switchteam].alphaKey += ((splitscreen+1) * 8); + MPauseMenu[mpause_options].alphaKey += 8; + MPauseMenu[mpause_title].alphaKey += 8; + MPauseMenu[mpause_quit].alphaKey += 8; + } + else if (G_GametypeHasSpectators()) + { + MPauseMenu[mpause_switchspectate].status = IT_STRING | IT_SUBMENU; + MPauseMenu[mpause_switchspectate].alphaKey += ((splitscreen+1) * 8); + MPauseMenu[mpause_options].alphaKey += 8; + MPauseMenu[mpause_title].alphaKey += 8; + MPauseMenu[mpause_quit].alphaKey += 8; + } + } + + if (splitscreen > 1) + { + MPauseMenu[mpause_psetupsplit3].status = IT_STRING | IT_CALL; + + MPauseMenu[mpause_options].alphaKey += 8; + MPauseMenu[mpause_title].alphaKey += 8; + MPauseMenu[mpause_quit].alphaKey += 8; + + if (splitscreen > 2) + { + MPauseMenu[mpause_psetupsplit4].status = IT_STRING | IT_CALL; + MPauseMenu[mpause_options].alphaKey += 8; + MPauseMenu[mpause_title].alphaKey += 8; + MPauseMenu[mpause_quit].alphaKey += 8; + } + } + } + else + { + MPauseMenu[mpause_psetup].status = IT_STRING | IT_CALL; + + if (G_GametypeHasTeams()) + MPauseMenu[mpause_switchteam].status = IT_STRING | IT_SUBMENU; + else if (G_GametypeHasSpectators()) + { + if (!players[consoleplayer].spectator) + MPauseMenu[mpause_spectate].status = IT_STRING | IT_CALL; + else if (players[consoleplayer].pflags & PF_WANTSTOJOIN) + MPauseMenu[mpause_canceljoin].status = IT_STRING | IT_CALL; + else + MPauseMenu[mpause_entergame].status = IT_STRING | IT_CALL; + } + else // in this odd case, we still want something to be on the menu even if it's useless + MPauseMenu[mpause_spectate].status = IT_GRAYEDOUT; + } + +#ifdef HAVE_DISCORDRPC + { + UINT8 i; + + for (i = 0; i < mpause_discordrequests; i++) + MPauseMenu[i].alphaKey -= 8; + + MPauseMenu[mpause_discordrequests].alphaKey = MPauseMenu[i].alphaKey; + + M_RefreshPauseMenu(); + } +#endif + + currentMenu = &MPauseDef; + itemOn = mpause_continue; + } + + CON_ToggleOff(); // move away console +} + +void M_EndModeAttackRun(void) +{ + M_ModeAttackEndGame(0); +} + +// +// M_ClearMenus +// +void M_ClearMenus(boolean callexitmenufunc) +{ + if (!menuactive) + return; + + if (currentMenu->quitroutine && callexitmenufunc && !currentMenu->quitroutine()) + return; // we can't quit this menu (also used to set parameter from the menu) + +#ifndef DC // Save the config file. I'm sick of crashing the game later and losing all my changes! + COM_BufAddText(va("saveconfig \"%s\" -silent\n", configfile)); +#endif //Alam: But not on the Dreamcast's VMUs + + if (currentMenu == &MessageDef) // Oh sod off! + currentMenu = &MainDef; // Not like it matters + menuactive = false; + hidetitlemap = false; +} + +// +// M_SetupNextMenu +// +void M_SetupNextMenu(menu_t *menudef) +{ + INT16 i; + + if (currentMenu->quitroutine) + { + // If you're going from a menu to itself, why are you running the quitroutine? You're not quitting it! -SH + if (currentMenu != menudef && !currentMenu->quitroutine()) + return; // we can't quit this menu (also used to set parameter from the menu) + } + currentMenu = menudef; + itemOn = currentMenu->lastOn; + + // in case of... + if (itemOn >= currentMenu->numitems) + itemOn = currentMenu->numitems - 1; + + // the curent item can be disabled, + // this code go up until an enabled item found + if ((currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_SPACE) + { + for (i = 0; i < currentMenu->numitems; i++) + { + if ((currentMenu->menuitems[i].status & IT_TYPE) != IT_SPACE) + { + itemOn = i; + break; + } + } + } + + hidetitlemap = false; +} + +// Guess I'll put this here, idk +boolean M_MouseNeeded(void) +{ + return false; +} + +// +// M_Ticker +// +void M_Ticker(void) +{ + // reset input trigger + noFurtherInput = false; + + if (dedicated) + return; + + if (--skullAnimCounter <= 0) + skullAnimCounter = 8; + + followertimer++; + + if (currentMenu == &PlaybackMenuDef) + { + if (playback_enterheld > 0) + playback_enterheld--; + } + else + playback_enterheld = 0; + + //added : 30-01-98 : test mode for five seconds + if (vidm_testingmode > 0) + { + // restore the previous video mode + if (--vidm_testingmode == 0) + setmodeneeded = vidm_previousmode + 1; + } + +#if defined (MASTERSERVER) && defined (HAVE_THREADS) + I_lock_mutex(&ms_ServerList_mutex); + { + if (ms_ServerList) + { + CL_QueryServerList(ms_ServerList); + free(ms_ServerList); + ms_ServerList = NULL; + } + } + I_unlock_mutex(ms_ServerList_mutex); +#endif +} + +// +// M_Init +// +void M_Init(void) +{ + UINT8 i; + + CV_RegisterVar(&cv_nextmap); + CV_RegisterVar(&cv_newgametype); + CV_RegisterVar(&cv_chooseskin); + CV_RegisterVar(&cv_autorecord); + + if (dedicated) + return; + + COM_AddCommand("manual", Command_Manual_f); + + // Menu hacks + CV_RegisterVar(&cv_dummymenuplayer); + CV_RegisterVar(&cv_dummyteam); + CV_RegisterVar(&cv_dummyspectate); + CV_RegisterVar(&cv_dummyscramble); + CV_RegisterVar(&cv_dummyrings); + CV_RegisterVar(&cv_dummylives); + CV_RegisterVar(&cv_dummystaff); + + CV_RegisterVar(&cv_dummygpdifficulty); + CV_RegisterVar(&cv_dummygpencore); + CV_RegisterVar(&cv_dummygpcup); + + 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)"); + quitmsg[QUITMSG1] = M_GetText("What would Tails say if\nhe saw you quitting the game?\n\n(Press 'Y' to quit)"); + quitmsg[QUITMSG2] = M_GetText("Hey!\nWhere do ya think you're goin'?\n\n(Press 'Y' to quit)"); + quitmsg[QUITMSG3] = M_GetText("Forget your studies!\nPlay some more!\n\n(Press 'Y' to quit)"); + quitmsg[QUITMSG4] = M_GetText("You're trying to say you\nlike Sonic R better than\nthis, aren't you?\n\n(Press 'Y' to quit)"); + quitmsg[QUITMSG5] = M_GetText("Don't leave yet -- there's a\nsuper emerald around that corner!\n\n(Press 'Y' to quit)"); + quitmsg[QUITMSG6] = M_GetText("You'd rather work than play?\n\n(Press 'Y' to quit)"); + quitmsg[QUITMSG7] = M_GetText("Go ahead and leave. See if I care...\n*sniffle*\n\n(Press 'Y' to quit)"); + + quitmsg[QUIT2MSG] = M_GetText("If you leave now,\nEggman will take over the world!\n\n(Press 'Y' to quit)"); + quitmsg[QUIT2MSG1] = M_GetText("On your mark,\nget set,\nhit the 'N' key!\n\n(Press 'Y' to quit)"); + quitmsg[QUIT2MSG2] = M_GetText("Aw c'mon, just\na few more laps!\n\n(Press 'Y' to quit)"); + quitmsg[QUIT2MSG3] = M_GetText("Did you get all those Chaos Emeralds?\n\n(Press 'Y' to quit)"); + quitmsg[QUIT2MSG4] = M_GetText("If you leave, I'll use\nmy Jawz on you!\n\n(Press 'Y' to quit)"); + quitmsg[QUIT2MSG5] = M_GetText("Don't go!\nYou might find the hidden\nlevels!\n\n(Press 'Y' to quit)"); + quitmsg[QUIT2MSG6] = M_GetText("Hit the 'N' key, Sonic!\nThe 'N' key!\n\n(Press 'Y' to quit)"); + + quitmsg[QUIT3MSG] = M_GetText("Are you really going to give up?\nWe certainly would never give you up.\n\n(Press 'Y' to quit)"); + quitmsg[QUIT3MSG1] = M_GetText("Come on, just ONE more netgame!\n\n(Press 'Y' to quit)"); + quitmsg[QUIT3MSG2] = M_GetText("Press 'N' to unlock\nthe Golden Kart!\n\n(Press 'Y' to quit)"); + quitmsg[QUIT3MSG3] = M_GetText("Couldn't handle\nthe banana meta?\n\n(Press 'Y' to quit)"); + quitmsg[QUIT3MSG4] = M_GetText("Every time you press 'Y', an\nSRB2Kart Developer cries...\n\n(Press 'Y' to quit)"); + quitmsg[QUIT3MSG5] = M_GetText("You'll be back to play soon, though...\n...right?\n\n(Press 'Y' to quit)"); + quitmsg[QUIT3MSG6] = M_GetText("Aww, is Eggman's Nightclub too\ndifficult for you?\n\n(Press 'Y' to quit)"); + + // Setup PlayerMenu table + for (i = 0; i < MAXSKINS; i++) + { + PlayerMenu[i].status = (i == 0 ? IT_CALL : IT_DISABLED); + PlayerMenu[i].patch = PlayerMenu[i].text = NULL; + PlayerMenu[i].itemaction = M_ChoosePlayer; + PlayerMenu[i].alphaKey = 0; + } + +#ifdef HWRENDER + // Permanently hide some options based on render mode + if (rendermode == render_soft) + OP_VideoOptionsMenu[op_video_ogl].status = IT_DISABLED; +#endif + +#ifndef NONET + CV_RegisterVar(&cv_serversort); +#endif +} + +void M_InitCharacterTables(void) +{ + UINT8 i; + + // Setup PlayerMenu table + for (i = 0; i < MAXSKINS; i++) + { + PlayerMenu[i].status = (i < 4 ? IT_CALL : IT_DISABLED); + PlayerMenu[i].patch = PlayerMenu[i].text = NULL; + PlayerMenu[i].itemaction = M_ChoosePlayer; + PlayerMenu[i].alphaKey = 0; + } + + // Setup description table + for (i = 0; i < MAXSKINS; i++) + { + if (i == 0) + { + strcpy(description[i].notes, "\x82Sonic\x80 is the fastest of the three, but also the hardest to control. Beginners beware, but experts will find Sonic very powerful.\n\n\x82""Ability:\x80 Speed Thok\nDouble jump to zoom forward with a huge burst of speed.\n\n\x82Tip:\x80 Simply letting go of forward does not slow down in SRB2. To slow down, hold the opposite direction."); + strcpy(description[i].picname, ""); + strcpy(description[i].skinname, "sonic"); + } + else if (i == 1) + { + strcpy(description[i].notes, "\x82Tails\x80 is the most mobile of the three, but has the slowest speed. Because of his mobility, he's well-\nsuited to beginners.\n\n\x82""Ability:\x80 Fly\nDouble jump to start flying for a limited time. Repetitively hit the jump button to ascend.\n\n\x82Tip:\x80 To quickly descend while flying, hit the spin button."); + strcpy(description[i].picname, ""); + strcpy(description[i].skinname, "tails"); + } + else if (i == 2) + { + strcpy(description[i].notes, "\x82Knuckles\x80 is well-\nrounded and can destroy breakable walls simply by touching them, but he can't jump as high as the other two.\n\n\x82""Ability:\x80 Glide & Climb\nDouble jump to glide in the air as long as jump is held. Glide into a wall to climb it.\n\n\x82Tip:\x80 Press spin while climbing to jump off the wall; press jump instead to jump off\nand face away from\nthe wall."); + strcpy(description[i].picname, ""); + strcpy(description[i].skinname, "knuckles"); + } + else if (i == 3) + { + strcpy(description[i].notes, "\x82Sonic & Tails\x80 team up to take on Dr. Eggman!\nControl Sonic while Tails desperately struggles to keep up.\n\nPlayer 2 can control Tails directly by setting the controls in the options menu.\nTails's directional controls are relative to Player 1's camera.\n\nTails can pick up Sonic while flying and carry him around."); + strcpy(description[i].picname, "CHRS&T"); + strcpy(description[i].skinname, "sonic&tails"); + } + else + { + strcpy(description[i].notes, "???"); + strcpy(description[i].picname, ""); + strcpy(description[i].skinname, ""); + } + } +} + +// ========================================================================== +// SPECIAL MENU OPTION DRAW ROUTINES GO HERE +// ========================================================================== + +// Converts a string into question marks. +// Used for the secrets menu, to hide yet-to-be-unlocked stuff. +static const char *M_CreateSecretMenuOption(const char *str) +{ + static char qbuf[32]; + int i; + + for (i = 0; i < 31; ++i) + { + if (!str[i]) + { + qbuf[i] = '\0'; + return qbuf; + } + else if (str[i] != ' ') + qbuf[i] = '?'; + else + qbuf[i] = ' '; + } + + qbuf[31] = '\0'; + return qbuf; +} + +static void M_DrawThermo(INT32 x, INT32 y, consvar_t *cv) +{ + INT32 xx = x, i; + lumpnum_t leftlump, rightlump, centerlump[2], cursorlump; + patch_t *p; + + leftlump = W_GetNumForName("M_THERML"); + rightlump = W_GetNumForName("M_THERMR"); + centerlump[0] = W_GetNumForName("M_THERMM"); + centerlump[1] = W_GetNumForName("M_THERMM"); + cursorlump = W_GetNumForName("M_THERMO"); + + V_DrawScaledPatch(xx, y, 0, p = W_CachePatchNum(leftlump,PU_CACHE)); + xx += SHORT(p->width) - SHORT(p->leftoffset); + for (i = 0; i < 16; i++) + { + V_DrawScaledPatch(xx, y, 0, W_CachePatchNum(centerlump[i & 1], PU_CACHE)); + xx += 8; + } + V_DrawScaledPatch(xx, y, 0, W_CachePatchNum(rightlump, PU_CACHE)); + + xx = (cv->value - cv->PossibleValue[0].value) * (15*8) / + (cv->PossibleValue[1].value - cv->PossibleValue[0].value); + + V_DrawScaledPatch((x + 8) + xx, y, 0, W_CachePatchNum(cursorlump, PU_CACHE)); +} + +// A smaller 'Thermo', with range given as percents (0-100) +static void M_DrawSlider(INT32 x, INT32 y, const consvar_t *cv, boolean ontop) +{ + INT32 i; + INT32 range; + patch_t *p; + + for (i = 0; cv->PossibleValue[i+1].strvalue; i++); + + x = BASEVIDWIDTH - x - SLIDER_WIDTH; + + if (ontop) + { + V_DrawCharacter(x - 16 - (skullAnimCounter/5), y, + '\x1C' | highlightflags, false); // left arrow + V_DrawCharacter(x+(SLIDER_RANGE*8) + 8 + (skullAnimCounter/5), y, + '\x1D' | highlightflags, false); // right arrow + } + + if ((range = atoi(cv->defaultvalue)) != cv->value) + { + range = ((range - cv->PossibleValue[0].value) * 100 / + (cv->PossibleValue[1].value - cv->PossibleValue[0].value)); + + if (range < 0) + range = 0; + if (range > 100) + range = 100; + + // draw the default + p = W_CachePatchName("M_SLIDEC", PU_CACHE); + V_DrawScaledPatch(x - 4 + (((SLIDER_RANGE)*8 + 4)*range)/100, y, 0, p); + } + + V_DrawScaledPatch(x - 8, y, 0, W_CachePatchName("M_SLIDEL", PU_CACHE)); + + p = W_CachePatchName("M_SLIDEM", PU_CACHE); + for (i = 0; i < SLIDER_RANGE; i++) + V_DrawScaledPatch (x+i*8, y, 0,p); + + p = W_CachePatchName("M_SLIDER", PU_CACHE); + V_DrawScaledPatch(x+SLIDER_RANGE*8, y, 0, p); + + range = ((cv->value - cv->PossibleValue[0].value) * 100 / + (cv->PossibleValue[1].value - cv->PossibleValue[0].value)); + + if (range < 0) + range = 0; + if (range > 100) + range = 100; + + // draw the slider cursor + p = W_CachePatchName("M_SLIDEC", PU_CACHE); + V_DrawScaledPatch(x - 4 + (((SLIDER_RANGE)*8 + 4)*range)/100, y, 0, p); +} + +// +// Draw a textbox, like Quake does, because sometimes it's difficult +// to read the text with all the stuff in the background... +// +void M_DrawTextBox(INT32 x, INT32 y, INT32 width, INT32 boxlines) +{ + // Solid color textbox. + V_DrawFill(x+5, y+5, width*8+6, boxlines*8+6, 159); + //V_DrawFill(x+8, y+8, width*8, boxlines*8, 31); +/* + patch_t *p; + INT32 cx, cy, n; + INT32 step, boff; + + step = 8; + boff = 8; + + // draw left side + cx = x; + cy = y; + V_DrawScaledPatch(cx, cy, 0, W_CachePatchNum(viewborderlump[BRDR_TL], PU_CACHE)); + cy += boff; + p = W_CachePatchNum(viewborderlump[BRDR_L], PU_CACHE); + for (n = 0; n < boxlines; n++) + { + V_DrawScaledPatch(cx, cy, V_WRAPY, p); + cy += step; + } + V_DrawScaledPatch(cx, cy, 0, W_CachePatchNum(viewborderlump[BRDR_BL], PU_CACHE)); + + // draw middle + V_DrawFlatFill(x + boff, y + boff, width*step, boxlines*step, st_borderpatchnum); + + cx += boff; + cy = y; + while (width > 0) + { + V_DrawScaledPatch(cx, cy, 0, W_CachePatchNum(viewborderlump[BRDR_T], PU_CACHE)); + V_DrawScaledPatch(cx, y + boff + boxlines*step, 0, W_CachePatchNum(viewborderlump[BRDR_B], PU_CACHE)); + width--; + cx += step; + } + + // draw right side + cy = y; + V_DrawScaledPatch(cx, cy, 0, W_CachePatchNum(viewborderlump[BRDR_TR], PU_CACHE)); + cy += boff; + p = W_CachePatchNum(viewborderlump[BRDR_R], PU_CACHE); + for (n = 0; n < boxlines; n++) + { + V_DrawScaledPatch(cx, cy, V_WRAPY, p); + cy += step; + } + V_DrawScaledPatch(cx, cy, 0, W_CachePatchNum(viewborderlump[BRDR_BR], PU_CACHE)); +*/ +} + +// +// Draw border for the savegame description +// +/*static void M_DrawSaveLoadBorder(INT32 x,INT32 y) +{ + INT32 i; + + V_DrawScaledPatch (x-8,y+7,0,W_CachePatchName("M_LSLEFT",PU_CACHE)); + + for (i = 0;i < 24;i++) + { + V_DrawScaledPatch (x,y+7,0,W_CachePatchName("M_LSCNTR",PU_CACHE)); + x += 8; + } + + V_DrawScaledPatch (x,y+7,0,W_CachePatchName("M_LSRGHT",PU_CACHE)); +}*/ + +// horizontally centered text +static void M_CentreText(INT32 y, const char *string) +{ + INT32 x; + //added : 02-02-98 : centre on 320, because V_DrawString centers on vid.width... + x = (BASEVIDWIDTH - V_StringWidth(string, V_OLDSPACING))>>1; + V_DrawString(x,y,V_OLDSPACING,string); +} + +// +// M_DrawMapEmblems +// +// used by pause & statistics to draw a row of emblems for a map +// +static void M_DrawMapEmblems(INT32 mapnum, INT32 x, INT32 y) +{ + UINT8 lasttype = UINT8_MAX, curtype; + emblem_t *emblem = M_GetLevelEmblems(mapnum); + + while (emblem) + { + switch (emblem->type) + { + case ET_TIME: //case ET_SCORE: case ET_RINGS: + curtype = 1; break; + /*case ET_NGRADE: case ET_NTIME: + curtype = 2; break;*/ + default: + curtype = 0; break; + } + + // Shift over if emblem is of a different discipline + if (lasttype != UINT8_MAX && lasttype != curtype) + x -= 4; + lasttype = curtype; + + if (emblem->collected) + V_DrawSmallMappedPatch(x, y, 0, W_CachePatchName(M_GetEmblemPatch(emblem, false), PU_CACHE), + R_GetTranslationColormap(TC_DEFAULT, M_GetEmblemColor(emblem), GTC_MENUCACHE)); + else + V_DrawSmallScaledPatch(x, y, 0, W_CachePatchName("NEEDIT", PU_CACHE)); + + emblem = M_GetLevelEmblems(-1); + x -= 8; + } +} + +static void M_DrawMenuTitle(void) +{ + if (currentMenu->menutitlepic) + { + patch_t *p = W_CachePatchName(currentMenu->menutitlepic, PU_CACHE); + + if (p->height > 24) // title is larger than normal + { + INT32 xtitle = (BASEVIDWIDTH - (SHORT(p->width)/2))/2; + INT32 ytitle = (30 - (SHORT(p->height)/2))/2; + + if (xtitle < 0) + xtitle = 0; + if (ytitle < 0) + ytitle = 0; + + V_DrawSmallScaledPatch(xtitle, ytitle, 0, p); + } + else + { + INT32 xtitle = (BASEVIDWIDTH - SHORT(p->width))/2; + INT32 ytitle = (30 - SHORT(p->height))/2; + + if (xtitle < 0) + xtitle = 0; + if (ytitle < 0) + ytitle = 0; + + V_DrawScaledPatch(xtitle, ytitle, 0, p); + } + } +} + +static void M_DrawGenericMenu(void) +{ + INT32 x, y, w, i, cursory = 0; + + // DRAW MENU + x = currentMenu->x; + y = currentMenu->y; + + // draw title (or big pic) + M_DrawMenuTitle(); + + for (i = 0; i < currentMenu->numitems; i++) + { + if (i == itemOn) + cursory = y; + switch (currentMenu->menuitems[i].status & IT_DISPLAY) + { + case IT_PATCH: + if (currentMenu->menuitems[i].patch && currentMenu->menuitems[i].patch[0]) + { + if (currentMenu->menuitems[i].status & IT_CENTER) + { + patch_t *p; + p = W_CachePatchName(currentMenu->menuitems[i].patch, PU_CACHE); + V_DrawScaledPatch((BASEVIDWIDTH - SHORT(p->width))/2, y, 0, p); + } + else + { + V_DrawScaledPatch(x, y, 0, + W_CachePatchName(currentMenu->menuitems[i].patch, PU_CACHE)); + } + } + /* FALLTHRU */ + case IT_NOTHING: + case IT_DYBIGSPACE: + y = currentMenu->y+currentMenu->menuitems[i].alphaKey;//+= LINEHEIGHT; + break; + case IT_BIGSLIDER: + M_DrawThermo(x, y, (consvar_t *)currentMenu->menuitems[i].itemaction); + y += LINEHEIGHT; + break; + case IT_STRING: + case IT_WHITESTRING: + if (currentMenu->menuitems[i].alphaKey) + y = currentMenu->y+currentMenu->menuitems[i].alphaKey; + if (i == itemOn) + cursory = y; + + if ((currentMenu->menuitems[i].status & IT_DISPLAY)==IT_STRING) + V_DrawString(x, y, 0, currentMenu->menuitems[i].text); + else + V_DrawString(x, y, highlightflags, currentMenu->menuitems[i].text); + + // Cvar specific handling + switch (currentMenu->menuitems[i].status & IT_TYPE) + case IT_CVAR: + { + consvar_t *cv = (consvar_t *)currentMenu->menuitems[i].itemaction; + switch (currentMenu->menuitems[i].status & IT_CVARTYPE) + { + case IT_CV_SLIDER: + M_DrawSlider(x, y, cv, (i == itemOn)); + case IT_CV_NOPRINT: // color use this + case IT_CV_INVISSLIDER: // monitor toggles use this + break; + case IT_CV_STRING: + M_DrawTextBox(x, y + 4, MAXSTRINGLENGTH, 1); + V_DrawString(x + 8, y + 12, V_ALLOWLOWERCASE, cv->string); + if (skullAnimCounter < 4 && i == itemOn) + V_DrawCharacter(x + 8 + V_StringWidth(cv->string, 0), y + 12, + '_' | 0x80, false); + y += 16; + break; + default: + w = V_StringWidth(cv->string, 0); + V_DrawString(BASEVIDWIDTH - x - w, y, + ((cv->flags & CV_CHEAT) && !CV_IsSetToDefault(cv) ? warningflags : highlightflags), cv->string); + if (i == itemOn) + { + V_DrawCharacter(BASEVIDWIDTH - x - 10 - w - (skullAnimCounter/5), y, + '\x1C' | highlightflags, false); // left arrow + V_DrawCharacter(BASEVIDWIDTH - x + 2 + (skullAnimCounter/5), y, + '\x1D' | highlightflags, false); // right arrow + } + break; + } + break; + } + y += STRINGHEIGHT; + break; + case IT_STRING2: + V_DrawString(x, y, 0, currentMenu->menuitems[i].text); + /* FALLTHRU */ + case IT_DYLITLSPACE: + y += SMALLLINEHEIGHT; + break; + case IT_GRAYPATCH: + if (currentMenu->menuitems[i].patch && currentMenu->menuitems[i].patch[0]) + V_DrawMappedPatch(x, y, 0, + W_CachePatchName(currentMenu->menuitems[i].patch,PU_CACHE), graymap); + y += LINEHEIGHT; + break; + case IT_TRANSTEXT: + if (currentMenu->menuitems[i].alphaKey) + y = currentMenu->y+currentMenu->menuitems[i].alphaKey; + /* FALLTHRU */ + case IT_TRANSTEXT2: + V_DrawString(x, y, V_TRANSLUCENT, currentMenu->menuitems[i].text); + y += SMALLLINEHEIGHT; + break; + case IT_QUESTIONMARKS: + if (currentMenu->menuitems[i].alphaKey) + y = currentMenu->y+currentMenu->menuitems[i].alphaKey; + + V_DrawString(x, y, V_TRANSLUCENT|V_OLDSPACING, M_CreateSecretMenuOption(currentMenu->menuitems[i].text)); + y += SMALLLINEHEIGHT; + break; + case IT_HEADERTEXT: // draws 16 pixels to the left, in yellow text + if (currentMenu->menuitems[i].alphaKey) + y = currentMenu->y+currentMenu->menuitems[i].alphaKey; + + V_DrawString(x-16, y, highlightflags, currentMenu->menuitems[i].text); + y += SMALLLINEHEIGHT; + break; + } + } + + // DRAW THE SKULL CURSOR + if (((currentMenu->menuitems[itemOn].status & IT_DISPLAY) == IT_PATCH) + || ((currentMenu->menuitems[itemOn].status & IT_DISPLAY) == IT_NOTHING)) + { + V_DrawScaledPatch(currentMenu->x + SKULLXOFF, cursory - 5, 0, + W_CachePatchName("M_CURSOR", PU_CACHE)); + } + else + { + V_DrawScaledPatch(currentMenu->x - 24, cursory, 0, + W_CachePatchName("M_CURSOR", PU_CACHE)); + V_DrawString(currentMenu->x, cursory, highlightflags, currentMenu->menuitems[itemOn].text); + } +} + +static void M_DrawGenericBackgroundMenu(void) +{ + V_DrawPatchFill(W_CachePatchName("SRB2BACK", PU_CACHE)); + M_DrawGenericMenu(); +} + +static void M_DrawPauseMenu(void) +{ +#if 0 + if (!netgame && !multiplayer && (gamestate == GS_LEVEL || gamestate == GS_INTERMISSION || gamestate == GS_VOTING)) + { + emblem_t *emblem_detail[3] = {NULL, NULL, NULL}; + char emblem_text[3][20]; + INT32 i; + + M_DrawTextBox(27, 16, 32, 6); + + // Draw any and all emblems at the top. + M_DrawMapEmblems(gamemap, 272, 28); + + if (strlen(mapheaderinfo[gamemap-1]->zonttl) > 0) + { + if (mapheaderinfo[gamemap-1]->actnum > 0) + V_DrawString(40, 28, highlightflags, va("%s %s %d", mapheaderinfo[gamemap-1]->lvlttl, mapheaderinfo[gamemap-1]->zonttl, mapheaderinfo[gamemap-1]->actnum)); + else + V_DrawString(40, 28, highlightflags, va("%s %s", mapheaderinfo[gamemap-1]->lvlttl, mapheaderinfo[gamemap-1]->zonttl)); + } + else + { + if (mapheaderinfo[gamemap-1]->actnum > 0) + V_DrawString(40, 28, highlightflags, va("%s %d", mapheaderinfo[gamemap-1]->lvlttl, mapheaderinfo[gamemap-1]->actnum)); + else + V_DrawString(40, 28, highlightflags, mapheaderinfo[gamemap-1]->lvlttl); + } + + // Set up the detail boxes. + { + emblem_t *emblem = M_GetLevelEmblems(gamemap); + while (emblem) + { + INT32 emblemslot; + char targettext[9], currenttext[9]; + + switch (emblem->type) + { + /*case ET_SCORE: + snprintf(targettext, 9, "%d", emblem->var); + snprintf(currenttext, 9, "%u", G_GetBestScore(gamemap)); + + targettext[8] = 0; + currenttext[8] = 0; + + emblemslot = 0; + break;*/ + case ET_TIME: + emblemslot = emblem->var; // dumb hack + snprintf(targettext, 9, "%i:%02i.%02i", + G_TicsToMinutes((tic_t)emblemslot, false), + G_TicsToSeconds((tic_t)emblemslot), + G_TicsToCentiseconds((tic_t)emblemslot)); + + emblemslot = (INT32)G_GetBestTime(gamemap); // dumb hack pt ii + if ((tic_t)emblemslot == UINT32_MAX) + snprintf(currenttext, 9, "-:--.--"); + else + snprintf(currenttext, 9, "%i:%02i.%02i", + G_TicsToMinutes((tic_t)emblemslot, false), + G_TicsToSeconds((tic_t)emblemslot), + G_TicsToCentiseconds((tic_t)emblemslot)); + + targettext[8] = 0; + currenttext[8] = 0; + + emblemslot = 1; + break; + /*case ET_RINGS: + snprintf(targettext, 9, "%d", emblem->var); + snprintf(currenttext, 9, "%u", G_GetBestRings(gamemap)); + + targettext[8] = 0; + currenttext[8] = 0; + + emblemslot = 2; + break; + case ET_NGRADE: + snprintf(targettext, 9, "%u", P_GetScoreForGrade(gamemap, 0, emblem->var)); + snprintf(currenttext, 9, "%u", G_GetBestNightsScore(gamemap, 0)); + + targettext[8] = 0; + currenttext[8] = 0; + + emblemslot = 1; + break; + case ET_NTIME: + emblemslot = emblem->var; // dumb hack pt iii + snprintf(targettext, 9, "%i:%02i.%02i", + G_TicsToMinutes((tic_t)emblemslot, false), + G_TicsToSeconds((tic_t)emblemslot), + G_TicsToCentiseconds((tic_t)emblemslot)); + + emblemslot = (INT32)G_GetBestNightsTime(gamemap, 0); // dumb hack pt iv + if ((tic_t)emblemslot == UINT32_MAX) + snprintf(currenttext, 9, "-:--.--"); + else + snprintf(currenttext, 9, "%i:%02i.%02i", + G_TicsToMinutes((tic_t)emblemslot, false), + G_TicsToSeconds((tic_t)emblemslot), + G_TicsToCentiseconds((tic_t)emblemslot)); + + targettext[8] = 0; + currenttext[8] = 0; + + emblemslot = 2; + break;*/ + default: + goto bademblem; + } + if (emblem_detail[emblemslot]) + goto bademblem; + + emblem_detail[emblemslot] = emblem; + snprintf(emblem_text[emblemslot], 20, "%8s /%8s", currenttext, targettext); + emblem_text[emblemslot][19] = 0; + + bademblem: + emblem = M_GetLevelEmblems(-1); + } + } + for (i = 0; i < 3; ++i) + { + emblem_t *emblem = emblem_detail[i]; + if (!emblem) + continue; + + if (emblem->collected) + V_DrawSmallMappedPatch(40, 44 + (i*8), 0, W_CachePatchName(M_GetEmblemPatch(emblem, false), PU_CACHE), + R_GetTranslationColormap(TC_DEFAULT, M_GetEmblemColor(emblem), GTC_MENUCACHE)); + else + V_DrawSmallScaledPatch(40, 44 + (i*8), 0, W_CachePatchName("NEEDIT", PU_CACHE)); + + switch (emblem->type) + { + /*case ET_SCORE: + case ET_NGRADE: + V_DrawString(56, 44 + (i*8), highlightflags, "SCORE:"); + break;*/ + case ET_TIME: + //case ET_NTIME: + V_DrawString(56, 44 + (i*8), highlightflags, "TIME:"); + break; + /*case ET_RINGS: + V_DrawString(56, 44 + (i*8), highlightflags, "RINGS:"); + break;*/ + } + V_DrawRightAlignedString(284, 44 + (i*8), V_MONOSPACE, emblem_text[i]); + } + } +#endif + +#ifdef HAVE_DISCORDRPC + // kind of hackily baked in here + if (currentMenu == &MPauseDef && discordRequestList != NULL) + { + const tic_t freq = TICRATE/2; + + if ((leveltime % freq) >= freq/2) + { + V_DrawFixedPatch(204 * FRACUNIT, + (currentMenu->y + MPauseMenu[mpause_discordrequests].alphaKey - 1) * FRACUNIT, + FRACUNIT, + 0, + W_CachePatchName("K_REQUE2", PU_CACHE), + NULL + ); + } + } +#endif + + M_DrawGenericMenu(); +} + +static void M_DrawCenteredMenu(void) +{ + INT32 x, y, i, cursory = 0; + + // DRAW MENU + x = currentMenu->x; + y = currentMenu->y; + + // draw title (or big pic) + M_DrawMenuTitle(); + + for (i = 0; i < currentMenu->numitems; i++) + { + if (i == itemOn) + cursory = y; + switch (currentMenu->menuitems[i].status & IT_DISPLAY) + { + case IT_PATCH: + if (currentMenu->menuitems[i].patch && currentMenu->menuitems[i].patch[0]) + { + if (currentMenu->menuitems[i].status & IT_CENTER) + { + patch_t *p; + p = W_CachePatchName(currentMenu->menuitems[i].patch, PU_CACHE); + V_DrawScaledPatch((BASEVIDWIDTH - SHORT(p->width))/2, y, 0, p); + } + else + { + V_DrawScaledPatch(x, y, 0, + W_CachePatchName(currentMenu->menuitems[i].patch, PU_CACHE)); + } + } + /* FALLTHRU */ + case IT_NOTHING: + case IT_DYBIGSPACE: + y += LINEHEIGHT; + break; + case IT_BIGSLIDER: + M_DrawThermo(x, y, (consvar_t *)currentMenu->menuitems[i].itemaction); + y += LINEHEIGHT; + break; + case IT_STRING: + case IT_WHITESTRING: + if (currentMenu->menuitems[i].alphaKey) + y = currentMenu->y+currentMenu->menuitems[i].alphaKey; + if (i == itemOn) + cursory = y; + + if ((currentMenu->menuitems[i].status & IT_DISPLAY)==IT_STRING) + V_DrawCenteredString(x, y, 0, currentMenu->menuitems[i].text); + else + V_DrawCenteredString(x, y, highlightflags, currentMenu->menuitems[i].text); + + // Cvar specific handling + switch(currentMenu->menuitems[i].status & IT_TYPE) + case IT_CVAR: + { + consvar_t *cv = (consvar_t *)currentMenu->menuitems[i].itemaction; + switch(currentMenu->menuitems[i].status & IT_CVARTYPE) + { + case IT_CV_SLIDER: + M_DrawSlider(x, y, cv, (i == itemOn)); + case IT_CV_NOPRINT: // color use this + break; + case IT_CV_STRING: + M_DrawTextBox(x, y + 4, MAXSTRINGLENGTH, 1); + V_DrawString(x + 8, y + 12, V_ALLOWLOWERCASE, cv->string); + if (skullAnimCounter < 4 && i == itemOn) + V_DrawCharacter(x + 8 + V_StringWidth(cv->string, 0), y + 12, + '_' | 0x80, false); + y += 16; + break; + default: + V_DrawString(BASEVIDWIDTH - x - V_StringWidth(cv->string, 0), y, + ((cv->flags & CV_CHEAT) && !CV_IsSetToDefault(cv) ? warningflags : highlightflags), cv->string); + break; + } + break; + } + y += STRINGHEIGHT; + break; + case IT_STRING2: + V_DrawCenteredString(x, y, 0, currentMenu->menuitems[i].text); + /* FALLTHRU */ + case IT_DYLITLSPACE: + y += SMALLLINEHEIGHT; + break; + case IT_QUESTIONMARKS: + if (currentMenu->menuitems[i].alphaKey) + y = currentMenu->y+currentMenu->menuitems[i].alphaKey; + + V_DrawCenteredString(x, y, V_TRANSLUCENT|V_OLDSPACING, M_CreateSecretMenuOption(currentMenu->menuitems[i].text)); + y += SMALLLINEHEIGHT; + break; + case IT_GRAYPATCH: + if (currentMenu->menuitems[i].patch && currentMenu->menuitems[i].patch[0]) + V_DrawMappedPatch(x, y, 0, + W_CachePatchName(currentMenu->menuitems[i].patch,PU_CACHE), graymap); + y += LINEHEIGHT; + break; + case IT_TRANSTEXT: + if (currentMenu->menuitems[i].alphaKey) + y = currentMenu->y+currentMenu->menuitems[i].alphaKey; + /* FALLTHRU */ + case IT_TRANSTEXT2: + V_DrawCenteredString(x, y, V_TRANSLUCENT, currentMenu->menuitems[i].text); + y += SMALLLINEHEIGHT; + break; + } + } + + // DRAW THE SKULL CURSOR + if (((currentMenu->menuitems[itemOn].status & IT_DISPLAY) == IT_PATCH) + || ((currentMenu->menuitems[itemOn].status & IT_DISPLAY) == IT_NOTHING)) + { + V_DrawScaledPatch(x + SKULLXOFF, cursory - 5, 0, + W_CachePatchName("M_CURSOR", PU_CACHE)); + } + else + { + V_DrawScaledPatch(x - V_StringWidth(currentMenu->menuitems[itemOn].text, 0)/2 - 24, cursory, 0, + W_CachePatchName("M_CURSOR", PU_CACHE)); + V_DrawCenteredString(x, cursory, highlightflags, currentMenu->menuitems[itemOn].text); + } +} + +// +// M_StringHeight +// +// Find string height from hu_font chars +// +static inline size_t M_StringHeight(const char *string) +{ + size_t h = 8, i; + + for (i = 0; i < strlen(string); i++) + if (string[i] == '\n') + h += 8; + + return h; +} + +// ========================================================================== +// Extraneous menu patching functions +// ========================================================================== + +// +// M_PatchSkinNameTable +// +// Like M_PatchLevelNameTable, but for cv_chooseskin +// +static void M_PatchSkinNameTable(void) +{ + INT32 j; + + memset(skins_cons_t, 0, sizeof (skins_cons_t)); + + for (j = 0; j < MAXSKINS; j++) + { + if (skins[j].name[0] != '\0') + { + skins_cons_t[j].strvalue = skins[j].name; + skins_cons_t[j].value = j+1; + } + else + { + skins_cons_t[j].strvalue = NULL; + skins_cons_t[j].value = 0; + break; + } + } + + j = R_SkinAvailable(cv_skin[0].string); + if (j == -1) + j = 0; + + CV_SetValue(&cv_chooseskin, j+1); // This causes crash sometimes?! + + return; +} + +// +// M_PrepareCupList +// +static void M_PrepareCupList(void) +{ + cupheader_t *cup = kartcupheaders; + INT32 i = 0; + + memset(dummygpcup_cons_t, 0, sizeof (dummygpcup_cons_t)); + + while (cup != NULL) + { + dummygpcup_cons_t[i].strvalue = cup->name; + dummygpcup_cons_t[i].value = i+1; + // this will probably crash or do something stupid at over 50 cups, + // but this is all behavior that gets completely overwritten in new-menus, so I'm not worried + i++; + cup = cup->next; + } + + for (; i < 50; i++) + { + dummygpcup_cons_t[i].strvalue = NULL; + dummygpcup_cons_t[i].value = 0; + } + + CV_SetValue(&cv_dummygpcup, 1); // This causes crash sometimes?! +} + +// Call before showing any level-select menus +static void M_PrepareLevelSelect(void) +{ + if (levellistmode != LLM_CREATESERVER) + CV_SetValue(&cv_nextmap, M_GetFirstLevelInList()); + else + Newgametype_OnChange(); // Make sure to start on an appropriate map if wads have been added +} + +// +// M_CanShowLevelInList +// +// Determines whether to show a given map in the various level-select lists. +// Set gt = -1 to ignore gametype. +// +boolean M_CanShowLevelInList(INT32 mapnum, INT32 gt) +{ + // Random map! + if (mapnum == -1) + return (gamestate != GS_TIMEATTACK && !modeattacking); + + // Does the map exist? + if (!mapheaderinfo[mapnum]) + return false; + + // Does the map have a name? + if (!mapheaderinfo[mapnum]->lvlttl[0]) + return false; + + switch (levellistmode) + { + case LLM_CREATESERVER: + // Should the map be hidden? + if (mapheaderinfo[mapnum]->menuflags & LF2_HIDEINMENU && mapnum+1 != gamemap) + return false; + + if (M_MapLocked(mapnum+1)) + return false; // not unlocked + + if (gt >= 0 && gt < gametypecount && mapheaderinfo[mapnum]->typeoflevel & gametypetol[gt]) + return true; + + return false; + + /*case LLM_LEVELSELECT: + if (mapheaderinfo[mapnum]->levelselect != maplistoption) + return false; + + if (M_MapLocked(mapnum+1)) + return false; // not unlocked + + return true;*/ + case LLM_TIMEATTACK: + case LLM_BREAKTHECAPSULES: + if (!(mapheaderinfo[mapnum]->menuflags & LF2_TIMEATTACK)) + return false; + + if ((levellistmode == LLM_TIMEATTACK && !(mapheaderinfo[mapnum]->typeoflevel & TOL_RACE)) + || (levellistmode == LLM_BREAKTHECAPSULES && !(mapheaderinfo[mapnum]->typeoflevel & TOL_BATTLE))) + return false; + + if (M_MapLocked(mapnum+1)) + return false; // not unlocked + + if (M_SecretUnlocked(SECRET_HELLATTACK)) + return true; // now you're in hell + + if (mapheaderinfo[mapnum]->menuflags & LF2_HIDEINMENU) + return false; // map hell + + if ((mapheaderinfo[mapnum]->menuflags & LF2_VISITNEEDED) && !mapvisited[mapnum]) + return false; + + return true; + default: + return false; + } + + // Hmm? Couldn't decide? + return false; +} + +static INT32 M_CountLevelsToShowInList(void) +{ + INT32 mapnum, count = 0; + + for (mapnum = 0; mapnum < NUMMAPS; mapnum++) + if (M_CanShowLevelInList(mapnum, -1)) + count++; + + return count; +} + +static INT32 M_GetFirstLevelInList(void) +{ + INT32 mapnum; + + for (mapnum = 0; mapnum < NUMMAPS; mapnum++) + if (M_CanShowLevelInList(mapnum, -1)) + return mapnum + 1; + + return 1; +} + +// ================================================== +// MESSAGE BOX (aka: a hacked, cobbled together menu) +// ================================================== +static void M_DrawMessageMenu(void); + +// Because this is just a hack-ish 'menu', I'm not putting this with the others +static menuitem_t MessageMenu[] = +{ + // TO HACK + {0,NULL,NULL,NULL,0} +}; + +menu_t MessageDef = +{ + MN_NONE, // id + NULL, // title + 1, // # of menu items + NULL, // previous menu (TO HACK) + MessageMenu, // menuitem_t -> + M_DrawMessageMenu, // drawing routine -> + 0, 0, // x, y (TO HACK) + 0, // lastOn, flags (TO HACK) + NULL +}; + + +void M_StartMessage(const char *string, void *routine, + menumessagetype_t itemtype) +{ + size_t max = 0, start = 0, i, strlines; + static char *message = NULL; + Z_Free(message); + message = Z_StrDup(string); + DEBFILE(message); + + // Rudementary word wrapping. + // Simple and effective. Does not handle nonuniform letter sizes, colors, etc. but who cares. + strlines = 0; + for (i = 0; message[i]; i++) + { + if (message[i] == ' ') + { + start = i; + max += 4; + } + else if (message[i] == '\n') + { + strlines = i; + start = 0; + max = 0; + continue; + } + else + max += 8; + + // Start trying to wrap if presumed length exceeds the screen width. + if (max >= BASEVIDWIDTH && start > 0) + { + message[start] = '\n'; + max -= (start-strlines)*8; + strlines = start; + start = 0; + } + } + + start = 0; + max = 0; + + M_StartControlPanel(); // can't put menuactive to true + + if (currentMenu == &MessageDef) // Prevent recursion + MessageDef.prevMenu = ((demo.playback) ? &PlaybackMenuDef : &MainDef); + else + MessageDef.prevMenu = currentMenu; + + MessageDef.menuitems[0].text = message; + MessageDef.menuitems[0].alphaKey = (UINT8)itemtype; + if (!routine && itemtype != MM_NOTHING) itemtype = MM_NOTHING; + switch (itemtype) + { + case MM_NOTHING: + MessageDef.menuitems[0].status = IT_MSGHANDLER; + MessageDef.menuitems[0].itemaction = M_StopMessage; + break; + case MM_YESNO: + MessageDef.menuitems[0].status = IT_MSGHANDLER; + MessageDef.menuitems[0].itemaction = routine; + break; + case MM_EVENTHANDLER: + MessageDef.menuitems[0].status = IT_MSGHANDLER; + MessageDef.menuitems[0].itemaction = routine; + break; + } + //added : 06-02-98: now draw a textbox around the message + // compute lenght max and the numbers of lines + for (strlines = 0; *(message+start); strlines++) + { + for (i = 0;i < strlen(message+start);i++) + { + if (*(message+start+i) == '\n') + { + if (i > max) + max = i; + start += i; + i = (size_t)-1; //added : 07-02-98 : damned! + start++; + break; + } + } + + if (i == strlen(message+start)) + start += i; + } + + MessageDef.x = (INT16)((BASEVIDWIDTH - 8*max-16)/2); + MessageDef.y = (INT16)((BASEVIDHEIGHT - M_StringHeight(message))/2); + + MessageDef.lastOn = (INT16)((strlines<<8)+max); + + //M_SetupNextMenu(); + currentMenu = &MessageDef; + itemOn = 0; +} + +#define MAXMSGLINELEN 256 + +static void M_DrawMessageMenu(void) +{ + INT32 y = currentMenu->y; + size_t i, start = 0; + INT16 max; + char string[MAXMSGLINELEN]; + INT32 mlines; + const char *msg = currentMenu->menuitems[0].text; + + mlines = currentMenu->lastOn>>8; + max = (INT16)((UINT8)(currentMenu->lastOn & 0xFF)*8); + + // hack: draw RA background in RA menus + if (gamestate == GS_TIMEATTACK) + V_DrawPatchFill(W_CachePatchName("SRB2BACK", PU_CACHE)); + + M_DrawTextBox(currentMenu->x, y - 8, (max+7)>>3, mlines); + + while (*(msg+start)) + { + size_t len = strlen(msg+start); + + for (i = 0; i < len; i++) + { + if (*(msg+start+i) == '\n') + { + memset(string, 0, MAXMSGLINELEN); + if (i >= MAXMSGLINELEN) + { + CONS_Printf("M_DrawMessageMenu: too long segment in %s\n", msg); + return; + } + else + { + strncpy(string,msg+start, i); + string[i] = '\0'; + start += i; + i = (size_t)-1; //added : 07-02-98 : damned! + start++; + } + break; + } + } + + if (i == strlen(msg+start)) + { + if (i >= MAXMSGLINELEN) + { + CONS_Printf("M_DrawMessageMenu: too long segment in %s\n", msg); + return; + } + else + { + strcpy(string, msg + start); + start += i; + } + } + + V_DrawString((BASEVIDWIDTH - V_StringWidth(string, 0))/2,y,V_ALLOWLOWERCASE,string); + y += 8; //SHORT(hu_font[0]->height); + } +} + +// default message handler +static void M_StopMessage(INT32 choice) +{ + (void)choice; + if (menuactive) + M_SetupNextMenu(MessageDef.prevMenu); +} + +// ========= +// IMAGEDEFS +// ========= + +// Draw an Image Def. Aka, Help images. +// Defines what image is used in (menuitem_t)->text. +// You can even put multiple images in one menu! +static void M_DrawImageDef(void) +{ + patch_t *patch = W_CachePatchName(currentMenu->menuitems[itemOn].text,PU_CACHE); + if (patch->width <= BASEVIDWIDTH) + V_DrawScaledPatch(0,0,0,patch); + else + V_DrawSmallScaledPatch(0,0,0,patch); + + if (currentMenu->menuitems[itemOn].alphaKey) + { + V_DrawString(2,BASEVIDHEIGHT-10, V_YELLOWMAP, va("%d", (itemOn<<1)-1)); // intentionally not highlightflags, unlike below + V_DrawRightAlignedString(BASEVIDWIDTH-2,BASEVIDHEIGHT-10, V_YELLOWMAP, va("%d", itemOn<<1)); // ditto + } + else + { + INT32 x = BASEVIDWIDTH>>1, y = (BASEVIDHEIGHT>>1) - 4; + x += (itemOn ? 1 : -1)*((BASEVIDWIDTH>>2) + 10); + V_DrawCenteredString(x, y-10, highlightflags, "USE ARROW KEYS"); + V_DrawCharacter(x - 10 - (skullAnimCounter/5), y, + '\x1C' | highlightflags, false); // left arrow + V_DrawCharacter(x + 2 + (skullAnimCounter/5), y, + '\x1D' | highlightflags, false); // right arrow + V_DrawCenteredString(x, y+10, highlightflags, "TO LEAF THROUGH"); + } +} + +// Handles the ImageDefs. Just a specialized function that +// uses left and right movement. +static void M_HandleImageDef(INT32 choice) +{ + boolean exitmenu = false; + + switch (choice) + { + case KEY_RIGHTARROW: + if (itemOn >= (INT16)(currentMenu->numitems-1)) + break; + S_StartSound(NULL, sfx_menu1); + itemOn++; + break; + + case KEY_LEFTARROW: + if (!itemOn) + break; + + S_StartSound(NULL, sfx_menu1); + itemOn--; + break; + + case KEY_ESCAPE: + case KEY_ENTER: + exitmenu = true; + break; + } + + if (exitmenu) + { + if (currentMenu->prevMenu) + M_SetupNextMenu(currentMenu->prevMenu); + else + M_ClearMenus(true); + } +} + +// ====================== +// MISC MAIN MENU OPTIONS +// ====================== + +static void M_AddonsOptions(INT32 choice) +{ + (void)choice; + Addons_option_Onchange(); + + M_SetupNextMenu(&OP_AddonsOptionsDef); +} + +#define LOCATIONSTRING1 "Visit \x83SRB2.ORG/MODS\x80 to get & make addons!" +#define LOCATIONSTRING2 "Visit \x88SRB2.ORG/MODS\x80 to get & make addons!" + +static void M_Addons(INT32 choice) +{ + const char *pathname = "."; + + (void)choice; + +#if 1 + if (cv_addons_option.value == 0) + pathname = usehome ? srb2home : srb2path; + else if (cv_addons_option.value == 1) + pathname = srb2home; + else if (cv_addons_option.value == 2) + pathname = srb2path; + else +#endif + if (cv_addons_option.value == 3 && *cv_addons_folder.string != '\0') + pathname = cv_addons_folder.string; + + strlcpy(menupath, pathname, 1024); + menupathindex[(menudepthleft = menudepth-1)] = strlen(menupath) + 1; + + if (menupath[menupathindex[menudepthleft]-2] != PATHSEP[0]) + { + menupath[menupathindex[menudepthleft]-1] = PATHSEP[0]; + menupath[menupathindex[menudepthleft]] = 0; + } + else + --menupathindex[menudepthleft]; + + if (!preparefilemenu(false, false)) + { + M_StartMessage(va("No files/folders found.\n\n%s\n\n(Press a key)\n", (recommendedflags == V_SKYMAP ? LOCATIONSTRING2 : LOCATIONSTRING1)),NULL,MM_NOTHING); + return; + } + else + dir_on[menudepthleft] = 0; + + if (addonsp[0]) // never going to have some provided but not all, saves individually checking + { + size_t i; + for (i = 0; i < NUM_EXT+5; i++) + W_UnlockCachedPatch(addonsp[i]); + } + + addonsp[EXT_FOLDER] = W_CachePatchName("M_FFLDR", PU_STATIC); + addonsp[EXT_UP] = W_CachePatchName("M_FBACK", PU_STATIC); + addonsp[EXT_NORESULTS] = W_CachePatchName("M_FNOPE", PU_STATIC); + addonsp[EXT_TXT] = W_CachePatchName("M_FTXT", PU_STATIC); + addonsp[EXT_CFG] = W_CachePatchName("M_FCFG", PU_STATIC); + addonsp[EXT_WAD] = W_CachePatchName("M_FWAD", PU_STATIC); +#ifdef USE_KART + addonsp[EXT_KART] = W_CachePatchName("M_FKART", PU_STATIC); +#endif + addonsp[EXT_PK3] = W_CachePatchName("M_FPK3", PU_STATIC); + addonsp[EXT_SOC] = W_CachePatchName("M_FSOC", PU_STATIC); + addonsp[EXT_LUA] = W_CachePatchName("M_FLUA", PU_STATIC); + addonsp[NUM_EXT] = W_CachePatchName("M_FUNKN", PU_STATIC); + addonsp[NUM_EXT+1] = W_CachePatchName("M_FSEL", PU_STATIC); + addonsp[NUM_EXT+2] = W_CachePatchName("M_FLOAD", PU_STATIC); + addonsp[NUM_EXT+3] = W_CachePatchName("M_FSRCH", PU_STATIC); + addonsp[NUM_EXT+4] = W_CachePatchName("M_FSAVE", PU_STATIC); + + MISC_AddonsDef.prevMenu = currentMenu; + M_SetupNextMenu(&MISC_AddonsDef); +} + +#define width 4 +#define vpadding 27 +#define h (BASEVIDHEIGHT-(2*vpadding)) +#define NUMCOLOURS 8 // when toast's coding it's british english hacker fucker +static void M_DrawTemperature(INT32 x, fixed_t t) +{ + INT32 y; + + // bounds check + if (t > FRACUNIT) + t = FRACUNIT; + /*else if (t < 0) -- not needed + t = 0;*/ + + // scale + if (t > 1) + t = (FixedMul(h<>FRACBITS); + + // border + V_DrawFill(x - 1, vpadding, 1, h, 0); + V_DrawFill(x + width, vpadding, 1, h, 0); + V_DrawFill(x - 1, vpadding-1, width+2, 1, 0); + V_DrawFill(x - 1, vpadding+h, width+2, 1, 0); + + // bar itself + y = h; + if (t) + for (t = h - t; y > 0; y--) + { + UINT8 colours[NUMCOLOURS] = {135, 133, 92, 77, 114, 178, 161, 162}; + UINT8 c; + if (y <= t) break; + if (y+vpadding >= BASEVIDHEIGHT/2) + c = 185; + else + c = colours[(NUMCOLOURS*(y-1))/(h/2)]; + V_DrawFill(x, y-1 + vpadding, width, 1, c); + } + + // fill the rest of the backing + if (y) + V_DrawFill(x, vpadding, width, y, 30); +} +#undef width +#undef vpadding +#undef h +#undef NUMCOLOURS + +static char *M_AddonsHeaderPath(void) +{ + UINT32 len; + static char header[1024]; + + strlcpy(header, va("%s folder%s", cv_addons_option.string, menupath+menupathindex[menudepth-1]-1), 1024); + len = strlen(header); + if (len > 34) + { + len = len-34; + header[len] = header[len+1] = header[len+2] = '.'; + } + else + len = 0; + + return header+len; +} + +#define UNEXIST S_StartSound(NULL, sfx_s26d);\ + M_SetupNextMenu(MISC_AddonsDef.prevMenu);\ + 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) + +#define CLEARNAME Z_Free(refreshdirname);\ + refreshdirname = NULL + +static boolean prevmajormods = false; + +static void M_AddonsClearName(INT32 choice) +{ + if (!majormods || prevmajormods) + { + CLEARNAME; + } + M_StopMessage(choice); +} + +// returns whether to do message draw +static boolean M_AddonsRefresh(void) +{ + if ((refreshdirmenu & REFRESHDIR_NORMAL) && !preparefilemenu(true, false)) + { + UNEXIST; + if (refreshdirname) + { + CLEARNAME; + } + return true; + } + + if (!majormods && prevmajormods) + prevmajormods = false; + + if ((refreshdirmenu & REFRESHDIR_ADDFILE) || (majormods && !prevmajormods)) + { + char *message = NULL; + + if (refreshdirmenu & REFRESHDIR_NOTLOADED) + { + S_StartSound(NULL, sfx_s26d); + if (refreshdirmenu & REFRESHDIR_MAX) + message = va("%c%s\x80\nMaximum number of addons reached.\nA file could not be loaded.\nIf you wish to play with this addon, restart the game to clear existing ones.\n\n(Press a key)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), refreshdirname); + else + 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); + } + else if (refreshdirmenu & (REFRESHDIR_WARNING|REFRESHDIR_ERROR)) + { + S_StartSound(NULL, sfx_s224); + 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")); + } + else if (majormods && !prevmajormods) + { + S_StartSound(NULL, sfx_s221); + message = va("%c%s\x80\nYou've loaded a gameplay-modifying addon.\n\nRecord Attack has been disabled, but you\ncan still play alone in local Multiplayer.\n\nIf you wish to play Record Attack mode, restart the game to disable loaded addons.\n\n(Press a key)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), refreshdirname); + prevmajormods = majormods; + } + + if (message) + { + M_StartMessage(message,M_AddonsClearName,MM_EVENTHANDLER); + return true; + } + + S_StartSound(NULL, sfx_s221); + CLEARNAME; + } + + return false; +} + +static void M_DrawAddons(void) +{ + INT32 x, y; + ssize_t i, m; + const UINT8 *flashcol = NULL; + UINT8 hilicol; + + // hack - need to refresh at end of frame to handle addfile... + if (refreshdirmenu & M_AddonsRefresh()) + { + M_DrawMessageMenu(); + return; + } + + if (Playing()) + V_DrawCenteredString(BASEVIDWIDTH/2, 5, warningflags, "Adding files mid-game may cause problems."); + else + V_DrawCenteredString(BASEVIDWIDTH/2, 5, 0, (recommendedflags == V_SKYMAP ? LOCATIONSTRING2 : LOCATIONSTRING1)); + + if (numwadfiles <= mainwads+1) + y = 0; + else if (numwadfiles >= MAX_WADFILES) + y = FRACUNIT; + else + { + y = FixedDiv(((ssize_t)(numwadfiles) - (ssize_t)(mainwads+1))< FRACUNIT) // happens because of how we're shrinkin' it a little + y = FRACUNIT; + } + + M_DrawTemperature(BASEVIDWIDTH - 19 - 5, y); + + // DRAW MENU + x = currentMenu->x; + y = currentMenu->y + 1; + + hilicol = V_GetStringColormap(highlightflags)[0]; + + V_DrawString(x-21, (y - 16) + (lsheadingheight - 12), highlightflags|V_ALLOWLOWERCASE, M_AddonsHeaderPath()); + V_DrawFill(x-21, (y - 16) + (lsheadingheight - 3), MAXSTRINGLENGTH*8+6, 1, hilicol); + V_DrawFill(x-21, (y - 16) + (lsheadingheight - 2), MAXSTRINGLENGTH*8+6, 1, 30); + + m = (BASEVIDHEIGHT - currentMenu->y + 2) - (y - 1); + V_DrawFill(x - 21, y - 1, MAXSTRINGLENGTH*8+6, m, 159); + + // scrollbar! + if (sizedirmenu <= (2*numaddonsshown + 1)) + i = 0; + else + { + ssize_t q = m; + m = ((2*numaddonsshown + 1) * m)/sizedirmenu; + if (dir_on[menudepthleft] <= numaddonsshown) // all the way up + i = 0; + else if (sizedirmenu <= (dir_on[menudepthleft] + numaddonsshown + 1)) // all the way down + i = q-m; + else + i = ((dir_on[menudepthleft] - numaddonsshown) * (q-m))/(sizedirmenu - (2*numaddonsshown + 1)); + } + + V_DrawFill(x + MAXSTRINGLENGTH*8+5 - 21, (y - 1) + i, 1, m, hilicol); + + // get bottom... + m = dir_on[menudepthleft] + numaddonsshown + 1; + if (m > (ssize_t)sizedirmenu) + m = sizedirmenu; + + // then compute top and adjust bottom if needed! + if (m < (2*numaddonsshown + 1)) + { + m = min(sizedirmenu, 2*numaddonsshown + 1); + i = 0; + } + else + i = m - (2*numaddonsshown + 1); + + if (i != 0) + V_DrawString(19, y+4 - (skullAnimCounter/5), highlightflags, "\x1A"); + + if (skullAnimCounter < 4) + flashcol = V_GetStringColormap(highlightflags); + + for (; i < m; i++) + { + UINT32 flags = V_ALLOWLOWERCASE; + if (y > BASEVIDHEIGHT) break; + if (dirmenu[i]) +#define type (UINT8)(dirmenu[i][DIR_TYPE]) + { + if (type & EXT_LOADED) + { + flags |= V_TRANSLUCENT; + V_DrawSmallScaledPatch(x-(16+4), y, V_TRANSLUCENT, addonsp[(type & ~EXT_LOADED)]); + V_DrawSmallScaledPatch(x-(16+4), y, 0, addonsp[NUM_EXT+2]); + } + else + V_DrawSmallScaledPatch(x-(16+4), y, 0, addonsp[(type & ~EXT_LOADED)]); + + if ((size_t)i == dir_on[menudepthleft]) + { + V_DrawFixedPatch((x-(16+4))< (charsonside*2 + 3)) + V_DrawString(x, y+4, flags, va("%.*s...%s", charsonside, dirmenu[i]+DIR_STRING, dirmenu[i]+DIR_STRING+dirmenu[i][DIR_LEN]-(charsonside+1))); +#undef charsonside + else + V_DrawString(x, y+4, flags, dirmenu[i]+DIR_STRING); + } +#undef type + y += 16; + } + + if (m != (ssize_t)sizedirmenu) + V_DrawString(19, y-12 + (skullAnimCounter/5), highlightflags, "\x1B"); + + y = BASEVIDHEIGHT - currentMenu->y + 1; + + M_DrawTextBox(x - (21 + 5), y, MAXSTRINGLENGTH, 1); + if (menusearch[0]) + V_DrawString(x - 18, y + 8, V_ALLOWLOWERCASE, menusearch+1); + else + V_DrawString(x - 18, y + 8, V_ALLOWLOWERCASE|V_TRANSLUCENT, "Type to search..."); + if (skullAnimCounter < 4) + V_DrawCharacter(x - 18 + V_StringWidth(menusearch+1, 0), y + 8, + '_' | 0x80, false); + + x -= (21 + 5 + 16); + V_DrawSmallScaledPatch(x, y + 4, (menusearch[0] ? 0 : V_TRANSLUCENT), addonsp[NUM_EXT+3]); + + x = BASEVIDWIDTH - x - 16; + V_DrawSmallScaledPatch(x, y + 4, ((!majormods) ? 0 : V_TRANSLUCENT), addonsp[NUM_EXT+4]); + + if (modifiedgame) + V_DrawSmallScaledPatch(x, y + 4, 0, addonsp[NUM_EXT+2]); +} + +static void M_AddonExec(INT32 ch) +{ + if (ch != 'y' && ch != KEY_ENTER) + return; + + S_StartSound(NULL, sfx_zoom); + COM_BufAddText(va("exec \"%s%s\"", menupath, dirmenu[dir_on[menudepthleft]]+DIR_STRING)); +} + +#define len menusearch[0] +static boolean M_ChangeStringAddons(INT32 choice) +{ + if (shiftdown && choice >= 32 && choice <= 127) + choice = shiftxform[choice]; + + switch (choice) + { + case KEY_DEL: + if (len) + { + len = menusearch[1] = 0; + return true; + } + break; + case KEY_BACKSPACE: + if (len) + { + menusearch[1+--len] = 0; + return true; + } + break; + default: + if (choice >= 32 && choice <= 127) + { + if (len < MAXSTRINGLENGTH - 1) + { + menusearch[1+len++] = (char)choice; + menusearch[1+len] = 0; + return true; + } + } + break; + } + return false; +} +#undef len + +static void M_HandleAddons(INT32 choice) +{ + boolean exitmenu = false; // exit to previous menu + + if (M_ChangeStringAddons(choice)) + { + char *tempname = NULL; + if (dirmenu && dirmenu[dir_on[menudepthleft]]) + tempname = Z_StrDup(dirmenu[dir_on[menudepthleft]]+DIR_STRING); // don't need to I_Error if can't make - not important, just QoL +#if 0 // much slower + if (!preparefilemenu(true, false)) + { + UNEXIST; + return; + } +#else // streamlined + searchfilemenu(tempname); +#endif + } + + switch (choice) + { + case KEY_DOWNARROW: + if (dir_on[menudepthleft] < sizedirmenu-1) + dir_on[menudepthleft]++; + S_StartSound(NULL, sfx_menu1); + break; + case KEY_UPARROW: + if (dir_on[menudepthleft]) + dir_on[menudepthleft]--; + S_StartSound(NULL, sfx_menu1); + break; + case KEY_PGDN: + { + UINT8 i; + for (i = numaddonsshown; i && (dir_on[menudepthleft] < sizedirmenu-1); i--) + dir_on[menudepthleft]++; + } + S_StartSound(NULL, sfx_menu1); + break; + case KEY_PGUP: + { + UINT8 i; + for (i = numaddonsshown; i && (dir_on[menudepthleft]); i--) + dir_on[menudepthleft]--; + } + S_StartSound(NULL, sfx_menu1); + break; + case KEY_ENTER: + { + boolean refresh = true; + if (!dirmenu[dir_on[menudepthleft]]) + S_StartSound(NULL, sfx_s26d); + else + { + switch (dirmenu[dir_on[menudepthleft]][DIR_TYPE]) + { + case EXT_FOLDER: + strcpy(&menupath[menupathindex[menudepthleft]],dirmenu[dir_on[menudepthleft]]+DIR_STRING); + if (menudepthleft) + { + menupathindex[--menudepthleft] = strlen(menupath); + menupath[menupathindex[menudepthleft]] = 0; + + if (!preparefilemenu(false, false)) + { + S_StartSound(NULL, sfx_s224); + 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); + menupath[menupathindex[++menudepthleft]] = 0; + + if (!preparefilemenu(true, false)) + { + UNEXIST; + return; + } + } + else + { + S_StartSound(NULL, sfx_menu1); + dir_on[menudepthleft] = 1; + } + refresh = false; + } + else + { + S_StartSound(NULL, sfx_s26d); + 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); + menupath[menupathindex[menudepthleft]] = 0; + } + break; + case EXT_UP: + S_StartSound(NULL, sfx_menu1); + menupath[menupathindex[++menudepthleft]] = 0; + if (!preparefilemenu(false, false)) + { + UNEXIST; + return; + } + break; + case EXT_TXT: + 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); + break; + case EXT_CFG: + M_AddonExec(KEY_ENTER); + break; + case EXT_LUA: + case EXT_SOC: + case EXT_WAD: +#ifdef USE_KART + case EXT_KART: +#endif + case EXT_PK3: + COM_BufAddText(va("addfile \"%s%s\"", menupath, dirmenu[dir_on[menudepthleft]]+DIR_STRING)); + break; + default: + S_StartSound(NULL, sfx_s26d); + } + } + if (refresh) + refreshdirmenu |= REFRESHDIR_NORMAL; + } + break; + + case KEY_ESCAPE: + exitmenu = true; + break; + + default: + break; + } + if (exitmenu) + { + closefilemenu(true); + + // Secret menu! + //MainMenu[secrets].status = (M_AnySecretUnlocked()) ? (IT_STRING | IT_CALL) : (IT_DISABLED); + + if (currentMenu->prevMenu) + M_SetupNextMenu(currentMenu->prevMenu); + else + M_ClearMenus(true); + } +} + +// ---- REPLAY HUT ----- +menudemo_t *demolist; + +#define DF_ENCORE 0x40 +static INT16 replayScrollTitle = 0; +static SINT8 replayScrollDelay = TICRATE, replayScrollDir = 1; + +static void PrepReplayList(void) +{ + size_t i; + + if (demolist) + Z_Free(demolist); + + demolist = Z_Calloc(sizeof(menudemo_t) * sizedirmenu, PU_STATIC, NULL); + + for (i = 0; i < sizedirmenu; i++) + { + if (dirmenu[i][DIR_TYPE] == EXT_UP) + { + demolist[i].type = MD_SUBDIR; + sprintf(demolist[i].title, "UP"); + } + else if (dirmenu[i][DIR_TYPE] == EXT_FOLDER) + { + demolist[i].type = MD_SUBDIR; + strncpy(demolist[i].title, dirmenu[i] + DIR_STRING, 64); + } + else + { + demolist[i].type = MD_NOTLOADED; + snprintf(demolist[i].filepath, 255, "%s%s", menupath, dirmenu[i] + DIR_STRING); + sprintf(demolist[i].title, "....."); + } + } +} + +void M_ReplayHut(INT32 choice) +{ + (void)choice; + + if (!demo.inreplayhut) + { + snprintf(menupath, 1024, "%s"PATHSEP"media"PATHSEP"replay"PATHSEP"online"PATHSEP, srb2home); + menupathindex[(menudepthleft = menudepth-1)] = strlen(menupath); + } + if (!preparefilemenu(false, true)) + { + M_StartMessage("No replays found.\n\n(Press a key)\n", NULL, MM_NOTHING); + return; + } + else if (!demo.inreplayhut) + dir_on[menudepthleft] = 0; + demo.inreplayhut = true; + + replayScrollTitle = 0; replayScrollDelay = TICRATE; replayScrollDir = 1; + + PrepReplayList(); + + menuactive = true; + M_SetupNextMenu(&MISC_ReplayHutDef); + G_SetGamestate(GS_TIMEATTACK); + titlemapinaction = TITLEMAP_OFF; // Nope don't give us HOMs please + + demo.rewinding = false; + CL_ClearRewinds(); + + S_ChangeMusicInternal("replst", true); +} + +static void M_HandleReplayHutList(INT32 choice) +{ + switch (choice) + { + case KEY_UPARROW: + if (dir_on[menudepthleft]) + dir_on[menudepthleft]--; + else + return; + //M_PrevOpt(); + + S_StartSound(NULL, sfx_menu1); + replayScrollTitle = 0; replayScrollDelay = TICRATE; replayScrollDir = 1; + break; + + case KEY_DOWNARROW: + if (dir_on[menudepthleft] < sizedirmenu-1) + dir_on[menudepthleft]++; + else + return; + //itemOn = 0; // Not M_NextOpt because that would take us to the extra dummy item + + S_StartSound(NULL, sfx_menu1); + replayScrollTitle = 0; replayScrollDelay = TICRATE; replayScrollDir = 1; + break; + + case KEY_ESCAPE: + M_QuitReplayHut(); + break; + + case KEY_ENTER: + switch (dirmenu[dir_on[menudepthleft]][DIR_TYPE]) + { + case EXT_FOLDER: + strcpy(&menupath[menupathindex[menudepthleft]],dirmenu[dir_on[menudepthleft]]+DIR_STRING); + if (menudepthleft) + { + menupathindex[--menudepthleft] = strlen(menupath); + menupath[menupathindex[menudepthleft]] = 0; + + if (!preparefilemenu(false, true)) + { + S_StartSound(NULL, sfx_s224); + 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); + menupath[menupathindex[++menudepthleft]] = 0; + + if (!preparefilemenu(true, true)) + { + M_QuitReplayHut(); + return; + } + } + else + { + S_StartSound(NULL, sfx_menu1); + dir_on[menudepthleft] = 1; + PrepReplayList(); + } + } + else + { + S_StartSound(NULL, sfx_s26d); + 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); + menupath[menupathindex[menudepthleft]] = 0; + } + break; + case EXT_UP: + S_StartSound(NULL, sfx_menu1); + menupath[menupathindex[++menudepthleft]] = 0; + if (!preparefilemenu(false, true)) + { + M_QuitReplayHut(); + return; + } + PrepReplayList(); + break; + default: + // We can't just use M_SetupNextMenu because that'll run ReplayDef's quitroutine and boot us back to the title screen! + currentMenu->lastOn = itemOn; + currentMenu = &MISC_ReplayStartDef; + + replayScrollTitle = 0; replayScrollDelay = TICRATE; replayScrollDir = 1; + + switch (demolist[dir_on[menudepthleft]].addonstatus) + { + case DFILE_ERROR_CANNOTLOAD: + // Only show "Watch Replay Without Addons" + MISC_ReplayStartMenu[0].status = IT_DISABLED; + MISC_ReplayStartMenu[1].status = IT_CALL|IT_STRING; + //MISC_ReplayStartMenu[1].alphaKey = 0; + MISC_ReplayStartMenu[2].status = IT_DISABLED; + itemOn = 1; + break; + + case DFILE_ERROR_NOTLOADED: + case DFILE_ERROR_INCOMPLETEOUTOFORDER: + // Show "Load Addons and Watch Replay" and "Watch Replay Without Addons" + MISC_ReplayStartMenu[0].status = IT_CALL|IT_STRING; + MISC_ReplayStartMenu[1].status = IT_CALL|IT_STRING; + //MISC_ReplayStartMenu[1].alphaKey = 10; + MISC_ReplayStartMenu[2].status = IT_DISABLED; + itemOn = 0; + break; + + case DFILE_ERROR_EXTRAFILES: + case DFILE_ERROR_OUTOFORDER: + default: + // Show "Watch Replay" + MISC_ReplayStartMenu[0].status = IT_DISABLED; + MISC_ReplayStartMenu[1].status = IT_DISABLED; + MISC_ReplayStartMenu[2].status = IT_CALL|IT_STRING; + //MISC_ReplayStartMenu[2].alphaKey = 0; + itemOn = 2; + break; + } + } + + break; + } +} + +#define SCALEDVIEWWIDTH (vid.width/vid.dupx) +#define SCALEDVIEWHEIGHT (vid.height/vid.dupy) +static void DrawReplayHutReplayInfo(void) +{ + lumpnum_t lumpnum; + patch_t *patch; + UINT8 *colormap; + INT32 x, y, w, h; + + switch (demolist[dir_on[menudepthleft]].type) + { + case MD_NOTLOADED: + V_DrawCenteredString(160, 40, V_SNAPTOTOP, "Loading replay information..."); + break; + + case MD_INVALID: + V_DrawCenteredString(160, 40, V_SNAPTOTOP|warningflags, "This replay cannot be played."); + break; + + case MD_SUBDIR: + break; // Can't think of anything to draw here right now + + case MD_OUTDATED: + V_DrawThinString(17, 64, V_SNAPTOTOP|V_ALLOWLOWERCASE|V_TRANSLUCENT|highlightflags, "Recorded on an outdated version."); + /* FALLTHRU */ + default: + // Draw level stuff + x = 15; y = 15; + + // A 160x100 image of the level as entry MAPxxP + //CONS_Printf("%d %s\n", demolist[dir_on[menudepthleft]].map, G_BuildMapName(demolist[dir_on[menudepthleft]].map)); + lumpnum = W_CheckNumForName(va("%sP", G_BuildMapName(demolist[dir_on[menudepthleft]].map))); + if (lumpnum != LUMPERROR) + patch = W_CachePatchNum(lumpnum, PU_CACHE); + else + patch = W_CachePatchName("M_NOLVL", PU_CACHE); + + if (!(demolist[dir_on[menudepthleft]].kartspeed & DF_ENCORE)) + V_DrawSmallScaledPatch(x, y, V_SNAPTOTOP, patch); + else + { + w = SHORT(patch->width); + h = SHORT(patch->height); + V_DrawSmallScaledPatch(x+(w>>1), y, V_SNAPTOTOP|V_FLIP, patch); + + { + static angle_t rubyfloattime = 0; + const fixed_t rubyheight = FINESINE(rubyfloattime>>ANGLETOFINESHIFT); + V_DrawFixedPatch((x+(w>>2))<>2))<width), y+20, V_SNAPTOTOP, patch, colormap); + + break; + } +} + +static void M_DrawReplayHut(void) +{ + INT32 x, y, cursory = 0; + INT16 i; + INT16 replaylistitem = currentMenu->numitems-2; + boolean processed_one_this_frame = false; + + static UINT16 replayhutmenuy = 0; + + V_DrawPatchFill(W_CachePatchName("SRB2BACK", PU_CACHE)); + + if (cv_vhseffect.value) + V_DrawVhsEffect(false); + + // Draw menu choices + x = currentMenu->x; + y = currentMenu->y; + + if (itemOn > replaylistitem) + { + itemOn = replaylistitem; + dir_on[menudepthleft] = sizedirmenu-1; + replayScrollTitle = 0; replayScrollDelay = TICRATE; replayScrollDir = 1; + } + else if (itemOn < replaylistitem) + { + dir_on[menudepthleft] = 0; + replayScrollTitle = 0; replayScrollDelay = TICRATE; replayScrollDir = 1; + } + + if (itemOn == replaylistitem) + { + INT32 maxy; + // Scroll menu items if needed + cursory = y + currentMenu->menuitems[replaylistitem].alphaKey + dir_on[menudepthleft]*10; + maxy = y + currentMenu->menuitems[replaylistitem].alphaKey + sizedirmenu*10; + + if (cursory > maxy - 20) + cursory = maxy - 20; + + if (cursory - replayhutmenuy > SCALEDVIEWHEIGHT-50) + replayhutmenuy += (cursory-SCALEDVIEWHEIGHT-replayhutmenuy + 51)/2; + else if (cursory - replayhutmenuy < 110) + replayhutmenuy += (max(0, cursory-110)-replayhutmenuy - 1)/2; + } + else + replayhutmenuy /= 2; + + y -= replayhutmenuy; + + // Draw static menu items + for (i = 0; i < replaylistitem; i++) + { + INT32 localy = y + currentMenu->menuitems[i].alphaKey; + + if (localy < 65) + continue; + + if (i == itemOn) + cursory = localy; + + if ((currentMenu->menuitems[i].status & IT_DISPLAY)==IT_STRING) + V_DrawString(x, localy, V_SNAPTOTOP|V_SNAPTOLEFT, currentMenu->menuitems[i].text); + else + V_DrawString(x, localy, V_SNAPTOTOP|V_SNAPTOLEFT|highlightflags, currentMenu->menuitems[i].text); + } + + y += currentMenu->menuitems[replaylistitem].alphaKey; + + for (i = 0; i < (INT16)sizedirmenu; i++) + { + INT32 localy = y+i*10; + INT32 localx = x; + + if (localy < 65) + continue; + if (localy >= SCALEDVIEWHEIGHT) + break; + + if (demolist[i].type == MD_NOTLOADED && !processed_one_this_frame) + { + processed_one_this_frame = true; + G_LoadDemoInfo(&demolist[i]); + } + + if (demolist[i].type == MD_SUBDIR) + { + localx += 8; + V_DrawScaledPatch(x - 4, localy, V_SNAPTOTOP|V_SNAPTOLEFT, W_CachePatchName(dirmenu[i][DIR_TYPE] == EXT_UP ? "M_RBACK" : "M_RFLDR", PU_CACHE)); + } + + if (itemOn == replaylistitem && i == (INT16)dir_on[menudepthleft]) + { + cursory = localy; + + if (replayScrollDelay) + replayScrollDelay--; + else if (replayScrollDir > 0) + { + if (replayScrollTitle < (V_StringWidth(demolist[i].title, 0) - (SCALEDVIEWWIDTH - (x<<1)))<<1) + replayScrollTitle++; + else + { + replayScrollDelay = TICRATE; + replayScrollDir = -1; + } + } + else + { + if (replayScrollTitle > 0) + replayScrollTitle--; + else + { + replayScrollDelay = TICRATE; + replayScrollDir = 1; + } + } + + V_DrawString(localx - (replayScrollTitle>>1), localy, V_SNAPTOTOP|V_SNAPTOLEFT|highlightflags|V_ALLOWLOWERCASE, demolist[i].title); + } + else + V_DrawString(localx, localy, V_SNAPTOTOP|V_SNAPTOLEFT|V_ALLOWLOWERCASE, demolist[i].title); + } + + // Draw scrollbar + y = sizedirmenu*10 + currentMenu->menuitems[replaylistitem].alphaKey + 30; + if (y > SCALEDVIEWHEIGHT-80) + { + V_DrawFill(BASEVIDWIDTH-4, 75, 4, SCALEDVIEWHEIGHT-80, V_SNAPTOTOP|V_SNAPTORIGHT|159); + V_DrawFill(BASEVIDWIDTH-3, 76 + (SCALEDVIEWHEIGHT-80) * replayhutmenuy / y, 2, (((SCALEDVIEWHEIGHT-80) * (SCALEDVIEWHEIGHT-80))-1) / y - 1, V_SNAPTOTOP|V_SNAPTORIGHT|149); + } + + // Draw the cursor + V_DrawScaledPatch(currentMenu->x - 24, cursory, V_SNAPTOTOP|V_SNAPTOLEFT, + W_CachePatchName("M_CURSOR", PU_CACHE)); + V_DrawString(currentMenu->x, cursory, V_SNAPTOTOP|V_SNAPTOLEFT|highlightflags, currentMenu->menuitems[itemOn].text); + + // Now draw some replay info! + V_DrawFill(10, 10, 300, 60, V_SNAPTOTOP|159); + + if (itemOn == replaylistitem) + { + DrawReplayHutReplayInfo(); + } +} + +static void M_DrawReplayStartMenu(void) +{ + const char *warning; + UINT8 i; + + M_DrawGenericBackgroundMenu(); + +#define STARTY 62-(replayScrollTitle>>1) + // Draw rankings beyond first + for (i = 1; i < MAXPLAYERS && demolist[dir_on[menudepthleft]].standings[i].ranking; i++) + { + patch_t *patch; + UINT8 *colormap; + + V_DrawRightAlignedString(BASEVIDWIDTH-100, STARTY + i*20, V_SNAPTOTOP|highlightflags, va("%2d", demolist[dir_on[menudepthleft]].standings[i].ranking)); + V_DrawThinString(BASEVIDWIDTH-96, STARTY + i*20, V_SNAPTOTOP|V_ALLOWLOWERCASE, demolist[dir_on[menudepthleft]].standings[i].name); + + if (demolist[dir_on[menudepthleft]].standings[i].timeorscore == UINT32_MAX-1) + V_DrawThinString(BASEVIDWIDTH-92, STARTY + i*20 + 9, V_SNAPTOTOP, "NO CONTEST"); + else if (demolist[dir_on[menudepthleft]].gametype == GT_RACE) + V_DrawRightAlignedString(BASEVIDWIDTH-40, STARTY + i*20 + 9, V_SNAPTOTOP, va("%d'%02d\"%02d", + G_TicsToMinutes(demolist[dir_on[menudepthleft]].standings[i].timeorscore, true), + G_TicsToSeconds(demolist[dir_on[menudepthleft]].standings[i].timeorscore), + G_TicsToCentiseconds(demolist[dir_on[menudepthleft]].standings[i].timeorscore) + )); + else + V_DrawString(BASEVIDWIDTH-92, STARTY + i*20 + 9, V_SNAPTOTOP, va("%d", demolist[dir_on[menudepthleft]].standings[i].timeorscore)); + + // Character face! + + // Lat: 08/06/2020: For some reason missing skins have their value set to 255 (don't even ask me why I didn't write this) + // and for an even STRANGER reason this passes the first check below, so we're going to make sure that the skin here ISN'T 255 before we do anything stupid. + + if (demolist[dir_on[menudepthleft]].standings[i].skin != 0xFF) + { + patch = faceprefix[demolist[dir_on[menudepthleft]].standings[i].skin][FACE_RANK]; + colormap = R_GetTranslationColormap( + demolist[dir_on[menudepthleft]].standings[i].skin, + demolist[dir_on[menudepthleft]].standings[i].color, + GTC_MENUCACHE); + } + else + { + patch = W_CachePatchName("M_NORANK", PU_CACHE); + colormap = R_GetTranslationColormap( + TC_RAINBOW, + demolist[dir_on[menudepthleft]].standings[i].color, + GTC_MENUCACHE); + } + + V_DrawMappedPatch(BASEVIDWIDTH-5 - SHORT(patch->width), STARTY + i*20, V_SNAPTOTOP, patch, colormap); + } +#undef STARTY + + // Handle scrolling rankings + if (replayScrollDelay) + replayScrollDelay--; + else if (replayScrollDir > 0) + { + if (replayScrollTitle < (i*20 - SCALEDVIEWHEIGHT + 100)<<1) + replayScrollTitle++; + else + { + replayScrollDelay = TICRATE; + replayScrollDir = -1; + } + } + else + { + if (replayScrollTitle > 0) + replayScrollTitle--; + else + { + replayScrollDelay = TICRATE; + replayScrollDir = 1; + } + } + + V_DrawFill(10, 10, 300, 60, V_SNAPTOTOP|159); + DrawReplayHutReplayInfo(); + + V_DrawString(10, 72, V_SNAPTOTOP|highlightflags|V_ALLOWLOWERCASE, demolist[dir_on[menudepthleft]].title); + + // Draw a warning prompt if needed + switch (demolist[dir_on[menudepthleft]].addonstatus) + { + case DFILE_ERROR_CANNOTLOAD: + warning = "Some addons in this replay cannot be loaded.\nYou can watch anyway, but desyncs may occur."; + break; + + case DFILE_ERROR_NOTLOADED: + case DFILE_ERROR_INCOMPLETEOUTOFORDER: + warning = "Loading addons will mark your game as modified, and Record Attack may be unavailable.\nYou can watch without loading addons, but desyncs may occur."; + break; + + case DFILE_ERROR_EXTRAFILES: + warning = "You have addons loaded that were not present in this replay.\nYou can watch anyway, but desyncs may occur."; + break; + + case DFILE_ERROR_OUTOFORDER: + warning = "You have this replay's addons loaded, but they are out of order.\nYou can watch anyway, but desyncs may occur."; + break; + + default: + return; + } + + V_DrawSmallString(4, BASEVIDHEIGHT-14, V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_ALLOWLOWERCASE, warning); +} + +static boolean M_QuitReplayHut(void) +{ + // D_StartTitle does its own wipe, since GS_TIMEATTACK is now a complete gamestate. + menuactive = false; + D_StartTitle(); + + if (demolist) + Z_Free(demolist); + demolist = NULL; + + demo.inreplayhut = false; + + return true; +} + +static void M_HutStartReplay(INT32 choice) +{ + (void)choice; + + M_ClearMenus(false); + demo.loadfiles = (itemOn == 0); + demo.ignorefiles = (itemOn != 0); + + G_DoPlayDemo(demolist[dir_on[menudepthleft]].filepath); +} + +void M_SetPlaybackMenuPointer(void) +{ + itemOn = playback_pause; +} + +static void M_DrawPlaybackMenu(void) +{ + INT16 i; + patch_t *icon; + UINT8 *activemap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_GOLD, GTC_MENUCACHE); + UINT32 transmap = max(0, (INT32)(leveltime - playback_last_menu_interaction_leveltime - 4*TICRATE)) / 5; + transmap = min(8, transmap) << V_ALPHASHIFT; + + if (leveltime - playback_last_menu_interaction_leveltime >= 6*TICRATE) + playback_last_menu_interaction_leveltime = leveltime - 6*TICRATE; + + // Toggle items + if (paused && !demo.rewinding) + { + PlaybackMenu[playback_pause].status = PlaybackMenu[playback_fastforward].status = PlaybackMenu[playback_rewind].status = IT_DISABLED; + PlaybackMenu[playback_resume].status = PlaybackMenu[playback_advanceframe].status = PlaybackMenu[playback_backframe].status = IT_CALL|IT_STRING; + + if (itemOn >= playback_rewind && itemOn <= playback_fastforward) + itemOn += playback_backframe - playback_rewind; + } + else + { + PlaybackMenu[playback_pause].status = PlaybackMenu[playback_fastforward].status = PlaybackMenu[playback_rewind].status = IT_CALL|IT_STRING; + PlaybackMenu[playback_resume].status = PlaybackMenu[playback_advanceframe].status = PlaybackMenu[playback_backframe].status = IT_DISABLED; + + if (itemOn >= playback_backframe && itemOn <= playback_advanceframe) + itemOn -= playback_backframe - playback_rewind; + } + + if (modeattacking) + { + for (i = playback_viewcount; i <= playback_view4; i++) + PlaybackMenu[i].status = IT_DISABLED; + PlaybackMenu[playback_freecamera].alphaKey = 72; + PlaybackMenu[playback_quit].alphaKey = 88; + + currentMenu->x = BASEVIDWIDTH/2 - 52; + } + else + { + PlaybackMenu[playback_viewcount].status = IT_ARROWS|IT_STRING; + + for (i = 0; i <= r_splitscreen; i++) + PlaybackMenu[playback_view1+i].status = IT_ARROWS|IT_STRING; + for (i = r_splitscreen+1; i < 4; i++) + PlaybackMenu[playback_view1+i].status = IT_DISABLED; + + PlaybackMenu[playback_freecamera].alphaKey = 156; + PlaybackMenu[playback_quit].alphaKey = 172; + currentMenu->x = BASEVIDWIDTH/2 - 88; + } + + // wip + //M_DrawTextBox(currentMenu->x-68, currentMenu->y-7, 15, 15); + //M_DrawCenteredMenu(); + + for (i = 0; i < currentMenu->numitems; i++) + { + UINT8 *inactivemap = NULL; + + if (i >= playback_view1 && i <= playback_view4) + { + if (modeattacking) continue; + + if (r_splitscreen >= i - playback_view1) + { + INT32 ply = displayplayers[i - playback_view1]; + + icon = faceprefix[players[ply].skin][FACE_RANK]; + if (i != itemOn) + inactivemap = R_GetTranslationColormap(players[ply].skin, players[ply].skincolor, GTC_MENUCACHE); + } + else if (currentMenu->menuitems[i].patch && W_CheckNumForName(currentMenu->menuitems[i].patch) != LUMPERROR) + icon = W_CachePatchName(currentMenu->menuitems[i].patch, PU_CACHE); + else + icon = W_CachePatchName("PLAYRANK", PU_CACHE); // temp + } + else if (currentMenu->menuitems[i].status == IT_DISABLED) + continue; + else if (currentMenu->menuitems[i].patch && W_CheckNumForName(currentMenu->menuitems[i].patch) != LUMPERROR) + icon = W_CachePatchName(currentMenu->menuitems[i].patch, PU_CACHE); + else + icon = W_CachePatchName("PLAYRANK", PU_CACHE); // temp + + if ((i == playback_fastforward && cv_playbackspeed.value > 1) || (i == playback_rewind && demo.rewinding)) + V_DrawMappedPatch(currentMenu->x + currentMenu->menuitems[i].alphaKey, currentMenu->y, transmap|V_SNAPTOTOP, icon, R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_JAWZ, GTC_MENUCACHE)); + else + V_DrawMappedPatch(currentMenu->x + currentMenu->menuitems[i].alphaKey, currentMenu->y, transmap|V_SNAPTOTOP, icon, (i == itemOn) ? activemap : inactivemap); + + if (i == itemOn) + { + V_DrawCharacter(currentMenu->x + currentMenu->menuitems[i].alphaKey + 4, currentMenu->y + 14, + '\x1A' | transmap|V_SNAPTOTOP|highlightflags, false); + + V_DrawCenteredString(BASEVIDWIDTH/2, currentMenu->y + 18, transmap|V_SNAPTOTOP|V_ALLOWLOWERCASE, currentMenu->menuitems[i].text); + + if ((currentMenu->menuitems[i].status & IT_TYPE) == IT_ARROWS) + { + char *str; + + if (!(i == playback_viewcount && r_splitscreen == 3)) + V_DrawCharacter(BASEVIDWIDTH/2 - 4, currentMenu->y + 28 - (skullAnimCounter/5), + '\x1A' | transmap|V_SNAPTOTOP|highlightflags, false); // up arrow + + if (!(i == playback_viewcount && r_splitscreen == 0)) + V_DrawCharacter(BASEVIDWIDTH/2 - 4, currentMenu->y + 48 + (skullAnimCounter/5), + '\x1B' | transmap|V_SNAPTOTOP|highlightflags, false); // down arrow + + switch (i) + { + case playback_viewcount: + str = va("%d", r_splitscreen+1); + break; + + case playback_view1: + case playback_view2: + case playback_view3: + case playback_view4: + str = player_names[displayplayers[i - playback_view1]]; // 0 to 3 + break; + + default: // shouldn't ever be reached but whatever + continue; + } + + V_DrawCenteredString(BASEVIDWIDTH/2, currentMenu->y + 38, transmap|V_SNAPTOTOP|V_ALLOWLOWERCASE|highlightflags, str); + } + } + } +} + +static void M_PlaybackRewind(INT32 choice) +{ + static tic_t lastconfirmtime; + + (void)choice; + + if (!demo.rewinding) + { + if (paused) + { + G_ConfirmRewind(leveltime-1); + paused = true; + S_PauseAudio(); + } + else + demo.rewinding = paused = true; + } + else if (lastconfirmtime + TICRATE/2 < I_GetTime()) + { + lastconfirmtime = I_GetTime(); + G_ConfirmRewind(leveltime); + } + + CV_SetValue(&cv_playbackspeed, 1); +} + +static void M_PlaybackPause(INT32 choice) +{ + (void)choice; + + paused = !paused; + + if (demo.rewinding) + { + G_ConfirmRewind(leveltime); + paused = true; + S_PauseAudio(); + } + else if (paused) + S_PauseAudio(); + else + S_ResumeAudio(); + + CV_SetValue(&cv_playbackspeed, 1); +} + +static void M_PlaybackFastForward(INT32 choice) +{ + (void)choice; + + if (demo.rewinding) + { + G_ConfirmRewind(leveltime); + paused = false; + S_ResumeAudio(); + } + CV_SetValue(&cv_playbackspeed, cv_playbackspeed.value == 1 ? 4 : 1); +} + +static void M_PlaybackAdvance(INT32 choice) +{ + (void)choice; + + paused = false; + TryRunTics(1); + paused = true; +} + + +static void M_PlaybackSetViews(INT32 choice) +{ + + if (demo.freecam) + return; // not here. + + if (choice > 0) + { + if (r_splitscreen < 3) + G_AdjustView(r_splitscreen + 2, 0, true); + } + else if (r_splitscreen) + { + r_splitscreen--; + R_ExecuteSetViewSize(); + } +} + +static void M_PlaybackAdjustView(INT32 choice) +{ + G_AdjustView(itemOn - playback_viewcount, (choice > 0) ? 1 : -1, true); +} + +// this one's rather tricky +static void M_PlaybackToggleFreecam(INT32 choice) +{ + (void)choice; + M_ClearMenus(true); + + // remove splitscreen: + splitscreen = 0; + R_ExecuteSetViewSize(); + + P_InitCameraCmd(); // init camera controls + if (!demo.freecam) // toggle on + { + demo.freecam = true; + democam.cam = &camera[0]; // this is rather useful + } + else // toggle off + { + demo.freecam = false; + // reset democam vars: + democam.cam = NULL; + democam.keyboardlook = false; // reset only these. localangle / aiming gets set before the cam does anything anyway + } +} + + +static void M_PlaybackQuit(INT32 choice) +{ + (void)choice; + G_StopDemo(); + + if (demo.inreplayhut) + M_ReplayHut(choice); + else if (modeattacking) + { + M_EndModeAttackRun(); + S_ChangeMusicInternal("racent", true); + } + else + D_StartTitle(); +} + +static void M_PandorasBox(INT32 choice) +{ + (void)choice; + CV_StealthSetValue(&cv_dummyrings, players[consoleplayer].rings); + CV_StealthSetValue(&cv_dummylives, players[consoleplayer].lives); + M_SetupNextMenu(&SR_PandoraDef); +} + +static boolean M_ExitPandorasBox(void) +{ + if (cv_dummyrings.value != players[consoleplayer].rings) + COM_ImmedExecute(va("setrings %d", cv_dummyrings.value)); + if (cv_dummylives.value != players[consoleplayer].lives) + COM_ImmedExecute(va("setlives %d", cv_dummylives.value)); + return true; +} + +static void M_ChangeLevel(INT32 choice) +{ + char mapname[6]; + (void)choice; + + strlcpy(mapname, G_BuildMapName(cv_nextmap.value), sizeof (mapname)); + strlwr(mapname); + mapname[5] = '\0'; + + M_ClearMenus(true); + COM_BufAddText(va("map %s -gametype \"%s\"\n", mapname, cv_newgametype.string)); +} + +static void M_ConfirmSpectate(INT32 choice) +{ + (void)choice; + // We allow switching to spectator even if team changing is not allowed + M_ClearMenus(true); + COM_ImmedExecute("changeteam spectator"); +} + +static void M_ConfirmEnterGame(INT32 choice) +{ + (void)choice; + if (!cv_allowteamchange.value) + { + M_StartMessage(M_GetText("The server is not allowing\nteam changes at this time.\nPress a key.\n"), NULL, MM_NOTHING); + return; + } + M_ClearMenus(true); + COM_ImmedExecute("changeteam playing"); +} + +static void M_ConfirmTeamScramble(INT32 choice) +{ + (void)choice; + M_ClearMenus(true); + + COM_ImmedExecute(va("teamscramble %d", cv_dummyscramble.value+1)); +} + +static void M_ConfirmTeamChange(INT32 choice) +{ + (void)choice; + + if (cv_dummymenuplayer.value > splitscreen+1) + return; + + if (!cv_allowteamchange.value && cv_dummyteam.value) + { + M_StartMessage(M_GetText("The server is not allowing\nteam changes at this time.\nPress a key.\n"), NULL, MM_NOTHING); + return; + } + + M_ClearMenus(true); + + switch (cv_dummymenuplayer.value) + { + case 1: + default: + COM_ImmedExecute(va("changeteam %s", cv_dummyteam.string)); + break; + case 2: + COM_ImmedExecute(va("changeteam2 %s", cv_dummyteam.string)); + break; + case 3: + COM_ImmedExecute(va("changeteam3 %s", cv_dummyteam.string)); + break; + case 4: + COM_ImmedExecute(va("changeteam4 %s", cv_dummyteam.string)); + break; + } +} + +static void M_ConfirmSpectateChange(INT32 choice) +{ + (void)choice; + + if (cv_dummymenuplayer.value > splitscreen+1) + return; + + if (!cv_allowteamchange.value && cv_dummyspectate.value) + { + M_StartMessage(M_GetText("The server is not allowing\nteam changes at this time.\nPress a key.\n"), NULL, MM_NOTHING); + return; + } + + M_ClearMenus(true); + + switch (cv_dummymenuplayer.value) + { + case 1: + default: + COM_ImmedExecute(va("changeteam %s", cv_dummyspectate.string)); + break; + case 2: + COM_ImmedExecute(va("changeteam2 %s", cv_dummyspectate.string)); + break; + case 3: + COM_ImmedExecute(va("changeteam3 %s", cv_dummyspectate.string)); + break; + case 4: + COM_ImmedExecute(va("changeteam4 %s", cv_dummyspectate.string)); + break; + } +} + +static void M_Options(INT32 choice) +{ + (void)choice; + + // if the player is not admin or server, disable gameplay & server options + OP_MainMenu[4].status = OP_MainMenu[5].status = (Playing() && !(server || IsPlayerAdmin(consoleplayer))) ? (IT_GRAYEDOUT) : (IT_STRING|IT_SUBMENU); + + OP_MainMenu[8].status = (Playing()) ? (IT_GRAYEDOUT) : (IT_STRING|IT_CALL); // Play credits + +#ifdef HAVE_DISCORDRPC + OP_DataOptionsMenu[4].status = (Playing()) ? (IT_GRAYEDOUT) : (IT_STRING|IT_SUBMENU); // Erase data +#else + OP_DataOptionsMenu[3].status = (Playing()) ? (IT_GRAYEDOUT) : (IT_STRING|IT_SUBMENU); // Erase data +#endif + + OP_GameOptionsMenu[3].status = + (M_SecretUnlocked(SECRET_ENCORE)) ? (IT_CVAR|IT_STRING) : IT_SECRET; // cv_kartencore + + OP_MainDef.prevMenu = currentMenu; + M_SetupNextMenu(&OP_MainDef); +} + +static void M_Manual(INT32 choice) +{ + (void)choice; + + MISC_HelpDef.prevMenu = (choice == INT32_MAX ? NULL : currentMenu); + M_SetupNextMenu(&MISC_HelpDef); +} + +static void M_RetryResponse(INT32 ch) +{ + if (ch != 'y' && ch != KEY_ENTER) + return; + + if (!&players[consoleplayer] || netgame || multiplayer) // Should never happen! + return; + + M_ClearMenus(true); + G_SetRetryFlag(); +} + +static void M_Retry(INT32 choice) +{ + (void)choice; + M_StartMessage(M_GetText("Start this race over?\n\n(Press 'Y' to confirm)\n"),M_RetryResponse,MM_YESNO); +} + +static void M_SelectableClearMenus(INT32 choice) +{ + (void)choice; + M_ClearMenus(true); +} + +void M_RefreshPauseMenu(void) +{ +#ifdef HAVE_DISCORDRPC + if (discordRequestList != NULL) + { + MPauseMenu[mpause_discordrequests].status = IT_STRING | IT_SUBMENU; + } + else + { + MPauseMenu[mpause_discordrequests].status = IT_GRAYEDOUT; + } +#endif +} + +// ====== +// CHEATS +// ====== + +static void M_UltimateCheat(INT32 choice) +{ + (void)choice; + I_Quit(); +} + +static void M_GetAllEmeralds(INT32 choice) +{ + (void)choice; + + emeralds = EMERALD_ALL; + M_StartMessage(M_GetText("You now have all 7 emeralds.\nUse them wisely.\nWith great power comes great ring drain.\n"),NULL,MM_NOTHING); + + G_SetGameModified(multiplayer, true); +} + +static void M_DestroyRobotsResponse(INT32 ch) +{ + if (ch != 'y' && ch != KEY_ENTER) + return; + + // Destroy all robots + P_DestroyRobots(); + + G_SetGameModified(multiplayer, true); +} + +static void M_DestroyRobots(INT32 choice) +{ + (void)choice; + + 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); +} + +/*static void M_LevelSelectWarp(INT32 choice) +{ + boolean fromloadgame = (currentMenu == &SP_LevelSelectDef); + + (void)choice; + + if (W_CheckNumForName(G_BuildMapName(cv_nextmap.value)) == LUMPERROR) + { +// CONS_Alert(CONS_WARNING, "Internal game map '%s' not found\n", G_BuildMapName(cv_nextmap.value)); + return; + } + + startmap = (INT16)(cv_nextmap.value); + + fromlevelselect = true; + + if (fromloadgame) + G_LoadGame((UINT32)cursaveslot, startmap); + else + { + cursaveslot = -1; + M_SetupChoosePlayer(0); + } +}*/ + +// ======== +// SKY ROOM +// ======== + +UINT8 skyRoomMenuTranslations[MAXUNLOCKABLES]; + +static char *M_GetConditionString(condition_t cond) +{ + switch(cond.type) + { + case UC_PLAYTIME: + return va("Play for %i:%02i:%02i", + G_TicsToHours(cond.requirement), + G_TicsToMinutes(cond.requirement, false), + G_TicsToSeconds(cond.requirement)); + case UC_MATCHESPLAYED: + return va("Play %d matches", cond.requirement); + case UC_POWERLEVEL: + return va("Reach power level %d in %s", cond.requirement, (cond.extrainfo1 == PWRLV_BATTLE ? "Battle" : "Race")); + case UC_GAMECLEAR: + if (cond.requirement > 1) + return va("Beat game %d times", cond.requirement); + else + return va("Beat the game"); + case UC_OVERALLTIME: + return va("Get overall Time Attack of %i:%02i:%02i", + G_TicsToHours(cond.requirement), + G_TicsToMinutes(cond.requirement, false), + G_TicsToSeconds(cond.requirement)); + case UC_MAPVISITED: + return va("Visit %s", G_BuildMapTitle(cond.requirement-1)); + case UC_MAPBEATEN: + return va("Beat %s", G_BuildMapTitle(cond.requirement-1)); + case UC_MAPENCORE: + return va("Beat %s in Encore Mode", G_BuildMapTitle(cond.requirement-1)); + case UC_MAPTIME: + return va("Beat %s in %i:%02i.%02i", G_BuildMapTitle(cond.extrainfo1-1), + G_TicsToMinutes(cond.requirement, true), + G_TicsToSeconds(cond.requirement), + G_TicsToCentiseconds(cond.requirement)); + case UC_TOTALEMBLEMS: + return va("Get %d medals", cond.requirement); + case UC_EXTRAEMBLEM: + return va("Get \"%s\" medal", extraemblems[cond.requirement-1].name); + default: + return NULL; + } +} + +#define NUMCHECKLIST 23 +static void M_DrawChecklist(void) +{ + UINT32 i, line = 0, c; + INT32 lastid; + boolean secret = false; + + for (i = 0; i < MAXUNLOCKABLES; i++) + { + const char *secretname; + + secret = (!M_Achieved(unlockables[i].showconditionset - 1) && !unlockables[i].unlocked); + + if (unlockables[i].name[0] == 0 || unlockables[i].nochecklist + || !unlockables[i].conditionset || unlockables[i].conditionset > MAXCONDITIONSETS + || (unlockables[i].type == SECRET_HELLATTACK && secret)) // TODO: turn this into an unlockable setting instead of tying it to Hell Attack + continue; + + ++line; + secretname = M_CreateSecretMenuOption(unlockables[i].name); + + V_DrawString(8, (line*8), (unlockables[i].unlocked ? recommendedflags : warningflags), (secret ? secretname : unlockables[i].name)); + + if (conditionSets[unlockables[i].conditionset - 1].numconditions) + { + c = 0; + lastid = -1; + + for (c = 0; c < conditionSets[unlockables[i].conditionset - 1].numconditions; c++) + { + condition_t cond = conditionSets[unlockables[i].conditionset - 1].condition[c]; + UINT8 achieved = M_CheckCondition(&cond); + char *str = M_GetConditionString(cond); + const char *secretstr = M_CreateSecretMenuOption(str); + + if (!str) + continue; + + ++line; + + if (lastid == -1 || cond.id != (UINT32)lastid) + { + V_DrawString(16, (line*8), V_MONOSPACE|V_ALLOWLOWERCASE|(achieved ? highlightflags : 0), "*"); + V_DrawString(32, (line*8), V_MONOSPACE|V_ALLOWLOWERCASE|(achieved ? highlightflags : 0), (secret ? secretstr : str)); + } + else + { + V_DrawString(32, (line*8), V_MONOSPACE|V_ALLOWLOWERCASE|(achieved ? highlightflags : 0), (secret ? "?" : "&")); + V_DrawString(48, (line*8), V_MONOSPACE|V_ALLOWLOWERCASE|(achieved ? highlightflags : 0), (secret ? secretstr : str)); + } + + lastid = cond.id; + } + } + + ++line; + + if (line >= NUMCHECKLIST) + break; + } +} +#undef NUMCHECKLIST + +#define NUMHINTS 5 +static void M_EmblemHints(INT32 choice) +{ + (void)choice; + SR_EmblemHintMenu[0].status = (M_SecretUnlocked(SECRET_ITEMFINDER)) ? (IT_CVAR|IT_STRING) : (IT_SECRET); + M_SetupNextMenu(&SR_EmblemHintDef); + itemOn = 1; // always start on back. +} + +static void M_DrawEmblemHints(void) +{ + INT32 i, j = 0; + UINT32 collected = 0; + emblem_t *emblem; + const char *hint; + + for (i = 0; i < numemblems; i++) + { + emblem = &emblemlocations[i]; + if (emblem->level != gamemap || emblem->type != ET_GLOBAL) + continue; + + if (emblem->collected) + { + collected = recommendedflags; + V_DrawMappedPatch(12, 12+(28*j), 0, W_CachePatchName(M_GetEmblemPatch(emblem, false), PU_CACHE), + R_GetTranslationColormap(TC_DEFAULT, M_GetEmblemColor(emblem), GTC_MENUCACHE)); + } + else + { + collected = 0; + V_DrawScaledPatch(12, 12+(28*j), 0, W_CachePatchName("NEEDIT", PU_CACHE)); + } + + if (emblem->hint[0]) + hint = emblem->hint; + else + hint = M_GetText("No hints available."); + hint = V_WordWrap(40, BASEVIDWIDTH-12, 0, hint); + V_DrawString(40, 8+(28*j), V_ALLOWLOWERCASE|collected, hint); + + if (++j >= NUMHINTS) + break; + } + if (!j) + V_DrawCenteredString(160, 48, highlightflags, "No hidden medals on this map."); + + M_DrawGenericMenu(); +} + +static void M_DrawSkyRoom(void) +{ + INT32 i, y = 0; + INT32 lengthstring = 0; + + M_DrawGenericMenu(); + + if (currentMenu == &OP_SoundOptionsDef) + { + V_DrawRightAlignedString(BASEVIDWIDTH - currentMenu->x, + currentMenu->y+currentMenu->menuitems[0].alphaKey, + (sound_disabled ? warningflags : highlightflags), + (sound_disabled ? "OFF" : "ON")); + + V_DrawRightAlignedString(BASEVIDWIDTH - currentMenu->x, + currentMenu->y+currentMenu->menuitems[2].alphaKey, + (digital_disabled ? warningflags : highlightflags), + (digital_disabled ? "OFF" : "ON")); + + if (itemOn == 0) + lengthstring = 8*(sound_disabled ? 3 : 2); + else if (itemOn == 2) + lengthstring = 8*(digital_disabled ? 3 : 2); + } + + for (i = 0; i < currentMenu->numitems; ++i) + { + if (currentMenu->menuitems[i].itemaction == M_HandleSoundTest) + { + y = currentMenu->menuitems[i].alphaKey; + break; + } + } + + if (y) + { + y += currentMenu->y; + + V_DrawRightAlignedString(BASEVIDWIDTH - currentMenu->x, y, highlightflags, cv_soundtest.string); + if (cv_soundtest.value) + V_DrawRightAlignedString(BASEVIDWIDTH - currentMenu->x, y + 8, highlightflags, S_sfx[cv_soundtest.value].name); + + if (i == itemOn) + lengthstring = V_StringWidth(cv_soundtest.string, 0); + } + + if (lengthstring) + { + V_DrawCharacter(BASEVIDWIDTH - currentMenu->x - 10 - lengthstring - (skullAnimCounter/5), currentMenu->y+currentMenu->menuitems[itemOn].alphaKey, + '\x1C' | highlightflags, false); // left arrow + V_DrawCharacter(BASEVIDWIDTH - currentMenu->x + 2 + (skullAnimCounter/5), currentMenu->y+currentMenu->menuitems[itemOn].alphaKey, + '\x1D' | highlightflags, false); // right arrow + } +} + +static void M_HandleSoundTest(INT32 choice) +{ + boolean exitmenu = false; // exit to previous menu + + switch (choice) + { + case KEY_DOWNARROW: + M_NextOpt(); + S_StartSound(NULL, sfx_menu1); + break; + case KEY_UPARROW: + M_PrevOpt(); + S_StartSound(NULL, sfx_menu1); + break; + case KEY_BACKSPACE: + case KEY_ESCAPE: + exitmenu = true; + break; + + case KEY_RIGHTARROW: + CV_AddValue(&cv_soundtest, 1); + break; + case KEY_LEFTARROW: + CV_AddValue(&cv_soundtest, -1); + break; + case KEY_ENTER: + S_StopSounds(); + S_StartSound(NULL, cv_soundtest.value); + break; + + default: + break; + } + if (exitmenu) + { + if (currentMenu->prevMenu) + M_SetupNextMenu(currentMenu->prevMenu); + else + M_ClearMenus(true); + } +} + +// Entering secrets menu +/*static void M_SecretsMenu(INT32 choice) +{ + INT32 i, j, ul; + UINT8 done[MAXUNLOCKABLES]; + UINT16 curheight; + + (void)choice; + + // Clear all before starting + for (i = 1; i < MAXUNLOCKABLES+1; ++i) + SR_MainMenu[i].status = IT_DISABLED; + + memset(skyRoomMenuTranslations, 0, sizeof(skyRoomMenuTranslations)); + memset(done, 0, sizeof(done)); + + for (i = 1; i < MAXUNLOCKABLES+1; ++i) + { + curheight = UINT16_MAX; + ul = -1; + + // Autosort unlockables + for (j = 0; j < MAXUNLOCKABLES; ++j) + { + if (!unlockables[j].height || done[j] || unlockables[j].type < 0) + continue; + + if (unlockables[j].height < curheight) + { + curheight = unlockables[j].height; + ul = j; + } + } + if (ul < 0) + break; + + done[ul] = true; + + skyRoomMenuTranslations[i-1] = (UINT8)ul; + SR_MainMenu[i].text = unlockables[ul].name; + SR_MainMenu[i].alphaKey = (UINT8)unlockables[ul].height; + + if (unlockables[ul].type == SECRET_HEADER) + { + SR_MainMenu[i].status = IT_HEADER; + continue; + } + + SR_MainMenu[i].status = IT_SECRET; + + if (unlockables[ul].unlocked) + { + switch (unlockables[ul].type) + { + case SECRET_LEVELSELECT: + SR_MainMenu[i].status = IT_STRING|IT_CALL; + SR_MainMenu[i].itemaction = M_CustomLevelSelect; + break; + case SECRET_WARP: + SR_MainMenu[i].status = IT_STRING|IT_CALL; + SR_MainMenu[i].itemaction = M_CustomWarp; + break; + case SECRET_CREDITS: + SR_MainMenu[i].status = IT_STRING|IT_CALL; + SR_MainMenu[i].itemaction = M_Credits; + break; + case SECRET_SOUNDTEST: + SR_MainMenu[i].status = IT_STRING|IT_KEYHANDLER; + SR_MainMenu[i].itemaction = M_HandleSoundTest; + default: + break; + } + } + } + + M_SetupNextMenu(&SR_MainDef); +}*/ + +// ================== +// NEW GAME FUNCTIONS +// ================== + +/*INT32 ultimate_selectable = false; + +static void M_NewGame(void) +{ + fromlevelselect = false; + + startmap = spstage_start; + CV_SetValue(&cv_newgametype, GT_RACE); // SRB2kart + + M_SetupChoosePlayer(0); +}*/ + +/*static void M_CustomWarp(INT32 choice) +{ + INT32 ul = skyRoomMenuTranslations[choice-1]; + + startmap = (INT16)(unlockables[ul].variable); + + M_SetupChoosePlayer(0); +}*/ + +static void M_Credits(INT32 choice) +{ + (void)choice; + cursaveslot = -2; + M_ClearMenus(true); + F_StartCredits(); +} + +/*static void M_CustomLevelSelect(INT32 choice) +{ + INT32 ul = skyRoomMenuTranslations[choice-1]; + + SR_LevelSelectDef.prevMenu = currentMenu; + levellistmode = LLM_LEVELSELECT; + maplistoption = (UINT8)(unlockables[ul].variable); + if (M_CountLevelsToShowInList() == 0) + { + M_StartMessage(M_GetText("No selectable levels found.\n"),NULL,MM_NOTHING); + return; + } + + M_PrepareLevelSelect(); + M_SetupNextMenu(&SR_LevelSelectDef); +}*/ + +// ================== +// SINGLE PLAYER MENU +// ================== + +#ifndef TESTERS +static void M_SinglePlayerMenu(INT32 choice) +{ + (void)choice; + + SP_MainMenu[spgrandprix].status = IT_CALL|IT_STRING; + SP_MainMenu[sptimeattack].status = + (M_SecretUnlocked(SECRET_TIMEATTACK)) ? IT_CALL|IT_STRING : IT_SECRET; + SP_MainMenu[spbreakthecapsules].status = + (M_SecretUnlocked(SECRET_BREAKTHECAPSULES)) ? IT_CALL|IT_STRING : IT_SECRET; + + M_SetupNextMenu(&SP_MainDef); +} +#endif + +/*static void M_LoadGameLevelSelect(INT32 choice) +{ + (void)choice; + levellistmode = LLM_LEVELSELECT; + maplistoption = 1; + if (M_CountLevelsToShowInList() == 0) + { + M_StartMessage(M_GetText("No selectable levels found.\n"),NULL,MM_NOTHING); + return; + } + + SP_LevelSelectDef.prevMenu = currentMenu; + + M_PrepareLevelSelect(); + M_SetupNextMenu(&SP_LevelSelectDef); +}*/ + +// ============== +// LOAD GAME MENU +// ============== + +/*static INT32 saveSlotSelected = 0; +static short menumovedir = 0; + +static void M_DrawLoadGameData(void) +{ + INT32 ecks; + INT32 i; + + ecks = SP_LoadDef.x + 24; + M_DrawTextBox(SP_LoadDef.x-12,144, 24, 4); + + if (saveSlotSelected == NOSAVESLOT) // last slot is play without saving + { + if (ultimate_selectable) + { + V_DrawCenteredString(ecks + 68, 144, V_ORANGEMAP, "ULTIMATE MODE"); + V_DrawCenteredString(ecks + 68, 156, 0, "NO RINGS, NO ONE-UPS,"); + V_DrawCenteredString(ecks + 68, 164, 0, "NO CONTINUES, ONE LIFE,"); + V_DrawCenteredString(ecks + 68, 172, 0, "FINAL DESTINATION."); + } + else + { + V_DrawCenteredString(ecks + 68, 144, V_ORANGEMAP, "PLAY WITHOUT SAVING"); + V_DrawCenteredString(ecks + 68, 156, 0, "THIS GAME WILL NOT BE"); + V_DrawCenteredString(ecks + 68, 164, 0, "SAVED, BUT YOU CAN STILL"); + V_DrawCenteredString(ecks + 68, 172, 0, "GET MEDALS AND SECRETS."); + } + return; + } + + if (savegameinfo[saveSlotSelected].lives == -42) // Empty + { + V_DrawCenteredString(ecks + 68, 160, 0, "NO DATA"); + return; + } + + if (savegameinfo[saveSlotSelected].lives == -666) // savegame is bad + { + V_DrawCenteredString(ecks + 68, 144, warningflags, "CORRUPT SAVE FILE"); + V_DrawCenteredString(ecks + 68, 156, 0, "THIS SAVE FILE"); + V_DrawCenteredString(ecks + 68, 164, 0, "CAN NOT BE LOADED."); + V_DrawCenteredString(ecks + 68, 172, 0, "DELETE USING BACKSPACE."); + return; + } + + // Draw the back sprite, it looks ugly if we don't + V_DrawScaledPatch(SP_LoadDef.x, 144+8, 0, livesback); + if (savegameinfo[saveSlotSelected].skincolor == 0) + V_DrawScaledPatch(SP_LoadDef.x,144+8,0,W_CachePatchName(skins[savegameinfo[saveSlotSelected].skinnum].face, PU_CACHE)); + else + { + UINT8 *colormap = R_GetTranslationColormap(savegameinfo[saveSlotSelected].skinnum, savegameinfo[saveSlotSelected].skincolor, GTC_MENUCACHE); + V_DrawMappedPatch(SP_LoadDef.x,144+8,0,W_CachePatchName(skins[savegameinfo[saveSlotSelected].skinnum].face, PU_CACHE), colormap); + } + + V_DrawString(ecks + 12, 152, 0, savegameinfo[saveSlotSelected].playername); + +#ifdef SAVEGAMES_OTHERVERSIONS + if (savegameinfo[saveSlotSelected].gamemap & 16384) + V_DrawCenteredString(ecks + 68, 144, warningflags, "OUTDATED SAVE FILE!"); +#endif + + if (savegameinfo[saveSlotSelected].gamemap & 8192) + V_DrawString(ecks + 12, 160, recommendedflags, "CLEAR!"); + else + V_DrawString(ecks + 12, 160, 0, va("%s", savegameinfo[saveSlotSelected].levelname)); + + // Use the big face pic for lives, duh. :3 + V_DrawScaledPatch(ecks + 12, 175, 0, W_CachePatchName("STLIVEX", PU_HUDGFX)); + V_DrawTallNum(ecks + 40, 172, 0, savegameinfo[saveSlotSelected].lives); + + // Absolute ridiculousness, condensed into another function. + V_DrawContinueIcon(ecks + 58, 182, 0, savegameinfo[saveSlotSelected].skinnum, savegameinfo[saveSlotSelected].skincolor); + V_DrawScaledPatch(ecks + 68, 175, 0, W_CachePatchName("STLIVEX", PU_HUDGFX)); + V_DrawTallNum(ecks + 96, 172, 0, savegameinfo[saveSlotSelected].continues); + + for (i = 0; i < 7; ++i) + { + if (savegameinfo[saveSlotSelected].numemeralds & (1 << i)) + V_DrawScaledPatch(ecks + 104 + (i * 8), 172, 0, tinyemeraldpics[i]); + } +} + +#define LOADBARHEIGHT SP_LoadDef.y + (LINEHEIGHT * (j+1)) + ymod +#define CURSORHEIGHT SP_LoadDef.y + (LINEHEIGHT*3) - 1 +static void M_DrawLoad(void) +{ + INT32 i, j; + INT32 ymod = 0, offset = 0; + + M_DrawMenuTitle(); + + if (menumovedir != 0) //movement illusion + { + ymod = (-(LINEHEIGHT/4))*menumovedir; + offset = ((menumovedir > 0) ? -1 : 1); + } + + V_DrawCenteredString(BASEVIDWIDTH/2, 40, 0, "Press backspace to delete a save."); + + for (i = MAXSAVEGAMES + saveSlotSelected - 2 + offset, j = 0;i <= MAXSAVEGAMES + saveSlotSelected + 2 + offset; i++, j++) + { + if ((menumovedir < 0 && j == 4) || (menumovedir > 0 && j == 0)) + continue; //this helps give the illusion of movement + + M_DrawSaveLoadBorder(SP_LoadDef.x, LOADBARHEIGHT); + + if ((i%MAXSAVEGAMES) == NOSAVESLOT) // play without saving + { + if (ultimate_selectable) + V_DrawCenteredString(SP_LoadDef.x+92, LOADBARHEIGHT - 1, V_ORANGEMAP, "ULTIMATE MODE"); + else + V_DrawCenteredString(SP_LoadDef.x+92, LOADBARHEIGHT - 1, V_ORANGEMAP, "PLAY WITHOUT SAVING"); + continue; + } + + if (savegameinfo[i%MAXSAVEGAMES].lives == -42) + V_DrawString(SP_LoadDef.x-6, LOADBARHEIGHT - 1, V_TRANSLUCENT, "NO DATA"); + else if (savegameinfo[i%MAXSAVEGAMES].lives == -666) + V_DrawString(SP_LoadDef.x-6, LOADBARHEIGHT - 1, warningflags, "CORRUPT SAVE FILE"); + else if (savegameinfo[i%MAXSAVEGAMES].gamemap & 8192) + V_DrawString(SP_LoadDef.x-6, LOADBARHEIGHT - 1, recommendedflags, "CLEAR!"); + else + V_DrawString(SP_LoadDef.x-6, LOADBARHEIGHT - 1, 0, va("%s", savegameinfo[i%MAXSAVEGAMES].levelname)); + + //Draw the save slot number on the right side + V_DrawRightAlignedString(SP_LoadDef.x+192, LOADBARHEIGHT - 1, 0, va("%d",(i%MAXSAVEGAMES) + 1)); + } + + //Draw cursors on both sides. + V_DrawScaledPatch( 32, CURSORHEIGHT, 0, W_CachePatchName("M_CURSOR", PU_CACHE)); + V_DrawScaledPatch(274, CURSORHEIGHT, 0, W_CachePatchName("M_CURSOR", PU_CACHE)); + + M_DrawLoadGameData(); + + //finishing the movement illusion + if (menumovedir) + menumovedir += ((menumovedir > 0) ? 1 : -1); + if (abs(menumovedir) > 3) + menumovedir = 0; +} +#undef LOADBARHEIGHT +#undef CURSORHEIGHT + +// +// User wants to load this game +// +static void M_LoadSelect(INT32 choice) +{ + (void)choice; + + if (saveSlotSelected == NOSAVESLOT) //last slot is play without saving + { + M_NewGame(); + cursaveslot = -1; + return; + } + + if (!FIL_ReadFileOK(va(savegamename, saveSlotSelected))) + { + // This slot is empty, so start a new game here. + M_NewGame(); + } + else if (savegameinfo[saveSlotSelected].gamemap & 8192) // Completed + M_LoadGameLevelSelect(saveSlotSelected + 1); + else + G_LoadGame((UINT32)saveSlotSelected, 0); + + cursaveslot = saveSlotSelected; +} + +#define VERSIONSIZE 16 +#define BADSAVE { savegameinfo[slot].lives = -666; Z_Free(savebuffer); return; } +#define CHECKPOS if (save_p >= end_p) BADSAVE +// Reads the save file to list lives, level, player, etc. +// Tails 05-29-2003 +static void M_ReadSavegameInfo(UINT32 slot) +{ + size_t length; + char savename[255]; + UINT8 *savebuffer; + UINT8 *end_p; // buffer end point, don't read past here + UINT8 *save_p; + INT32 fake; // Dummy variable + char temp[sizeof(timeattackfolder)]; + char vcheck[VERSIONSIZE]; +#ifdef SAVEGAMES_OTHERVERSIONS + boolean oldversion = false; +#endif + + sprintf(savename, savegamename, slot); + + length = FIL_ReadFile(savename, &savebuffer); + if (length == 0) + { + savegameinfo[slot].lives = -42; + return; + } + + end_p = savebuffer + length; + + // skip the description field + save_p = savebuffer; + + // Version check + memset(vcheck, 0, sizeof (vcheck)); + sprintf(vcheck, "version %d", VERSION); + if (strcmp((const char *)save_p, (const char *)vcheck)) + { +#ifdef SAVEGAMES_OTHERVERSIONS + oldversion = true; +#else + BADSAVE // Incompatible versions? +#endif + } + save_p += VERSIONSIZE; + + // dearchive all the modifications + // P_UnArchiveMisc() + + CHECKPOS + fake = READINT16(save_p); + + if (((fake-1) & 8191) >= NUMMAPS) BADSAVE + + if(!mapheaderinfo[(fake-1) & 8191]) + { + savegameinfo[slot].levelname[0] = '\0'; + savegameinfo[slot].actnum = 0; + } + else + { + strcpy(savegameinfo[slot].levelname, mapheaderinfo[(fake-1) & 8191]->lvlttl); + savegameinfo[slot].actnum = 0; //mapheaderinfo[(fake-1) & 8191]->actnum + } + +#ifdef SAVEGAMES_OTHERVERSIONS + if (oldversion) + { + if (fake == 24) //meh, let's count old Clear! saves too + fake |= 8192; + fake |= 16384; // marker for outdated version + } +#endif + savegameinfo[slot].gamemap = fake; + + CHECKPOS + fake = READUINT16(save_p)-357; // emeralds + + savegameinfo[slot].numemeralds = (UINT8)fake; + + CHECKPOS + READSTRINGN(save_p, temp, sizeof(temp)); // mod it belongs to + + if (strcmp(temp, timeattackfolder)) BADSAVE + + // P_UnArchivePlayer() + CHECKPOS + savegameinfo[slot].skincolor = READUINT8(save_p); + CHECKPOS + savegameinfo[slot].skinnum = READUINT8(save_p); + + CHECKPOS + (void)READINT32(save_p); // Score + + CHECKPOS + savegameinfo[slot].lives = READINT32(save_p); // lives + CHECKPOS + savegameinfo[slot].continues = READINT32(save_p); // continues + + if (fake & (1<<10)) + { + CHECKPOS + savegameinfo[slot].botskin = READUINT8(save_p); + if (savegameinfo[slot].botskin-1 >= numskins) + savegameinfo[slot].botskin = 0; + CHECKPOS + savegameinfo[slot].botcolor = READUINT8(save_p); // because why not. + } + else + savegameinfo[slot].botskin = 0; + + if (savegameinfo[slot].botskin) + snprintf(savegameinfo[slot].playername, 36, "%s & %s", + skins[savegameinfo[slot].skinnum].realname, + skins[savegameinfo[slot].botskin-1].realname); + else + strcpy(savegameinfo[slot].playername, skins[savegameinfo[slot].skinnum].realname); + + savegameinfo[slot].playername[31] = 0; + + // File end marker check + CHECKPOS + if (READUINT8(save_p) != 0x1d) BADSAVE; + + // done + Z_Free(savebuffer); +} +#undef CHECKPOS +#undef BADSAVE + +// +// M_ReadSaveStrings +// read the strings from the savegame files +// and put it in savegamestrings global variable +// +static void M_ReadSaveStrings(void) +{ + FILE *handle; + UINT32 i; + char name[256]; + + for (i = 0; i < MAXSAVEGAMES; i++) + { + snprintf(name, sizeof name, savegamename, i); + name[sizeof name - 1] = '\0'; + + handle = fopen(name, "rb"); + if (handle == NULL) + { + savegameinfo[i].lives = -42; + continue; + } + fclose(handle); + M_ReadSavegameInfo(i); + } +} + +// +// User wants to delete this game +// +static void M_SaveGameDeleteResponse(INT32 ch) +{ + char name[256]; + + if (ch != 'y' && ch != KEY_ENTER) + return; + + // delete savegame + snprintf(name, sizeof name, savegamename, saveSlotSelected); + name[sizeof name - 1] = '\0'; + remove(name); + + // Refresh savegame menu info + M_ReadSaveStrings(); +} + +static void M_HandleLoadSave(INT32 choice) +{ + boolean exitmenu = false; // exit to previous menu + + switch (choice) + { + case KEY_DOWNARROW: + S_StartSound(NULL, sfx_menu1); + ++saveSlotSelected; + if (saveSlotSelected >= MAXSAVEGAMES) + saveSlotSelected -= MAXSAVEGAMES; + menumovedir = 1; + break; + + case KEY_UPARROW: + S_StartSound(NULL, sfx_menu1); + --saveSlotSelected; + if (saveSlotSelected < 0) + saveSlotSelected += MAXSAVEGAMES; + menumovedir = -1; + break; + + case KEY_ENTER: + S_StartSound(NULL, sfx_menu1); + if (savegameinfo[saveSlotSelected].lives != -666) // don't allow loading of "bad saves" + M_LoadSelect(saveSlotSelected); + break; + + case KEY_ESCAPE: + exitmenu = true; + break; + + case KEY_BACKSPACE: + S_StartSound(NULL, sfx_menu1); + // Don't allow people to 'delete' "Play without Saving." + // Nor allow people to 'delete' slots with no saves in them. + if (saveSlotSelected != NOSAVESLOT && savegameinfo[saveSlotSelected].lives != -42) + M_StartMessage(M_GetText("Are you sure you want to delete\nthis save game?\n\n(Press 'Y' to confirm)\n"),M_SaveGameDeleteResponse,MM_YESNO); + break; + } + if (exitmenu) + { + if (currentMenu->prevMenu) + M_SetupNextMenu(currentMenu->prevMenu); + else + M_ClearMenus(true); + } +} + +// +// Selected from SRB2 menu +// +static void M_LoadGame(INT32 choice) +{ + (void)choice; + + M_ReadSaveStrings(); + M_SetupNextMenu(&SP_LoadDef); +} + +// +// Used by cheats to force the save menu to a specific spot. +// +void M_ForceSaveSlotSelected(INT32 sslot) +{ + // Already there? Out of bounds? Whatever, then! + if (sslot == saveSlotSelected || sslot >= MAXSAVEGAMES) + return; + + // Figure out whether to display up movement or down movement + menumovedir = (saveSlotSelected - sslot) > 0 ? -1 : 1; + if (abs(saveSlotSelected - sslot) > (MAXSAVEGAMES>>1)) + menumovedir *= -1; + + saveSlotSelected = sslot; +} + +// ================ +// CHARACTER SELECT +// ================ + +static void M_SetupChoosePlayer(INT32 choice) +{ + (void)choice; + + if (mapheaderinfo[startmap-1] && mapheaderinfo[startmap-1]->forcecharacter[0] != '\0') + { + M_ChoosePlayer(0); //oh for crying out loud just get STARTED, it doesn't matter! + return; + } + + if (Playing() == false) + { + S_StopMusic(); + S_ChangeMusicInternal("chrsel", true); + } + + SP_PlayerDef.prevMenu = currentMenu; + M_SetupNextMenu(&SP_PlayerDef); + char_scroll = itemOn*128*FRACUNIT; // finish scrolling the menu + Z_Free(char_notes); + char_notes = NULL; +} + +// Draw the choose player setup menu, had some fun with player anim +static void M_DrawSetupChoosePlayerMenu(void) +{ + const INT32 my = 24; + patch_t *patch; + INT32 i, o, j; + char *picname; + + // Black BG + V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31); + //V_DrawPatchFill(W_CachePatchName("SRB2BACK", PU_CACHE)); + + // Character select profile images!1 + M_DrawTextBox(0, my, 16, 20); + + if (abs(itemOn*128*FRACUNIT - char_scroll) > 256*FRACUNIT) + char_scroll = itemOn*128*FRACUNIT; + else if (itemOn*128*FRACUNIT - char_scroll > 128*FRACUNIT) + char_scroll += 48*FRACUNIT; + else if (itemOn*128*FRACUNIT - char_scroll < -128*FRACUNIT) + char_scroll -= 48*FRACUNIT; + else if (itemOn*128*FRACUNIT > char_scroll+16*FRACUNIT) + char_scroll += 16*FRACUNIT; + else if (itemOn*128*FRACUNIT < char_scroll-16*FRACUNIT) + char_scroll -= 16*FRACUNIT; + else // close enough. + char_scroll = itemOn*128*FRACUNIT; // just be exact now. + i = (char_scroll+16*FRACUNIT)/(128*FRACUNIT); + o = ((char_scroll/FRACUNIT)+16)%128; + + // prev character + if (i-1 >= 0 && PlayerMenu[i-1].status != IT_DISABLED + && o < 32) + { + picname = description[i-1].picname; + if (picname[0] == '\0') + { + picname = strtok(Z_StrDup(description[i-1].skinname), "&"); + for (j = 0; j < numskins; j++) + if (stricmp(skins[j].name, picname) == 0) + { + Z_Free(picname); + picname = skins[j].charsel; + break; + } + if (j == numskins) // AAAAAAAAAA + picname = skins[0].charsel; + } + patch = W_CachePatchName(picname, PU_CACHE); + if (SHORT(patch->width) >= 256) + V_DrawCroppedPatch(8<height) - 64 + o*2, SHORT(patch->width), SHORT(patch->height)); + else + V_DrawCroppedPatch(8<height) - 32 + o, SHORT(patch->width), SHORT(patch->height)); + W_UnlockCachedPatch(patch); + } + + // next character + if (i+1 < currentMenu->numitems && PlayerMenu[i+1].status != IT_DISABLED + && o < 128) + { + picname = description[i+1].picname; + if (picname[0] == '\0') + { + picname = strtok(Z_StrDup(description[i+1].skinname), "&"); + for (j = 0; j < numskins; j++) + if (stricmp(skins[j].name, picname) == 0) + { + Z_Free(picname); + picname = skins[j].charsel; + break; + } + if (j == numskins) // AAAAAAAAAA + picname = skins[0].charsel; + } + patch = W_CachePatchName(picname, PU_CACHE); + if (SHORT(patch->width) >= 256) + V_DrawCroppedPatch(8<width), o*2); + else + V_DrawCroppedPatch(8<width), o); + W_UnlockCachedPatch(patch); + } + + // current character + if (i < currentMenu->numitems && PlayerMenu[i].status != IT_DISABLED) + { + picname = description[i].picname; + if (picname[0] == '\0') + { + picname = strtok(Z_StrDup(description[i].skinname), "&"); + for (j = 0; j < numskins; j++) + if (stricmp(skins[j].name, picname) == 0) + { + Z_Free(picname); + picname = skins[j].charsel; + break; + } + if (j == numskins) // AAAAAAAAAA + picname = skins[0].charsel; + } + patch = W_CachePatchName(picname, PU_CACHE); + if (o >= 0 && o <= 32) + { + if (SHORT(patch->width) >= 256) + V_DrawSmallScaledPatch(8, my + 40 - o, 0, patch); + else + V_DrawScaledPatch(8, my + 40 - o, 0, patch); + } + else + { + if (SHORT(patch->width) >= 256) + V_DrawCroppedPatch(8<width), SHORT(patch->height)); + else + V_DrawCroppedPatch(8<width), SHORT(patch->height)); + } + W_UnlockCachedPatch(patch); + } + + // draw title (or big pic) + M_DrawMenuTitle(); + + // Character description + M_DrawTextBox(136, my, 21, 20); + if (!char_notes) + char_notes = V_WordWrap(0, 21*8, V_ALLOWLOWERCASE, description[itemOn].notes); + V_DrawString(146, my + 9, V_ALLOWLOWERCASE, char_notes); +} + +// Chose the player you want to use Tails 03-02-2002 +static void M_ChoosePlayer(INT32 choice) +{ + char *skin1,*skin2; + INT32 skinnum; + //boolean ultmode = (ultimate_selectable && SP_PlayerDef.prevMenu == &SP_LoadDef && saveSlotSelected == NOSAVESLOT); + + // skip this if forcecharacter + if (mapheaderinfo[startmap-1] && mapheaderinfo[startmap-1]->forcecharacter[0] == '\0') + { + // M_SetupChoosePlayer didn't call us directly, that means we've been properly set up. + char_scroll = itemOn*128*FRACUNIT; // finish scrolling the menu + M_DrawSetupChoosePlayerMenu(); // draw the finally selected character one last time for the fadeout + } + M_ClearMenus(true); + + skin1 = strtok(description[choice].skinname, "&"); + skin2 = strtok(NULL, "&"); + + if (skin2) { + // this character has a second skin + skinnum = R_SkinAvailable(skin1); + botskin = (UINT8)(R_SkinAvailable(skin2)+1); + botingame = true; + + botcolor = skins[botskin-1].prefcolor; + + // undo the strtok + description[choice].skinname[strlen(skin1)] = '&'; + } else { + skinnum = R_SkinAvailable(description[choice].skinname); + botingame = false; + botskin = 0; + botcolor = 0; + } + + if (startmap != spstage_start) + cursaveslot = -1; + + lastmapsaved = 0; + gamecomplete = false; + + G_DeferedInitNew(false, G_BuildMapName(startmap), (UINT8)skinnum, 0, fromlevelselect); + COM_BufAddText("dummyconsvar 1\n"); // G_DeferedInitNew doesn't do this +}*/ + +// =============== +// STATISTICS MENU +// =============== + +static INT32 statsLocation; +static INT32 statsMax; +static INT16 statsMapList[NUMMAPS+1]; + +static void M_Statistics(INT32 choice) +{ + INT16 i, j = 0; + + (void)choice; + + memset(statsMapList, 0, sizeof(statsMapList)); + + for (i = 0; i < NUMMAPS; i++) + { + if (!mapheaderinfo[i] || mapheaderinfo[i]->lvlttl[0] == '\0') + continue; + + if (mapheaderinfo[i]->menuflags & (LF2_HIDEINSTATS|LF2_HIDEINMENU)) + continue; + + if (M_MapLocked(i+1)) // !mapvisited[i] + continue; + + statsMapList[j++] = i; + } + statsMapList[j] = -1; + statsMax = j - 11 + numextraemblems; + statsLocation = 0; + + if (statsMax < 0) + statsMax = 0; + + M_SetupNextMenu(&SP_LevelStatsDef); +} + +static void M_DrawStatsMaps(int location) +{ + INT32 y = 88, i = -1; + INT16 mnum; + extraemblem_t *exemblem; + boolean dotopname = true, dobottomarrow = (location < statsMax); + + if (location) + V_DrawCharacter(10, y-(skullAnimCounter/5), + '\x1A' | highlightflags, false); // up arrow + + while (statsMapList[++i] != -1) + { + if (location) + { + --location; + continue; + } + else if (dotopname) + { + V_DrawString(20, y, highlightflags, "LEVEL NAME"); + V_DrawString(256, y, highlightflags, "MEDALS"); + y += 8; + dotopname = false; + } + + mnum = statsMapList[i]; + M_DrawMapEmblems(mnum+1, 295, y); + + if (mapheaderinfo[mnum]->levelflags & LF_NOZONE) + V_DrawString(20, y, 0, va("%s %d", + mapheaderinfo[mnum]->lvlttl, + mapheaderinfo[mnum]->actnum)); + else + V_DrawString(20, y, 0, va("%s %s %d", + mapheaderinfo[mnum]->lvlttl, + (mapheaderinfo[mnum]->zonttl[0] ? mapheaderinfo[mnum]->zonttl : "Zone"), + mapheaderinfo[mnum]->actnum)); + + y += 8; + + if (y >= BASEVIDHEIGHT-8) + goto bottomarrow; + } + if (dotopname && !location) + { + V_DrawString(20, y, highlightflags, "LEVEL NAME"); + V_DrawString(256, y, highlightflags, "MEDALS"); + y += 8; + } + else if (location) + --location; + + // Extra Emblems + for (i = -2; i < numextraemblems; ++i) + { + if (i == -1) + { + V_DrawString(20, y, highlightflags, "EXTRA MEDALS"); + if (location) + { + y += 8; + location++; + } + } + if (location) + { + --location; + continue; + } + + if (i >= 0) + { + exemblem = &extraemblems[i]; + + if (exemblem->collected) + V_DrawSmallMappedPatch(295, y, 0, W_CachePatchName(M_GetExtraEmblemPatch(exemblem, false), PU_CACHE), + R_GetTranslationColormap(TC_DEFAULT, M_GetExtraEmblemColor(exemblem), GTC_MENUCACHE)); + else + V_DrawSmallScaledPatch(295, y, 0, W_CachePatchName("NEEDIT", PU_CACHE)); + + V_DrawString(20, y, 0, va("%s", exemblem->description)); + } + + y += 8; + + if (y >= BASEVIDHEIGHT-8) + goto bottomarrow; + } +bottomarrow: + if (dobottomarrow) + V_DrawCharacter(10, y-8 + (skullAnimCounter/5), + '\x1B' | highlightflags, false); // down arrow +} + +static void M_DrawLevelStats(void) +{ + char beststr[40]; + + tic_t besttime = 0; + + INT32 i; + INT32 mapsunfinished = 0; + + M_DrawMenuTitle(); + + V_DrawString(20, 24, highlightflags, "Total Play Time:"); + V_DrawCenteredString(BASEVIDWIDTH/2, 32, 0, va("%i hours, %i minutes, %i seconds", + G_TicsToHours(totalplaytime), + G_TicsToMinutes(totalplaytime, false), + G_TicsToSeconds(totalplaytime))); + + V_DrawString(20, 42, highlightflags, "Total Matches:"); + V_DrawRightAlignedString(BASEVIDWIDTH-16, 42, 0, va("%i played", matchesplayed)); + + V_DrawString(20, 52, highlightflags, "Online Power Level:"); + V_DrawRightAlignedString(BASEVIDWIDTH-16, 52, 0, va("Race: %i", vspowerlevel[PWRLV_RACE])); + V_DrawRightAlignedString(BASEVIDWIDTH-16, 60, 0, va("Battle: %i", vspowerlevel[PWRLV_BATTLE])); + + for (i = 0; i < NUMMAPS; i++) + { + if (!mapheaderinfo[i] || !(mapheaderinfo[i]->menuflags & LF2_TIMEATTACK)) + continue; + + if (!mainrecords[i] || mainrecords[i]->time <= 0) + { + mapsunfinished++; + continue; + } + + besttime += mainrecords[i]->time; + } + + V_DrawString(20, 70, highlightflags, "Combined time records:"); + + sprintf(beststr, "%i:%02i:%02i.%02i", G_TicsToHours(besttime), G_TicsToMinutes(besttime, false), G_TicsToSeconds(besttime), G_TicsToCentiseconds(besttime)); + V_DrawRightAlignedString(BASEVIDWIDTH-16, 70, (mapsunfinished ? warningflags : 0), beststr); + + if (mapsunfinished) + V_DrawRightAlignedString(BASEVIDWIDTH-16, 78, warningflags, va("(%d unfinished)", mapsunfinished)); + else + V_DrawRightAlignedString(BASEVIDWIDTH-16, 78, recommendedflags, "(complete)"); + + V_DrawString(32, 78, V_ALLOWLOWERCASE, va("x %d/%d", M_CountEmblems(), numemblems+numextraemblems)); + V_DrawSmallScaledPatch(20, 78, 0, W_CachePatchName("GOTITA", PU_STATIC)); + + M_DrawStatsMaps(statsLocation); +} + +// Handle statistics. +static void M_HandleLevelStats(INT32 choice) +{ + boolean exitmenu = false; // exit to previous menu + + switch (choice) + { + case KEY_DOWNARROW: + S_StartSound(NULL, sfx_menu1); + if (statsLocation < statsMax) + ++statsLocation; + break; + + case KEY_UPARROW: + S_StartSound(NULL, sfx_menu1); + if (statsLocation) + --statsLocation; + break; + + case KEY_PGDN: + S_StartSound(NULL, sfx_menu1); + statsLocation += (statsLocation+13 >= statsMax) ? statsMax-statsLocation : 13; + break; + + case KEY_PGUP: + S_StartSound(NULL, sfx_menu1); + statsLocation -= (statsLocation < 13) ? statsLocation : 13; + break; + + case KEY_ESCAPE: + exitmenu = true; + break; + } + if (exitmenu) + { + if (currentMenu->prevMenu) + M_SetupNextMenu(currentMenu->prevMenu); + else + M_ClearMenus(true); + } +} + +static void M_GrandPrixTemp(INT32 choice) +{ + (void)choice; + M_PatchSkinNameTable(); + M_PrepareCupList(); + M_SetupNextMenu(&SP_GrandPrixTempDef); +} + +// Start Grand Prix! +static void M_StartGrandPrix(INT32 choice) +{ + cupheader_t *gpcup = kartcupheaders; + + (void)choice; + + if (gpcup == NULL) + { + // welp + I_Error("No cup definitions for GP\n"); + return; + } + + M_ClearMenus(true); + + memset(&grandprixinfo, 0, sizeof(struct grandprixinfo)); + + switch (cv_dummygpdifficulty.value) + { + case KARTSPEED_EASY: + case KARTSPEED_NORMAL: + case KARTSPEED_HARD: + grandprixinfo.gamespeed = cv_dummygpdifficulty.value; + break; + case KARTGP_MASTER: + grandprixinfo.gamespeed = KARTSPEED_HARD; + grandprixinfo.masterbots = true; + break; + default: + CONS_Alert(CONS_WARNING, "Invalid GP difficulty\n"); + grandprixinfo.gamespeed = KARTSPEED_NORMAL; + break; + } + + grandprixinfo.encore = (boolean)(cv_dummygpencore.value); + + while (gpcup != NULL && gpcup->id != cv_dummygpcup.value-1) + { + gpcup = gpcup->next; + } + + if (gpcup == NULL) + { + gpcup = kartcupheaders; + } + + grandprixinfo.cup = gpcup; + + grandprixinfo.gp = true; + grandprixinfo.roundnum = 1; + grandprixinfo.wonround = false; + + grandprixinfo.initalize = true; + + G_DeferedInitNew( + false, + G_BuildMapName(grandprixinfo.cup->levellist[0] + 1), + (UINT8)(cv_chooseskin.value - 1), + (UINT8)(cv_splitplayers.value - 1), + false + ); +} + +// =========== +// MODE ATTACK +// =========== + +// Drawing function for Time Attack +void M_DrawTimeAttackMenu(void) +{ + INT32 i, x, y, cursory = 0; + UINT16 dispstatus; + + //S_ChangeMusicInternal("racent", true); // Eww, but needed for when user hits escape during demo playback + + V_DrawPatchFill(W_CachePatchName("SRB2BACK", PU_CACHE)); + + M_DrawMenuTitle(); + if (currentMenu == &SP_TimeAttackDef) + M_DrawLevelSelectOnly(true, false); + + // draw menu (everything else goes on top of it) + // Sadly we can't just use generic mode menus because we need some extra hacks + x = currentMenu->x; + y = currentMenu->y; + + // Character face! + { + UINT8 *colormap = R_GetTranslationColormap(cv_chooseskin.value-1, cv_playercolor[0].value, GTC_MENUCACHE); + V_DrawMappedPatch(BASEVIDWIDTH-x - SHORT(faceprefix[cv_chooseskin.value-1][FACE_WANTED]->width), y, 0, faceprefix[cv_chooseskin.value-1][FACE_WANTED], colormap); + } + + for (i = 0; i < currentMenu->numitems; ++i) + { + dispstatus = (currentMenu->menuitems[i].status & IT_DISPLAY); + if (dispstatus != IT_STRING && dispstatus != IT_WHITESTRING) + continue; + + y = currentMenu->y+currentMenu->menuitems[i].alphaKey; + if (i == itemOn) + cursory = y; + + V_DrawString(x, y, (dispstatus == IT_WHITESTRING) ? highlightflags : 0 , currentMenu->menuitems[i].text); + + // Cvar specific handling + if ((currentMenu->menuitems[i].status & IT_TYPE) == IT_CVAR) + { + consvar_t *cv = (consvar_t *)currentMenu->menuitems[i].itemaction; + if (currentMenu->menuitems[i].status & IT_CV_STRING) + { + M_DrawTextBox(x + 32, y - 8, MAXPLAYERNAME, 1); + V_DrawString(x + 40, y, V_ALLOWLOWERCASE, cv->string); + if (itemOn == i && skullAnimCounter < 4) // blink cursor + V_DrawCharacter(x + 40 + V_StringWidth(cv->string, V_ALLOWLOWERCASE), y, '_',false); + } + else + { + const char *str = ((cv == &cv_chooseskin) ? skins[cv_chooseskin.value-1].realname : cv->string); + INT32 soffset = 40, strw = V_StringWidth(str, 0); + + // hack to keep the menu from overlapping the level icon + if (currentMenu != &SP_TimeAttackDef || cv == &cv_nextmap) + soffset = 0; + + // Should see nothing but strings + V_DrawString(BASEVIDWIDTH - x - soffset - strw, y, highlightflags, str); + + if (i == itemOn) + { + V_DrawCharacter(BASEVIDWIDTH - x - soffset - 10 - strw - (skullAnimCounter/5), y, + '\x1C' | highlightflags, false); // left arrow + V_DrawCharacter(BASEVIDWIDTH - x - soffset + 2 + (skullAnimCounter/5), y, + '\x1D' | highlightflags, false); // right arrow + } + } + } + else if ((currentMenu->menuitems[i].status & IT_TYPE) == IT_KEYHANDLER && cv_dummystaff.value) // bad hacky assumption: IT_KEYHANDLER is assumed to be staff ghost selector + { + INT32 strw = V_StringWidth(dummystaffname, V_ALLOWLOWERCASE); + V_DrawString(BASEVIDWIDTH - x - strw, y, highlightflags|V_ALLOWLOWERCASE, dummystaffname); + if (i == itemOn) + { + V_DrawCharacter(BASEVIDWIDTH - x - 10 - strw - (skullAnimCounter/5), y, + '\x1C' | highlightflags, false); // left arrow + V_DrawCharacter(BASEVIDWIDTH - x + 2 + (skullAnimCounter/5), y, + '\x1D' | highlightflags, false); // right arrow + } + } + } + + x = currentMenu->x; + y = currentMenu->y; + + // DRAW THE SKULL CURSOR + V_DrawScaledPatch(x - 24, cursory, 0, W_CachePatchName("M_CURSOR", PU_CACHE)); + V_DrawString(x, cursory, highlightflags, currentMenu->menuitems[itemOn].text); + + // Level record list + if (cv_nextmap.value) + { + INT32 dupadjust = (vid.width/vid.dupx); + tic_t lap = 0, time = 0; + if (mainrecords[cv_nextmap.value-1]) + { + lap = mainrecords[cv_nextmap.value-1]->lap; + time = mainrecords[cv_nextmap.value-1]->time; + } + + V_DrawFill((BASEVIDWIDTH - dupadjust)>>1, 78, dupadjust, 36, 159); + + if (levellistmode != LLM_BREAKTHECAPSULES) + { + V_DrawRightAlignedString(149, 80, highlightflags, "BEST LAP:"); + K_drawKartTimestamp(lap, 19, 86, 0, 2); + } + + V_DrawRightAlignedString(292, 80, highlightflags, "BEST TIME:"); + K_drawKartTimestamp(time, 162, 86, cv_nextmap.value, 1); + } + /*{ + char beststr[40]; + emblem_t *em; + + if (!mainrecords[cv_nextmap.value-1] || !mainrecords[cv_nextmap.value-1]->time) + sprintf(beststr, "(none)"); + else + sprintf(beststr, "%i:%02i.%02i", G_TicsToMinutes(mainrecords[cv_nextmap.value-1]->time, true), + G_TicsToSeconds(mainrecords[cv_nextmap.value-1]->time), + G_TicsToCentiseconds(mainrecords[cv_nextmap.value-1]->time)); + + V_DrawString(64, y+48, highlightflags, "BEST TIME:"); + V_DrawRightAlignedString(BASEVIDWIDTH - 64 - 24 - 8, y+48, V_ALLOWLOWERCASE, beststr); + + if (!mainrecords[cv_nextmap.value-1] || !mainrecords[cv_nextmap.value-1]->lap) + sprintf(beststr, "(none)"); + else + sprintf(beststr, "%i:%02i.%02i", G_TicsToMinutes(mainrecords[cv_nextmap.value-1]->lap, true), + G_TicsToSeconds(mainrecords[cv_nextmap.value-1]->lap), + G_TicsToCentiseconds(mainrecords[cv_nextmap.value-1]->lap)); + + V_DrawString(64, y+56, highlightflags, "BEST LAP:"); + V_DrawRightAlignedString(BASEVIDWIDTH - 64 - 24 - 8, y+56, V_ALLOWLOWERCASE, beststr); + + // Draw record emblems. + em = M_GetLevelEmblems(cv_nextmap.value); + while (em) + { + switch (em->type) + { + case ET_TIME: break; + default: + goto skipThisOne; + } + + if (em->collected) + V_DrawMappedPatch(BASEVIDWIDTH - 64 - 24, y+48, 0, W_CachePatchName(M_GetEmblemPatch(em, false), PU_CACHE), + R_GetTranslationColormap(TC_DEFAULT, M_GetEmblemColor(em), GTC_MENUCACHE)); + else + V_DrawScaledPatch(BASEVIDWIDTH - 64 - 24, y+48, 0, W_CachePatchName("NEEDIT", PU_CACHE)); + + skipThisOne: + em = M_GetLevelEmblems(-1); + } + }*/ + + // ALWAYS DRAW player name, level name, skin and color even when not on this menu! + if (currentMenu != &SP_TimeAttackDef) + { + consvar_t *ncv; + + for (i = 0; i < 4; ++i) + { + y = currentMenu->y+SP_TimeAttackMenu[i].alphaKey; + V_DrawString(x, y, V_TRANSLUCENT, SP_TimeAttackMenu[i].text); + ncv = (consvar_t *)SP_TimeAttackMenu[i].itemaction; + if (SP_TimeAttackMenu[i].status & IT_CV_STRING) + { + M_DrawTextBox(x + 32, y - 8, MAXPLAYERNAME, 1); + V_DrawString(x + 40, y, V_TRANSLUCENT|V_ALLOWLOWERCASE, ncv->string); + } + else + { + const char *str = ((ncv == &cv_chooseskin) ? skins[cv_chooseskin.value-1].realname : ncv->string); + INT32 soffset = 40, strw = V_StringWidth(str, 0); + + // hack to keep the menu from overlapping the level icon + if (ncv == &cv_nextmap) + soffset = 0; + + // Should see nothing but strings + V_DrawString(BASEVIDWIDTH - x - soffset - strw, y, highlightflags|V_TRANSLUCENT, str); + } + } + } +} + +// Going to Time Attack menu... +static void M_TimeAttack(INT32 choice) +{ + (void)choice; + + memset(skins_cons_t, 0, sizeof (skins_cons_t)); + + levellistmode = LLM_TIMEATTACK; // Don't be dependent on cv_newgametype + + if (M_CountLevelsToShowInList() == 0) + { + M_StartMessage(M_GetText("No levels found for Time Attack.\n"),NULL,MM_NOTHING); + return; + } + + M_PatchSkinNameTable(); + + M_PrepareLevelSelect(); + M_SetupNextMenu(&SP_TimeAttackDef); + + G_SetGamestate(GS_TIMEATTACK); + titlemapinaction = TITLEMAP_OFF; // Nope don't give us HOMs please + + if (cv_nextmap.value) + Nextmap_OnChange(); + else + CV_AddValue(&cv_nextmap, 1); + + itemOn = tastart; // "Start" is selected. + + S_ChangeMusicInternal("racent", true); +} + +// Same as above, but sets a different levellistmode. Should probably be merged... +static void M_BreakTheCapsules(INT32 choice) +{ + (void)choice; + + memset(skins_cons_t, 0, sizeof (skins_cons_t)); + + levellistmode = LLM_BREAKTHECAPSULES; // Don't be dependent on cv_newgametype + + if (M_CountLevelsToShowInList() == 0) + { + M_StartMessage(M_GetText("No levels found for Break the Capsules.\n"),NULL,MM_NOTHING); + return; + } + + M_PatchSkinNameTable(); + + M_PrepareLevelSelect(); + M_SetupNextMenu(&SP_TimeAttackDef); + + G_SetGamestate(GS_TIMEATTACK); + titlemapinaction = TITLEMAP_OFF; // Nope don't give us HOMs please + + if (cv_nextmap.value) + Nextmap_OnChange(); + else + CV_AddValue(&cv_nextmap, 1); + + itemOn = tastart; // "Start" is selected. + + S_ChangeMusicInternal("racent", true); +} + +static boolean M_QuitTimeAttackMenu(void) +{ + // you know what? always putting these in the buffer won't hurt anything. + COM_BufAddText(va("skin \"%s\"\n", cv_chooseskin.string)); + return true; +} + +// Player has selected the "START" from the time attack screen +static void M_ChooseTimeAttack(INT32 choice) +{ + char *gpath; + const size_t glen = strlen("media")+1+strlen("replay")+1+strlen(timeattackfolder)+1+strlen("MAPXX")+1; + char nameofdemo[256]; + (void)choice; + emeralds = 0; + M_ClearMenus(true); + modeattacking = (levellistmode == LLM_BREAKTHECAPSULES ? ATTACKING_CAPSULES : ATTACKING_TIME); + + gpath = va("%s"PATHSEP"media"PATHSEP"replay"PATHSEP"%s", + srb2home, timeattackfolder); + M_MkdirEach(gpath, M_PathParts(gpath) - 3, 0755); + + if ((gpath = malloc(glen)) == NULL) + I_Error("Out of memory for replay filepath\n"); + + sprintf(gpath,"media"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s", timeattackfolder, G_BuildMapName(cv_nextmap.value)); + snprintf(nameofdemo, sizeof nameofdemo, "%s-%s-last", gpath, cv_chooseskin.string); + + if (!cv_autorecord.value) + remove(va("%s"PATHSEP"%s.lmp", srb2home, nameofdemo)); + else + G_RecordDemo(nameofdemo); + + G_DeferedInitNew(false, G_BuildMapName(cv_nextmap.value), (UINT8)(cv_chooseskin.value-1), 0, false); +} + +static void M_HandleStaffReplay(INT32 choice) +{ + boolean exitmenu = false; // exit to previous menu + lumpnum_t l = W_CheckNumForName(va("%sS%02u",G_BuildMapName(cv_nextmap.value),cv_dummystaff.value)); + + switch (choice) + { + case KEY_DOWNARROW: + M_NextOpt(); + S_StartSound(NULL, sfx_menu1); + break; + case KEY_UPARROW: + M_PrevOpt(); + S_StartSound(NULL, sfx_menu1); + break; + case KEY_BACKSPACE: + case KEY_ESCAPE: + exitmenu = true; + break; + case KEY_RIGHTARROW: + CV_AddValue(&cv_dummystaff, 1); + S_StartSound(NULL, sfx_menu1); + break; + case KEY_LEFTARROW: + CV_AddValue(&cv_dummystaff, -1); + S_StartSound(NULL, sfx_menu1); + break; + case KEY_ENTER: + if (l == LUMPERROR) + break; + M_ClearMenus(true); + modeattacking = (levellistmode == LLM_BREAKTHECAPSULES ? ATTACKING_CAPSULES : ATTACKING_TIME); + demo.loadfiles = false; demo.ignorefiles = true; // Just assume that record attack replays have the files needed + G_DoPlayDemo(va("%sS%02u",G_BuildMapName(cv_nextmap.value),cv_dummystaff.value)); + break; + default: + break; + } + if (exitmenu) + { + if (currentMenu->prevMenu) + M_SetupNextMenu(currentMenu->prevMenu); + else + M_ClearMenus(true); + } +} + +// Player has selected the "REPLAY" from the time attack screen +static void M_ReplayTimeAttack(INT32 choice) +{ + const char *which; + M_ClearMenus(true); + modeattacking = (levellistmode == LLM_BREAKTHECAPSULES ? ATTACKING_CAPSULES : ATTACKING_TIME); // set modeattacking before G_DoPlayDemo so the map loader knows + demo.loadfiles = false; demo.ignorefiles = true; // Just assume that record attack replays have the files needed + + if (currentMenu == &SP_ReplayDef) + { + switch(choice) { + default: + case 0: // best time + which = "time-best"; + break; + case 1: // best lap + which = "lap-best"; + break; + case 2: // last + which = "last"; + break; + case 3: // guest + // srb2/replay/main/map01-guest.lmp + G_DoPlayDemo(va("%s"PATHSEP"media"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-guest.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value))); + return; + } + // srb2/replay/main/map01-sonic-time-best.lmp + G_DoPlayDemo(va("%s"PATHSEP"media"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s-%s.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), cv_chooseskin.string, which)); + } + /*else if (currentMenu == &SP_NightsReplayDef) + { + switch(choice) { + default: + case 0: // best score + which = "score-best"; + break; + case 1: // best time + which = "time-best"; + break; + case 2: // last + which = "last"; + break; + case 3: // staff + return; // M_HandleStaffReplay + case 4: // guest + which = "guest"; + break; + } + // srb2/replay/main/map01-score-best.lmp + G_DoPlayDemo(va("%s"PATHSEP"media"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), which)); + }*/ +} + +static void M_EraseGuest(INT32 choice) +{ + const char *rguest = va("%s"PATHSEP"media"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-guest.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value)); + (void)choice; + if (FIL_FileExists(rguest)) + remove(rguest); + /*if (currentMenu == &SP_NightsGuestReplayDef) + M_SetupNextMenu(&SP_NightsAttackDef); + else*/ + M_SetupNextMenu(&SP_TimeAttackDef); + CV_AddValue(&cv_nextmap, -1); + CV_AddValue(&cv_nextmap, 1); + M_StartMessage(M_GetText("Guest replay data erased.\n"),NULL,MM_NOTHING); +} + +static void M_OverwriteGuest(const char *which) +{ + char *rguest = Z_StrDup(va("%s"PATHSEP"media"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-guest.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value))); + UINT8 *buf; + size_t len; + len = FIL_ReadFile(va("%s"PATHSEP"media"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s-%s.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), cv_chooseskin.string, which), &buf); + if (!len) { + return; + } + if (FIL_FileExists(rguest)) { + M_StopMessage(0); + remove(rguest); + } + FIL_WriteFile(rguest, buf, len); + Z_Free(rguest); + /*if (currentMenu == &SP_NightsGuestReplayDef) + M_SetupNextMenu(&SP_NightsAttackDef); + else*/ + M_SetupNextMenu(&SP_TimeAttackDef); + CV_AddValue(&cv_nextmap, -1); + CV_AddValue(&cv_nextmap, 1); + M_StartMessage(M_GetText("Guest replay data saved.\n"),NULL,MM_NOTHING); +} + +static void M_OverwriteGuest_Time(INT32 choice) +{ + (void)choice; + M_OverwriteGuest("time-best"); +} + +static void M_OverwriteGuest_Lap(INT32 choice) +{ + (void)choice; + M_OverwriteGuest("lap-best"); +} + +/* SRB2Kart +static void M_OverwriteGuest_Score(INT32 choice) +{ + (void)choice; + M_OverwriteGuest("score-best"); +} + +static void M_OverwriteGuest_Rings(INT32 choice) +{ + (void)choice; + M_OverwriteGuest("rings-best"); +}*/ + +static void M_OverwriteGuest_Last(INT32 choice) +{ + (void)choice; + M_OverwriteGuest("last"); +} + +static void M_SetGuestReplay(INT32 choice) +{ + void (*which)(INT32); + switch(choice) + { + case 0: // best time + which = M_OverwriteGuest_Time; + break; + case 1: // best lap + which = M_OverwriteGuest_Lap; + break; + case 2: // last + which = M_OverwriteGuest_Last; + break; + case 3: // guest + default: + 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); + return; + } + if (FIL_FileExists(va("%s"PATHSEP"media"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-guest.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value)))) + 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); + else + which(0); +} + +void M_ModeAttackRetry(INT32 choice) +{ + (void)choice; + G_CheckDemoStatus(); // Cancel recording + if (modeattacking) + M_ChooseTimeAttack(0); +} + +static void M_ModeAttackEndGame(INT32 choice) +{ + (void)choice; + G_CheckDemoStatus(); // Cancel recording + + if (gamestate == GS_LEVEL || gamestate == GS_INTERMISSION || gamestate == GS_VOTING) + Command_ExitGame_f(); + + M_StartControlPanel(); + + if (modeattacking) + currentMenu = &SP_TimeAttackDef; + + itemOn = currentMenu->lastOn; + G_SetGamestate(GS_TIMEATTACK); + modeattacking = ATTACKING_NONE; + S_ChangeMusicInternal("racent", true); + // Update replay availability. + CV_AddValue(&cv_nextmap, 1); + CV_AddValue(&cv_nextmap, -1); +} + +// ======== +// END GAME +// ======== + +static void M_ExitGameResponse(INT32 ch) +{ + if (ch != 'y' && ch != KEY_ENTER) + return; + + //Command_ExitGame_f(); + G_SetExitGameFlag(); + M_ClearMenus(true); +} + +static void M_EndGame(INT32 choice) +{ + (void)choice; + if (demo.playback) + return; + + if (!Playing()) + return; + + M_StartMessage(M_GetText("Are you sure you want to end the game?\n\n(Press 'Y' to confirm)\n"), M_ExitGameResponse, MM_YESNO); +} + +//=========================================================================== +// Connect Menu +//=========================================================================== + +void +M_SetWaitingMode (int mode) +{ +#ifdef HAVE_THREADS + I_lock_mutex(&m_menu_mutex); +#endif + { + m_waiting_mode = mode; + } +#ifdef HAVE_THREADS + I_unlock_mutex(m_menu_mutex); +#endif +} + +int +M_GetWaitingMode (void) +{ + int mode; + +#ifdef HAVE_THREADS + I_lock_mutex(&m_menu_mutex); +#endif + { + mode = m_waiting_mode; + } +#ifdef HAVE_THREADS + I_unlock_mutex(m_menu_mutex); +#endif + + return mode; +} + +#ifdef MASTERSERVER +#ifdef HAVE_THREADS +static void +Spawn_masterserver_thread (const char *name, void (*thread)(int*)) +{ + int *id = malloc(sizeof *id); + + I_lock_mutex(&ms_QueryId_mutex); + { + *id = ms_QueryId; + } + I_unlock_mutex(ms_QueryId_mutex); + + I_spawn_thread(name, (I_thread_fn)thread, id); +} + +static int +Same_instance (int id) +{ + int okay; + + I_lock_mutex(&ms_QueryId_mutex); + { + okay = ( id == ms_QueryId ); + } + I_unlock_mutex(ms_QueryId_mutex); + + return okay; +} +#endif/*HAVE_THREADS*/ + +static void +Fetch_servers_thread (int *id) +{ + msg_server_t * server_list; + + (void)id; + + M_SetWaitingMode(M_WAITING_SERVERS); + +#ifdef HAVE_THREADS + server_list = GetShortServersList(*id); +#else + server_list = GetShortServersList(0); +#endif + + if (server_list) + { +#ifdef HAVE_THREADS + if (Same_instance(*id)) +#endif + { + M_SetWaitingMode(M_NOT_WAITING); + +#ifdef HAVE_THREADS + I_lock_mutex(&ms_ServerList_mutex); + { + ms_ServerList = server_list; + } + I_unlock_mutex(ms_ServerList_mutex); +#else + CL_QueryServerList(server_list); + free(server_list); +#endif + } +#ifdef HAVE_THREADS + else + { + free(server_list); + } +#endif + } + +#ifdef HAVE_THREADS + free(id); +#endif +} +#endif/*MASTERSERVER*/ + +#define SERVERHEADERHEIGHT 36 +#define SERVERLINEHEIGHT 12 + +#define S_LINEY(n) currentMenu->y + SERVERHEADERHEIGHT + (n * SERVERLINEHEIGHT) + +#ifndef NONET +static UINT32 localservercount; + +static void M_HandleServerPage(INT32 choice) +{ + boolean exitmenu = false; // exit to previous menu + + switch (choice) + { + case KEY_DOWNARROW: + M_NextOpt(); + S_StartSound(NULL, sfx_menu1); + break; + case KEY_UPARROW: + M_PrevOpt(); + S_StartSound(NULL, sfx_menu1); + break; + case KEY_BACKSPACE: + case KEY_ESCAPE: + exitmenu = true; + break; + + case KEY_ENTER: + case KEY_RIGHTARROW: + S_StartSound(NULL, sfx_menu1); + if ((serverlistpage + 1) * SERVERS_PER_PAGE < serverlistcount) + serverlistpage++; + break; + case KEY_LEFTARROW: + S_StartSound(NULL, sfx_menu1); + if (serverlistpage > 0) + serverlistpage--; + break; + + default: + break; + } + if (exitmenu) + { + if (currentMenu->prevMenu) + M_SetupNextMenu(currentMenu->prevMenu); + else + M_ClearMenus(true); + } +} + +static void M_Connect(INT32 choice) +{ + // do not call menuexitfunc + M_ClearMenus(false); + + COM_BufAddText(va("connect node %d\n", serverlist[choice-FIRSTSERVERLINE + serverlistpage * SERVERS_PER_PAGE].node)); +} + +static void M_Refresh(INT32 choice) +{ + (void)choice; + + // Display a little "please wait" message. + M_DrawTextBox(52, BASEVIDHEIGHT/2-10, 25, 3); + V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT/2, 0, "Searching for servers..."); + V_DrawCenteredString(BASEVIDWIDTH/2, (BASEVIDHEIGHT/2)+12, 0, "Please wait."); + I_OsPolling(); + I_UpdateNoBlit(); + if (rendermode == render_soft) + I_FinishUpdate(); // page flip or blit buffer + + // first page of servers + serverlistpage = 0; + +#ifdef MASTERSERVER +#ifdef HAVE_THREADS + Spawn_masterserver_thread("fetch-servers", Fetch_servers_thread); +#else/*HAVE_THREADS*/ + Fetch_servers_thread(NULL); +#endif/*HAVE_THREADS*/ +#else/*MASTERSERVER*/ + CL_UpdateServerList(); +#endif/*MASTERSERVER*/ +} + +static void M_DrawConnectMenu(void) +{ + UINT16 i; + //const char *gt = "Unknown"; + //const char *spd = ""; + const char *pwr = "----"; + INT32 numPages = (serverlistcount+(SERVERS_PER_PAGE-1))/SERVERS_PER_PAGE; + int waiting; + + for (i = FIRSTSERVERLINE; i < min(localservercount, SERVERS_PER_PAGE)+FIRSTSERVERLINE; i++) + MP_ConnectMenu[i].status = IT_STRING | IT_SPACE; + + if (!numPages) + numPages = 1; + + // Page num + V_DrawRightAlignedString(BASEVIDWIDTH - currentMenu->x, currentMenu->y + MP_ConnectMenu[mp_connect_page].alphaKey, + highlightflags, va("%u of %d", serverlistpage+1, numPages)); + + // Horizontal line! + V_DrawFill(1, currentMenu->y+32, 318, 1, 0); + + if (serverlistcount <= 0) + V_DrawString(currentMenu->x,currentMenu->y+SERVERHEADERHEIGHT, 0, "No servers found"); + else + for (i = 0; i < min(serverlistcount - serverlistpage * SERVERS_PER_PAGE, SERVERS_PER_PAGE); i++) + { + INT32 slindex = i + serverlistpage * SERVERS_PER_PAGE; + UINT32 globalflags = ((serverlist[slindex].info.numberofplayer >= serverlist[slindex].info.maxplayer) ? V_TRANSLUCENT : 0) + |((itemOn == FIRSTSERVERLINE+i) ? highlightflags : 0)|V_ALLOWLOWERCASE; + + V_DrawString(currentMenu->x, S_LINEY(i), globalflags, serverlist[slindex].info.servername); + + V_DrawSmallString(currentMenu->x, S_LINEY(i)+8, globalflags, + va("Ping: %u", (UINT32)LONG(serverlist[slindex].info.time))); + + V_DrawSmallString(currentMenu->x+44,S_LINEY(i)+8, globalflags, + va("Players: %02d/%02d", serverlist[slindex].info.numberofplayer, serverlist[slindex].info.maxplayer)); + + V_DrawSmallString(currentMenu->x+108, S_LINEY(i)+8, globalflags, serverlist[slindex].info.gametypename); + + // display game speed for race gametypes + /* todo: send if the gametype is GTR_CIRCUIT, and uses game speed + if (serverlist[slindex].info.gametype == GT_RACE) + { + spd = kartspeed_cons_t[(serverlist[slindex].info.kartvars & SV_SPEEDMASK)+1].strvalue; + V_DrawSmallString(currentMenu->x+128, S_LINEY(i)+8, globalflags, va("(%s)", spd)); + } + */ + + pwr = "----"; + if (serverlist[slindex].info.avgpwrlv == -1) + pwr = "Off"; + else if (serverlist[slindex].info.avgpwrlv > 0) + pwr = va("%04d", serverlist[slindex].info.avgpwrlv); + V_DrawSmallString(currentMenu->x+171, S_LINEY(i)+8, globalflags, va("Power Level: %s", pwr)); + + // Don't use color flags intentionally, the global yellow color will auto override the text color code + if (serverlist[slindex].info.modifiedgame) + V_DrawSmallString(currentMenu->x+245, S_LINEY(i)+8, globalflags, "\x85" "Mod"); + if (serverlist[slindex].info.cheatsenabled) + V_DrawSmallString(currentMenu->x+265, S_LINEY(i)+8, globalflags, "\x83" "Cheats"); + + MP_ConnectMenu[i+FIRSTSERVERLINE].status = IT_STRING | IT_CALL; + } + + localservercount = serverlistcount; + + M_DrawGenericMenu(); + + waiting = M_GetWaitingMode(); + + if (waiting) + { + const char *message; + + if (waiting == M_WAITING_VERSION) + message = "Checking for updates..."; + else + message = "Searching for servers..."; + + // Display a little "please wait" message. + M_DrawTextBox(52, BASEVIDHEIGHT/2-10, 25, 3); + V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT/2, 0, message); + V_DrawCenteredString(BASEVIDWIDTH/2, (BASEVIDHEIGHT/2)+12, 0, "Please wait."); + } +} + +static boolean M_CancelConnect(void) +{ + D_CloseConnection(); + return true; +} + +// Ascending order, not descending. +// The casts are safe as long as the caller doesn't do anything stupid. +#define SERVER_LIST_ENTRY_COMPARATOR(key) \ +static int ServerListEntryComparator_##key(const void *entry1, const void *entry2) \ +{ \ + const serverelem_t *sa = (const serverelem_t*)entry1, *sb = (const serverelem_t*)entry2; \ + if (sa->info.key != sb->info.key) \ + return sa->info.key - sb->info.key; \ + return strcmp(sa->info.servername, sb->info.servername); \ +} + +// This does descending instead of ascending. +#define SERVER_LIST_ENTRY_COMPARATOR_REVERSE(key) \ +static int ServerListEntryComparator_##key##_reverse(const void *entry1, const void *entry2) \ +{ \ + const serverelem_t *sa = (const serverelem_t*)entry1, *sb = (const serverelem_t*)entry2; \ + if (sb->info.key != sa->info.key) \ + return sb->info.key - sa->info.key; \ + return strcmp(sb->info.servername, sa->info.servername); \ +} + +SERVER_LIST_ENTRY_COMPARATOR(time) +SERVER_LIST_ENTRY_COMPARATOR(numberofplayer) +SERVER_LIST_ENTRY_COMPARATOR_REVERSE(numberofplayer) +SERVER_LIST_ENTRY_COMPARATOR_REVERSE(maxplayer) + +static int ServerListEntryComparator_gametypename(const void *entry1, const void *entry2) +{ + const serverelem_t *sa = (const serverelem_t*)entry1, *sb = (const serverelem_t*)entry2; + int c; + if (( c = strcasecmp(sa->info.gametypename, sb->info.gametypename) )) + return c; + return strcmp(sa->info.servername, sb->info.servername); \ +} + +// Special one for modified state. +static int ServerListEntryComparator_modified(const void *entry1, const void *entry2) +{ + const serverelem_t *sa = (const serverelem_t*)entry1, *sb = (const serverelem_t*)entry2; + + // Modified acts as 2 points, cheats act as one point. + int modstate_a = (sa->info.cheatsenabled ? 1 : 0) | (sa->info.modifiedgame ? 2 : 0); + int modstate_b = (sb->info.cheatsenabled ? 1 : 0) | (sb->info.modifiedgame ? 2 : 0); + + if (modstate_a != modstate_b) + return modstate_a - modstate_b; + + // Default to strcmp. + return strcmp(sa->info.servername, sb->info.servername); +} +#endif + +void M_SortServerList(void) +{ +#ifndef NONET + switch(cv_serversort.value) + { + case 0: // Ping. + qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_time); + break; + case 1: // Modified state. + qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_modified); + break; + case 2: // Most players. + qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_numberofplayer_reverse); + break; + case 3: // Least players. + qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_numberofplayer); + break; + case 4: // Max players. + qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_maxplayer_reverse); + break; + case 5: // Gametype. + qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_gametypename); + break; + } +#endif +} + +#ifndef NONET +#ifdef UPDATE_ALERT +static void M_CheckMODVersion(int id) +{ + char updatestring[500]; + const char *updatecheck = GetMODVersion(id); + if(updatecheck) + { + sprintf(updatestring, UPDATE_ALERT_STRING, VERSIONSTRING, updatecheck); +#ifdef HAVE_THREADS + I_lock_mutex(&m_menu_mutex); +#endif + M_StartMessage(updatestring, NULL, MM_NOTHING); +#ifdef HAVE_THREADS + I_unlock_mutex(m_menu_mutex); +#endif + } +} +#endif/*UPDATE_ALERT*/ + +#if defined (UPDATE_ALERT) && defined (HAVE_THREADS) +static void +Check_new_version_thread (int *id) +{ + M_SetWaitingMode(M_WAITING_VERSION); + + M_CheckMODVersion(*id); + + if (Same_instance(*id)) + { + Fetch_servers_thread(id); + } + else + { + free(id); + } +} +#endif/*defined (UPDATE_ALERT) && defined (HAVE_THREADS)*/ + +static void M_ConnectMenu(INT32 choice) +{ + (void)choice; + // modified game check: no longer handled + // we don't request a restart unless the filelist differs + + // first page of servers + serverlistpage = 0; + M_SetupNextMenu(&MP_ConnectDef); + itemOn = 0; + +#if defined (MASTERSERVER) && defined (HAVE_THREADS) + I_lock_mutex(&ms_QueryId_mutex); + { + ms_QueryId++; + } + I_unlock_mutex(ms_QueryId_mutex); + + I_lock_mutex(&ms_ServerList_mutex); + { + if (ms_ServerList) + { + free(ms_ServerList); + ms_ServerList = NULL; + } + } + I_unlock_mutex(ms_ServerList_mutex); + +#ifdef UPDATE_ALERT + Spawn_masterserver_thread("check-new-version", Check_new_version_thread); +#else/*UPDATE_ALERT*/ + Spawn_masterserver_thread("fetch-servers", Fetch_servers_thread); +#endif/*UPDATE_ALERT*/ +#else/*defined (MASTERSERVER) && defined (HAVE_THREADS)*/ +#ifdef UPDATE_ALERT + M_CheckMODVersion(0); +#endif/*UPDATE_ALERT*/ + M_Refresh(0); +#endif/*defined (MASTERSERVER) && defined (HAVE_THREADS)*/ +} + +static void M_ConnectMenuModChecks(INT32 choice) +{ + (void)choice; + // 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 + + if (modifiedgame) + { + M_StartMessage(M_GetText("You have addons loaded.\nYou won't be able to join netgames!\n\nTo play online, restart the game\nand don't load any addons.\nSRB2Kart will automatically add\neverything you need when you join.\n\n(Press a key)\n"),M_ConnectMenu,MM_EVENTHANDLER); + return; + } + + M_ConnectMenu(-1); +} +#endif //NONET + +//=========================================================================== +// Start Server Menu +//=========================================================================== + +// +// FindFirstMap +// +// Finds the first map of a particular gametype (or returns the current map) +// Defaults to 1 if nothing found. +// +static INT32 M_FindFirstMap(INT32 gtype) +{ + INT32 i; + + if (mapheaderinfo[gamemap] && (mapheaderinfo[gamemap]->typeoflevel & gtype)) + return gamemap; + + for (i = 0; i < NUMMAPS; i++) + { + if (!mapheaderinfo[i]) + continue; + if (!(mapheaderinfo[i]->typeoflevel & gtype)) + continue; + return i + 1; + } + + return 1; +} + +static void M_StartServer(INT32 choice) +{ + UINT8 ssplayers = cv_splitplayers.value-1; + + (void)choice; + + if (currentMenu == &MP_OfflineServerDef) + netgame = false; + else + netgame = true; + + multiplayer = true; + + strncpy(connectedservername, cv_servername.string, MAXSERVERNAME); + + // Still need to reset devmode + cv_debug = 0; + + if (demo.playback) + G_StopDemo(); + if (metalrecording) + G_StopMetalDemo(); + + if (!cv_nextmap.value) + CV_SetValue(&cv_nextmap, G_RandMap(G_TOLFlag(cv_newgametype.value), -1, false, 0, false, NULL)+1); + + if (cv_maxplayers.value < ssplayers+1) + CV_SetValue(&cv_maxplayers, ssplayers+1); + + if (splitscreen != ssplayers) + { + splitscreen = ssplayers; + SplitScreen_OnChange(); + } + + if (currentMenu == &MP_OfflineServerDef) // offline server + { + paused = false; + SV_StartSinglePlayerServer(); + multiplayer = true; // yeah, SV_StartSinglePlayerServer clobbers this... + D_MapChange(cv_nextmap.value, cv_newgametype.value, (cv_kartencore.value == 1), 1, 1, false, false); + } + else + { + D_MapChange(cv_nextmap.value, cv_newgametype.value, (cv_kartencore.value == 1), 1, 1, false, false); + COM_BufAddText("dummyconsvar 1\n"); + } + + M_ClearMenus(true); +} + +static void M_DrawLevelSelectOnly(boolean leftfade, boolean rightfade) +{ + lumpnum_t lumpnum; + patch_t *PictureOfLevel; + INT32 x, y, w, i, oldval, trans, dupadjust = ((vid.width/vid.dupx) - BASEVIDWIDTH)>>1; + + // A 160x100 image of the level as entry MAPxxP + if (cv_nextmap.value) + { + lumpnum = W_CheckNumForName(va("%sP", G_BuildMapName(cv_nextmap.value))); + if (lumpnum != LUMPERROR) + PictureOfLevel = W_CachePatchNum(lumpnum, PU_CACHE); + else + PictureOfLevel = W_CachePatchName("BLANKLVL", PU_CACHE); + } + else + PictureOfLevel = W_CachePatchName("RANDOMLV", PU_CACHE); + + w = SHORT(PictureOfLevel->width)/2; + i = SHORT(PictureOfLevel->height)/2; + x = BASEVIDWIDTH/2 - w/2; + y = currentMenu->y + 130 + 8 - i; + + if (currentMenu->menuitems[itemOn].itemaction == &cv_nextmap && skullAnimCounter < 4) + trans = 0; + else + trans = G_GetGametypeColor(cv_newgametype.value); + + V_DrawFill(x-1, y-1, w+2, i+2, trans); // variable reuse... + + if ((cv_kartencore.value != 1) || gamestate == GS_TIMEATTACK || cv_newgametype.value != GT_RACE) + V_DrawSmallScaledPatch(x, y, 0, PictureOfLevel); + else + { + /*UINT8 *mappingforencore = NULL; + if ((lumpnum = W_CheckNumForName(va("%sE", mapname))) != LUMPERROR) + mappingforencore = W_CachePatchNum(lumpnum, PU_CACHE);*/ + + V_DrawFixedPatch((x+w)<>ANGLETOFINESHIFT); + V_DrawFixedPatch((x+w/2)< horizspac-dupadjust); + + x = (BASEVIDWIDTH + w)/2 + horizspac; + i = cv_nextmap.value - 1; + trans = (rightfade ? V_TRANSLUCENT : 0); + + while (x < BASEVIDWIDTH+dupadjust-horizspac) + { + oldval = i; + do + { + i++; + if (i == NUMMAPS) + i = -1; + + if (i == oldval) + return; + + if(!mapheaderinfo[i]) + continue; // Don't allocate the header. That just makes memory usage skyrocket. + + } while (!M_CanShowLevelInList(i, cv_newgametype.value)); + + // A 160x100 image of the level as entry MAPxxP + if (i+1) + { + lumpnum = W_CheckNumForName(va("%sP", G_BuildMapName(i+1))); + if (lumpnum != LUMPERROR) + PictureOfLevel = W_CachePatchNum(lumpnum, PU_CACHE); + else + PictureOfLevel = W_CachePatchName("BLANKLVL", PU_CACHE); + } + else + PictureOfLevel = W_CachePatchName("RANDOMLV", PU_CACHE); + + V_DrawTinyScaledPatch(x, y, trans, PictureOfLevel); + + x += horizspac + w/2; + } +#undef horizspac +} + +static void M_DrawServerMenu(void) +{ + M_DrawLevelSelectOnly(false, false); + M_DrawGenericMenu(); +} + +static void M_MapChange(INT32 choice) +{ + (void)choice; + + levellistmode = LLM_CREATESERVER; + + CV_SetValue(&cv_newgametype, gametype); + CV_SetValue(&cv_nextmap, gamemap); + + M_PrepareLevelSelect(); + M_SetupNextMenu(&MISC_ChangeLevelDef); +} + +#ifndef TESTERS +static void M_StartOfflineServerMenu(INT32 choice) +{ + (void)choice; + levellistmode = LLM_CREATESERVER; + M_PrepareLevelSelect(); + M_SetupNextMenu(&MP_OfflineServerDef); +} +#endif + +#ifndef NONET +#ifndef TESTERS +static void M_StartServerMenu(INT32 choice) +{ + (void)choice; + levellistmode = LLM_CREATESERVER; + M_PrepareLevelSelect(); + M_SetupNextMenu(&MP_ServerDef); + +} +#endif + +// ============== +// CONNECT VIA IP +// ============== + +static char setupm_ip[28]; +#endif +static UINT8 setupm_pselect = 1; + +// Draw the funky Connect IP menu. Tails 11-19-2002 +// So much work for such a little thing! +static void M_DrawMPMainMenu(void) +{ + INT32 x = currentMenu->x; + INT32 y = currentMenu->y; + + // use generic drawer for cursor, items and title + M_DrawGenericMenu(); + +#ifndef NOMENUHOST +#if MAXPLAYERS != 16 +Update the maxplayers label... +#endif + V_DrawRightAlignedString(BASEVIDWIDTH-x, y+MP_MainMenu[4].alphaKey, + ((itemOn == 4) ? highlightflags : 0), "(2-16 players)"); +#endif + +#ifndef TESTERS + V_DrawRightAlignedString(BASEVIDWIDTH-x, y+MP_MainMenu[5].alphaKey, + ((itemOn == 5) ? highlightflags : 0), + "(2-4 players)" + ); +#endif + +#ifndef NONET + y += MP_MainMenu[8].alphaKey; + + V_DrawFill(x+5, y+4+5, /*16*8 + 6,*/ BASEVIDWIDTH - 2*(x+5), 8+6, 159); + + // draw name string + V_DrawString(x+8,y+12, V_ALLOWLOWERCASE, setupm_ip); + + // draw text cursor for name + if (itemOn == 8 + && skullAnimCounter < 4) //blink cursor + V_DrawCharacter(x+8+V_StringWidth(setupm_ip, V_ALLOWLOWERCASE),y+12,'_',false); +#endif + + // character bar, ripped off the color bar :V + { +#define iconwidth 32 +#define spacingwidth 32 +#define incrwidth (iconwidth + spacingwidth) + UINT8 i = 0, pskin, pcol; + // player arrangement width, but there's also a chance i'm a furry, shhhhhh + const INT32 paw = iconwidth + 3*incrwidth; + INT32 trans = 0; + UINT8 *colmap; + x = BASEVIDWIDTH/2 - paw/2; + y = currentMenu->y + 32; + + while (++i <= 4) + { + pskin = R_SkinAvailable(cv_skin[i-1].string); + pcol = cv_playercolor[i-1].value; + + if (pskin >= MAXSKINS) + pskin = 0; + + if (!trans && i > cv_splitplayers.value) + trans = V_TRANSLUCENT; + + colmap = R_GetTranslationColormap(pskin, pcol, GTC_MENUCACHE); + + V_DrawFixedPatch(x< 7) + cursorframe = 0; + V_DrawFixedPatch(x< 1) + { + if (--setupm_pselect < 1) + setupm_pselect = cv_splitplayers.value; + S_StartSound(NULL,sfx_menu1); // Tails + } + break; + + case KEY_RIGHTARROW: + if (cv_splitplayers.value > 1) + { + if (++setupm_pselect > cv_splitplayers.value) + setupm_pselect = 1; + S_StartSound(NULL,sfx_menu1); // Tails + } + break; + + case KEY_DOWNARROW: + M_NextOpt(); + S_StartSound(NULL,sfx_menu1); // Tails + break; + + case KEY_UPARROW: + M_PrevOpt(); + S_StartSound(NULL,sfx_menu1); // Tails + break; + + case KEY_ENTER: + { + S_StartSound(NULL,sfx_menu1); // Tails + currentMenu->lastOn = itemOn; + switch (setupm_pselect) + { + case 2: + M_SetupMultiPlayer2(0); + return; + case 3: + M_SetupMultiPlayer3(0); + return; + case 4: + M_SetupMultiPlayer4(0); + return; + default: + M_SetupMultiPlayer(0); + return; + } + break; + } + + case KEY_ESCAPE: + exitmenu = true; + break; + } + + if (exitmenu) + { + if (currentMenu->prevMenu) + M_SetupNextMenu (currentMenu->prevMenu); + else + M_ClearMenus(true); + } +} + +#ifndef NONET + +// Tails 11-19-2002 +static void M_ConnectIP(INT32 choice) +{ + (void)choice; + + if (*setupm_ip == 0) + { + M_StartMessage("You must specify an IP address.\n", NULL, MM_NOTHING); + return; + } + + M_ClearMenus(true); + + COM_BufAddText(va("connect \"%s\"\n", setupm_ip)); + + // A little "please wait" message. + M_DrawTextBox(56, BASEVIDHEIGHT/2-12, 24, 2); + V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT/2, 0, "Connecting to server..."); + I_OsPolling(); + I_UpdateNoBlit(); + if (rendermode == render_soft) + I_FinishUpdate(); // page flip or blit buffer +} + +// Tails 11-19-2002 +static void M_HandleConnectIP(INT32 choice) +{ + size_t l; + boolean exitmenu = false; // exit to previous menu and send name change + + switch (choice) + { + case KEY_DOWNARROW: + M_NextOpt(); + S_StartSound(NULL,sfx_menu1); // Tails + break; + + case KEY_UPARROW: + M_PrevOpt(); + S_StartSound(NULL,sfx_menu1); // Tails + break; + + case KEY_ENTER: + S_StartSound(NULL,sfx_menu1); // Tails + currentMenu->lastOn = itemOn; + M_ConnectIP(1); + break; + + case KEY_ESCAPE: + exitmenu = true; + break; + + case KEY_BACKSPACE: + if ((l = strlen(setupm_ip)) != 0) + { + S_StartSound(NULL,sfx_menu1); // Tails + setupm_ip[l-1] = 0; + } + break; + + case KEY_DEL: + if (setupm_ip[0]) + { + S_StartSound(NULL,sfx_menu1); // Tails + setupm_ip[0] = 0; + } + break; + + default: + l = strlen(setupm_ip); + if (l >= 28-1) + break; + + // Rudimentary number and period enforcing - also allows letters so hostnames can be used instead + if ((choice >= '-' && choice <= ':') || (choice >= 'A' && choice <= 'Z') || (choice >= 'a' && choice <= 'z')) + { + S_StartSound(NULL,sfx_menu1); // Tails + setupm_ip[l] = (char)choice; + setupm_ip[l+1] = 0; + } + else if (choice >= 199 && choice <= 211 && choice != 202 && choice != 206) //numpad too! + { + char keypad_translation[] = {'7','8','9','-','4','5','6','+','1','2','3','0','.'}; + choice = keypad_translation[choice - 199]; + S_StartSound(NULL,sfx_menu1); // Tails + setupm_ip[l] = (char)choice; + setupm_ip[l+1] = 0; + } + break; + } + + if (exitmenu) + { + if (currentMenu->prevMenu) + M_SetupNextMenu (currentMenu->prevMenu); + else + M_ClearMenus(true); + } +} +#endif //!NONET + +// ======================== +// MULTIPLAYER PLAYER SETUP +// ======================== +// Tails 03-02-2002 + +// used for skin display on player setup menu +static INT32 multi_tics; +static state_t *multi_state; +static UINT8 multi_spr2; + +// used for follower display on player setup menu +static INT32 follower_tics; +static UINT32 follower_frame; // used for FF_ANIMATE garbo +static state_t *follower_state; + +// this is set before entering the MultiPlayer setup menu, +// for either player 1 or 2 +static char setupm_name[MAXPLAYERNAME+1]; +static player_t *setupm_player; +static consvar_t *setupm_cvskin; +static consvar_t *setupm_cvcolor; +static consvar_t *setupm_cvname; +static consvar_t *setupm_cvfollower; +static INT32 setupm_fakeskin; +static menucolor_t *setupm_fakecolor; +static INT32 setupm_fakefollower; // -1 is for none, our followers start at 0 + +static void M_DrawSetupMultiPlayerMenu(void) +{ + INT32 mx, my, st, flags = 0; + spritedef_t *sprdef; + spriteframe_t *sprframe; + patch_t *statbg = W_CachePatchName("K_STATBG", PU_CACHE); + patch_t *statlr = W_CachePatchName("K_STATLR", PU_CACHE); + patch_t *statud = W_CachePatchName("K_STATUD", PU_CACHE); + patch_t *statdot = W_CachePatchName("K_SDOT0", PU_CACHE); + patch_t *patch; + UINT8 frame; + UINT8 speed; + UINT8 weight; + const UINT8 *flashcol = V_GetStringColormap(highlightflags); + INT32 statx, staty; + char *fname; + INT16 i; + + mx = MP_PlayerSetupDef.x; + my = MP_PlayerSetupDef.y; + + statx = (BASEVIDWIDTH - mx - 118); + staty = (my+62); + + // use generic drawer for cursor, items and title + M_DrawGenericMenu(); + + // draw name string + M_DrawTextBox(mx + 32, my - 8, MAXPLAYERNAME, 1); + V_DrawString(mx + 40, my, V_ALLOWLOWERCASE, setupm_name); + + // draw text cursor for name + if (!itemOn && skullAnimCounter < 4) // blink cursor + V_DrawCharacter(mx + 40 + V_StringWidth(setupm_name, V_ALLOWLOWERCASE), my, '_',false); + + // draw skin string + st = V_StringWidth(skins[setupm_fakeskin].realname, 0); + V_DrawString(BASEVIDWIDTH - mx - st, my + 16, + ((MP_PlayerSetupMenu[2].status & IT_TYPE) == IT_SPACE ? V_TRANSLUCENT : 0)|highlightflags|V_ALLOWLOWERCASE, + skins[setupm_fakeskin].realname); + if (itemOn == 1) + { + V_DrawCharacter(BASEVIDWIDTH - mx - 10 - st - (skullAnimCounter/5), my + 16, + '\x1C' | highlightflags, false); // left arrow + V_DrawCharacter(BASEVIDWIDTH - mx + 2 + (skullAnimCounter/5), my + 16, + '\x1D' | highlightflags, false); // right arrow + } + + // draw follower string + fname = malloc(SKINNAMESIZE+1); + + if (setupm_fakefollower == -1) + strcpy(fname, "None"); + else + strcpy(fname, followers[setupm_fakefollower].name); + + st = V_StringWidth(fname, 0); + V_DrawString(BASEVIDWIDTH - mx - st, my + 26, + ((MP_PlayerSetupMenu[2].status & IT_TYPE) == IT_SPACE ? V_TRANSLUCENT : 0)|highlightflags|V_ALLOWLOWERCASE, + fname); + if (itemOn == 2) + { + V_DrawCharacter(BASEVIDWIDTH - mx - 10 - st - (skullAnimCounter/5), my + 26, + '\x1C' | highlightflags, false); // left arrow + V_DrawCharacter(BASEVIDWIDTH - mx + 2 + (skullAnimCounter/5), my + 26, + '\x1D' | highlightflags, false); // right arrow + } + + // draw the name of the color you have chosen + // Just so people don't go thinking that "Default" is Green. + st = V_StringWidth(skincolors[setupm_fakecolor->color].name, 0); + V_DrawString(BASEVIDWIDTH - mx - st, my + 152, highlightflags|V_ALLOWLOWERCASE, skincolors[setupm_fakecolor->color].name); // SRB2kart + if (itemOn == 3) + { + V_DrawCharacter(BASEVIDWIDTH - mx - 10 - st - (skullAnimCounter/5), my + 152, + '\x1C' | highlightflags, false); // left arrow + V_DrawCharacter(BASEVIDWIDTH - mx + 2 + (skullAnimCounter/5), my + 152, + '\x1D' | highlightflags, false); // right arrow + } + + // SRB2Kart: draw the stat backer + // labels + V_DrawThinString(statx+16, staty, V_6WIDTHSPACE|highlightflags, "Acceleration"); + V_DrawThinString(statx+91, staty, V_6WIDTHSPACE|highlightflags, "Max Speed"); + V_DrawThinString(statx, staty+12, V_6WIDTHSPACE|highlightflags, "Handling"); + V_DrawThinString(statx+7, staty+77, V_6WIDTHSPACE|highlightflags, "Weight"); + // label arrows + V_DrawFixedPatch((statx+64)<color) + V_DrawFixedPatch(((BASEVIDWIDTH - mx - 80) + ((speed-1)*8))<color, GTC_MENUCACHE)); + + // 2.2 color bar backported with permission +#define charw 72 +#define indexwidth 8 + { + const INT32 numcolors = (250-charw)/(2*indexwidth); // Number of colors per side + INT32 x = mx; + INT32 w = indexwidth; // Width of a singular color block + menucolor_t *mc = setupm_fakecolor->prev; // Last accessed color + UINT8 h; + + // Draw color in the middle + x += numcolors*w; + for (h = 0; h < 16; h++) + V_DrawFill(x, my+162+h, charw, 1, skincolors[setupm_fakecolor->color].ramp[h]); + + //Draw colors from middle to left + for (i=0; icolor].accessible) + mc = mc->prev; + for (h = 0; h < 16; h++) + V_DrawFill(x, my+162+h, w, 1, skincolors[mc->color].ramp[h]); + mc = mc->prev; + } + + // Draw colors from middle to right + mc = setupm_fakecolor->next; + x += numcolors*w + charw; + for (i=0; icolor].accessible) + mc = mc->next; + for (h = 0; h < 16; h++) + V_DrawFill(x, my+162+h, w, 1, skincolors[mc->color].ramp[h]); + x += w; + mc = mc->next; + } + } +#undef indexwidth + + // character bar, ripped off the color bar :V + if (setupm_fakecolor->color) // inverse should never happen +#define iconwidth 32 + { + const INT32 icons = 4; + INT32 k = -icons; + INT16 col = (setupm_fakeskin - icons) % numskins; + INT32 x = BASEVIDWIDTH/2 - ((icons+1)*24) - 4; + fixed_t scale = FRACUNIT/2; + INT32 offx = 8, offy = 8; + patch_t *cursor; + static UINT8 cursorframe = 0; + patch_t *face; + UINT8 *colmap; + + if (skullAnimCounter % 4 == 0) + cursorframe++; + if (cursorframe > 7) + cursorframe = 0; + + cursor = W_CachePatchName(va("K_BHILI%d", cursorframe+1), PU_CACHE); + + if (col < 0) + col += numskins; + while (k <= icons) + { + if (!(k++)) + { + scale = FRACUNIT; + face = faceprefix[col][FACE_WANTED]; + offx = 12; + offy = 0; + } + else + { + scale = FRACUNIT/2; + face = faceprefix[col][FACE_RANK]; + offx = 8; + offy = 8; + } + colmap = R_GetTranslationColormap(col, setupm_fakecolor->color, GTC_MENUCACHE); + if (face) + V_DrawFixedPatch((x+offx)<= numskins) + col -= numskins; + x += FixedMul(iconwidth<nextstate; + if (st != S_NULL) + multi_state = &states[st]; + multi_tics = multi_state->tics; + if (multi_tics == -1) + multi_tics = 15; + } + + // skin 0 is default player sprite + sprdef = &skins[setupm_fakeskin].sprites[multi_spr2]; + + if (!sprdef->numframes) // No frames ?? + return; // Can't render! + + frame = multi_state->frame & FF_FRAMEMASK; + if (frame >= sprdef->numframes) // Walking animation missing + frame = 0; // Try to use standing frame + + sprframe = &sprdef->spriteframes[frame]; + patch = W_CachePatchNum(sprframe->lumppat[1], PU_CACHE); + if (sprframe->flip & 2) // Only for first sprite + flags |= V_FLIP; // This sprite is left/right flipped! + + // draw box around guy + V_DrawFill(mx + 43 - (charw/2), my+65, charw, 84, 159); + + // draw player sprite + if (setupm_fakecolor->color) // inverse should never happen + { + UINT8 *colormap = R_GetTranslationColormap(setupm_fakeskin, setupm_fakecolor->color, GTC_MENUCACHE); + + if (skins[setupm_fakeskin].flags & SF_HIRES) + { + V_DrawFixedPatch((mx+43)< -1 && setupm_fakefollower < numfollowers) + { + // animate the follower + + if (--follower_tics <= 0) + { + + // FF_ANIMATE; cycle through FRAMES and get back afterwards. This will be prominent amongst followers hence why it's being supported here. + if (follower_state->frame & FF_ANIMATE) + { + follower_frame++; + follower_tics = follower_state->var2; + if (follower_frame > (follower_state->frame & FF_FRAMEMASK) + follower_state->var1) // that's how it works, right? + follower_frame = follower_state->frame & FF_FRAMEMASK; + } + else + { + st = follower_state->nextstate; + if (st != S_NULL) + follower_state = &states[st]; + follower_tics = follower_state->tics; + if (follower_tics == -1) + follower_tics = 15; // er, what? + // get spritedef: + follower_frame = follower_state->frame & FF_FRAMEMASK; + } + } + sprdef = &sprites[follower_state->sprite]; + + // draw the follower + + if (follower_frame >= sprdef->numframes) + follower_frame = 0; // frame doesn't exist, we went beyond it... what? + sprframe = &sprdef->spriteframes[follower_frame]; + patch = W_CachePatchNum(sprframe->lumppat[1], PU_CACHE); + if (sprframe->flip & 2) // Only for first sprite + flags |= V_FLIP; // This sprite is left/right flipped! + + // @TODO: Reminder that followers on the menu right now do NOT support the 'followercolor' command, considering this whole menu is getting remade anyway, I see no point in incorporating it in right now. + + // draw follower sprite + if (setupm_fakecolor->color) // inverse should never happen + { + + // Fake the follower's in game appearance by now also applying some of its variables! coolio, eh? + follower_t fl = followers[setupm_fakefollower]; // shortcut for our sanity + // smooth floating, totally not stolen from rocket sneakers. + const fixed_t pi = (22<>ANGLETOFINESHIFT) & FINEMASK); + + UINT8 *colormap = R_GetTranslationColormap(-1, setupm_fakecolor->color, 0); + V_DrawFixedPatch((mx+65)*FRACUNIT, (my+131-fl.zoffs)*FRACUNIT+sine, fl.scale, flags, patch, colormap); + Z_Free(colormap); + } + } + +#undef charw +} + +// follower state update. This is its own function so that it's at least somewhat clean +static void M_GetFollowerState(void) +{ + + if (setupm_fakefollower <= -1 || setupm_fakefollower > numfollowers-1) // yikes, there's none! + return; + // ^ we don't actually need to set anything since it won't be displayed anyway. + + //followertimer = 0; // reset timer. not like it'll overflow anytime soon but whatever. + + // set follower state + follower_state = &states[followers[setupm_fakefollower].followstate]; + + if (follower_state->frame & FF_ANIMATE) + follower_tics = follower_state->var2; // support for FF_ANIMATE + else + follower_tics = follower_state->tics; + + follower_frame = follower_state->frame & FF_FRAMEMASK; +} + +// Handle 1P/2P MP Setup +static void M_HandleSetupMultiPlayer(INT32 choice) +{ + size_t l; + INT32 prev_setupm_fakeskin; + boolean exitmenu = false; // exit to previous menu and send name change + + if ((choice == gamecontrol[0][gc_fire][0] || choice == gamecontrol[0][gc_fire][1]) && itemOn == 2) + choice = KEY_BACKSPACE; // Hack to allow resetting prefcolor on controllers + + switch (choice) + { + case KEY_DOWNARROW: + M_NextOpt(); + S_StartSound(NULL,sfx_menu1); // Tails + break; + + case KEY_UPARROW: + M_PrevOpt(); + S_StartSound(NULL,sfx_menu1); // Tails + break; + + case KEY_LEFTARROW: + if (itemOn == 1) //player skin + { + S_StartSound(NULL,sfx_menu1); // Tails + prev_setupm_fakeskin = setupm_fakeskin; + do + { + setupm_fakeskin--; + if (setupm_fakeskin < 0) + setupm_fakeskin = numskins-1; + } + while ((prev_setupm_fakeskin != setupm_fakeskin) && !(R_SkinUsable(-1, setupm_fakeskin))); + multi_spr2 = P_GetSkinSprite2(&skins[setupm_fakeskin], SPR2_FSTN, NULL); + } + else if (itemOn == 2) // follower + { + S_StartSound(NULL,sfx_menu1); + setupm_fakefollower--; + M_GetFollowerState(); // update follower state + } + else if (itemOn == 3) // player color + { + S_StartSound(NULL,sfx_menu1); // Tails + setupm_fakecolor = setupm_fakecolor->prev; + } + break; + + case KEY_RIGHTARROW: + if (itemOn == 1) //player skin + { + S_StartSound(NULL,sfx_menu1); // Tails + prev_setupm_fakeskin = setupm_fakeskin; + do + { + setupm_fakeskin++; + if (setupm_fakeskin > numskins-1) + setupm_fakeskin = 0; + } + while ((prev_setupm_fakeskin != setupm_fakeskin) && !(R_SkinUsable(-1, setupm_fakeskin))); + multi_spr2 = P_GetSkinSprite2(&skins[setupm_fakeskin], SPR2_FSTN, NULL); + } + else if (itemOn == 2) // follower + { + S_StartSound(NULL,sfx_menu1); + setupm_fakefollower++; + M_GetFollowerState(); + } + else if (itemOn == 3) // player color + { + S_StartSound(NULL,sfx_menu1); // Tails + setupm_fakecolor = setupm_fakecolor->next; + } + break; + + case KEY_ESCAPE: + exitmenu = true; + break; + + case KEY_BACKSPACE: + if (itemOn == 0) + { + if ((l = strlen(setupm_name))!=0) + { + S_StartSound(NULL,sfx_menu1); // Tails + setupm_name[l-1] =0; + } + } + else if (itemOn == 2) // follower + { + S_StartSound(NULL,sfx_menu1); + setupm_fakefollower = -1; + } + else if (itemOn == 3) + { + UINT16 col = skins[setupm_fakeskin].prefcolor; + if ((setupm_fakecolor->color != col) && skincolors[col].accessible) + { + S_StartSound(NULL,sfx_menu1); // Tails + for (setupm_fakecolor=menucolorhead;;setupm_fakecolor=setupm_fakecolor->next) + if (setupm_fakecolor->color == col || setupm_fakecolor == menucolortail) + break; + } + } + break; + + case KEY_DEL: + if (itemOn == 0 && (l = strlen(setupm_name))!=0) + { + S_StartSound(NULL,sfx_menu1); // Tails + setupm_name[0] = 0; + } + break; + + default: + if (choice < 32 || choice > 127 || itemOn != 0) + break; + l = strlen(setupm_name); + if (l < MAXPLAYERNAME) + { + S_StartSound(NULL,sfx_menu1); // Tails + setupm_name[l] =(char)choice; + setupm_name[l+1] =0; + } + break; + } + + // check followers: + if (setupm_fakefollower < -1) + { + setupm_fakefollower = numfollowers-1; + M_GetFollowerState(); // update follower state + } + if (setupm_fakefollower > numfollowers-1) + { + setupm_fakefollower = -1; + M_GetFollowerState(); // update follower state + } + + // check color + if (itemOn == 2 && !skincolors[setupm_fakecolor->color].accessible) { + if (choice == KEY_LEFTARROW) + while (!skincolors[setupm_fakecolor->color].accessible) + setupm_fakecolor = setupm_fakecolor->prev; + else if (choice == KEY_RIGHTARROW || choice == KEY_ENTER) + while (!skincolors[setupm_fakecolor->color].accessible) + setupm_fakecolor = setupm_fakecolor->next; + } + + if (exitmenu) + { + if (currentMenu->prevMenu) + M_SetupNextMenu (currentMenu->prevMenu); + else + M_ClearMenus(true); + } +} + +// start the multiplayer setup menu +static void M_SetupMultiPlayer(INT32 choice) +{ + (void)choice; + + multi_state = &states[mobjinfo[MT_PLAYER].seestate]; + multi_tics = multi_state->tics; + + strcpy(setupm_name, cv_playername[0].string); + + // set for player 1 + setupm_player = &players[consoleplayer]; + setupm_cvskin = &cv_skin[0]; + setupm_cvcolor = &cv_playercolor[0]; + setupm_cvname = &cv_playername[0]; + setupm_cvfollower = &cv_follower[0]; + + setupm_fakefollower = atoi(setupm_cvfollower->string); // update fake follower value + + // yikes, we don't want none of that... + if (setupm_fakefollower > numfollowers-1) + setupm_fakefollower = -1; + + M_GetFollowerState(); // update follower state + + // For whatever reason this doesn't work right if you just use ->value + setupm_fakeskin = R_SkinAvailable(setupm_cvskin->string); + if (setupm_fakeskin == -1) + setupm_fakeskin = 0; + + for (setupm_fakecolor=menucolorhead;;setupm_fakecolor=setupm_fakecolor->next) + if (setupm_fakecolor->color == setupm_cvcolor->value || setupm_fakecolor == menucolortail) + break; + + // disable skin changes if we can't actually change skins + if (!CanChangeSkin(consoleplayer)) + MP_PlayerSetupMenu[2].status = (IT_GRAYEDOUT); + else + MP_PlayerSetupMenu[2].status = (IT_KEYHANDLER|IT_STRING); + + MP_PlayerSetupDef.prevMenu = currentMenu; + M_SetupNextMenu(&MP_PlayerSetupDef); +} + +// start the multiplayer setup menu, for secondary player (splitscreen mode) +static void M_SetupMultiPlayer2(INT32 choice) +{ + (void)choice; + + multi_state = &states[mobjinfo[MT_PLAYER].seestate]; + multi_tics = multi_state->tics; + strcpy (setupm_name, cv_playername[1].string); + + // set for splitscreen secondary player + setupm_player = &players[g_localplayers[1]]; + setupm_cvskin = &cv_skin[1]; + setupm_cvcolor = &cv_playercolor[1]; + setupm_cvname = &cv_playername[1]; + setupm_cvfollower = &cv_follower[1]; + + setupm_fakefollower = atoi(setupm_cvfollower->string); // update fake follower value + + // yikes, we don't want none of that... + if (setupm_fakefollower > numfollowers-1) + setupm_fakefollower = -1; + + M_GetFollowerState(); // update follower state + + // For whatever reason this doesn't work right if you just use ->value + setupm_fakeskin = R_SkinAvailable(setupm_cvskin->string); + if (setupm_fakeskin == -1) + setupm_fakeskin = 0; + + for (setupm_fakecolor=menucolorhead;;setupm_fakecolor=setupm_fakecolor->next) + if (setupm_fakecolor->color == setupm_cvcolor->value || setupm_fakecolor == menucolortail) + break; + + // disable skin changes if we can't actually change skins + if (splitscreen && !CanChangeSkin(g_localplayers[1])) + MP_PlayerSetupMenu[2].status = (IT_GRAYEDOUT); + else + MP_PlayerSetupMenu[2].status = (IT_KEYHANDLER | IT_STRING); + + MP_PlayerSetupDef.prevMenu = currentMenu; + M_SetupNextMenu(&MP_PlayerSetupDef); +} + +// start the multiplayer setup menu, for third player (splitscreen mode) +static void M_SetupMultiPlayer3(INT32 choice) +{ + (void)choice; + + multi_state = &states[mobjinfo[MT_PLAYER].seestate]; + multi_tics = multi_state->tics; + strcpy(setupm_name, cv_playername[2].string); + + // set for splitscreen third player + setupm_player = &players[g_localplayers[2]]; + setupm_cvskin = &cv_skin[2]; + setupm_cvcolor = &cv_playercolor[2]; + setupm_cvname = &cv_playername[2]; + setupm_cvfollower = &cv_follower[2]; + + setupm_fakefollower = atoi(setupm_cvfollower->string); // update fake follower value + + // yikes, we don't want none of that... + if (setupm_fakefollower > numfollowers-1) + setupm_fakefollower = -1; + + M_GetFollowerState(); // update follower state + + // For whatever reason this doesn't work right if you just use ->value + setupm_fakeskin = R_SkinAvailable(setupm_cvskin->string); + if (setupm_fakeskin == -1) + setupm_fakeskin = 0; + + for (setupm_fakecolor=menucolorhead;;setupm_fakecolor=setupm_fakecolor->next) + if (setupm_fakecolor->color == setupm_cvcolor->value || setupm_fakecolor == menucolortail) + break; + + // disable skin changes if we can't actually change skins + if (splitscreen > 1 && !CanChangeSkin(g_localplayers[2])) + MP_PlayerSetupMenu[2].status = (IT_GRAYEDOUT); + else + MP_PlayerSetupMenu[2].status = (IT_KEYHANDLER | IT_STRING); + + MP_PlayerSetupDef.prevMenu = currentMenu; + M_SetupNextMenu(&MP_PlayerSetupDef); +} + +// start the multiplayer setup menu, for third player (splitscreen mode) +static void M_SetupMultiPlayer4(INT32 choice) +{ + (void)choice; + + multi_state = &states[mobjinfo[MT_PLAYER].seestate]; + multi_tics = multi_state->tics; + strcpy(setupm_name, cv_playername[3].string); + + // set for splitscreen fourth player + setupm_player = &players[g_localplayers[3]]; + setupm_cvskin = &cv_skin[3]; + setupm_cvcolor = &cv_playercolor[3]; + setupm_cvname = &cv_playername[3]; + setupm_cvfollower = &cv_follower[3]; + + setupm_fakefollower = atoi(setupm_cvfollower->string); // update fake follower value + + // yikes, we don't want none of that... + if (setupm_fakefollower > numfollowers-1) + setupm_fakefollower = -1; + + M_GetFollowerState(); // update follower state + + // For whatever reason this doesn't work right if you just use ->value + setupm_fakeskin = R_SkinAvailable(setupm_cvskin->string); + if (setupm_fakeskin == -1) + setupm_fakeskin = 0; + + for (setupm_fakecolor=menucolorhead;;setupm_fakecolor=setupm_fakecolor->next) + if (setupm_fakecolor->color == setupm_cvcolor->value || setupm_fakecolor == menucolortail) + break; + + // disable skin changes if we can't actually change skins + if (splitscreen > 2 && !CanChangeSkin(g_localplayers[3])) + MP_PlayerSetupMenu[2].status = (IT_GRAYEDOUT); + else + MP_PlayerSetupMenu[2].status = (IT_KEYHANDLER | IT_STRING); + + MP_PlayerSetupDef.prevMenu = currentMenu; + M_SetupNextMenu(&MP_PlayerSetupDef); +} + +static boolean M_QuitMultiPlayerMenu(void) +{ + size_t l; + // send name if changed + if (strcmp(setupm_name, setupm_cvname->string)) + { + // remove trailing whitespaces + for (l= strlen(setupm_name)-1; + (signed)l >= 0 && setupm_name[l] ==' '; l--) + setupm_name[l] =0; + COM_BufAddText (va("%s \"%s\"\n",setupm_cvname->name,setupm_name)); + } + // you know what? always putting these in the buffer won't hurt anything. + COM_BufAddText (va("%s \"%s\"\n",setupm_cvskin->name,skins[setupm_fakeskin].name)); + COM_BufAddText (va("%s %d\n",setupm_cvcolor->name,setupm_fakecolor->color)); + COM_BufAddText (va("%s %d\n",setupm_cvfollower->name,setupm_fakefollower)); + return true; +} + +void M_AddMenuColor(UINT16 color) { + menucolor_t *c; + + // SRB2Kart: I do not understand vanilla doesn't need this but WE do???!?!??! + if (!skincolors[color].accessible) { + return; + } + + if (color >= numskincolors) { + CONS_Printf("M_AddMenuColor: color %d does not exist.",color); + return; + } + + c = (menucolor_t *)malloc(sizeof(menucolor_t)); + c->color = color; + if (menucolorhead == NULL) { + c->next = c; + c->prev = c; + menucolorhead = c; + menucolortail = c; + } else { + c->next = menucolorhead; + c->prev = menucolortail; + menucolortail->next = c; + menucolorhead->prev = c; + menucolortail = c; + } +} + +void M_MoveColorBefore(UINT16 color, UINT16 targ) { + menucolor_t *look, *c = NULL, *t = NULL; + + if (color == targ) + return; + if (color >= numskincolors) { + CONS_Printf("M_MoveColorBefore: color %d does not exist.",color); + return; + } + if (targ >= numskincolors) { + CONS_Printf("M_MoveColorBefore: target color %d does not exist.",targ); + return; + } + + for (look=menucolorhead;;look=look->next) { + if (look->color == color) + c = look; + else if (look->color == targ) + t = look; + if (c != NULL && t != NULL) + break; + if (look==menucolortail) + return; + } + + if (c == t->prev) + return; + + if (t==menucolorhead) + menucolorhead = c; + if (c==menucolortail) + menucolortail = c->prev; + + c->prev->next = c->next; + c->next->prev = c->prev; + + c->prev = t->prev; + c->next = t; + t->prev->next = c; + t->prev = c; +} + +void M_MoveColorAfter(UINT16 color, UINT16 targ) { + menucolor_t *look, *c = NULL, *t = NULL; + + if (color == targ) + return; + if (color >= numskincolors) { + CONS_Printf("M_MoveColorAfter: color %d does not exist.\n",color); + return; + } + if (targ >= numskincolors) { + CONS_Printf("M_MoveColorAfter: target color %d does not exist.\n",targ); + return; + } + + for (look=menucolorhead;;look=look->next) { + if (look->color == color) + c = look; + else if (look->color == targ) + t = look; + if (c != NULL && t != NULL) + break; + if (look==menucolortail) + return; + } + + if (t == c->prev) + return; + + if (t==menucolortail) + menucolortail = c; + else if (c==menucolortail) + menucolortail = c->prev; + + c->prev->next = c->next; + c->next->prev = c->prev; + + c->next = t->next; + c->prev = t; + t->next->prev = c; + t->next = c; +} + +UINT16 M_GetColorBefore(UINT16 color) { + menucolor_t *look; + + if (color >= numskincolors) { + CONS_Printf("M_GetColorBefore: color %d does not exist.\n",color); + return 0; + } + + for (look=menucolorhead;;look=look->next) { + if (look->color == color) + return look->prev->color; + if (look==menucolortail) + return 0; + } +} + +UINT16 M_GetColorAfter(UINT16 color) { + menucolor_t *look; + + if (color >= numskincolors) { + CONS_Printf("M_GetColorAfter: color %d does not exist.\n",color); + return 0; + } + + for (look=menucolorhead;;look=look->next) { + if (look->color == color) + return look->next->color; + if (look==menucolortail) + return 0; + } +} + +void M_InitPlayerSetupColors(void) { + UINT8 i; + numskincolors = SKINCOLOR_FIRSTFREESLOT; + menucolorhead = menucolortail = NULL; + for (i=0; inext; + free(tmp); + } else { + free(look); + return; + } + } + + menucolorhead = menucolortail = NULL; +} + +// ================= +// DATA OPTIONS MENU +// ================= +static UINT8 erasecontext = 0; + +static void M_EraseDataResponse(INT32 ch) +{ + UINT8 i; + + if (ch != 'y' && ch != KEY_ENTER) + return; + + S_StartSound(NULL, sfx_itrole); // bweh heh heh + + // Delete the data + if (erasecontext == 2) + { + // SRB2Kart: This actually needs to be done FIRST, so that you don't immediately regain playtime/matches secrets + totalplaytime = 0; + matchesplayed = 0; + for (i = 0; i < PWRLV_NUMTYPES; i++) + vspowerlevel[i] = PWRLVRECORD_START; + F_StartIntro(); + } + if (erasecontext != 1) + G_ClearRecords(); + if (erasecontext != 0) + M_ClearSecrets(); + M_ClearMenus(true); +} + +static void M_EraseData(INT32 choice) +{ + const char *eschoice, *esstr = M_GetText("Are you sure you want to erase\n%s?\n\n(Press 'Y' to confirm)\n"); + + erasecontext = (UINT8)choice; + + if (choice == 0) + eschoice = M_GetText("Record Attack data"); + else if (choice == 1) + eschoice = M_GetText("Secrets data"); + else + eschoice = M_GetText("ALL game data"); + + M_StartMessage(va(esstr, eschoice),M_EraseDataResponse,MM_YESNO); +} + +static void M_ScreenshotOptions(INT32 choice) +{ + (void)choice; + Screenshot_option_Onchange(); + Moviemode_mode_Onchange(); + + M_SetupNextMenu(&OP_ScreenshotOptionsDef); +} + +// ============= +// JOYSTICK MENU +// ============= + +// Start the controls menu, setting it up for either the console player, +// or the secondary splitscreen player + +static void M_DrawJoystick(void) +{ + INT32 i; + INT32 compareval; + + M_DrawGenericMenu(); + + for (i = 0; i < 8; i++) + { + M_DrawTextBox(OP_JoystickSetDef.x-8, OP_JoystickSetDef.y+LINEHEIGHT*i-12, 28, 1); + //M_DrawSaveLoadBorder(OP_JoystickSetDef.x, OP_JoystickSetDef.y+LINEHEIGHT*i); + +#ifdef JOYSTICK_HOTPLUG + if (atoi(cv_usejoystick[setupcontrolplayer-1].string) > I_NumJoys()) + compareval = atoi(cv_usejoystick[setupcontrolplayer-1].string); + else +#endif + compareval = cv_usejoystick[setupcontrolplayer-1].value; + + V_DrawString(OP_JoystickSetDef.x, OP_JoystickSetDef.y+LINEHEIGHT*i-4, (i == compareval) ? V_GREENMAP : 0, joystickInfo[i]); + } +} + +void M_SetupJoystickMenu(INT32 choice) +{ + const char *joyNA = "Unavailable"; + const INT32 n = I_NumJoys(); + + INT32 i = 0; + INT32 j; + + (void)choice; + + strcpy(joystickInfo[i], "None"); + + for (i = 1; i < 8; i++) + { + if (i <= n && (I_GetJoyName(i)) != NULL) + strncpy(joystickInfo[i], I_GetJoyName(i), 28); + else + strcpy(joystickInfo[i], joyNA); + +#ifdef JOYSTICK_HOTPLUG + // We use cv_usejoystick.string as the USER-SET var + // and cv_usejoystick.value as the INTERNAL var + // + // In practice, if cv_usejoystick.string == 0, this overrides + // cv_usejoystick.value and always disables + // + // Update cv_usejoystick.string here so that the user can + // properly change this value. + for (j = 0; j < MAXSPLITSCREENPLAYERS; j++) + { + if (i == cv_usejoystick[j].value) + CV_SetValue(&cv_usejoystick[j], i); + } +#endif + } + + M_SetupNextMenu(&OP_JoystickSetDef); +} + +static void M_Setup1PJoystickMenu(INT32 choice) +{ + setupcontrolplayer = 1; + OP_JoystickSetDef.prevMenu = &OP_Joystick1Def; + M_SetupJoystickMenu(choice); +} + +static void M_Setup2PJoystickMenu(INT32 choice) +{ + setupcontrolplayer = 2; + OP_JoystickSetDef.prevMenu = &OP_Joystick2Def; + M_SetupJoystickMenu(choice); +} + +static void M_Setup3PJoystickMenu(INT32 choice) +{ + setupcontrolplayer = 3; + OP_JoystickSetDef.prevMenu = &OP_Joystick3Def; + M_SetupJoystickMenu(choice); +} + +static void M_Setup4PJoystickMenu(INT32 choice) +{ + setupcontrolplayer = 4; + OP_JoystickSetDef.prevMenu = &OP_Joystick4Def; + M_SetupJoystickMenu(choice); +} + +static void M_AssignJoystick(INT32 choice) +{ + const UINT8 p = setupcontrolplayer-1; + +#ifdef JOYSTICK_HOTPLUG + INT32 oldchoice, oldstringchoice; + INT32 numjoys = I_NumJoys(); + + oldchoice = oldstringchoice = atoi(cv_usejoystick[p].string) > numjoys ? atoi(cv_usejoystick[p].string) : cv_usejoystick[p].value; + CV_SetValue(&cv_usejoystick[p], choice); + + // Just in case last-minute changes were made to cv_usejoystick.value, + // update the string too + // But don't do this if we're intentionally setting higher than numjoys + if (choice <= numjoys) + { + CV_SetValue(&cv_usejoystick[p], cv_usejoystick[p].value); + + // reset this so the comparison is valid + if (oldchoice > numjoys) + oldchoice = cv_usejoystick[p].value; + + if (oldchoice != choice) + { + if (choice && oldstringchoice > numjoys) // if we did not select "None", we likely selected a used device + CV_SetValue(&cv_usejoystick[p], (oldstringchoice > numjoys ? oldstringchoice : oldchoice)); + + if (oldstringchoice == + (atoi(cv_usejoystick[p].string) > numjoys ? atoi(cv_usejoystick[p].string) : cv_usejoystick[p].value)) + M_StartMessage("This joystick is used by another\n" + "player. Reset the joystick\n" + "for that player first.\n\n" + "(Press a key)\n", NULL, MM_NOTHING); + } + } +#else + CV_SetValue(&cv_usejoystick[p], choice); +#endif +} + +// ============= +// CONTROLS MENU +// ============= + +static void M_Setup1PControlsMenu(INT32 choice) +{ + (void)choice; + setupcontrolplayer = 1; + setupcontrols = gamecontrol[0]; // was called from main Options (for console player, then) + currentMenu->lastOn = itemOn; + + // Set proper gamepad options + OP_AllControlsMenu[0].itemaction = &OP_Joystick1Def; + + // Unhide P1-only controls + OP_AllControlsMenu[16].status = IT_CONTROL; // Chat + //OP_AllControlsMenu[17].status = IT_CONTROL; // Team-chat + OP_AllControlsMenu[17].status = IT_CONTROL; // Rankings + //OP_AllControlsMenu[18].status = IT_CONTROL; // Viewpoint + // 19 is Reset Camera, 20 is Toggle Chasecam + OP_AllControlsMenu[21].status = IT_CONTROL; // Pause + OP_AllControlsMenu[22].status = IT_CONTROL; // Screenshot + OP_AllControlsMenu[23].status = IT_CONTROL; // GIF + OP_AllControlsMenu[24].status = IT_CONTROL; // System Menu + OP_AllControlsMenu[25].status = IT_CONTROL; // Console + /*OP_AllControlsMenu[26].status = IT_HEADER; // Spectator Controls header + OP_AllControlsMenu[27].status = IT_SPACE; // Spectator Controls space + OP_AllControlsMenu[28].status = IT_CONTROL; // Spectate + OP_AllControlsMenu[29].status = IT_CONTROL; // Look Up + OP_AllControlsMenu[30].status = IT_CONTROL; // Look Down + OP_AllControlsMenu[31].status = IT_CONTROL; // Center View + */ + + M_SetupNextMenu(&OP_AllControlsDef); +} + +static void M_Setup2PControlsMenu(INT32 choice) +{ + (void)choice; + setupcontrolplayer = 2; + setupcontrols = gamecontrol[1]; + currentMenu->lastOn = itemOn; + + // Set proper gamepad options + OP_AllControlsMenu[0].itemaction = &OP_Joystick2Def; + + // Hide P1-only controls + OP_AllControlsMenu[16].status = IT_GRAYEDOUT2; // Chat + //OP_AllControlsMenu[17].status = IT_GRAYEDOUT2; // Team-chat + OP_AllControlsMenu[17].status = IT_GRAYEDOUT2; // Rankings + //OP_AllControlsMenu[18].status = IT_GRAYEDOUT2; // Viewpoint + // 19 is Reset Camera, 20 is Toggle Chasecam + OP_AllControlsMenu[21].status = IT_GRAYEDOUT2; // Pause + OP_AllControlsMenu[22].status = IT_GRAYEDOUT2; // Screenshot + OP_AllControlsMenu[23].status = IT_GRAYEDOUT2; // GIF + OP_AllControlsMenu[24].status = IT_GRAYEDOUT2; // System Menu + OP_AllControlsMenu[25].status = IT_GRAYEDOUT2; // Console + /*OP_AllControlsMenu[26].status = IT_GRAYEDOUT2; // Spectator Controls header + OP_AllControlsMenu[27].status = IT_GRAYEDOUT2; // Spectator Controls space + OP_AllControlsMenu[28].status = IT_GRAYEDOUT2; // Spectate + OP_AllControlsMenu[29].status = IT_GRAYEDOUT2; // Look Up + OP_AllControlsMenu[30].status = IT_GRAYEDOUT2; // Look Down + OP_AllControlsMenu[31].status = IT_GRAYEDOUT2; // Center View + */ + + M_SetupNextMenu(&OP_AllControlsDef); +} + +static void M_Setup3PControlsMenu(INT32 choice) +{ + (void)choice; + setupcontrolplayer = 3; + setupcontrols = gamecontrol[2]; + currentMenu->lastOn = itemOn; + + // Set proper gamepad options + OP_AllControlsMenu[0].itemaction = &OP_Joystick3Def; + + // Hide P1-only controls + OP_AllControlsMenu[16].status = IT_GRAYEDOUT2; // Chat + //OP_AllControlsMenu[17].status = IT_GRAYEDOUT2; // Team-chat + OP_AllControlsMenu[17].status = IT_GRAYEDOUT2; // Rankings + //OP_AllControlsMenu[18].status = IT_GRAYEDOUT2; // Viewpoint + // 19 is Reset Camera, 20 is Toggle Chasecam + OP_AllControlsMenu[21].status = IT_GRAYEDOUT2; // Pause + OP_AllControlsMenu[22].status = IT_GRAYEDOUT2; // Screenshot + OP_AllControlsMenu[23].status = IT_GRAYEDOUT2; // GIF + OP_AllControlsMenu[24].status = IT_GRAYEDOUT2; // System Menu + OP_AllControlsMenu[25].status = IT_GRAYEDOUT2; // Console + /*OP_AllControlsMenu[26].status = IT_GRAYEDOUT2; // Spectator Controls header + OP_AllControlsMenu[27].status = IT_GRAYEDOUT2; // Spectator Controls space + OP_AllControlsMenu[28].status = IT_GRAYEDOUT2; // Spectate + OP_AllControlsMenu[29].status = IT_GRAYEDOUT2; // Look Up + OP_AllControlsMenu[30].status = IT_GRAYEDOUT2; // Look Down + OP_AllControlsMenu[31].status = IT_GRAYEDOUT2; // Center View + */ + + M_SetupNextMenu(&OP_AllControlsDef); +} + +static void M_Setup4PControlsMenu(INT32 choice) +{ + (void)choice; + setupcontrolplayer = 4; + setupcontrols = gamecontrol[3]; + currentMenu->lastOn = itemOn; + + // Set proper gamepad options + OP_AllControlsMenu[0].itemaction = &OP_Joystick4Def; + + // Hide P1-only controls + OP_AllControlsMenu[16].status = IT_GRAYEDOUT2; // Chat + //OP_AllControlsMenu[17].status = IT_GRAYEDOUT2; // Team-chat + OP_AllControlsMenu[17].status = IT_GRAYEDOUT2; // Rankings + //OP_AllControlsMenu[18].status = IT_GRAYEDOUT2; // Viewpoint + // 19 is Reset Camera, 20 is Toggle Chasecam + OP_AllControlsMenu[21].status = IT_GRAYEDOUT2; // Pause + OP_AllControlsMenu[22].status = IT_GRAYEDOUT2; // Screenshot + OP_AllControlsMenu[23].status = IT_GRAYEDOUT2; // GIF + OP_AllControlsMenu[24].status = IT_GRAYEDOUT2; // System Menu + OP_AllControlsMenu[25].status = IT_GRAYEDOUT2; // Console + /*OP_AllControlsMenu[26].status = IT_GRAYEDOUT2; // Spectator Controls header + OP_AllControlsMenu[27].status = IT_GRAYEDOUT2; // Spectator Controls space + OP_AllControlsMenu[28].status = IT_GRAYEDOUT2; // Spectate + OP_AllControlsMenu[29].status = IT_GRAYEDOUT2; // Look Up + OP_AllControlsMenu[30].status = IT_GRAYEDOUT2; // Look Down + OP_AllControlsMenu[31].status = IT_GRAYEDOUT2; // Center View + */ + + M_SetupNextMenu(&OP_AllControlsDef); +} + +#define controlheight 18 + +// Draws the Customise Controls menu +static void M_DrawControl(void) +{ + char tmp[50]; + INT32 x, y, i, max, cursory = 0, iter; + INT32 keys[2]; + + x = currentMenu->x; + y = currentMenu->y; + + /*i = itemOn - (controlheight/2); + if (i < 0) + i = 0; + */ + + iter = (controlheight/2); + for (i = itemOn; ((iter || currentMenu->menuitems[i].status == IT_GRAYEDOUT2) && i > 0); i--) + { + if (currentMenu->menuitems[i].status != IT_GRAYEDOUT2) + iter--; + } + if (currentMenu->menuitems[i].status == IT_GRAYEDOUT2) + i--; + + iter += (controlheight/2); + for (max = itemOn; (iter && max < currentMenu->numitems); max++) + { + if (currentMenu->menuitems[max].status != IT_GRAYEDOUT2) + iter--; + } + + if (iter) + { + iter += (controlheight/2); + for (i = itemOn; ((iter || currentMenu->menuitems[i].status == IT_GRAYEDOUT2) && i > 0); i--) + { + if (currentMenu->menuitems[i].status != IT_GRAYEDOUT2) + iter--; + } + } + + /*max = i + controlheight; + if (max > currentMenu->numitems) + { + max = currentMenu->numitems; + if (max < controlheight) + i = 0; + else + i = max - controlheight; + }*/ + + // draw title (or big pic) + M_DrawMenuTitle(); + + M_CentreText(28, + (setupcontrolplayer > 1 ? va("\x86""Set controls for ""\x82""Player %d", setupcontrolplayer) : + "\x86""Press ""\x82""ENTER""\x86"" to change, ""\x82""BACKSPACE""\x86"" to clear")); + + if (i) + V_DrawCharacter(currentMenu->x - 16, y-(skullAnimCounter/5), + '\x1A' | highlightflags, false); // up arrow + if (max != currentMenu->numitems) + V_DrawCharacter(currentMenu->x - 16, y+(SMALLLINEHEIGHT*(controlheight-1))+(skullAnimCounter/5) + (skullAnimCounter/5), + '\x1B' | highlightflags, false); // down arrow + + for (; i < max; i++) + { + if (currentMenu->menuitems[i].status == IT_GRAYEDOUT2) + continue; + + if (i == itemOn) + cursory = y; + + if (currentMenu->menuitems[i].status == IT_CONTROL) + { + V_DrawString(x, y, ((i == itemOn) ? highlightflags : 0), currentMenu->menuitems[i].text); + keys[0] = setupcontrols[currentMenu->menuitems[i].alphaKey][0]; + keys[1] = setupcontrols[currentMenu->menuitems[i].alphaKey][1]; + + tmp[0] ='\0'; + if (keys[0] == KEY_NULL && keys[1] == KEY_NULL) + { + strcpy(tmp, "---"); + } + else + { + if (keys[0] != KEY_NULL) + strcat (tmp, G_KeynumToString (keys[0])); + + if (keys[0] != KEY_NULL && keys[1] != KEY_NULL) + strcat(tmp,", "); + + if (keys[1] != KEY_NULL) + strcat (tmp, G_KeynumToString (keys[1])); + + } + V_DrawRightAlignedString(BASEVIDWIDTH-currentMenu->x, y, highlightflags, tmp); + } + /*else if (currentMenu->menuitems[i].status == IT_GRAYEDOUT2) + V_DrawString(x, y, V_TRANSLUCENT, currentMenu->menuitems[i].text);*/ + else if ((currentMenu->menuitems[i].status == IT_HEADER) && (i != max-1)) + V_DrawString(19, y+6, highlightflags, currentMenu->menuitems[i].text); + else if (currentMenu->menuitems[i].status & IT_STRING) + V_DrawString(x, y, ((i == itemOn) ? highlightflags : 0), currentMenu->menuitems[i].text); + + y += SMALLLINEHEIGHT; + } + + V_DrawScaledPatch(currentMenu->x - 20, cursory, 0, + W_CachePatchName("M_CURSOR", PU_CACHE)); +} + +#undef controlheight + +static INT32 controltochange; +static char controltochangetext[33]; + +static void M_ChangecontrolResponse(event_t *ev) +{ + INT32 control; + INT32 found; + INT32 ch = ev->data1; + + // ESCAPE cancels; dummy out PAUSE + if (ch != KEY_ESCAPE && ch != KEY_PAUSE) + { + + switch (ev->type) + { + // ignore mouse/joy movements, just get buttons + case ev_mouse: + case ev_joystick: + case ev_joystick2: + case ev_joystick3: + case ev_joystick4: + ch = KEY_NULL; // no key + break; + + // keypad arrows are converted for the menu in cursor arrows + // so use the event instead of ch + case ev_keydown: + ch = ev->data1; + break; + + default: + break; + } + + control = controltochange; + + // check if we already entered this key + found = -1; + if (setupcontrols[control][0] ==ch) + found = 0; + else if (setupcontrols[control][1] ==ch) + found = 1; + if (found >= 0) + { + // replace mouse and joy clicks by double clicks + if (ch >= KEY_MOUSE1 && ch <= KEY_MOUSE1+MOUSEBUTTONS) + setupcontrols[control][found] = ch-KEY_MOUSE1+KEY_DBLMOUSE1; + else if (ch >= KEY_JOY1 && ch <= KEY_JOY1+JOYBUTTONS) + setupcontrols[control][found] = ch-KEY_JOY1+KEY_DBLJOY1; + else if (ch >= KEY_2MOUSE1 && ch <= KEY_2MOUSE1+MOUSEBUTTONS) + setupcontrols[control][found] = ch-KEY_2MOUSE1+KEY_DBL2MOUSE1; + else if (ch >= KEY_2JOY1 && ch <= KEY_2JOY1+JOYBUTTONS) + setupcontrols[control][found] = ch-KEY_2JOY1+KEY_DBL2JOY1; + else if (ch >= KEY_3JOY1 && ch <= KEY_3JOY1+JOYBUTTONS) + setupcontrols[control][found] = ch-KEY_3JOY1+KEY_DBL3JOY1; + else if (ch >= KEY_4JOY1 && ch <= KEY_4JOY1+JOYBUTTONS) + setupcontrols[control][found] = ch-KEY_4JOY1+KEY_DBL4JOY1; + } + else + { + // check if change key1 or key2, or replace the two by the new + found = 0; + if (setupcontrols[control][0] == KEY_NULL) + found++; + if (setupcontrols[control][1] == KEY_NULL) + found++; + if (found == 2) + { + found = 0; + setupcontrols[control][1] = KEY_NULL; //replace key 1,clear key2 + } + (void)G_CheckDoubleUsage(ch, true); + setupcontrols[control][found] = ch; + } + S_StartSound(NULL, sfx_s221); + } + else if (ch == KEY_PAUSE) + { + // This buffer assumes a 125-character message plus a 32-character control name (per controltochangetext buffer size) + static char tmp[158]; + menu_t *prev = currentMenu->prevMenu; + + if (controltochange == gc_pause) + sprintf(tmp, M_GetText("The \x82Pause Key \x80is enabled, but \nyou may select another key. \n\nHit another key for\n%s\nESC for Cancel"), + controltochangetext); + else + sprintf(tmp, M_GetText("The \x82Pause Key \x80is enabled, but \nit is not configurable. \n\nHit another key for\n%s\nESC for Cancel"), + controltochangetext); + + M_StartMessage(tmp, M_ChangecontrolResponse, MM_EVENTHANDLER); + currentMenu->prevMenu = prev; + + S_StartSound(NULL, sfx_s3k42); + return; + } + else + S_StartSound(NULL, sfx_s224); + + M_StopMessage(0); +} + +static void M_ChangeControl(INT32 choice) +{ + // This buffer assumes a 35-character message (per below) plus a max control name limit of 32 chars (per controltochangetext) + // If you change the below message, then change the size of this buffer! + static char tmp[68]; + + controltochange = currentMenu->menuitems[choice].alphaKey; + sprintf(tmp, M_GetText("Hit the new key for\n%s\nESC for Cancel"), + currentMenu->menuitems[choice].text); + strlcpy(controltochangetext, currentMenu->menuitems[choice].text, 33); + + M_StartMessage(tmp, M_ChangecontrolResponse, MM_EVENTHANDLER); +} + +static void M_ResetControlsResponse(INT32 ch) +{ + const UINT8 p = setupcontrolplayer-1; + INT32 i; + + if (ch != 'y' && ch != KEY_ENTER) + return; + + // clear all controls + for (i = 0; i < num_gamecontrols; i++) + { + G_ClearControlKeys(gamecontrol[p], i); + } + + // Setup original defaults + G_CopyControls(gamecontrol[p], gamecontroldefault[p][gcs_kart], NULL, 0); + + // Setup gamepad option defaults (yucky) + CV_StealthSet(&cv_usejoystick[p], cv_usejoystick[p].defaultvalue); + CV_StealthSet(&cv_turnaxis[p], cv_turnaxis[p].defaultvalue); + CV_StealthSet(&cv_moveaxis[p], cv_moveaxis[p].defaultvalue); + CV_StealthSet(&cv_brakeaxis[p], cv_brakeaxis[p].defaultvalue); + CV_StealthSet(&cv_aimaxis[p], cv_aimaxis[p].defaultvalue); + CV_StealthSet(&cv_lookaxis[p], cv_lookaxis[p].defaultvalue); + CV_StealthSet(&cv_fireaxis[p], cv_fireaxis[p].defaultvalue); + CV_StealthSet(&cv_driftaxis[p], cv_driftaxis[p].defaultvalue); + + S_StartSound(NULL, sfx_s224); +} + +static void M_ResetControls(INT32 choice) +{ + (void)choice; + M_StartMessage(va(M_GetText("Reset Player %d's controls to defaults?\n\n(Press 'Y' to confirm)\n"), setupcontrolplayer), M_ResetControlsResponse, MM_YESNO); +} + +// ===== +// SOUND +// ===== + +/*static void M_RestartAudio(void) +{ + COM_ImmedExecute("restartaudio"); +}*/ + +// =============== +// VIDEO MODE MENU +// =============== + +//added : 30-01-98: +#define MAXCOLUMNMODES 12 //max modes displayed in one column +#define MAXMODEDESCS (MAXCOLUMNMODES*3) + +static modedesc_t modedescs[MAXMODEDESCS]; + +static void M_VideoModeMenu(INT32 choice) +{ + INT32 i, j, vdup, nummodes, width, height; + const char *desc; + + (void)choice; + + memset(modedescs, 0, sizeof(modedescs)); + +#if (defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL) + VID_PrepareModeList(); // FIXME: hack +#endif + vidm_nummodes = 0; + vidm_selected = 0; + nummodes = VID_NumModes(); + +#ifdef _WINDOWS + // clean that later: skip windowed mode 0, video modes menu only shows FULL SCREEN modes + if (nummodes <= NUMSPECIALMODES) + i = 0; // unless we have nothing + else + i = NUMSPECIALMODES; +#else + // DOS does not skip mode 0, because mode 0 is ALWAYS present + i = 0; +#endif + for (; i < nummodes && vidm_nummodes < MAXMODEDESCS; i++) + { + desc = VID_GetModeName(i); + if (desc) + { + vdup = 0; + + // when a resolution exists both under VGA and VESA, keep the + // VESA mode, which is always a higher modenum + for (j = 0; j < vidm_nummodes; j++) + { + if (!strcmp(modedescs[j].desc, desc)) + { + // mode(0): 320x200 is always standard VGA, not vesa + if (modedescs[j].modenum) + { + modedescs[j].modenum = i; + vdup = 1; + + if (i == vid.modenum) + vidm_selected = j; + } + else + vdup = 1; + + break; + } + } + + if (!vdup) + { + modedescs[vidm_nummodes].modenum = i; + modedescs[vidm_nummodes].desc = desc; + + if (i == vid.modenum) + vidm_selected = vidm_nummodes; + + // Pull out the width and height + sscanf(desc, "%u%*c%u", &width, &height); + + // Show multiples of 320x200 as green. + if (SCR_IsAspectCorrect(width, height)) + modedescs[vidm_nummodes].goodratio = 1; + + vidm_nummodes++; + } + } + } + + vidm_column_size = (vidm_nummodes+2) / 3; + + M_SetupNextMenu(&OP_VideoModeDef); +} + +static void M_DrawVideoMenu(void) +{ + M_DrawGenericMenu(); + + V_DrawRightAlignedString(BASEVIDWIDTH - currentMenu->x, currentMenu->y + OP_VideoOptionsMenu[0].alphaKey, + (SCR_IsAspectCorrect(vid.width, vid.height) ? recommendedflags : highlightflags), + va("%dx%d", vid.width, vid.height)); +} + +static void M_DrawHUDOptions(void) +{ + const char *str0 = ")"; + const char *str1 = " Warning highlight"; + const char *str2 = ","; + const char *str3 = "Good highlight"; + INT32 x = BASEVIDWIDTH - currentMenu->x + 2, y = currentMenu->y + 115; + INT32 w0 = V_StringWidth(str0, 0), w1 = V_StringWidth(str1, 0), w2 = V_StringWidth(str2, 0), w3 = V_StringWidth(str3, 0); + + M_DrawGenericMenu(); + + x -= w0; + V_DrawString(x, y, highlightflags, str0); + x -= w1; + V_DrawString(x, y, warningflags, str1); + x -= w2; + V_DrawString(x, y, highlightflags, str2); + x -= w3; + V_DrawString(x, y, recommendedflags, str3); + V_DrawRightAlignedString(x, y, highlightflags, "("); +} + +// Draw the video modes list, a-la-Quake +static void M_DrawVideoMode(void) +{ + INT32 i, j, row, col; + + // draw title + M_DrawMenuTitle(); + + V_DrawCenteredString(BASEVIDWIDTH/2, OP_VideoModeDef.y, + highlightflags, "Choose mode, reselect to change default"); + + row = 41; + col = OP_VideoModeDef.y + 14; + for (i = 0; i < vidm_nummodes; i++) + { + if (i == vidm_selected) + V_DrawString(row, col, highlightflags, modedescs[i].desc); + // Show multiples of 320x200 as green. + else + V_DrawString(row, col, (modedescs[i].goodratio) ? recommendedflags : 0, modedescs[i].desc); + + col += 8; + if ((i % vidm_column_size) == (vidm_column_size-1)) + { + row += 7*13; + col = OP_VideoModeDef.y + 14; + } + } + + if (vidm_testingmode > 0) + { + INT32 testtime = (vidm_testingmode/TICRATE) + 1; + + M_CentreText(OP_VideoModeDef.y + 116, + va("Previewing mode %c%dx%d", + (SCR_IsAspectCorrect(vid.width, vid.height)) ? 0x83 : 0x80, + vid.width, vid.height)); + M_CentreText(OP_VideoModeDef.y + 138, + "Press ENTER again to keep this mode"); + M_CentreText(OP_VideoModeDef.y + 150, + va("Wait %d second%s", testtime, (testtime > 1) ? "s" : "")); + M_CentreText(OP_VideoModeDef.y + 158, + "or press ESC to return"); + + } + else + { + M_CentreText(OP_VideoModeDef.y + 116, + va("Current mode is %c%dx%d", + (SCR_IsAspectCorrect(vid.width, vid.height)) ? 0x83 : 0x80, + vid.width, vid.height)); + M_CentreText(OP_VideoModeDef.y + 124, + va("Default mode is %c%dx%d", + (SCR_IsAspectCorrect(cv_scr_width.value, cv_scr_height.value)) ? 0x83 : 0x80, + cv_scr_width.value, cv_scr_height.value)); + + V_DrawCenteredString(BASEVIDWIDTH/2, OP_VideoModeDef.y + 138, + recommendedflags, "Marked modes are recommended."); + V_DrawCenteredString(BASEVIDWIDTH/2, OP_VideoModeDef.y + 146, + highlightflags, "Other modes may have visual errors."); + V_DrawCenteredString(BASEVIDWIDTH/2, OP_VideoModeDef.y + 158, + highlightflags, "Larger modes may have performance issues."); + } + + // Draw the cursor for the VidMode menu + i = 41 - 10 + ((vidm_selected / vidm_column_size)*7*13); + j = OP_VideoModeDef.y + 14 + ((vidm_selected % vidm_column_size)*8); + + V_DrawScaledPatch(i - 8, j, 0, + W_CachePatchName("M_CURSOR", PU_CACHE)); +} + +// special menuitem key handler for video mode list +static void M_HandleVideoMode(INT32 ch) +{ + if (vidm_testingmode > 0) switch (ch) + { + // change back to the previous mode quickly + case KEY_ESCAPE: + setmodeneeded = vidm_previousmode + 1; + vidm_testingmode = 0; + break; + + case KEY_ENTER: + S_StartSound(NULL, sfx_menu1); + vidm_testingmode = 0; // stop testing + } + + else switch (ch) + { + case KEY_DOWNARROW: + S_StartSound(NULL, sfx_menu1); + if (++vidm_selected >= vidm_nummodes) + vidm_selected = 0; + break; + + case KEY_UPARROW: + S_StartSound(NULL, sfx_menu1); + if (--vidm_selected < 0) + vidm_selected = vidm_nummodes - 1; + break; + + case KEY_LEFTARROW: + S_StartSound(NULL, sfx_menu1); + vidm_selected -= vidm_column_size; + if (vidm_selected < 0) + vidm_selected = (vidm_column_size*3) + vidm_selected; + if (vidm_selected >= vidm_nummodes) + vidm_selected = vidm_nummodes - 1; + break; + + case KEY_RIGHTARROW: + S_StartSound(NULL, sfx_menu1); + vidm_selected += vidm_column_size; + if (vidm_selected >= (vidm_column_size*3)) + vidm_selected %= vidm_column_size; + if (vidm_selected >= vidm_nummodes) + vidm_selected = vidm_nummodes - 1; + break; + + case KEY_ENTER: + S_StartSound(NULL, sfx_menu1); + if (vid.modenum == modedescs[vidm_selected].modenum) + SCR_SetDefaultMode(); + else + { + vidm_testingmode = 15*TICRATE; + vidm_previousmode = vid.modenum; + if (!setmodeneeded) // in case the previous setmode was not finished + setmodeneeded = modedescs[vidm_selected].modenum + 1; + } + break; + + case KEY_ESCAPE: // this one same as M_Responder + if (currentMenu->prevMenu) + M_SetupNextMenu(currentMenu->prevMenu); + else + M_ClearMenus(true); + break; + + default: + break; + } +} + +// =============== +// Monitor Toggles +// =============== + +static tic_t shitsfree = 0; + +static void M_DrawMonitorToggles(void) +{ + const INT32 edges = 4; + const INT32 height = 4; + const INT32 spacing = 35; + const INT32 column = itemOn/height; + //const INT32 row = itemOn%height; + INT32 leftdraw, rightdraw, totaldraw; + INT32 x = currentMenu->x, y = currentMenu->y+(spacing/4); + INT32 onx = 0, ony = 0; + consvar_t *cv; + INT32 i, translucent, drawnum; + + M_DrawMenuTitle(); + + // Find the available space around column + leftdraw = rightdraw = column; + totaldraw = 0; + for (i = 0; (totaldraw < edges*2 && i < edges*4); i++) + { + if (rightdraw+1 < (currentMenu->numitems/height)+1) + { + rightdraw++; + totaldraw++; + } + if (leftdraw-1 >= 0) + { + leftdraw--; + totaldraw++; + } + } + + for (i = leftdraw; i <= rightdraw; i++) + { + INT32 j; + + for (j = 0; j < height; j++) + { + const INT32 thisitem = (i*height)+j; + + if (thisitem >= currentMenu->numitems) + continue; + + if (thisitem == itemOn) + { + onx = x; + ony = y; + y += spacing; + continue; + } + +#ifdef ITEMTOGGLEBOTTOMRIGHT + if (currentMenu->menuitems[thisitem].alphaKey == 255) + { + V_DrawScaledPatch(x, y, V_TRANSLUCENT, W_CachePatchName("K_ISBG", PU_CACHE)); + continue; + } +#endif + if (currentMenu->menuitems[thisitem].alphaKey == 0) + { + V_DrawScaledPatch(x, y, 0, W_CachePatchName("K_ISBG", PU_CACHE)); + V_DrawScaledPatch(x, y, 0, W_CachePatchName("K_ISTOGL", PU_CACHE)); + continue; + } + + cv = KartItemCVars[currentMenu->menuitems[thisitem].alphaKey-1]; + translucent = (cv->value ? 0 : V_TRANSLUCENT); + + switch (currentMenu->menuitems[thisitem].alphaKey) + { + case KRITEM_DUALSNEAKER: + case KRITEM_DUALJAWZ: + drawnum = 2; + break; + case KRITEM_TRIPLESNEAKER: + case KRITEM_TRIPLEBANANA: + case KRITEM_TRIPLEORBINAUT: + drawnum = 3; + break; + case KRITEM_QUADORBINAUT: + drawnum = 4; + break; + case KRITEM_TENFOLDBANANA: + drawnum = 10; + break; + default: + drawnum = 0; + break; + } + + if (cv->value) + V_DrawScaledPatch(x, y, 0, W_CachePatchName("K_ISBG", PU_CACHE)); + else + V_DrawScaledPatch(x, y, 0, W_CachePatchName("K_ISBGD", PU_CACHE)); + + if (drawnum != 0) + { + V_DrawScaledPatch(x, y, 0, W_CachePatchName("K_ISMUL", PU_CACHE)); + V_DrawScaledPatch(x, y, translucent, W_CachePatchName(K_GetItemPatch(currentMenu->menuitems[thisitem].alphaKey, true), PU_CACHE)); + V_DrawString(x+24, y+31, V_ALLOWLOWERCASE|translucent, va("x%d", drawnum)); + } + else + V_DrawScaledPatch(x, y, translucent, W_CachePatchName(K_GetItemPatch(currentMenu->menuitems[thisitem].alphaKey, true), PU_CACHE)); + + y += spacing; + } + + x += spacing; + y = currentMenu->y+(spacing/4); + } + + { +#ifdef ITEMTOGGLEBOTTOMRIGHT + if (currentMenu->menuitems[itemOn].alphaKey == 255) + { + V_DrawScaledPatch(onx-1, ony-2, V_TRANSLUCENT, W_CachePatchName("K_ITBG", PU_CACHE)); + if (shitsfree) + { + INT32 trans = V_TRANSLUCENT; + if (shitsfree-1 > TICRATE-5) + trans = ((10-TICRATE)+shitsfree-1)<menuitems[itemOn].alphaKey == 0) + { + V_DrawScaledPatch(onx-1, ony-2, 0, W_CachePatchName("K_ITBG", PU_CACHE)); + V_DrawScaledPatch(onx-1, ony-2, 0, W_CachePatchName("K_ITTOGL", PU_CACHE)); + } + else + { + cv = KartItemCVars[currentMenu->menuitems[itemOn].alphaKey-1]; + translucent = (cv->value ? 0 : V_TRANSLUCENT); + + switch (currentMenu->menuitems[itemOn].alphaKey) + { + case KRITEM_DUALSNEAKER: + case KRITEM_DUALJAWZ: + drawnum = 2; + break; + case KRITEM_TRIPLESNEAKER: + case KRITEM_TRIPLEBANANA: + drawnum = 3; + break; + case KRITEM_TENFOLDBANANA: + drawnum = 10; + break; + default: + drawnum = 0; + break; + } + + if (cv->value) + V_DrawScaledPatch(onx-1, ony-2, 0, W_CachePatchName("K_ITBG", PU_CACHE)); + else + V_DrawScaledPatch(onx-1, ony-2, 0, W_CachePatchName("K_ITBGD", PU_CACHE)); + + if (drawnum != 0) + { + V_DrawScaledPatch(onx-1, ony-2, 0, W_CachePatchName("K_ITMUL", PU_CACHE)); + V_DrawScaledPatch(onx-1, ony-2, translucent, W_CachePatchName(K_GetItemPatch(currentMenu->menuitems[itemOn].alphaKey, false), PU_CACHE)); + V_DrawScaledPatch(onx+27, ony+39, translucent, W_CachePatchName("K_ITX", PU_CACHE)); + V_DrawKartString(onx+37, ony+34, translucent, va("%d", drawnum)); + } + else + V_DrawScaledPatch(onx-1, ony-2, translucent, W_CachePatchName(K_GetItemPatch(currentMenu->menuitems[itemOn].alphaKey, false), PU_CACHE)); + } + } + + if (shitsfree) + shitsfree--; + + V_DrawCenteredString(BASEVIDWIDTH/2, currentMenu->y, highlightflags, va("* %s *", currentMenu->menuitems[itemOn].text)); +} + +static void M_HandleMonitorToggles(INT32 choice) +{ + const INT32 width = 6, height = 4; + INT32 column = itemOn/height, row = itemOn%height; + INT16 next; + UINT8 i; + boolean exitmenu = false; + + switch (choice) + { + case KEY_RIGHTARROW: + S_StartSound(NULL, sfx_menu1); + column++; + if (((column*height)+row) >= currentMenu->numitems) + column = 0; + next = min(((column*height)+row), currentMenu->numitems-1); + itemOn = next; + break; + + case KEY_LEFTARROW: + S_StartSound(NULL, sfx_menu1); + column--; + if (column < 0) + column = width-1; + if (((column*height)+row) >= currentMenu->numitems) + column--; + next = max(((column*height)+row), 0); + if (next >= currentMenu->numitems) + next = currentMenu->numitems-1; + itemOn = next; + break; + + case KEY_DOWNARROW: + S_StartSound(NULL, sfx_menu1); + row = (row+1) % height; + if (((column*height)+row) >= currentMenu->numitems) + row = 0; + next = min(((column*height)+row), currentMenu->numitems-1); + itemOn = next; + break; + + case KEY_UPARROW: + S_StartSound(NULL, sfx_menu1); + row = (row-1) % height; + if (row < 0) + row = height-1; + if (((column*height)+row) >= currentMenu->numitems) + row--; + next = max(((column*height)+row), 0); + if (next >= currentMenu->numitems) + next = currentMenu->numitems-1; + itemOn = next; + break; + + case KEY_ENTER: +#ifdef ITEMTOGGLEBOTTOMRIGHT + if (currentMenu->menuitems[itemOn].alphaKey == 255) + { + //S_StartSound(NULL, sfx_s26d); + if (!shitsfree) + { + shitsfree = TICRATE; + S_StartSound(NULL, sfx_itfree); + } + } + else +#endif + if (currentMenu->menuitems[itemOn].alphaKey == 0) + { + INT32 v = cv_sneaker.value; + S_StartSound(NULL, sfx_s1b4); + for (i = 0; i < NUMKARTRESULTS-1; i++) + { + if (KartItemCVars[i]->value == v) + CV_AddValue(KartItemCVars[i], 1); + } + } + else + { + S_StartSound(NULL, sfx_s1ba); + CV_AddValue(KartItemCVars[currentMenu->menuitems[itemOn].alphaKey-1], 1); + } + break; + + case KEY_ESCAPE: + exitmenu = true; + break; + } + + if (exitmenu) + { + if (currentMenu->prevMenu) + M_SetupNextMenu(currentMenu->prevMenu); + else + M_ClearMenus(true); + } +} + +// ========= +// Quit Game +// ========= +static INT32 quitsounds[] = +{ + // holy shit we're changing things up! + // srb2kart: you ain't seen nothing yet + sfx_kc2e, + sfx_kc2f, + sfx_cdfm01, + sfx_ddash, + sfx_s3ka2, + sfx_s3k49, + sfx_slip, + sfx_tossed, + sfx_s3k7b, + sfx_itrolf, + sfx_itrole, + sfx_cdpcm9, + sfx_s3k4e, + sfx_s259, + sfx_3db06, + sfx_s3k3a, + sfx_peel, + sfx_cdfm28, + sfx_s3k96, + sfx_s3kc0s, + sfx_cdfm39, + sfx_hogbom, + sfx_kc5a, + sfx_kc46, + sfx_s3k92, + sfx_s3k42, + sfx_kpogos, + sfx_screec +}; + +void M_QuitResponse(INT32 ch) +{ + tic_t ptime; + INT32 mrand; + + if (ch != 'y' && ch != KEY_ENTER) + return; + if (!(netgame || cv_debug)) + { + mrand = M_RandomKey(sizeof(quitsounds)/sizeof(INT32)); + if (quitsounds[mrand]) S_StartSound(NULL, quitsounds[mrand]); + + //added : 12-02-98: do that instead of I_WaitVbl which does not work + ptime = I_GetTime() + NEWTICRATE*2; // Shortened the quit time, used to be 2 seconds Tails 03-26-2001 + while (ptime > I_GetTime()) + { + V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31); + V_DrawSmallScaledPatch(0, 0, 0, W_CachePatchName("GAMEQUIT", PU_CACHE)); // Demo 3 Quit Screen Tails 06-16-2001 + I_FinishUpdate(); // Update the screen with the image Tails 06-19-2001 + I_Sleep(); + } + } + I_Quit(); +} + +static void M_QuitSRB2(INT32 choice) +{ + // We pick index 0 which is language sensitive, or one at random, + // between 1 and maximum number. + (void)choice; + M_StartMessage(quitmsg[M_RandomKey(NUM_QUITMESSAGES)], M_QuitResponse, MM_YESNO); +} + +#ifdef HAVE_DISCORDRPC +static const tic_t confirmLength = 3*TICRATE/4; +static tic_t confirmDelay = 0; +static boolean confirmAccept = false; + +static void M_HandleDiscordRequests(INT32 choice) +{ + if (confirmDelay > 0) + return; + + switch (choice) + { + case KEY_ENTER: + Discord_Respond(discordRequestList->userID, DISCORD_REPLY_YES); + confirmAccept = true; + confirmDelay = confirmLength; + S_StartSound(NULL, sfx_s3k63); + break; + + case KEY_ESCAPE: + Discord_Respond(discordRequestList->userID, DISCORD_REPLY_NO); + confirmAccept = false; + confirmDelay = confirmLength; + S_StartSound(NULL, sfx_s3kb2); + break; + } +} + +static const char *M_GetDiscordName(discordRequest_t *r) +{ + if (r == NULL) + return ""; + + if (cv_discordstreamer.value) + return r->username; + + return va("%s#%s", r->username, r->discriminator); +} + +// (this goes in k_hud.c when merged into v2) +static void M_DrawSticker(INT32 x, INT32 y, INT32 width, INT32 flags, boolean isSmall) +{ + patch_t *stickerEnd; + INT32 height; + + if (isSmall == true) + { + stickerEnd = W_CachePatchName("K_STIKE2", PU_CACHE); + height = 6; + } + else + { + stickerEnd = W_CachePatchName("K_STIKEN", PU_CACHE); + height = 11; + } + + V_DrawFixedPatch(x*FRACUNIT, y*FRACUNIT, FRACUNIT, flags, stickerEnd, NULL); + V_DrawFill(x, y, width, height, 24|flags); + V_DrawFixedPatch((x + width)*FRACUNIT, y*FRACUNIT, FRACUNIT, flags|V_FLIP, stickerEnd, NULL); +} + +static void M_DrawDiscordRequests(void) +{ + discordRequest_t *curRequest = discordRequestList; + UINT8 *colormap; + patch_t *hand = NULL; + boolean removeRequest = false; + + const char *wantText = "...would like to join!"; + const char *controlText = "\x82" "ENTER" "\x80" " - Accept " "\x82" "ESC" "\x80" " - Decline"; + + INT32 x = 100; + INT32 y = 133; + + INT32 slide = 0; + INT32 maxYSlide = 18; + + if (confirmDelay > 0) + { + if (confirmAccept == true) + { + colormap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_GREEN, GTC_MENUCACHE); + hand = W_CachePatchName("K_LAPH02", PU_CACHE); + } + else + { + colormap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_RED, GTC_MENUCACHE); + hand = W_CachePatchName("K_LAPH03", PU_CACHE); + } + + slide = confirmLength - confirmDelay; + + confirmDelay--; + + if (confirmDelay == 0) + removeRequest = true; + } + else + { + colormap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_GREY, GTC_MENUCACHE); + } + + V_DrawFixedPatch(56*FRACUNIT, 150*FRACUNIT, FRACUNIT, 0, W_CachePatchName("K_LAPE01", PU_CACHE), colormap); + + if (hand != NULL) + { + fixed_t handoffset = (4 - abs((signed)(skullAnimCounter - 4))) * FRACUNIT; + V_DrawFixedPatch(56*FRACUNIT, 150*FRACUNIT + handoffset, FRACUNIT, 0, hand, NULL); + } + + M_DrawSticker(x + (slide * 32), y - 1, V_ThinStringWidth(M_GetDiscordName(curRequest), V_ALLOWLOWERCASE|V_6WIDTHSPACE), 0, false); + V_DrawThinString(x + (slide * 32), y, V_ALLOWLOWERCASE|V_6WIDTHSPACE|V_YELLOWMAP, M_GetDiscordName(curRequest)); + + M_DrawSticker(x, y + 12, V_ThinStringWidth(wantText, V_ALLOWLOWERCASE|V_6WIDTHSPACE), 0, true); + V_DrawThinString(x, y + 10, V_ALLOWLOWERCASE|V_6WIDTHSPACE, wantText); + + M_DrawSticker(x, y + 26, V_ThinStringWidth(controlText, V_ALLOWLOWERCASE|V_6WIDTHSPACE), 0, true); + V_DrawThinString(x, y + 24, V_ALLOWLOWERCASE|V_6WIDTHSPACE, controlText); + + y -= 18; + + while (curRequest->next != NULL) + { + INT32 ySlide = min(slide * 4, maxYSlide); + + curRequest = curRequest->next; + + M_DrawSticker(x, y - 1 + ySlide, V_ThinStringWidth(M_GetDiscordName(curRequest), V_ALLOWLOWERCASE|V_6WIDTHSPACE), 0, false); + V_DrawThinString(x, y + ySlide, V_ALLOWLOWERCASE|V_6WIDTHSPACE, M_GetDiscordName(curRequest)); + + y -= 12; + maxYSlide = 12; + } + + if (removeRequest == true) + { + DRPC_RemoveRequest(discordRequestList); + + if (discordRequestList == NULL) + { + // No other requests + MPauseMenu[mpause_discordrequests].status = IT_GRAYEDOUT; + + if (currentMenu->prevMenu) + { + M_SetupNextMenu(currentMenu->prevMenu); + if (currentMenu == &MPauseDef) + itemOn = mpause_continue; + } + else + M_ClearMenus(true); + + return; + } + } +} +#endif