Introducing Marathon Run. (I was going to call it Marathon Mode, but NiGHTS Mode being right next to it on the menu looked terrible.)

Basically a dedicated Record Attack-like experience for speedrunning the game as a continuous chunk rather than ILs. Has several quality of life features.

Benefits include:
* An unambiguous real-time bar across the bottom of the screen, always displaying the current time, ticking up until you reach the ending.
* Disable the console (pausing is still allowed, but the timer will still increment).
* Automatically skip intermissions as if you're holding down the spin button.
* Show centiseconds on HUD automatically, like record attack.
* "Live Event Backups" - a category of run fit for major events like GDQ, where recovery from crashes or chokes makes for better entertainment. Essentially a modified SP savefile, down to using the same basic functions, but has its own filename and tweaked internal layout.
* "spmarathon_start" MainCfg block parameter and "marathonnext" mapheader parameter, allowing for a customised flow (makes this fit for purpose for an eventual SUGOI port).
* Disabling inter-level custom cutscenes by default with a menu option to toggle this (won't show up if the mod doesn't *have* any custom cutscenes), although either way ending cutscenes (vanilla or custom) remain intact since is time is called before them.
* Won't show up if you have a mod that consists of only one level (determined by spmarathon_start's nextlevel; this won't trip if you manually set its marathonnext).
* Unconditional gratitude on the evaluation screen, instead of a negging "Try again..." if you didn't get all the emeralds (which you may not have been aiming for).
* Gorgeous new menu (no new assets required, unless you wanna give it a header later).

Changes which were required for the above but affect other areas of the game include:
* "useBlackRock" MainCFG block parameter, which can be used to disable the presence of the Black Rock or Egg Rock in both the Evaluation screen and the Marathon Run menu (for total conversions with different stories).
* Disabling Continues in NiGHTS mode, to match the most common singleplayer experience post 2.2.4's release (is reverted if useContinues is set to true).
* Hiding the exitmove "powerup" outside of multiplayer. (Okay, this isn't really related, I just saw this bug in action a lot while doing test runs and got annoyed enough to fix it here.)
* The ability to use V_DrawPromptBack (in hardcode only at the moment, but) to draw in terms of pixels rather than rows of text, by providing negative instead of positive inputs).
* A refactoring of redundant game saves smattered across the ending, credits, and evaluation - in addition to saving the game slightly earlier.
* Minor m_menu.c touchups and refactorings here and there.

Built using feedback from the official server's #speedruns channel, among other places.
This commit is contained in:
toaster 2020-05-14 23:10:00 +01:00
parent feced5ec3c
commit d593e2e1bb
22 changed files with 692 additions and 96 deletions

View File

