RingRacers/src/k_menufunc.c
2025-10-25 19:06:26 -05:00

1552 lines
34 KiB
C

// DR. ROBOTNIK'S RING RACERS
//-----------------------------------------------------------------------------
// Copyright (C) 2025 by Kart Krew.
// Copyright (C) 2016 by Kay "Kaito" Sinclaire.
// Copyright (C) 2020 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 k_menufunc.c
/// \brief SRB2Kart's menu functions
#ifdef __GNUC__
#include <unistd.h>
#endif
#include "k_menu.h"
#include "doomdef.h"
#include "d_main.h"
#include "console.h"
#include "hu_stuff.h"
#include "s_sound.h"
#include "v_video.h"
#include "f_finale.h"
#include "m_misc.h"
#include "m_cond.h"
#include "music.h"
#ifdef HAVE_DISCORDRPC
#include "discord.h"
#endif
#ifdef PC_DOS
#include <stdio.h> // 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
// ==========================================================================
// GLOBAL VARIABLES
// ==========================================================================
#ifdef HAVE_THREADS
I_mutex k_menu_mutex;
#endif
boolean menuactive = false;
// current menudef
menu_t *currentMenu = &MAIN_ProfilesDef;
menu_t *restoreMenu = NULL;
INT16 itemOn = 0; // menu item skull is on, Hack by Tails 09-18-2002
INT16 skullAnimCounter = 8; // skull animation counter
struct menutransition_s menutransition; // Menu transition properties
INT32 menuKey = -1; // keyboard key pressed for menu
menucmd_t menucmd[MAXSPLITSCREENPLAYERS];
// Prevent early resetting in Attack modes when setting a new best time.
// I can't make demos save at the correct time, but I can do this!
boolean blockreset = false;
// finish wipes between screens
boolean menuwipe = false;
// 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;
// ==========================================================================
// CONSOLE VARIABLES AND THEIR POSSIBLE VALUES GO HERE.
// ==========================================================================
CV_PossibleValue_t dummystaff_cons_t[] = {{0, "MIN"}, {999, "MAX"}, {0, NULL}};
// =========================================================================
// BASIC MENU HANDLING
// =========================================================================
static void M_AddFloatVar(consvar_t *cv, fixed_t step)
{
int minopt = 0;
int maxopt = 0;
int curopt = -1;
int i;
const CV_PossibleValue_t *values = cv->PossibleValue;
if (values == NULL) //cvar is unbounded and will not work! return is here only as a failsafe to prevent crashes
return;
for (i = 0; values[i].strvalue; ++i)
{
if (cv->value == values[i].value)
{
curopt = i;
if (i > 1)
break;
}
else if (i > 1)
{
if (!minopt || values[minopt].value > values[i].value)
minopt = i;
if (!maxopt || values[maxopt].value < values[i].value)
maxopt = i;
}
}
if (curopt > 1 || curopt == (step > 0))
{
CV_Set(cv, step < 0 ? (maxopt ? values[maxopt].strvalue : "MAX") : (minopt ? values[minopt].strvalue : "MIN"));
return;
}
fixed_t n = cv->value;
if (step > 0)
{
if (values[1].value - n <= step)
{
CV_Set(cv, "MAX");
return;
}
n = n + step;
n -= n % step;
}
else
{
if (n - values[0].value <= -step)
{
CV_Set(cv, "MIN");
return;
}
fixed_t p = n % -step;
n -= p ? p : -step;
}
char s[20];
double f = FIXED_TO_FLOAT(n);
const char *d = M_Ftrim(f);
sprintf(s, "%ld%s", (long)f, *d ? d : ".0");
CV_Set(cv, s);
}
void M_ChangeCvarDirect(INT32 choice, consvar_t *cv)
{
// Backspace sets values to default value
if (choice == -1)
{
CV_Set(cv, cv->defaultvalue);
return;
}
choice = (choice == 0 ? -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))
{
CV_SetValue(cv, cv->value+choice);
}
else if (cv->flags & CV_FLOAT)
{
M_AddFloatVar(cv, (cv->step_amount ? cv->step_amount : FRACUNIT/16) * choice);
}
else
{
if (cv == &cv_nettimeout || cv == &cv_jointimeout)
choice *= (TICRATE/7);
else if (cv == &cv_maxsend)
choice *= 1024;
CV_AddValue(cv, choice);
}
}
static void M_ChangeCvarResponse(INT32 choice)
{
if (choice != MA_YES)
return;
consvar_t *cvar = currentMenu->menuitems[itemOn].itemaction.cvar;
M_ChangeCvarDirect(choice, cvar);
}
static void M_ChangeCvar(INT32 choice)
{
consvar_t *cvar = currentMenu->menuitems[itemOn].itemaction.cvar;
#ifdef HWRENDER
if (cvar == &cv_renderer &&
// Switching from Software [to Legacy GL]
cv_renderer.value == 1 &&
// Not setting to default (ie changing the value)
choice != -1)
{
M_StartMessage(
"Switching to Legacy GL",
"Legacy GL is \x85" "INCOMPLETE and BROKEN.\x80\n"
"\n"
"ARE YOU SURE?",
M_ChangeCvarResponse,
MM_YESNO,
NULL,
NULL
);
return;
}
#endif
if (cvar == &cv_dummyprofileautoroulette &&
// Turning Auto Roulette on
cv_dummyprofileautoroulette.value == 0 &&
// Not setting to default (ie changing the value)
choice != -1)
{
M_StartMessage(
"Turning on Auto Roulette",
"\"Ring Racers\" is not designed with random items in mind. With Auto Roulette, you cannot select the item results you want or select an item early."
"\n"
"You will be at a distinct \x85" "disadvantage. \x80\n"
"\n"
"ARE YOU SURE?",
M_ChangeCvarResponse,
MM_YESNO,
NULL,
NULL
);
return;
}
M_ChangeCvarDirect(choice, cvar);
}
static const char *M_QueryCvarAction(const char *replace)
{
consvar_t *cvar = currentMenu->menuitems[itemOn].itemaction.cvar;
if (replace)
CV_Set(cvar, replace);
return cvar->string;
}
boolean M_NextOpt(void)
{
INT16 oldItemOn = itemOn; // prevent infinite loop
if ((currentMenu->menuitems[itemOn].status & IT_CVARTYPE) == IT_CV_PASSWORD)
(currentMenu->menuitems[itemOn].itemaction.cvar)->value = 0;
do
{
if (itemOn + 1 > currentMenu->numitems - 1)
{
// Prevent looparound here
if (currentMenu->behaviourflags & MBF_NOLOOPENTRIES)
{
itemOn = oldItemOn;
return false;
}
itemOn = 0;
}
else
itemOn++;
} while (oldItemOn != itemOn && (currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_SPACE);
M_UpdateMenuBGImage(false);
M_FlipKartGamemodeMenu(true);
return true;
}
boolean M_PrevOpt(void)
{
INT16 oldItemOn = itemOn; // prevent infinite loop
if ((currentMenu->menuitems[itemOn].status & IT_CVARTYPE) == IT_CV_PASSWORD)
(currentMenu->menuitems[itemOn].itemaction.cvar)->value = 0;
do
{
if (!itemOn)
{
// Prevent looparound here
if (currentMenu->behaviourflags & MBF_NOLOOPENTRIES)
{
itemOn = oldItemOn;
return false;
}
itemOn = currentMenu->numitems - 1;
}
else
itemOn--;
} while (oldItemOn != itemOn && (currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_SPACE);
M_UpdateMenuBGImage(false);
M_FlipKartGamemodeMenu(true);
return true;
}
static boolean M_GamestateCanOpenMenu(void)
{
switch (gamestate)
{
case GS_INTRO:
case GS_CUTSCENE:
case GS_CREDITS:
case GS_EVALUATION:
case GS_CEREMONY:
return false;
default:
return true;
}
}
//
// M_Responder
//
boolean M_Responder(event_t *ev)
{
if (ev->type == ev_keydown && !ev->data2)
{
extern consvar_t cv_showhud;
switch (ev->data1)
{
case KEY_F3: // Toggle HUD
// I am lazy so this button is also
// hardcoded.
CV_SetValue(&cv_showhud, !cv_showhud.value);
return true;
case KEY_F11: // Fullscreen
// F11 can always be used to toggle
// fullscreen, it's a safe key.
CV_AddValue(&cv_fullscreen, 1);
return true;
}
}
if (dedicated
|| (demo.playback && demo.attract)
|| M_GamestateCanOpenMenu() == false)
{
return false;
}
if (noFurtherInput)
{
// Ignore input after enter/escape/other buttons
// (but still allow shift keyup so caps doesn't get stuck)
return false;
}
if (gamestate == GS_MENU && ev->type == ev_gamepad_device_removed && G_GetPlayerForDevice(ev->device) != -1)
{
int i;
INT32 player = G_GetPlayerForDevice(ev->device);
// Unassign all controllers
for (i = 0; i < MAXSPLITSCREENPLAYERS; i++)
{
G_SetDeviceForPlayer(i, -1);
}
// Return to the title because a controller was removed at the menu.
CONS_Alert(CONS_NOTICE, "Player %d's assigned gamepad was removed. Returning to the title screen.", player);
D_StartTitle();
}
if (ev->type == ev_keydown && ev->data1 > 0 && ev->data1 < NUMKEYS)
{
// Record keyboard presses
menuKey = ev->data1;
}
// Profiles: Control mapping.
// We take the WHOLE EVENT for convenience.
if (optionsmenu.bindtimer)
{
M_MapProfileControl(ev);
return true; // eat events.
}
// event handler for MM_EVENTHANDLER
/*if (menumessage.active && menumessage.flags == MM_EVENTHANDLER && menumessage.routine)
{
CONS_Printf("MM_EVENTHANDLER...\n");
menumessage.eroutine(ev); // What a terrible hack...
return true;
}*/
// Handle menu handling in-game.
if (menuactive == false)
{
noFurtherInput = true;
#if 0
// The Fx keys.
switch (menuKey)
{
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;
case KEY_F5: // Video Mode
if (modeattacking)
return true;
M_StartControlPanel();
M_Options(0);
M_VideoModeMenu(0);
return true;
case KEY_F6: // Empty
return true;
case KEY_F7: // Options
if (modeattacking)
return true;
M_StartControlPanel();
M_Options(0);
M_SetupNextMenu(&OP_MainDef, false);
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
}
#endif
if (CON_Ready() == false)
{
boolean allowmpause = true;
// Special mid-game input behaviours
if (Playing() && !demo.playback)
{
// Quick Retry (Y in modeattacking)
if (modeattacking && G_PlayerInputDown(0, gc_bail, splitscreen + 1) == true)
{
if (!blockreset)
{
M_TryAgain(0);
return true;
}
}
// Quick Spectate (L+R+A+Start online)
if (G_GametypeHasSpectators())
{
UINT8 workingpid = 0;
for (workingpid = 0; workingpid <= splitscreen; workingpid++)
{
if (players[g_localplayers[workingpid]].spectator == true)
continue;
if (G_PlayerInputDown(workingpid, gc_l, 0) == false)
continue;
if (G_PlayerInputDown(workingpid, gc_r, 0) == false)
continue;
if (G_PlayerInputDown(workingpid, gc_a, 0) == false)
continue;
if (G_PlayerInputDown(workingpid, gc_start, 0) == false)
continue;
if (workingpid == 0)
{
allowmpause = false;
COM_ImmedExecute("changeteam spectator");
continue;
}
COM_ImmedExecute(
va(
"changeteam%u spectator",
workingpid + 1
)
);
}
}
}
// Bog-standard Pause
if (allowmpause && G_PlayerInputDown(0, gc_start, splitscreen + 1) == true)
{
if (!chat_on)
{
M_StartControlPanel();
return true;
}
}
}
noFurtherInput = false; // turns out we didn't care
return false;
}
// We're in the menu itself now.
// M_Ticker will take care of the rest.
return true;
}
#define NotCurrentlyPlaying(desiredname) strcmp(desiredname, Music_CurrentSong())
void M_PlayMenuJam(void)
{
menu_t *refMenu = (menuactive ? currentMenu : restoreMenu);
static boolean musicstatepermitted = false;
if (challengesmenu.pending)
{
Music_StopAll();
musicstatepermitted = true;
return;
}
const boolean trulystarted = M_GameTrulyStarted();
const boolean profilemode = (
optionsmenu.profilemenu
&& !optionsmenu.resetprofilemenu
);
if (!profilemode && Playing())
{
if (optionsmenu.resetprofilemenu)
Music_Stop("menu");
return;
}
// trulystarted == false in the Tutorial.
// But profile menu music should play during the Tutorial (Playing()).
if (!trulystarted && !Playing())
{
if (M_GonerMusicPlayable() && NotCurrentlyPlaying("_GONER"))
{
Music_Remap("menu", "_GONER");
Music_Play("menu");
}
return;
}
gdmusic_t override = musicstatepermitted ? gamedata->musicstate : 0;
if (refMenu != NULL && refMenu->music != NULL)
{
if (refMenu->music[0] == '.' && refMenu->music[1] == '\0')
{
Music_StopAll();
return;
}
else if (override == 0)
{
if (NotCurrentlyPlaying(refMenu->music))
{
Music_Remap("menu", refMenu->music);
Music_Play("menu");
}
return;
}
}
if (override != 0)
{
// See also gdmusic_t
const char* overridetotrack[GDMUSIC_MAX-1] = {
"KEYGEN",
"LOSERC",
"TRACKA",
};
if (refMenu != NULL && NotCurrentlyPlaying(overridetotrack[override - 1]))
{
Music_Remap("menu", overridetotrack[override - 1]);
Music_Play("menu");
if (override < GDMUSIC_KEEPONMENU)
gamedata->musicstate = GDMUSIC_NONE;
}
return;
}
if (!profilemode && cv_menujam_update.value)
{
CV_AddValue(&cv_menujam, 1);
CV_SetValue(&cv_menujam_update, 0);
}
if (!NotCurrentlyPlaying(cv_menujam.string))
return;
Music_Remap("menu", cv_menujam.string);
Music_Play("menu");
}
#undef IsCurrentlyPlaying
boolean M_ConsiderSealedSwapAlert(void)
{
if (gamedata->sealedswapalerted == true)
return false;
if (gamedata->sealedswaps[GDMAX_SEALEDSWAPS-1] != NULL // all found
|| M_SecretUnlocked(SECRET_SPECIALATTACK, true)) // true order
{
gamedata->sealedswapalerted = true;
// Don't make a message if no Sealed Stars have yet been found.
if (gamedata->everseenspecial == false)
return false;
M_StartMessage(
"Message from the Stars",
"As if called by fate, the Emeralds you've\n"
"collected return to their rightful places...\n"
"\n"
"The Sealed Stars are now ordered via Cups!\n",
NULL, MM_NOTHING, NULL, NULL
);
if (gamedata->musicstate < GDMUSIC_TRACK10)
gamedata->musicstate = GDMUSIC_TRACK10;
return true;
}
return false;
}
void M_ValidateRestoreMenu(void)
{
if (restoreMenu == NULL || (restoreMenu->behaviourflags & MBF_CANTRESTORE))
restoreMenu = &MainDef;
}
//
// M_SpecificMenuRestore
//
menu_t *M_SpecificMenuRestore(menu_t *torestore)
{
// I'd advise the following not be a switch case because they're pointers...
if (torestore == &PLAY_CupSelectDef
|| torestore == &PLAY_LevelSelectDef
|| torestore == &PLAY_TimeAttackDef
|| torestore == &PLAY_TAReplayDef)
{
// Handle unlock restrictions
levellist = restorelevellist;
cupheader_t *currentcup = levellist.levelsearch.cup;
if (levellist.levelsearch.tutorial)
{
M_InitExtras(-1);
}
else
{
M_SetupGametypeMenu(-1);
if (levellist.newgametype == GT_RACE)
{
M_SetupRaceMenu(-1);
M_SetupDifficultyOptions((levellist.levelsearch.grandprix == false));
}
}
if (!M_LevelListFromGametype(-1))
{
torestore = levellist.backMenu;
}
else
{
if (currentcup != NULL && levellist.levelsearch.cup == NULL)
{
torestore = &PLAY_CupSelectDef;
}
else if (levellist.levelsearch.timeattack)
{
M_PrepareTimeAttack(true);
}
}
}
else if (torestore == &PLAY_MP_OptSelectDef)
{
// Ticker init
M_MPOptSelectInit(-1);
}
else if (torestore == &EXTRAS_MainDef)
{
// Disable or enable certain options
M_InitExtras(-1);
}
// One last catch.
M_SetupPlayMenu(-1);
PLAY_CharSelectDef.prevMenu = &MainDef;
if (torestore != &MISC_ChallengesDef)
{
M_ConsiderSealedSwapAlert();
}
return torestore;
}
//
// M_StartControlPanel
//
void M_StartControlPanel(void)
{
if (demo.playback && gamestate == GS_INTERMISSION)
{
// At this point the replay has ended.
// The only menu option that works is "Stop Playback".
// And intermission can be finished by pressing the
// A button, so having a menu at all is useless.
return;
}
INT32 i;
G_ResetAllDeviceGameKeyDown();
memset(menucmd, 0, sizeof (menucmd));
for (i = 0; i < MAXSPLITSCREENPLAYERS; i++)
{
menucmd[i].delay = MENUDELAYTIME;
}
// intro might call this repeatedly
if (mapchangepending || (menuactive && gamestate != GS_NULL))
{
CON_ToggleOff(); // move away console
return;
}
if (gamestate == GS_TITLESCREEN && restoreMenu == NULL) // Set up menu state
{
// No instantly skipping the titlescreen.
// (We can change this timer later when extra animation is added.)
if (finalecount < (
M_GameTrulyStarted()
? 1
: 3*TICRATE
)
)
{
return;
}
if (menumessage.active)
{
if (!menumessage.closing && menumessage.fadetimer == 9)
{
// The following doesn't work with MM_YESNO.
// However, because there's no guarantee a profile
// is selected or controls set up to our liking,
// we can't call M_HandleMenuMessage.
M_StopMessage(MA_NONE);
}
return;
}
}
menuactive = true;
if (demo.playback)
{
currentMenu = &PAUSE_PlaybackMenuDef;
}
else if (!Playing())
{
if (gamestate != GS_MENU)
{
if (titlemapinaction)
{
// We clear a LITTLE bit of state, but not a full D_ClearState.
// Just enough to guarantee SV_ResetServer is called before session start.
SV_StopServer();
SV_ResetServer();
}
G_SetGamestate(GS_MENU);
gameaction = ga_nothing;
paused = false;
CON_ToggleOff();
modeattacking = ATTACKING_NONE;
}
Music_Stop("title");
if (gamedata != NULL
&& gamedata->gonerlevel < GDGONER_OUTRO
&& M_GameAboutToStart())
{
gamedata->gonerlevel = GDGONER_OUTRO;
M_GonerBGImplyPassageOfTime();
}
if (M_GameTrulyStarted() == false)
{
// Are you ready for the First Boot Experience?
M_ResetOptions();
currentMenu = &MAIN_GonerAccessibilityDef;
restoreMenu = NULL;
M_PlayMenuJam();
}
else if (cv_currprofile.value == -1) // Only ask once per session.
{
// Make sure the profile data is ready now since we need to select a profile.
M_ResetOptions();
// we need to do this before setting ApplyProfile otherwise funky things are going to happen.
currentMenu = &MAIN_ProfilesDef;
optionsmenu.profilen = cv_ttlprofilen.value;
// options don't need initializing here.
// make sure we don't overstep that.
const INT32 maxp = PR_GetNumProfiles();
if (optionsmenu.profilen > maxp)
optionsmenu.profilen = maxp;
else if (optionsmenu.profilen < 0)
optionsmenu.profilen = 0;
currentMenu->lastOn = 0;
CV_StealthSetValue(&cv_currprofile, -1); // Make sure to reset that as it is set by PR_ApplyProfile which we kind of hack together to force it.
// Ambient ocean sounds
Music_Remap("menu_nocred", "_OCEAN");
Music_Play("menu_nocred");
}
else
{
M_ValidateRestoreMenu();
currentMenu = M_SpecificMenuRestore(M_InterruptMenuWithChallenges(restoreMenu));
restoreMenu = NULL;
M_PlayMenuJam();
}
itemOn = currentMenu->lastOn;
M_UpdateMenuBGImage(true);
#ifdef HAVE_DISCORDRPC
// currentMenu changed during GS_MENU
DRPC_UpdatePresence();
#endif
}
else
{
M_OpenPauseMenu();
}
CON_ToggleOff(); // move away console
}
//
// M_ClearMenus
//
void M_ClearMenus(boolean callexitmenufunc)
{
if (!menuactive)
return;
CON_ClearHUD();
if (currentMenu->quitroutine && callexitmenufunc && !currentMenu->quitroutine())
return; // we can't quit this menu (also used to set parameter from the menu)
currentMenu->lastOn = itemOn;
if (gamestate == GS_MENU) // Back to title screen
{
int i;
for (i = 0; i < MAXSPLITSCREENPLAYERS; i++)
{
G_SetDeviceForPlayer(i, -1);
}
D_StartTitle();
}
M_AbortVirtualKeyboard();
menumessage.active = false;
menuactive = false;
}
void M_ClearMenusNoTitle(boolean callexitmenufunc)
{
if (!menuactive)
return;
CON_ClearHUD();
if (currentMenu->quitroutine && callexitmenufunc && !currentMenu->quitroutine())
return; // we can't quit this menu (also used to set parameter from the menu)
currentMenu->lastOn = itemOn;
M_AbortVirtualKeyboard();
menumessage.active = false;
menuactive = false;
}
void M_SelectableClearMenus(INT32 choice)
{
(void)choice;
M_ClearMenus(true);
}
//
// M_SetupNextMenu
//
void M_SetupNextMenu(menu_t *menudef, boolean notransition)
{
INT16 i;
if (!notransition)
{
if (currentMenu->transitionID == menudef->transitionID
&& currentMenu->transitionTics)
{
menutransition.startmenu = currentMenu;
menutransition.endmenu = menudef;
menutransition.tics = 0;
menutransition.dest = currentMenu->transitionTics;
menutransition.in = false;
return; // Don't change menu yet, the transition will call this again
}
else if (gamestate == GS_MENU)
{
menuwipe = true;
M_FlipKartGamemodeMenu(false);
F_WipeStartScreen();
V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
F_WipeEndScreen();
F_RunWipe(wipe_menu_toblack, wipedefs[wipe_menu_toblack], false, "FADEMAP0", false, false);
}
}
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)
}
if (menudef->initroutine != NULL
#if 0
&& currentMenu != menudef // Unsure if we need this...
#endif
)
{
// Moving to a new menu, reinitialize.
menudef->initroutine();
}
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;
}
}
}
M_UpdateMenuBGImage(false);
M_PlayMenuJam();
#ifdef HAVE_DISCORDRPC
if (gamestate == GS_MENU)
{
// currentMenu changed during GS_MENU
DRPC_UpdatePresence();
}
#endif
}
void M_GoBack(INT32 choice)
{
const INT16 behaviourflags = currentMenu->behaviourflags;
(void)choice;
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;
}
M_SetupNextMenu(currentMenu->prevMenu, false);
}
else if (Playing() || M_GameTrulyStarted())
M_ClearMenus(true);
else // No returning to the title screen.
M_QuitSRB2(-1);
if (!(behaviourflags & MBF_SOUNDLESS))
S_StartSound(NULL, sfx_s3k5b);
}
//
// M_Ticker
//
void M_SetMenuDelay(UINT8 i)
{
menucmd[i].delayCount++;
if (menucmd[i].delayCount < 1)
{
// Shouldn't happen, but for safety.
menucmd[i].delayCount = 1;
}
menucmd[i].delay = (MENUDELAYTIME - min(MENUDELAYTIME - 1, menucmd[i].delayCount));
if (menucmd[i].delay < MENUMINDELAY)
{
menucmd[i].delay = MENUMINDELAY;
}
}
void M_UpdateMenuCMD(UINT8 i, boolean bailrequired, boolean chat_open)
{
UINT8 mp = max(1, setup_numplayers);
menucmd[i].prev_dpad_ud = menucmd[i].dpad_ud;
menucmd[i].prev_dpad_lr = menucmd[i].dpad_lr;
menucmd[i].dpad_ud = 0;
menucmd[i].dpad_lr = 0;
menucmd[i].buttonsHeld = menucmd[i].buttons;
menucmd[i].buttons = 0;
// Eat inputs made when chat is open
if (chat_open && pausemenu.closing)
return;
if (G_PlayerInputDown(i, gc_screenshot, mp)) { menucmd[i].buttons |= MBT_SCREENSHOT; }
if (G_PlayerInputDown(i, gc_startmovie, mp)) { menucmd[i].buttons |= MBT_STARTMOVIE; }
if (G_PlayerInputDown(i, gc_startlossless, mp)) { menucmd[i].buttons |= MBT_STARTLOSSLESS; }
// Screenshot et al take priority
if (menucmd[i].buttons != 0)
return;
if (G_PlayerInputDown(i, gc_up, mp)) { menucmd[i].dpad_ud--; }
if (G_PlayerInputDown(i, gc_down, mp)) { menucmd[i].dpad_ud++; }
if (G_PlayerInputDown(i, gc_left, mp)) { menucmd[i].dpad_lr--; }
if (G_PlayerInputDown(i, gc_right, mp)) { menucmd[i].dpad_lr++; }
if (G_PlayerInputDown(i, gc_a, mp)) { menucmd[i].buttons |= MBT_A; }
if (G_PlayerInputDown(i, gc_b, mp)) { menucmd[i].buttons |= MBT_B; }
if (G_PlayerInputDown(i, gc_c, mp)) { menucmd[i].buttons |= MBT_C; }
if (G_PlayerInputDown(i, gc_x, mp)) { menucmd[i].buttons |= MBT_X; }
if (G_PlayerInputDown(i, gc_y, mp)) { menucmd[i].buttons |= MBT_Y; }
if (G_PlayerInputDown(i, gc_z, mp)) { menucmd[i].buttons |= MBT_Z; }
if (G_PlayerInputDown(i, gc_l, mp)) { menucmd[i].buttons |= MBT_L; }
if (G_PlayerInputDown(i, gc_r, mp)) { menucmd[i].buttons |= MBT_R; }
if (G_PlayerInputDown(i, gc_start, mp)) { menucmd[i].buttons |= MBT_START; }
if (bailrequired && i == 0)
{
if (G_GetDeviceGameKeyDownArray(0)[KEY_ESCAPE]) { menucmd[i].buttons |= MBT_B; }
}
if (menucmd[i].dpad_ud == 0 && menucmd[i].dpad_lr == 0 && menucmd[i].buttons == 0)
{
// Reset delay count with no buttons.
menucmd[i].delay = min(menucmd[i].delay, MENUMINDELAY);
menucmd[i].delayCount = 0;
}
}
boolean M_MenuButtonPressed(UINT8 pid, UINT32 bt)
{
if (menucmd[pid].buttonsHeld & bt)
{
return false;
}
return !!(menucmd[pid].buttons & bt);
}
boolean M_MenuButtonHeld(UINT8 pid, UINT32 bt)
{
return !!(menucmd[pid].buttons & bt);
}
// Returns true if we press the confirmation button
boolean M_MenuConfirmPressed(UINT8 pid)
{
return M_MenuButtonPressed(pid, MBT_A);
}
boolean M_MenuConfirmHeld(UINT8 pid)
{
return M_MenuButtonHeld(pid, MBT_A);
}
// Returns true if we press the Cancel button
boolean M_MenuBackPressed(UINT8 pid)
{
return (M_MenuButtonPressed(pid, MBT_B) || M_MenuButtonPressed(pid, MBT_X));
}
boolean M_MenuBackHeld(UINT8 pid)
{
return (M_MenuButtonHeld(pid, MBT_B) || M_MenuButtonHeld(pid, MBT_X));
}
// Retrurns true if we press the tertiary option button (C)
boolean M_MenuExtraPressed(UINT8 pid)
{
return M_MenuButtonPressed(pid, MBT_C);
}
boolean M_MenuExtraHeld(UINT8 pid)
{
return M_MenuButtonHeld(pid, MBT_C);
}
static void M_HandleMenuInput(void)
{
void (*routine)(INT32 choice); // for some casting problem
UINT8 pid = 0; // todo: Add ability for any splitscreen player to bring up the menu.
SINT8 lr = 0, ud = 0;
INT32 thisMenuKey = menuKey;
menuKey = -1;
if (menuactive == false)
{
// We're not in the menu.
return;
}
if (menumessage.active)
{
M_HandleMenuMessage();
return;
}
// Typing for CV_IT_STRING
if (menutyping.active)
{
M_MenuTypingInput(-1);
return;
}
if (menucmd[pid].delay > 0)
{
return;
}
// Handle menu-specific input handling. If this returns true, we skip regular input handling.
if (currentMenu->inputroutine)
{
if (currentMenu->inputroutine(thisMenuKey))
{
return;
}
}
routine = currentMenu->menuitems[itemOn].itemaction.routine;
// Handle menuitems which need a specific key handling
if (routine && (currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_KEYHANDLER)
{
routine(-1);
return;
}
// 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)
{
// Routine is null either way
routine = NULL;
// If we're hovering over a IT_CV_STRING option, pressing A/X opens the typing submenu
if (M_MenuConfirmPressed(pid))
{
// If we entered this menu by pressing a menu Key, default to keyboard typing, otherwise use controller.
M_OpenVirtualKeyboard(
(currentMenu->menuitems[itemOn].itemaction.cvar == &cv_dummyprofilename) ? 6 // this sucks, but there's no time.
: MAXSTRINGLENGTH,
M_QueryCvarAction,
NULL
);
return;
}
else if (M_MenuExtraPressed(pid))
{
if (!(currentMenu->behaviourflags & MBF_SOUNDLESS))
S_StartSound(NULL, sfx_s3k5b);
M_ChangeCvar(-1);
M_SetMenuDelay(pid);
return;
}
}
else
{
routine = M_ChangeCvar;
}
}
lr = menucmd[pid].dpad_lr;
ud = menucmd[pid].dpad_ud;
if (currentMenu->behaviourflags & MBF_UD_LR_FLIPPED)
{
ud = menucmd[pid].dpad_lr;
lr = -menucmd[pid].dpad_ud;
}
// LR does nothing in the default menu, just remap as dpad.
if (menucmd[pid].buttons & MBT_L) { lr--; }
if (menucmd[pid].buttons & MBT_R) { lr++; }
// Keys usable within menu
if (ud > 0)
{
if (M_NextOpt() && !(currentMenu->behaviourflags & MBF_SOUNDLESS))
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
return;
}
else if (ud < 0)
{
if (M_PrevOpt() && !(currentMenu->behaviourflags & MBF_SOUNDLESS))
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
return;
}
else if (lr < 0)
{
if (routine && ((currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_ARROWS
|| (currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_CVAR))
{
if (!(currentMenu->behaviourflags & MBF_SOUNDLESS))
S_StartSound(NULL, sfx_s3k5b);
routine(0);
M_SetMenuDelay(pid);
}
return;
}
else if (lr > 0)
{
if (routine && ((currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_ARROWS
|| (currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_CVAR))
{
if (!(currentMenu->behaviourflags & MBF_SOUNDLESS))
S_StartSound(NULL, sfx_s3k5b);
routine(1);
M_SetMenuDelay(pid);
}
return;
}
else if (M_MenuConfirmPressed(pid) /*|| M_MenuButtonPressed(pid, MBT_START)*/)
{
noFurtherInput = true;
currentMenu->lastOn = itemOn;
if (routine)
{
if (!(currentMenu->behaviourflags & MBF_SOUNDLESS))
S_StartSound(NULL, sfx_s3k5b);
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)
{
M_StartMessage("Modified Game", M_GetText("This cannot be done with complex addons\nor in a cheated game."), NULL, MM_NOTHING, NULL, NULL);
return;
}
}
switch (currentMenu->menuitems[itemOn].status & IT_TYPE)
{
case IT_CVAR:
case IT_ARROWS:
routine(2); // usually right arrow
break;
case IT_CALL:
routine(itemOn);
break;
case IT_SUBMENU:
currentMenu->lastOn = itemOn;
M_SetupNextMenu((menu_t *)currentMenu->menuitems[itemOn].itemaction.submenu, false);
break;
}
}
M_SetMenuDelay(pid);
return;
}
else if (M_MenuBackPressed(pid))
{
M_GoBack(0);
M_SetMenuDelay(pid);
return;
}
else if (M_MenuExtraPressed(pid))
{
if (routine && ((currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_ARROWS
|| (currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_CVAR))
{
/*consvar_t *cv = currentMenu->menuitems[itemOn].itemaction.cvar;
// Make these CVar options?
if (cv == &cv_chooseskin
|| cv == &cv_dummystaff
|| cv == &cv_nextmap
|| cv == &cv_newgametype
)
{
return;
}*/
if (!(currentMenu->behaviourflags & MBF_SOUNDLESS))
S_StartSound(NULL, sfx_s3k5b);
routine(-1);
M_SetMenuDelay(pid);
return;
}
return;
}
return;
}
void M_Ticker(void)
{
INT32 i;
HU_TickSongCredits();
if (!menuactive)
{
noFurtherInput = false;
return;
}
if (menutransition.tics != 0 || menutransition.dest != 0)
{
noFurtherInput = true;
if (menutransition.tics < menutransition.dest)
menutransition.tics++;
else if (menutransition.tics > menutransition.dest)
menutransition.tics--;
// If dest is non-zero, we've started transition and want to switch menus
// If dest is zero, we're mid-transition and want to end it
if (menutransition.tics == menutransition.dest)
{
if (menutransition.endmenu != NULL
&& currentMenu != menutransition.endmenu)
{
if (menutransition.startmenu->transitionID == menutransition.endmenu->transitionID
&& menutransition.endmenu->transitionTics)
{
menutransition.tics = menutransition.endmenu->transitionTics;
menutransition.dest = 0;
menutransition.in = true;
}
else if (gamestate == GS_MENU)
{
memset(&menutransition, 0, sizeof(menutransition));
menuwipe = true;
F_WipeStartScreen();
V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
F_WipeEndScreen();
F_RunWipe(wipe_menu_toblack, wipedefs[wipe_menu_toblack], false, "FADEMAP0", false, false);
}
M_SetupNextMenu(menutransition.endmenu, true);
}
else
{
// Menu is done transitioning in
M_FlipKartGamemodeMenu(true);
}
}
}
else
{
if (menuwipe)
{
// try not to let people input during the fadeout
noFurtherInput = true;
}
else
{
// reset input trigger
noFurtherInput = false;
}
}
for (i = 0; i < MAXSPLITSCREENPLAYERS; i++)
{
if (menucmd[i].delay > 0)
{
menucmd[i].delay--;
}
}
if (noFurtherInput == false)
{
M_HandleMenuInput();
}
if (currentMenu->tickroutine)
{
currentMenu->tickroutine();
}
if (dedicated)
{
return;
}
if (--skullAnimCounter <= 0)
{
skullAnimCounter = 8;
}
if (!Playing())
{
// Anything in M_Ticker that isn't actively playing is considered "in menus" for time tracking
gamedata->totalmenutime++;
}
if (!Playing() && !M_GameTrulyStarted())
{
M_GonerBGTick();
}
#if 0
if (currentMenu == &PAUSE_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;
}
#endif
}
//
// M_Init
//
void M_Init(void)
{
#if 0
CV_RegisterVar(&cv_nextmap);
#endif
if (dedicated)
return;
//COM_AddCommand("manual", Command_Manual_f);
M_UpdateMenuBGImage(true);
}