From d593e2e1bb9ff8614c49c69bc13bb1d3d50f924e Mon Sep 17 00:00:00 2001 From: toaster Date: Thu, 14 May 2020 23:10:00 +0100 Subject: [PATCH] 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. --- src/console.c | 2 +- src/d_main.c | 4 + src/dehacked.c | 54 ++++- src/djgppdos/i_video.c | 3 + src/doomdef.h | 1 + src/doomstat.h | 19 +- src/f_finale.c | 26 +-- src/g_game.c | 88 ++++++-- src/hardware/hw_draw.c | 6 +- src/lua_maplib.c | 2 + src/lua_script.c | 8 +- src/m_menu.c | 502 ++++++++++++++++++++++++++++++++++++++--- src/m_menu.h | 3 + src/p_saveg.c | 4 +- src/p_setup.c | 3 +- src/screen.c | 32 +++ src/screen.h | 1 + src/sdl/i_video.c | 3 + src/st_stuff.c | 4 +- src/v_video.c | 12 +- src/win32/win_vid.c | 3 + src/y_inter.c | 8 +- 22 files changed, 692 insertions(+), 96 deletions(-) diff --git a/src/console.c b/src/console.c index 0f1ccbd33..c99203e33 100644 --- a/src/console.c +++ b/src/console.c @@ -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]) diff --git a/src/d_main.c b/src/d_main.c index 07e15aec4..dd25596e1 100644 --- a/src/d_main.c +++ b/src/d_main.c @@ -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 diff --git a/src/dehacked.c b/src/dehacked.c index a6c73e0b4..a6ef8dae7 100644 --- a/src/dehacked.c +++ b/src/dehacked.c @@ -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} }; diff --git a/src/djgppdos/i_video.c b/src/djgppdos/i_video.c index f525b96ca..b3363a14e 100644 --- a/src/djgppdos/i_video.c +++ b/src/djgppdos/i_video.c @@ -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); diff --git a/src/doomdef.h b/src/doomdef.h index 6112881fb..d5f420e3e 100644 --- a/src/doomdef.h +++ b/src/doomdef.h @@ -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 diff --git a/src/doomstat.h b/src/doomstat.h index 1ec03a86c..7ef4c7f32 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -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 diff --git a/src/f_finale.c b/src/f_finale.c index 825f646b0..b7ff0f8f3 100644 --- a/src/f_finale.c +++ b/src/f_finale.c @@ -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 diff --git a/src/g_game.c b/src/g_game.c index 92d71fbae..5baa257d3 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -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); diff --git a/src/hardware/hw_draw.c b/src/hardware/hw_draw.c index d01331765..6a938a142 100644 --- a/src/hardware/hw_draw.c +++ b/src/hardware/hw_draw.c @@ -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 diff --git a/src/lua_maplib.c b/src/lua_maplib.c index ece42b8d3..a90b83c99 100644 --- a/src/lua_maplib.c +++ b/src/lua_maplib.c @@ -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")) diff --git a/src/lua_script.c b/src/lua_script.c index d2069e8a7..2007b9ad4 100644 --- a/src/lua_script.c +++ b/src/lua_script.c @@ -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; diff --git a/src/m_menu.c b/src/m_menu.c index 2977b432f..7a17fe302 100644 --- a/src/m_menu.c +++ b/src/m_menu.c @@ -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)<>ANGLETOFINESHIFT) & FINEMASK; + y -= (10*FINECOSINE(fa)); + + recatkdrawtimer++; + + soffset = cnt = (recatkdrawtimer%50); + if (!useBlackRock) + { + if (cnt > 8) + cnt = 8; + V_DrawFixedPatch(x+(6< 8) + { + cnt = 8; + V_DrawFixedPatch(x, y, FRACUNIT, V_TRANSLUCENT, W_CachePatchName("ENDEGRK5", PU_PATCH), NULL); + } + else + { + V_DrawFixedPatch(x, y, FRACUNIT, cnt<= 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)<>ANGLETOFINESHIFT) & FINEMASK; + x = (BASEVIDWIDTH<<(FRACBITS-1)) + (60*FINESINE(fa)); + y = ((BASEVIDHEIGHT+16-20)<<(FRACBITS-1)) - (60*FINECOSINE(fa)); + w += (360<>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<= 360< (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]); diff --git a/src/m_menu.h b/src/m_menu.h index 565b98945..c04ab5ab7 100644 --- a/src/m_menu.h +++ b/src/m_menu.h @@ -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 diff --git a/src/p_saveg.c b/src/p_saveg.c index 34bd3724b..77a44dc94 100644 --- a/src/p_saveg.c +++ b/src/p_saveg.c @@ -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 diff --git a/src/p_setup.c b/src/p_setup.c index b4a5f2c3c..46296f789 100644 --- a/src/p_setup.c +++ b/src/p_setup.c @@ -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) diff --git a/src/screen.c b/src/screen.c index e01d1a81a..44d98f024 100644 --- a/src/screen.c +++ b/src/screen.c @@ -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); +} diff --git a/src/screen.h b/src/screen.h index ddbd499bb..91ec175f4 100644 --- a/src/screen.h +++ b/src/screen.h @@ -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__ diff --git a/src/sdl/i_video.c b/src/sdl/i_video.c index ec7bfc1c4..f35769e3c 100644 --- a/src/sdl/i_video.c +++ b/src/sdl/i_video.c @@ -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; diff --git a/src/st_stuff.c b/src/st_stuff.c index b6226d085..12dcae360 100644 --- a/src/st_stuff.c +++ b/src/st_stuff.c @@ -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); diff --git a/src/v_video.c b/src/v_video.c index 3ce0e79f5..142e85c68 100644 --- a/src/v_video.c +++ b/src/v_video.c @@ -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]; } diff --git a/src/win32/win_vid.c b/src/win32/win_vid.c index 5fa219586..eccec10bc 100644 --- a/src/win32/win_vid.c +++ b/src/win32/win_vid.c @@ -377,6 +377,9 @@ void I_FinishUpdate(void) if (cv_showping.value && netgame && consoleplayer != serverplayer) SCR_DisplayLocalPing(); + if (marathonmode) + SCR_DisplayMarathonInfo(); + // if (bDIBMode) { diff --git a/src/y_inter.c b/src/y_inter.c index f1764a816..bfd8e7aa5 100644 --- a/src/y_inter.c +++ b/src/y_inter.c @@ -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 {