@ -770,7 +770,7 @@ boolean CON_Responder(event_t *ev)
// check for console toggle key
if (ev->type != ev_console)
{
if (modeattacking || metalrecording)
if (modeattacking || metalrecording || marathonmode)
return false;
if (key == gamecontrol[gc_console][0] || key == gamecontrol[gc_console][1])

View File

@ -815,6 +815,7 @@ void D_StartTitle(void)
// In case someone exits out at the same time they start a time attack run,
// reset modeattacking
modeattacking = ATTACKING_NONE;
marathonmode = 0;
// empty maptol so mario/etc sounds don't play in sound test when they shouldn't
maptol = 0;
@ -1131,6 +1132,7 @@ void D_SRB2Main(void)
// default savegame
strcpy(savegamename, SAVEGAMENAME"%u.ssg");
strcpy(liveeventbackup,"liveevent.bkp"); // intentionally not ending with .ssg
{
const char *userhome = D_Home(); //Alam: path to home
@ -1159,6 +1161,7 @@ void D_SRB2Main(void)
// can't use sprintf since there is %u in savegamename
strcatbf(savegamename, srb2home, PATHSEP);
strcatbf(liveeventbackup, srb2home, PATHSEP);
snprintf(luafiledir, sizeof luafiledir, "%s" PATHSEP "luafiles", srb2home);
#else // DEFAULTDIR
@ -1171,6 +1174,7 @@ void D_SRB2Main(void)
// can't use sprintf since there is %u in savegamename
strcatbf(savegamename, userhome, PATHSEP);
strcatbf(liveeventbackup, userhome, PATHSEP);
snprintf(luafiledir, sizeof luafiledir, "%s" PATHSEP "luafiles", userhome);
#endif // DEFAULTDIR

View File

@ -1578,6 +1578,22 @@ static void readlevelheader(MYFILE *f, INT32 num)
mapheaderinfo[num-1]->nextlevel = (INT16)i;
}
else if (fastcmp(word, "MARATHONNEXT"))
{
if (fastcmp(word2, "TITLE")) i = 1100;
else if (fastcmp(word2, "EVALUATION")) i = 1101;
else if (fastcmp(word2, "CREDITS")) i = 1102;
else if (fastcmp(word2, "ENDING")) i = 1103;
else
// Support using the actual map name,
// i.e., MarathonNext = AB, MarathonNext = FZ, etc.
// Convert to map number
if (word2[0] >= 'A' && word2[0] <= 'Z' && word2[2] == '\0')
i = M_MapNumber(word2[0], word2[1]);
mapheaderinfo[num-1]->marathonnext = (INT16)i;
}
else if (fastcmp(word, "TYPEOFLEVEL"))
{
if (i) // it's just a number
@ -3920,7 +3936,20 @@ static void readmaincfg(MYFILE *f)
else
value = get_number(word2);
spstage_start = (INT16)value;
spstage_start = spmarathon_start = (INT16)value;
}
else if (fastcmp(word, "SPMARATHON_START"))
{
// Support using the actual map name,
// i.e., Level AB, Level FZ, etc.
// Convert to map number
if (word2[0] >= 'A' && word2[0] <= 'Z')
value = M_MapNumber(word2[0], word2[1]);
else
value = get_number(word2);
spmarathon_start = (INT16)value;
}
else if (fastcmp(word, "SSTAGE_START"))
{
@ -4014,6 +4043,17 @@ static void readmaincfg(MYFILE *f)
introtoplay = 128;
introchanged = true;
}
else if (fastcmp(word, "CREDITSCUTSCENE"))
{
creditscutscene = (UINT8)get_number(word2);
// range check, you morons.
if (creditscutscene > 128)
creditscutscene = 128;
}
else if (fastcmp(word, "USEBLACKROCK"))
{
useBlackRock = (UINT8)(value || word2[0] == 'T' || word2[0] == 'Y');
}
else if (fastcmp(word, "LOOPTITLE"))
{
looptitle = (value || word2[0] == 'T' || word2[0] == 'Y');
@ -4115,13 +4155,6 @@ static void readmaincfg(MYFILE *f)
titlescrollyspeed = get_number(word2);
titlechanged = true;
}
else if (fastcmp(word, "CREDITSCUTSCENE"))
{
creditscutscene = (UINT8)get_number(word2);
// range check, you morons.
if (creditscutscene > 128)
creditscutscene = 128;
}
else if (fastcmp(word, "DISABLESPEEDADJUST"))
{
disableSpeedAdjust = (value || word2[0] == 'T' || word2[0] == 'Y');
@ -9895,6 +9928,11 @@ struct {
{"TC_BLINK",TC_BLINK},
{"TC_DASHMODE",TC_DASHMODE},
// marathonmode flags
//{"MA_INIT",MA_INIT}, -- should never see this
{"MA_RUNNING",MA_RUNNING},
{"MA_NOCUTSCENES",MA_NOCUTSCENES},
{NULL,0}
};

View File

@ -101,6 +101,9 @@ void I_FinishUpdate (void)
if (cv_showping.value && netgame && consoleplayer != serverplayer)
SCR_DisplayLocalPing();
if (marathonmode)
SCR_DisplayMarathonInfo();
//blast it to the screen
// this code sucks
//memcpy(dascreen,screens[0],screenwidth*screenheight);

View File

@ -459,6 +459,7 @@ void CONS_Debug(INT32 debugflags, const char *fmt, ...) FUNCDEBUG;
// Things that used to be in dstrings.h
#define SAVEGAMENAME "srb2sav"
char savegamename[256];
char liveeventbackup[256];
// m_misc.h
#ifdef GETTEXT

View File

@ -45,7 +45,18 @@ extern INT32 curWeather;
extern INT32 cursaveslot;
//extern INT16 lastmapsaved;
extern INT16 lastmaploaded;
extern boolean gamecomplete;
extern UINT8 gamecomplete;
// Extra abilities/settings for skins (combinable stuff)
typedef enum
{
MA_RUNNING = 1, // In action
MA_INIT = 1<<1, // Initialisation
MA_NOCUTSCENES = 1<<2 // No cutscenes
} marathonmode_t;
extern marathonmode_t marathonmode;
extern tic_t marathontime;
#define maxgameovers 13
extern UINT8 numgameovers;
@ -127,7 +138,7 @@ extern INT32 displayplayer;
extern INT32 secondarydisplayplayer; // for splitscreen
// Maps of special importance
extern INT16 spstage_start;
extern INT16 spstage_start, spmarathon_start;
extern INT16 sstage_start, sstage_end, smpstage_start, smpstage_end;
extern INT16 titlemap;
@ -289,6 +300,7 @@ typedef struct
UINT8 actnum; ///< Act number or 0 for none.
UINT32 typeoflevel; ///< Combination of typeoflevel flags.
INT16 nextlevel; ///< Map number of next level, or 1100-1102 to end.
INT16 marathonnext; ///< See nextlevel, but for Marathon mode. Necessary to support hub worlds ala SUGOI.
char keywords[33]; ///< Keywords separated by space to search for. 32 characters.
char musname[7]; ///< Music track to play. "" for no music.
UINT16 mustrack; ///< Subsong to play. Only really relevant for music modules and specific formats supported by GME. 0 to ignore.
@ -575,11 +587,12 @@ extern UINT16 nightslinktics;
extern UINT8 introtoplay;
extern UINT8 creditscutscene;
extern UINT8 useBlackRock;
extern UINT8 use1upSound;
extern UINT8 maxXtraLife; // Max extra lives from rings
extern UINT8 useContinues;
#define continuesInSession (!multiplayer && (useContinues || ultimatemode || !(cursaveslot > 0)))
#define continuesInSession (!multiplayer && (ultimatemode || (useContinues && !marathonmode) || (!modeattacking && !(cursaveslot > 0))))
extern mobj_t *hunt1, *hunt2, *hunt3; // Emerald hunt locations

View File

@ -1330,10 +1330,6 @@ void F_StartCredits(void)
// Just in case they're open ... somehow
M_ClearMenus(true);
// Save the second we enter the credits
if ((!modifiedgame || savemoddata) && !(netgame || multiplayer) && cursaveslot > 0)
G_SaveGame((UINT32)cursaveslot);
if (creditscutscene)
{
F_StartCustomCutscene(creditscutscene - 1, false, false);
@ -1529,12 +1525,6 @@ void F_StartGameEvaluation(void)
// Just in case they're open ... somehow
M_ClearMenus(true);
// Save the second we enter the evaluation
// We need to do this again! Remember, it's possible a mod designed skipped
// the credits sequence!
if ((!modifiedgame || savemoddata) && !(netgame || multiplayer) && cursaveslot > 0)
G_SaveGame((UINT32)cursaveslot);
goodending = (ALL7EMERALDS(emeralds));
gameaction = ga_nothing;
@ -1551,13 +1541,20 @@ void F_GameEvaluationDrawer(void)
angle_t fa;
INT32 eemeralds_cur;
char patchname[7] = "CEMGx0";
const char* endingtext = (goodending ? "CONGRATULATIONS!" : "TRY AGAIN...");
const char* endingtext;
if (marathonmode)
endingtext = "THANKS FOR THE RUN!";
else if (goodending)
endingtext = "CONGRATULATIONS!";
else
endingtext = "TRY AGAIN...";
V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
// Draw all the good crap here.
if (finalecount > 0)
if (finalecount > 0 && useBlackRock)
{
INT32 scale = FRACUNIT;
patch_t *rockpat;
@ -1841,10 +1838,6 @@ void F_StartEnding(void)
// Just in case they're open ... somehow
M_ClearMenus(true);
// Save before the credits sequence.
if ((!modifiedgame || savemoddata) && !(netgame || multiplayer) && cursaveslot > 0)
G_SaveGame((UINT32)cursaveslot);
gameaction = ga_nothing;
paused = false;
CON_ToggleOff();
@ -3959,6 +3952,7 @@ static void F_AdvanceToNextScene(void)
animtimer = pictime = cutscenes[cutnum]->scene[scenenum].picduration[picnum];
}
// See also G_AfterIntermission, the only other place which handles intra-map/ending transitions
void F_EndCutScene(void)
{
cutsceneover = true; // do this first, just in case G_EndGame or something wants to turn it back false later

View File

@ -82,7 +82,10 @@ INT32 curWeather = PRECIP_NONE;
INT32 cursaveslot = 0; // Auto-save 1p savegame slot
//INT16 lastmapsaved = 0; // Last map we auto-saved at
INT16 lastmaploaded = 0; // Last map the game loaded
boolean gamecomplete = false;
UINT8 gamecomplete = 0;
marathonmode_t marathonmode = 0;
tic_t marathontime = 0;
UINT8 numgameovers = 0; // for startinglives balance
SINT8 startinglivesbalance[maxgameovers+1] = {3, 5, 7, 9, 12, 15, 20, 25, 30, 40, 50, 75, 99, 0x7F};
@ -118,7 +121,7 @@ UINT32 ssspheres; // old special stage
INT16 lastmap; // last level you were at (returning from special stages)
tic_t timeinmap; // Ticker for time spent in level (used for levelcard display)
INT16 spstage_start;
INT16 spstage_start, spmarathon_start;
INT16 sstage_start, sstage_end, smpstage_start, smpstage_end;
INT16 titlemap = 0;
@ -223,6 +226,7 @@ UINT8 useContinues = 0; // Set to 1 to enable continues outside of no-save scena
UINT8 introtoplay;
UINT8 creditscutscene;
UINT8 useBlackRock = 1;
// Emerald locations
mobj_t *hunt1;
@ -769,6 +773,8 @@ void G_SetGameModified(boolean silent)
// If in record attack recording, cancel it.
if (modeattacking)
M_EndModeAttackRun();
else if (marathonmode)
Command_ExitGame_f();
}
/** Builds an original game map name from a map number.
@ -3662,8 +3668,14 @@ static void G_DoCompleted(void)
// nextmap is 0-based, unlike gamemap
if (nextmapoverride != 0)
nextmap = (INT16)(nextmapoverride-1);
else if (marathonmode && mapheaderinfo[gamemap-1]->marathonnext)
nextmap = (INT16)(mapheaderinfo[gamemap-1]->marathonnext-1);
else
{
nextmap = (INT16)(mapheaderinfo[gamemap-1]->nextlevel-1);
if (marathonmode && nextmap == spmarathon_start-1)
nextmap = 1100-1; // No infinite loop for you
}
// Remember last map for when you come out of the special stage.
if (!spec)
@ -3687,10 +3699,12 @@ static void G_DoCompleted(void)
visitedmap[cm/8] |= (1<<(cm&7));
if (!mapheaderinfo[cm])
cm = -1; // guarantee error execution
else if (marathonmode && mapheaderinfo[cm]->marathonnext)
cm = (INT16)(mapheaderinfo[cm]->marathonnext-1);
else
cm = (INT16)(mapheaderinfo[cm]->nextlevel-1);
if (cm >= NUMMAPS || cm < 0) // out of range (either 1100-1102 or error)
if (cm >= NUMMAPS || cm < 0) // out of range (either 1100ish or error)
{
cm = nextmap; //Start the loop again so that the error checking below is executed.
@ -3759,6 +3773,25 @@ static void G_DoCompleted(void)
if (nextmap < NUMMAPS && !mapheaderinfo[nextmap])
P_AllocMapHeader(nextmap);
// do this before going to the intermission or starting a custom cutscene, mostly for the sake of marathon mode but it also massively reduces redundant file save events in f_finale.c
if (nextmap >= 1100-1)
{
if (!gamecomplete)
gamecomplete = 2; // special temporary mode to prevent using SP level select in pause menu until the intermission is over without restricting it in every intermission
if (cursaveslot > 0)
{
if (marathonmode)
{
// don't keep a backup around when the run is done!
if (FIL_FileExists(liveeventbackup))
remove(liveeventbackup);
cursaveslot = 0;
}
else if ((!modifiedgame || savemoddata) && !(netgame || multiplayer))
G_SaveGame((UINT32)cursaveslot);
}
}
if ((skipstats && !modeattacking) || (spec && modeattacking && stagefailed))
{
G_UpdateVisited();
@ -3772,6 +3805,7 @@ static void G_DoCompleted(void)
}
}
// See also F_EndCutscene, the only other place which handles intra-map/ending transitions
void G_AfterIntermission(void)
{
Y_CleanupScreenBuffer();
@ -3782,9 +3816,12 @@ void G_AfterIntermission(void)
return;
}
if (gamecomplete == 2) // special temporary mode to prevent using SP level select in pause menu until the intermission is over without restricting it in every intermission
gamecomplete = 1;
HU_ClearCEcho();
if ((gametyperules & GTR_CUTSCENES) && mapheaderinfo[gamemap-1]->cutscenenum && !modeattacking && skipstats <= 1) // Start a custom cutscene.
if ((gametyperules & GTR_CUTSCENES) && mapheaderinfo[gamemap-1]->cutscenenum && !modeattacking && skipstats <= 1 && !(marathonmode & MA_NOCUTSCENES)) // Start a custom cutscene.
F_StartCustomCutscene(mapheaderinfo[gamemap-1]->cutscenenum-1, false, false);
else
{
@ -3925,7 +3962,7 @@ void G_EndGame(void)
void G_LoadGameSettings(void)
{
// defaults
spstage_start = 1;
spstage_start = spmarathon_start = 1;
sstage_start = 50;
sstage_end = 56; // 7 special stages in vanilla SRB2
sstage_end++; // plus one weirdo
@ -4245,7 +4282,10 @@ void G_LoadGame(UINT32 slot, INT16 mapoverride)
startonmapnum = mapoverride;
#endif
sprintf(savename, savegamename, slot);
if (marathonmode)
strcpy(savename, liveeventbackup);
else
sprintf(savename, savegamename, slot);
length = FIL_ReadFile(savename, &savebuffer);
if (!length)
@ -4257,7 +4297,7 @@ void G_LoadGame(UINT32 slot, INT16 mapoverride)
save_p = savebuffer;
memset(vcheck, 0, sizeof (vcheck));
sprintf(vcheck, "version %d", VERSION);
sprintf(vcheck, (marathonmode ? "back-up %d" : "version %d"), VERSION);
if (strcmp((const char *)save_p, (const char *)vcheck))
{
#ifdef SAVEGAME_OTHERVERSIONS
@ -4297,6 +4337,11 @@ void G_LoadGame(UINT32 slot, INT16 mapoverride)
memset(&savedata, 0, sizeof(savedata));
return;
}
if (marathonmode)
{
marathontime = READUINT32(save_p);
marathonmode |= READUINT8(save_p);
}
// done
Z_Free(savebuffer);
@ -4325,13 +4370,12 @@ void G_SaveGame(UINT32 slot)
char savename[256] = "";
const char *backup;
sprintf(savename, savegamename, slot);
if (marathonmode)
strcpy(savename, liveeventbackup);
else
sprintf(savename, savegamename, slot);
backup = va("%s",savename);
// save during evaluation or credits? game's over, folks!
if (gamestate == GS_ENDING || gamestate == GS_CREDITS || gamestate == GS_EVALUATION)
gamecomplete = true;
gameaction = ga_nothing;
{
char name[VERSIONSIZE];
@ -4345,10 +4389,15 @@ void G_SaveGame(UINT32 slot)
}
memset(name, 0, sizeof (name));
sprintf(name, "version %d", VERSION);
sprintf(name, (marathonmode ? "back-up %d" : "version %d"), VERSION);
WRITEMEM(save_p, name, VERSIONSIZE);
P_SaveGame();
if (marathonmode)
{
WRITEUINT32(save_p, marathontime);
WRITEUINT8(save_p, (marathonmode & ~MA_INIT));
}
length = save_p - savebuffer;
saved = FIL_WriteFile(backup, savebuffer, length);
@ -4361,7 +4410,7 @@ void G_SaveGame(UINT32 slot)
if (cv_debug && saved)
CONS_Printf(M_GetText("Game saved.\n"));
else if (!saved)
CONS_Alert(CONS_ERROR, M_GetText("Error while writing to %s for save slot %u, base: %s\n"), backup, slot, savegamename);
CONS_Alert(CONS_ERROR, M_GetText("Error while writing to %s for save slot %u, base: %s\n"), backup, slot, (marathonmode ? liveeventbackup : savegamename));
}
#define BADSAVE goto cleanup;
@ -4374,7 +4423,10 @@ void G_SaveGameOver(UINT32 slot, boolean modifylives)
char savename[255];
const char *backup;
sprintf(savename, savegamename, slot);
if (marathonmode)
strcpy(savename, liveeventbackup);
else
sprintf(savename, savegamename, slot);
backup = va("%s",savename);
length = FIL_ReadFile(savename, &savebuffer);
@ -4393,7 +4445,7 @@ void G_SaveGameOver(UINT32 slot, boolean modifylives)
save_p = savebuffer;
// Version check
memset(vcheck, 0, sizeof (vcheck));
sprintf(vcheck, "version %d", VERSION);
sprintf(vcheck, (marathonmode ? "back-up %d" : "version %d"), VERSION);
if (strcmp((const char *)save_p, (const char *)vcheck)) BADSAVE
save_p += VERSIONSIZE;
@ -4460,7 +4512,7 @@ cleanup:
if (cv_debug && saved)
CONS_Printf(M_GetText("Game saved.\n"));
else if (!saved)
CONS_Alert(CONS_ERROR, M_GetText("Error while writing to %s for save slot %u, base: %s\n"), backup, slot, savegamename);
CONS_Alert(CONS_ERROR, M_GetText("Error while writing to %s for save slot %u, base: %s\n"), backup, slot, (marathonmode ? liveeventbackup : savegamename));
Z_Free(savebuffer);
save_p = savebuffer = NULL;
@ -4598,7 +4650,7 @@ void G_InitNew(UINT8 pultmode, const char *mapname, boolean resetplayer, boolean
automapactive = false;
imcontinuing = false;
if ((gametyperules & GTR_CUTSCENES) && !skipprecutscene && mapheaderinfo[gamemap-1]->precutscenenum && !modeattacking) // Start a custom cutscene.
if ((gametyperules & GTR_CUTSCENES) && !skipprecutscene && mapheaderinfo[gamemap-1]->precutscenenum && !modeattacking && !(marathonmode & MA_NOCUTSCENES)) // Start a custom cutscene.
F_StartCustomCutscene(mapheaderinfo[gamemap-1]->precutscenenum-1, true, resetplayer);
else
G_DoLoadLevel(resetplayer);

View File

@ -904,7 +904,11 @@ void HWR_DrawTutorialBack(UINT32 color, INT32 boxheight)
{
FOutVector v[4];
FSurfaceInfo Surf;
INT32 height = (boxheight * 4) + (boxheight/2)*5; // 4 lines of space plus gaps between and some leeway
INT32 height;
if (boxheight < 0)
height = -boxheight;
else
height = (boxheight * 4) + (boxheight/2)*5; // 4 lines of space plus gaps between and some leeway
// setup some neat-o translucency effect

View File

@ -2009,6 +2009,8 @@ static int mapheaderinfo_get(lua_State *L)
lua_pushinteger(L, header->typeoflevel);
else if (fastcmp(field,"nextlevel"))
lua_pushinteger(L, header->nextlevel);
else if (fastcmp(field,"marathonnext"))
lua_pushinteger(L, header->marathonnext);
else if (fastcmp(field,"keywords"))
lua_pushstring(L, header->keywords);
else if (fastcmp(field,"musname"))

View File

@ -115,7 +115,10 @@ int LUA_PushGlobals(lua_State *L, const char *word)
lua_pushboolean(L, splitscreen);
return 1;
} else if (fastcmp(word,"gamecomplete")) {
lua_pushboolean(L, gamecomplete);
lua_pushboolean(L, (gamecomplete != 0));
return 1;
} else if (fastcmp(word,"marathonmode")) {
lua_pushinteger(L, marathonmode);
return 1;
} else if (fastcmp(word,"devparm")) {
lua_pushboolean(L, devparm);
@ -145,6 +148,9 @@ int LUA_PushGlobals(lua_State *L, const char *word)
} else if (fastcmp(word,"spstage_start")) {
lua_pushinteger(L, spstage_start);
return 1;
} else if (fastcmp(word,"spmarathon_start")) {
lua_pushinteger(L, spmarathon_start);
return 1;
} else if (fastcmp(word,"sstage_start")) {
lua_pushinteger(L, sstage_start);
return 1;

View File

@ -182,6 +182,7 @@ static tic_t keydown = 0;
static void M_GoBack(INT32 choice);
static void M_StopMessage(INT32 choice);
static boolean stopstopmessage = false;
#ifndef NONET
static void M_HandleServerPage(INT32 choice);
@ -252,6 +253,7 @@ static void M_ConfirmTeamScramble(INT32 choice);
static void M_ConfirmTeamChange(INT32 choice);
static void M_SecretsMenu(INT32 choice);
static void M_SetupChoosePlayer(INT32 choice);
static UINT8 M_SetupChoosePlayerDirect(INT32 choice);
static void M_QuitSRB2(INT32 choice);
menu_t SP_MainDef, OP_MainDef;
menu_t MISC_ScrambleTeamDef, MISC_ChangeTeamDef;
@ -272,9 +274,14 @@ static void M_ModeAttackEndGame(INT32 choice);
static void M_SetGuestReplay(INT32 choice);
static void M_HandleChoosePlayerMenu(INT32 choice);
static void M_ChoosePlayer(INT32 choice);
static void M_MarathonLiveEventBackup(INT32 choice);
static void M_Marathon(INT32 choice);
static void M_HandleMarathonChoosePlayer(INT32 choice);
static void M_StartMarathon(INT32 choice);
menu_t SP_LevelStatsDef;
static menu_t SP_TimeAttackDef, SP_ReplayDef, SP_GuestReplayDef, SP_GhostDef;
static menu_t SP_NightsAttackDef, SP_NightsReplayDef, SP_NightsGuestReplayDef, SP_NightsGhostDef;
static menu_t SP_MarathonDef;
// Multiplayer
static void M_SetupMultiPlayer(INT32 choice);
@ -354,6 +361,7 @@ static void M_DrawLoad(void);
static void M_DrawLevelStats(void);
static void M_DrawTimeAttackMenu(void);
static void M_DrawNightsAttackMenu(void);
static void M_DrawMarathon(void);
static void M_DrawSetupChoosePlayerMenu(void);
static void M_DrawControlsDefMenu(void);
static void M_DrawCameraOptionsMenu(void);
@ -476,6 +484,11 @@ static consvar_t cv_dummylives = {"dummylives", "0", CV_HIDEN, liveslimit_cons_t
static consvar_t cv_dummycontinues = {"dummycontinues", "0", CV_HIDEN, contlimit_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
static consvar_t cv_dummymares = {"dummymares", "Overall", CV_HIDEN|CV_CALL, dummymares_cons_t, Dummymares_OnChange, 0, NULL, NULL, 0, 0, NULL};
CV_PossibleValue_t marathon_cons_t[] = {{0, "Standard"}, {1, "Live Event Backup"}, {2, "Ultimate"}, {0, NULL}};
consvar_t cv_dummymarathon = {"dummymarathon", "Standard", CV_HIDEN, marathon_cons_t, NULL, 0, NULL, NULL, 0, 0, NULL};
consvar_t cv_dummycutscenes = {"dummycutscenes", "Off", CV_HIDEN, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
// ==========================================================================
// ORGANIZATION START.
// ==========================================================================
@ -746,10 +759,11 @@ static menuitem_t SR_EmblemHintMenu[] =
// Single Player Main
static menuitem_t SP_MainMenu[] =
{
{IT_CALL | IT_STRING, NULL, "Start Game", M_LoadGame, 84},
{IT_SECRET, NULL, "Record Attack", M_TimeAttack, 92},
{IT_SECRET, NULL, "NiGHTS Mode", M_NightsAttack, 100},
{IT_CALL | IT_STRING, NULL, "Tutorial", M_StartTutorial, 108},
{IT_CALL | IT_STRING, NULL, "Start Game", M_LoadGame, 76},
{IT_SECRET, NULL, "Record Attack", M_TimeAttack, 84},
{IT_SECRET, NULL, "NiGHTS Mode", M_NightsAttack, 92},
{IT_CALL | IT_STRING, NULL, "Tutorial", M_StartTutorial, 100},
{IT_CALL | IT_STRING | IT_CALL_NOTMODIFIED, NULL, "Marathon Run", M_Marathon, 108},
{IT_CALL | IT_STRING | IT_CALL_NOTMODIFIED, NULL, "Statistics", M_Statistics, 116}
};
@ -759,6 +773,7 @@ enum
sprecordattack,
spnightsmode,
sptutorial,
spmarathon,
spstatistics
};
@ -901,6 +916,23 @@ enum
nastart
};
// Marathon
static menuitem_t SP_MarathonMenu[] =
{
{IT_STRING|IT_KEYHANDLER, NULL, "Character", M_HandleMarathonChoosePlayer, 100},
{IT_STRING|IT_CVAR, NULL, "Category", &cv_dummymarathon, 110},
{IT_STRING|IT_CVAR, NULL, "Cutscenes", &cv_dummycutscenes, 120},
{IT_WHITESTRING|IT_CALL, NULL, "Start", M_StartMarathon, 130},
};
enum
{
marathonplayer,
marathonultimate,
marathoncutscenes,
marathonstart
};
// Statistics
static menuitem_t SP_LevelStatsMenu[] =
{
@ -1908,6 +1940,18 @@ static menu_t SP_NightsGhostDef =
NULL
};
static menu_t SP_MarathonDef =
{
MTREE2(MN_SP_MAIN, MN_SP_MARATHON),
"M_ATTACK", // temporary
sizeof(SP_MarathonMenu)/sizeof(menuitem_t),
&MainDef, // Doesn't matter.
SP_MarathonMenu,
M_DrawMarathon,
32, 40,
0,
NULL
};
menu_t SP_PlayerDef =
{
@ -2524,6 +2568,8 @@ void M_InitMenuPresTables(void)
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)
@ -3018,7 +3064,7 @@ static void M_GoBack(INT32 choice)
netgame = multiplayer = false;
}
if ((currentMenu->prevMenu == &MainDef) && (currentMenu == &SP_TimeAttackDef || currentMenu == &SP_NightsAttackDef))
if ((currentMenu->prevMenu == &MainDef) && (currentMenu == &SP_TimeAttackDef || currentMenu == &SP_NightsAttackDef || currentMenu == &SP_MarathonDef))
{
// D_StartTitle does its own wipe, since GS_TIMEATTACK is now a complete gamestate.
@ -3403,11 +3449,14 @@ boolean M_Responder(event_t *ev)
{
if (currentMenu->menuitems[itemOn].alphaKey != MM_EVENTHANDLER)
{
if (ch == ' ' || ch == 'n' || ch == 'y' || ch == KEY_ESCAPE || ch == KEY_ENTER)
if (ch == ' ' || ch == 'n' || ch == 'y' || ch == KEY_ESCAPE || ch == KEY_ENTER || ch == KEY_DEL)
{
if (routine)
routine(ch);
M_StopMessage(0);
if (stopstopmessage)
stopstopmessage = false;
else
M_StopMessage(0);
noFurtherInput = true;
return true;
}
@ -3657,7 +3706,7 @@ void M_StartControlPanel(void)
{
INT32 numlives = 2;
SPauseMenu[spause_pandora].status = (M_SecretUnlocked(SECRET_PANDORA)) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
SPauseMenu[spause_pandora].status = (M_SecretUnlocked(SECRET_PANDORA) && !marathonmode) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
if (&players[consoleplayer])
{
@ -3675,7 +3724,7 @@ void M_StartControlPanel(void)
}
// We can always use level select though. :33
SPauseMenu[spause_levelselect].status = (gamecomplete) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
SPauseMenu[spause_levelselect].status = (gamecomplete == 1) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
// And emblem hints.
SPauseMenu[spause_hints].status = (M_SecretUnlocked(SECRET_EMBLEMHINTS)) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
@ -3859,6 +3908,8 @@ void M_Init(void)
CV_RegisterVar(&cv_dummylives);
CV_RegisterVar(&cv_dummycontinues);
CV_RegisterVar(&cv_dummymares);
CV_RegisterVar(&cv_dummymarathon);
CV_RegisterVar(&cv_dummycutscenes);
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)");
@ -6321,8 +6372,8 @@ static char *M_AddonsHeaderPath(void)
static void M_AddonsClearName(INT32 choice)
{
(void)choice;
CLEARNAME;
M_StopMessage(choice);
}
// returns whether to do message draw
@ -6354,7 +6405,7 @@ static boolean M_AddonsRefresh(void)
if (message)
{
M_StartMessage(message,M_AddonsClearName,MM_EVENTHANDLER);
M_StartMessage(message,M_AddonsClearName,MM_NOTHING);
return true;
}
@ -8011,6 +8062,15 @@ static void M_SinglePlayerMenu(INT32 choice)
SP_MainMenu[sptutorial].status = tutorialmap ? IT_CALL|IT_STRING : IT_NOTHING|IT_DISABLED;
// If the FIRST stage immediately leads to the ending, or itself (which gets converted to the title screen in G_DoCompleted for marathonmode only), there's no point in having this option on the menu. You should use Record Attack in that circumstance, although if marathonnext is set this behaviour can be overridden if you make some weird mod that requires multiple playthroughs of the same map in sequence and has some in-level mechanism to break the cycle.
if (mapheaderinfo[spmarathon_start-1]
&& !mapheaderinfo[spmarathon_start-1]->marathonnext
&& (mapheaderinfo[spmarathon_start-1]->nextlevel == spmarathon_start
|| mapheaderinfo[spmarathon_start-1]->nextlevel >= 1100))
SP_MainMenu[spmarathon].status = IT_NOTHING|IT_DISABLED;
else
SP_MainMenu[spmarathon].status = IT_CALL|IT_STRING|IT_CALL_NOTMODIFIED;
M_SetupNextMenu(&SP_MainDef);
}
@ -8101,7 +8161,7 @@ static void M_StartTutorial(INT32 choice)
emeralds = 0;
memset(&luabanks, 0, sizeof(luabanks));
M_ClearMenus(true);
gamecomplete = false;
gamecomplete = 0;
cursaveslot = 0;
G_DeferedInitNew(false, G_BuildMapName(tutorialmap), 0, false, false);
}
@ -8864,7 +8924,7 @@ static void M_CacheCharacterSelect(void)
{
INT32 i, skinnum;
for (i = 0; i < 32; i++)
for (i = 0; i < MAXSKINS; i++)
{
if (!description[i].used)
continue;
@ -8876,7 +8936,7 @@ static void M_CacheCharacterSelect(void)
}
}
static void M_SetupChoosePlayer(INT32 choice)
static UINT8 M_SetupChoosePlayerDirect(INT32 choice)
{
INT32 skinnum;
UINT8 i;
@ -8887,7 +8947,7 @@ static void M_SetupChoosePlayer(INT32 choice)
if (!mapheaderinfo[startmap-1] || mapheaderinfo[startmap-1]->forcecharacter[0] == '\0')
{
for (i = 0; i < 32; i++) // Handle charsels, availability, and unlocks.
for (i = 0; i < MAXSKINS; i++) // Handle charsels, availability, and unlocks.
{
if (description[i].used) // If the character's disabled through SOC, there's nothing we can do for it.
{
@ -8895,7 +8955,7 @@ static void M_SetupChoosePlayer(INT32 choice)
if (and)
{
char firstskin[SKINNAMESIZE+1];
if (mapheaderinfo[startmap-1]->typeoflevel & TOL_NIGHTS) // skip tagteam characters for NiGHTS levels
if (mapheaderinfo[startmap-1] && mapheaderinfo[startmap-1]->typeoflevel & TOL_NIGHTS) // skip tagteam characters for NiGHTS levels
continue;
strncpy(firstskin, description[i].skinname, (and - description[i].skinname));
firstskin[(and - description[i].skinname)] = '\0';
@ -8932,14 +8992,36 @@ static void M_SetupChoosePlayer(INT32 choice)
if (firstvalid == lastvalid) // We're being forced into a specific character, so might as well just skip it.
{
M_ChoosePlayer(firstvalid);
return;
return firstvalid;
}
// One last bit of order we can't do in the iteration above.
description[firstvalid].prev = lastvalid;
description[lastvalid].next = firstvalid;
if (!allowed)
{
char_on = firstvalid;
if (startchar > 0 && startchar < MAXSKINS)
{
INT16 workchar = startchar;
while (workchar--)
char_on = description[char_on].next;
}
}
return MAXSKINS;
}
static void M_SetupChoosePlayer(INT32 choice)
{
UINT8 skinset = M_SetupChoosePlayerDirect(choice);
if (skinset != MAXSKINS)
{
M_ChoosePlayer(skinset);
return;
}
M_ChangeMenuMusic("_chsel", true);
/* the menus suck -James */
@ -8954,16 +9036,6 @@ static void M_SetupChoosePlayer(INT32 choice)
SP_PlayerDef.prevMenu = currentMenu;
M_SetupNextMenu(&SP_PlayerDef);
if (!allowed)
{
char_on = firstvalid;
if (startchar > 0 && startchar < 32)
{
INT16 workchar = startchar;
while (workchar--)
char_on = description[char_on].next;
}
}
// finish scrolling the menu
char_scroll = 0;
@ -9022,6 +9094,10 @@ static void M_HandleChoosePlayerMenu(INT32 choice)
case KEY_ENTER:
S_StartSound(NULL, sfx_menu1);
char_scroll = 0; // finish scrolling the menu
M_DrawSetupChoosePlayerMenu(); // draw the finally selected character one last time for the fadeout
// Is this a hack?
charseltimer = 0;
M_ChoosePlayer(char_on);
break;
@ -9261,7 +9337,7 @@ static void M_DrawSetupChoosePlayerMenu(void)
// Chose the player you want to use Tails 03-02-2002
static void M_ChoosePlayer(INT32 choice)
{
boolean ultmode = (ultimate_selectable && SP_PlayerDef.prevMenu == &SP_LoadDef && saveSlotSelected == NOSAVESLOT);
boolean ultmode = (currentMenu == &SP_MarathonDef) ? (cv_dummymarathon.value == 2) : (ultimate_selectable && SP_PlayerDef.prevMenu == &SP_LoadDef && saveSlotSelected == NOSAVESLOT);
UINT8 skinnum;
// skip this if forcecharacter or no characters available
@ -9273,11 +9349,6 @@ static void M_ChoosePlayer(INT32 choice)
// M_SetupChoosePlayer didn't call us directly, that means we've been properly set up.
else
{
char_scroll = 0; // finish scrolling the menu
M_DrawSetupChoosePlayerMenu(); // draw the finally selected character one last time for the fadeout
// Is this a hack?
charseltimer = 0;
skinnum = description[choice].skinnum[0];
if ((botingame = (description[choice].skinnum[1] != -1))) {
@ -9291,11 +9362,11 @@ static void M_ChoosePlayer(INT32 choice)
M_ClearMenus(true);
if (startmap != spstage_start)
if (!marathonmode && startmap != spstage_start)
cursaveslot = 0;
//lastmapsaved = 0;
gamecomplete = false;
gamecomplete = 0;
G_DeferedInitNew(ultmode, G_BuildMapName(startmap), skinnum, false, fromlevelselect);
COM_BufAddText("dummyconsvar 1\n"); // G_DeferedInitNew doesn't do this
@ -10294,6 +10365,364 @@ static void M_ModeAttackEndGame(INT32 choice)
Nextmap_OnChange();
}
static void M_MarathonLiveEventBackup(INT32 choice)
{
if (choice == 'y' || choice == KEY_ENTER)
{
marathonmode = MA_INIT;
G_LoadGame(MARATHONSLOT, 0);
cursaveslot = MARATHONSLOT;
if (!(marathonmode & MA_RUNNING))
marathonmode = 0;
return;
}
else if (choice == KEY_DEL)
{
M_StopMessage(0);
if (FIL_FileExists(liveeventbackup)) // just in case someone deleted it while we weren't looking.
remove(liveeventbackup);
BwehHehHe();
M_StartMessage("Live event backup erased.\n",M_Marathon,MM_NOTHING);
stopstopmessage = true;
return;
}
M_Marathon(-1);
}
// Going to Marathon menu...
static void M_Marathon(INT32 choice)
{
UINT8 skinset;
INT32 mapnum = 0;
if (choice != -1 && FIL_FileExists(liveeventbackup))
{
M_StartMessage(\
"\x82Live event backup detected.\n\x80\
Do you want to resurrect the last run?\n\
(Fs in chat if we crashed on stream.)\n\
\n\
Press 'Y' or 'Enter' to resume,\n\
'Del' to delete, or any other\n\
key to continue to Marathon Run.",M_MarathonLiveEventBackup,MM_YESNO);
return;
}
fromlevelselect = false;
startmap = spmarathon_start;
CV_SetValue(&cv_newgametype, GT_COOP); // Graue 09-08-2004
skinset = M_SetupChoosePlayerDirect(-1);
SP_MarathonMenu[marathonplayer].status = (skinset == MAXSKINS) ? IT_KEYHANDLER|IT_STRING : IT_NOTHING|IT_DISABLED;
while (mapnum < NUMMAPS)
{
if (mapheaderinfo[mapnum])
{
if (mapheaderinfo[mapnum]->cutscenenum || mapheaderinfo[mapnum]->precutscenenum)
break;
}
mapnum++;
}
SP_MarathonMenu[marathoncutscenes].status = (mapnum < NUMMAPS) ? IT_CVAR |IT_STRING : IT_NOTHING|IT_DISABLED;
M_ChangeMenuMusic("spec8", true);
SP_MarathonDef.prevMenu = &MainDef;
G_SetGamestate(GS_TIMEATTACK); // do this before M_SetupNextMenu so that menu meta state knows that we're switching
titlemapinaction = TITLEMAP_OFF; // Nope don't give us HOMs please
M_SetupNextMenu(&SP_MarathonDef);
itemOn = marathonstart; // "Start" is selected.
recatkdrawtimer = 50-8;
char_scroll = 0;
}
static void M_HandleMarathonChoosePlayer(INT32 choice)
{
INT32 selectval;
if (keydown > 1)
return;
switch (choice)
{
case KEY_DOWNARROW:
M_NextOpt();
break;
case KEY_UPARROW:
M_PrevOpt();
break;
case KEY_LEFTARROW:
if ((selectval = description[char_on].prev) == char_on)
return;
char_on = selectval;
break;
case KEY_RIGHTARROW:
if ((selectval = description[char_on].next) == char_on)
return;
char_on = selectval;
break;
case KEY_ESCAPE:
noFurtherInput = true;
M_GoBack(0);
return;
default:
return;
}
S_StartSound(NULL, sfx_menu1);
}
static void M_StartMarathon(INT32 choice)
{
(void)choice;
marathontime = 0;
marathonmode = MA_RUNNING|MA_INIT;
if (cv_dummymarathon.value == 1)
cursaveslot = MARATHONSLOT;
if (!cv_dummycutscenes.value)
marathonmode |= MA_NOCUTSCENES;
M_ChoosePlayer(char_on);
}
// Drawing function for Marathon menu
void M_DrawMarathon(void)
{
INT32 i, x, y, cursory = 0, cnt, soffset = 0, w;
UINT16 dispstatus;
consvar_t *cv;
const char *cvstring;
char *work;
angle_t fa;
INT32 dupz = (vid.dupx < vid.dupy ? vid.dupx : vid.dupy), xspan = (vid.width/dupz), yspan = (vid.height/dupz), diffx = (xspan - BASEVIDWIDTH)/2, diffy = (yspan - BASEVIDHEIGHT)/2, maxy = BASEVIDHEIGHT + diffy;
// lactozilla: the renderer changed so recache patches
if (needpatchrecache)
M_CacheCharacterSelect();
curbgxspeed = 0;
curbgyspeed = 18;
M_ChangeMenuMusic("spec8", true); // Eww, but needed for when user hits escape during demo playback
V_DrawFill(-diffx, -diffy, diffx+(BASEVIDWIDTH-190)/2, yspan, 158);
V_DrawFill((BASEVIDWIDTH-190)/2, -diffy, 190, yspan, 31);
V_DrawFill((BASEVIDWIDTH+190)/2, -diffy, diffx+(BASEVIDWIDTH-190)/2, yspan, 158);
//M_DrawRecordAttackForeground();
if (curfadevalue)
V_DrawFadeScreen(0xFF00, curfadevalue);
x = (((BASEVIDWIDTH-82)/2)+11)<<FRACBITS;
y = (((BASEVIDHEIGHT-82)/2)+12-10)<<FRACBITS;
cnt = (36*(recatkdrawtimer<<FRACBITS))/TICRATE;
fa = (FixedAngle(cnt)>>ANGLETOFINESHIFT) & FINEMASK;
y -= (10*FINECOSINE(fa));
recatkdrawtimer++;
soffset = cnt = (recatkdrawtimer%50);
if (!useBlackRock)
{
if (cnt > 8)
cnt = 8;
V_DrawFixedPatch(x+(6<<FRACBITS), y, FRACUNIT/2, (cnt&~1)<<(V_ALPHASHIFT-1), W_CachePatchName("RECCLOCK", PU_PATCH), NULL);
}
else if (cnt > 8)
{
cnt = 8;
V_DrawFixedPatch(x, y, FRACUNIT, V_TRANSLUCENT, W_CachePatchName("ENDEGRK5", PU_PATCH), NULL);
}
else
{
V_DrawFixedPatch(x, y, FRACUNIT, cnt<<V_ALPHASHIFT, W_CachePatchName("ROID0000", PU_PATCH), NULL);
V_DrawFixedPatch(x, y, FRACUNIT, V_TRANSLUCENT, W_CachePatchName("ENDEGRK5", PU_PATCH), NULL);
V_DrawFixedPatch(x, y, FRACUNIT, cnt<<V_ALPHASHIFT, W_CachePatchName("ENDEGRK0", PU_PATCH), NULL);
}
{
UINT8 col;
i = 0;
w = (((8-cnt)+1)/3)+1;
w *= w;
cursory = 0;
while (i < cnt)
{
i++;
col = 158+((cnt-i)/3);
if (col >= 160)
col = 253;
V_DrawFill(((BASEVIDWIDTH-190)/2)-cursory-w, -diffy, w, yspan, col);
V_DrawFill(((BASEVIDWIDTH+190)/2)+cursory, -diffy, w, yspan, col);
cursory += w;
w *= 2;
}
}
w = char_scroll + (((8-cnt)*(8-cnt))<<(FRACBITS-5));
if (soffset == 50-1)
w += FRACUNIT/2;
{
patch_t *fg = W_CachePatchName("RECATKFG", PU_PATCH);
INT32 trans = V_60TRANS+((cnt&~3)<<(V_ALPHASHIFT-2));
INT32 height = (SHORT(fg->height)/2);
char patchname[7] = "CEMGx0";
dupz = (w*7)/6; //(w*42*120)/(360*6); -- I don't know why this works but I'm not going to complain.
dupz = ((dupz>>FRACBITS) % height);
y = height/2;
while (y+dupz >= -diffy)
y -= height;
while (y-2-dupz < maxy)
{
V_DrawFixedPatch(((BASEVIDWIDTH-190)<<(FRACBITS-1)), (y-2-dupz)<<FRACBITS, FRACUNIT/2, trans, fg, NULL);
V_DrawFixedPatch(((BASEVIDWIDTH+190)<<(FRACBITS-1)), (y+dupz)<<FRACBITS, FRACUNIT/2, trans|V_FLIP, fg, NULL);
y += height;
}
trans = V_40TRANS+((cnt&~1)<<(V_ALPHASHIFT-1));
for (i = 0; i < 7; ++i)
{
fa = (FixedAngle(w)>>ANGLETOFINESHIFT) & FINEMASK;
x = (BASEVIDWIDTH<<(FRACBITS-1)) + (60*FINESINE(fa));
y = ((BASEVIDHEIGHT+16-20)<<(FRACBITS-1)) - (60*FINECOSINE(fa));
w += (360<<FRACBITS)/7;
patchname[4] = 'A'+(char)i;
V_DrawFixedPatch(x, y, FRACUNIT, trans, W_CachePatchName(patchname, PU_PATCH), NULL);
}
height = 18; // prevents the need for the next line
//dupz = (w*height)/18;
dupz = ((w>>FRACBITS) % height);
y = dupz+(height/4);
x = 105+dupz;
while (y >= -diffy)
{
x -= height;
y -= height;
}
while (y-dupz < maxy && x < (xspan/2))
{
V_DrawFill((BASEVIDWIDTH/2)-x-height, -diffy, height, diffy+y+height, 153);
V_DrawFill((BASEVIDWIDTH/2)+x, (maxy-y)-height, height, height+y, 153);
y += height;
x += height;
}
}
if (!soffset)
{
char_scroll += (360<<FRACBITS)/42; // like a clock, ticking at 42bpm!
if (char_scroll >= 360<<FRACBITS)
char_scroll -= 360<<FRACBITS;
if (recatkdrawtimer > (10*TICRATE))
recatkdrawtimer -= (10*TICRATE);
}
//M_DrawMenuTitle();
// 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;
dispstatus = (currentMenu->menuitems[marathonplayer].status & IT_DISPLAY);
if (dispstatus == IT_STRING || dispstatus == IT_WHITESTRING)
{
soffset = 68;
if (description[char_on].charpic->width >= 256)
V_DrawTinyScaledPatch(224, 120, 0, description[char_on].charpic);
else
V_DrawSmallScaledPatch(224, 120, 0, description[char_on].charpic);
}
else
soffset = 0;
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) ? V_YELLOWMAP : 0 , currentMenu->menuitems[i].text);
cv = NULL;
cvstring = NULL;
work = NULL;
if ((currentMenu->menuitems[i].status & IT_TYPE) == IT_CVAR)
{
cv = (consvar_t *)currentMenu->menuitems[i].itemaction;
cvstring = cv->string;
}
else if (i == marathonplayer)
{
if (description[char_on].displayname[0])
{
work = Z_StrDup(description[char_on].displayname);
cnt = 0;
while (work[cnt])
{
if (work[cnt] == '\n')
work[cnt] = ' ';
cnt++;
}
cvstring = work;
}
else
cvstring = description[char_on].skinname;
}
// Cvar specific handling
if (cvstring)
{
INT32 flags = V_YELLOWMAP;
if (cv == &cv_dummymarathon && cv->value == 2) // ultimate_selectable
flags = V_REDMAP;
// Should see nothing but strings
if (cv == &cv_dummymarathon && cv->value == 1)
{
w = V_ThinStringWidth(cvstring, 0);
V_DrawThinString(BASEVIDWIDTH - x - soffset - w, y+1, flags, cvstring);
}
else
{
w = V_StringWidth(cvstring, 0);
V_DrawString(BASEVIDWIDTH - x - soffset - w, y, flags, cvstring);
}
if (i == itemOn)
{
V_DrawCharacter(BASEVIDWIDTH - x - soffset - 10 - w - (skullAnimCounter/5), y,
'\x1C' | V_YELLOWMAP, false);
V_DrawCharacter(BASEVIDWIDTH - x - soffset + 2 + (skullAnimCounter/5), y,
'\x1D' | V_YELLOWMAP, false);
}
if (work)
Z_Free(work);
}
}
// DRAW THE SKULL CURSOR
V_DrawScaledPatch(currentMenu->x - 24, cursory, 0, W_CachePatchName("M_CURSOR", PU_PATCH));
V_DrawString(currentMenu->x, cursory, V_YELLOWMAP, currentMenu->menuitems[itemOn].text);
// Draw press ESC to exit string on main record attack menu
V_DrawString(104-72, 180, V_TRANSLUCENT, M_GetText("Press ESC to exit"));
}
// ========
// END GAME
// ========
@ -12578,6 +13007,7 @@ void M_QuitResponse(INT32 ch)
if (!(netgame || cv_debug))
{
S_ResetCaptions();
marathonmode = 0;
mrand = M_RandomKey(sizeof(quitsounds)/sizeof(INT32));
if (quitsounds[mrand]) S_StartSound(NULL, quitsounds[mrand]);

View File

@ -61,6 +61,8 @@ typedef enum
MN_SP_NIGHTS_REPLAY,
MN_SP_NIGHTS_GHOST,
MN_SP_MARATHON,
// Multiplayer
MN_MP_MAIN,
MN_MP_SPLITSCREEN, // SplitServer
@ -419,6 +421,7 @@ extern INT16 char_on, startchar;
#define MAXSAVEGAMES 31
#define NOSAVESLOT 0 //slot where Play Without Saving appears
#define MARATHONSLOT 420 // just has to be nonzero, but let's use one that'll show up as an obvious error if something goes wrong while not using our existing saves
#define BwehHehHe() S_StartSound(NULL, sfx_bewar1+M_RandomKey(4)) // Bweh heh he

View File

@ -3807,10 +3807,10 @@ static inline void P_UnArchiveSPGame(INT16 mapoverride)
if (mapoverride != 0)
{
gamemap = mapoverride;
gamecomplete = true;
gamecomplete = 1;
}
else
gamecomplete = false;
gamecomplete = 0;
// gamemap changed; we assume that its map header is always valid,
// so make it so

View File

@ -217,6 +217,7 @@ static void P_ClearSingleMapHeaderInfo(INT16 i)
mapheaderinfo[num]->actnum = 0;
mapheaderinfo[num]->typeoflevel = 0;
mapheaderinfo[num]->nextlevel = (INT16)(i + 1);
mapheaderinfo[num]->marathonnext = 0;
mapheaderinfo[num]->startrings = 0;
mapheaderinfo[num]->sstimer = 90;
mapheaderinfo[num]->ssspheres = 1;
@ -3181,7 +3182,7 @@ static boolean CanSaveLevel(INT32 mapnum)
// Any levels that have the savegame flag can save normally.
// If the game is complete for this save slot, then any level can save!
// On the other side of the spectrum, if lastmaploaded is 0, then the save file has only just been created and needs to save ASAP!
return (mapheaderinfo[mapnum-1]->levelflags & LF_SAVEGAME || gamecomplete || !lastmaploaded);
return (mapheaderinfo[mapnum-1]->levelflags & LF_SAVEGAME || (gamecomplete != 0) || marathonmode || !lastmaploaded);
}
static void P_RunSpecialStageWipe(void)

View File

@ -622,3 +622,35 @@ void SCR_ClosedCaptions(void)
va("%c [%s]", dot, (closedcaptions[i].s->caption[0] ? closedcaptions[i].s->caption : closedcaptions[i].s->name)));
}
}
void SCR_DisplayMarathonInfo(void)
{
INT32 flags = V_SNAPTOBOTTOM;
static tic_t entertic, oldentertics = 0;
const char *str;
#if 0 // eh, this probably isn't going to be a problem
if (((signed)marathontime) < 0)
{
flags |= V_REDMAP;
str = "No waiting out the clock to submit a bogus time.";
}
else
#endif
{
entertic = I_GetTime();
if (gamecomplete)
flags |= V_YELLOWMAP;
else if (marathonmode & MA_INIT)
marathonmode &= ~MA_INIT;
else
marathontime += entertic - oldentertics;
str = va("%i:%02i:%02i.%02i",
G_TicsToHours(marathontime),
G_TicsToMinutes(marathontime, false),
G_TicsToSeconds(marathontime),
G_TicsToCentiseconds(marathontime));
oldentertics = entertic;
}
V_DrawPromptBack(-8, cons_backcolor.value);
V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-8, flags, str);
}

View File

@ -206,5 +206,6 @@ FUNCMATH boolean SCR_IsAspectCorrect(INT32 width, INT32 height);
void SCR_DisplayTicRate(void);
void SCR_ClosedCaptions(void);
void SCR_DisplayLocalPing(void);
void SCR_DisplayMarathonInfo(void);
#undef DNWH
#endif //__SCREEN_H__

View File

@ -1214,6 +1214,9 @@ void I_FinishUpdate(void)
if (cv_showping.value && netgame && consoleplayer != serverplayer)
SCR_DisplayLocalPing();
if (marathonmode)
SCR_DisplayMarathonInfo();
if (rendermode == render_soft && screens[0])
{
SDL_Rect rect;

View File

@ -763,7 +763,7 @@ static void ST_drawTime(void)
ST_DrawPatchFromHud(HUD_TIMECOLON, sbocolon, V_HUDTRANS); // Colon
ST_DrawPadNumFromHud(HUD_SECONDS, seconds, 2, V_HUDTRANS); // Seconds
if (cv_timetic.value == 1 || cv_timetic.value == 2 || modeattacking) // there's not enough room for tics in splitscreen, don't even bother trying!
if (cv_timetic.value == 1 || cv_timetic.value == 2 || modeattacking || marathonmode)
{
ST_DrawPatchFromHud(HUD_TIMETICCOLON, sboperiod, V_HUDTRANS); // Period
ST_DrawPadNumFromHud(HUD_TICS, tictrn, 2, V_HUDTRANS); // Tics
@ -1445,7 +1445,7 @@ static void ST_drawPowerupHUD(void)
// ---------
// Let's have a power-like icon to represent finishing the level!
if (stplyr->pflags & PF_FINISHED && cv_exitmove.value)
if (stplyr->pflags & PF_FINISHED && cv_exitmove.value && multiplayer)
{
finishoffs[q] = ICONSEP;
V_DrawSmallScaledPatch(offs, hudinfo[HUD_POWERUPS].y, V_PERPLAYER|hudinfo[HUD_POWERUPS].f|V_HUDTRANS, fnshico);

View File

@ -1870,7 +1870,10 @@ void V_DrawPromptBack(INT32 boxheight, INT32 color)
if (color >= 256 && color < 512)
{
boxheight = ((boxheight * 4) + (boxheight/2)*5);
if (boxheight < 0)
boxheight = -boxheight;
else // 4 lines of space plus gaps between and some leeway
boxheight = ((boxheight * 4) + (boxheight/2)*5);
V_DrawFill((BASEVIDWIDTH-(vid.width/vid.dupx))/2, BASEVIDHEIGHT-boxheight, (vid.width/vid.dupx),boxheight, (color-256)|V_SNAPTOBOTTOM);
return;
}
@ -1917,8 +1920,11 @@ void V_DrawPromptBack(INT32 boxheight, INT32 color)
// heavily simplified -- we don't need to know x or y position,
// just the start and stop positions
deststop = screens[0] + vid.rowbytes * vid.height;
buf = deststop - vid.rowbytes * ((boxheight * 4) + (boxheight/2)*5); // 4 lines of space plus gaps between and some leeway
buf = deststop = screens[0] + vid.rowbytes * vid.height;
if (boxheight < 0)
buf += vid.rowbytes * boxheight;
else // 4 lines of space plus gaps between and some leeway
buf -= vid.rowbytes * ((boxheight * 4) + (boxheight/2)*5);
for (; buf < deststop; ++buf)
*buf = promptbgmap[*buf];
}

View File

@ -377,6 +377,9 @@ void I_FinishUpdate(void)
if (cv_showping.value && netgame && consoleplayer != serverplayer)
SCR_DisplayLocalPing();
if (marathonmode)
SCR_DisplayMarathonInfo();
//
if (bDIBMode)
{

View File

@ -392,7 +392,7 @@ dontdrawbg:
if (gottoken) // first to be behind everything else
Y_IntermissionTokenDrawer();
if (!splitscreen)
if (!splitscreen) // there's not enough room in splitscreen, don't even bother trying!
{
// draw score
ST_DrawPatchFromHud(HUD_SCORE, sboscore);
@ -414,7 +414,7 @@ dontdrawbg:
ST_DrawPatchFromHud(HUD_TIMECOLON, sbocolon); // Colon
ST_DrawPadNumFromHud(HUD_SECONDS, seconds, 2); // Seconds
if (cv_timetic.value == 1 || cv_timetic.value == 2 || modeattacking) // there's not enough room for tics in splitscreen, don't even bother trying!
if (cv_timetic.value == 1 || cv_timetic.value == 2 || modeattacking || marathonmode)
{
ST_DrawPatchFromHud(HUD_TIMETICCOLON, sboperiod); // Period
ST_DrawPadNumFromHud(HUD_TICS, tictrn, 2); // Tics
@ -1004,7 +1004,7 @@ void Y_Ticker(void)
{
INT32 i;
UINT32 oldscore = data.coop.score;
boolean skip = false;
boolean skip = (marathonmode) ? true : false;
boolean anybonuses = false;
if (!intertic) // first time only
@ -1080,7 +1080,7 @@ void Y_Ticker(void)
{
INT32 i;
UINT32 oldscore = data.spec.score;
boolean skip = false, super = false, anybonuses = false;
boolean skip = (marathonmode) ? true : false, super = false, anybonuses = false;
if (!intertic) // first time only
{