From b34a7cb66b8f8bd2c7e1d9e0333824e383589aea Mon Sep 17 00:00:00 2001 From: GenericHeroGuy Date: Fri, 28 Mar 2025 23:08:57 +0100 Subject: [PATCH 01/15] The Wipes Upgrade * All those fugly main loop copies are GONE! Replaced by one powerful loop * Chat, menus and console can now be used during wipes * Wipes are now interpolation-enabled * Screenshotting now works consistently --- src/d_main.cpp | 224 ++++++++++++++++++++++++++++++++++++++----------- src/d_main.h | 10 +++ src/f_finale.c | 29 +------ src/f_finale.h | 4 +- src/f_wipe.c | 59 ++----------- src/g_game.c | 25 +----- src/p_setup.c | 21 +---- src/typedef.h | 3 + 8 files changed, 199 insertions(+), 176 deletions(-) diff --git a/src/d_main.cpp b/src/d_main.cpp index 828f2023b..67f6f2e81 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -132,6 +132,8 @@ boolean devparm = false; // started game with -devparm boolean singletics = false; // timedemo boolean lastdraw = false; +static INT32 lastwipetic = 0; + INT32 postimgparam[MAXSPLITSCREENPLAYERS]; // These variables are in effect @@ -734,6 +736,178 @@ static bool D_Display(void) return ranwipe; } +static void D_WipeTick(boolean menu) +{ + I_OsPolling(); + D_ProcessEvents(); + + // Update the network so we don't cause timeouts + NetKeepAlive(); + + HU_Ticker(); + if (menu) + { +#ifdef HAVE_THREADS + I_lock_mutex(&m_menu_mutex); +#endif + M_Ticker(); +#ifdef HAVE_THREADS + I_unlock_mutex(m_menu_mutex); +#endif + } + CON_Ticker(); +} + +static void D_WipeDraw(boolean menu) +{ + if (rendermode == render_none) + return; + + I_UpdateNoBlit(); + + HU_Erase(); + HU_Drawer(); + if (menu) + { +#ifdef HAVE_THREADS + I_lock_mutex(&m_menu_mutex); +#endif + M_Drawer(); +#ifdef HAVE_THREADS + I_unlock_mutex(m_menu_mutex); +#endif + } + CON_Drawer(); + + I_FinishUpdate(); // page flip or blit buffer +} + +static double D_EndFrame(precise_t enterprecise, int *frameskip) +{ + // Fully completed frame made. + precise_t finishprecise = I_GetPreciseTime(); + + // Use the time before sleep for frameskip calculations: + // post-sleep time is literally being intentionally wasted + double deltasecs = (double)((INT64)(finishprecise - enterprecise)) / I_GetPrecisePrecision(); + double deltatics = deltasecs * NEWTICRATE; + + // If time spent this game loop exceeds a single tic, + // it's probably because of rendering. + // + // Skip rendering the next frame, up to a limit of 3 + // frames before a frame is rendered no matter what. + if (frameskip) + { + if (*frameskip < 3 && deltatics > 1.0) + *frameskip += 1; + else + *frameskip = 0; + } + + if (!singletics) + { + precise_t elapsed = finishprecise - enterprecise; + + // capbudget is the minimum precise_t duration of a single loop iteration + precise_t capbudget = round((1.0 / R_GetFramerateCap()) * I_GetPrecisePrecision()); + + // in the case of "match refresh rate" + vsync, don't sleep at all + const boolean vsync_with_match_refresh = cv_vidwait.value && cv_fpscap.value == 0; + + if (elapsed > 0 && capbudget > elapsed && !vsync_with_match_refresh) + { + I_SleepDuration(capbudget - (finishprecise - enterprecise)); + } + } + + // Capture the time once more to get the real delta time. + finishprecise = I_GetPreciseTime(); + deltasecs = (double)((INT64)(finishprecise - enterprecise)) / I_GetPrecisePrecision(); + return deltasecs * NEWTICRATE; +} + +static INT32 endtimes[] = { + [WIPELOOP_RUNWIPE] = UINT8_MAX, + [WIPELOOP_TITLECARD] = PRELEVELTIME*NEWTICRATERATIO, + [WIPELOOP_ENCORE] = 3*TICRATE/2, + [WIPELOOP_TITLEBLACK] = NEWTICRATE, // Shortened the quit time, used to be 2 seconds +}; + +// one single function for all the extra main loops in this god-forsaken codebase +// should make it easier to fold these back into D_SRB2Loop at some point... +void D_WipeLoop(wipelooptype_t type, UINT8 wipetype, boolean drawMenu) +{ + tic_t nowtime, endtime; + UINT8 wipeframe = 0; + fademask_t *fmask; + double delta = 0.0; + + nowtime = lastwipetic = I_GetTime(); + endtime = nowtime + endtimes[type]; + + if (gamestate != GS_TITLESCREEN && gamestate != GS_TIMEATTACK) + drawMenu = true; + + // lastwipetic should either be 0 or the tic we last wiped + // on for fade-to-black + while (nowtime < endtime) + { + precise_t enterprecise = I_GetPreciseTime(); + I_UpdateTime(cv_timescale.value); + + nowtime = I_GetTime(); + + // wait loop + if (nowtime - lastwipetic) + { + renderisnewtic = true; + wipeframe++; + D_WipeTick(drawMenu); + if (type == WIPELOOP_TITLECARD) + ST_runTitleCard(); + } + else + renderisnewtic = false; + + // draw loop + rendertimefrac = g_time.timefrac; + renderdeltatics = FLOAT_TO_FIXED(delta); + +#ifndef NOWIPE + if (type == WIPELOOP_RUNWIPE) + { + // get fademask first so we can tell if it exists or not + fmask = F_GetFadeMask(wipetype, wipeframe); + if (!fmask) + break; + +#ifdef HWRENDER + if (rendermode == render_opengl) + HWR_DoWipe(wipetype, wipeframe); // send in the wipe type and wipeframe because we need to cache the graphic + else +#endif + if (rendermode != render_none) //this allows F_RunWipe to be called in dedicated servers + F_DoWipe(fmask); + } +#endif + + if (type == WIPELOOP_TITLECARD) + ST_preLevelTitleCardDrawer(); + + D_WipeDraw(drawMenu); + + // Only take screenshots after drawing. + if (moviemode) + M_SaveFrame(); + if (takescreenshot) + M_DoScreenShot(); + + delta = D_EndFrame(enterprecise, NULL); + lastwipetic = nowtime; + } +} + // ========================================================================= // D_SRB2Loop // ========================================================================= @@ -744,7 +918,6 @@ void D_SRB2Loop(void) { tic_t entertic = 0, oldentertics = 0, realtics = 0, rendertimeout = INFTICS; double deltatics = 0.0; - double deltasecs = 0.0; boolean interp = false; boolean doDisplay = false; @@ -797,20 +970,11 @@ void D_SRB2Loop(void) for (;;) { - // capbudget is the minimum precise_t duration of a single loop iteration - precise_t capbudget; precise_t enterprecise = I_GetPreciseTime(); - precise_t finishprecise = enterprecise; g_dc = {}; Z_Frame_Reset(); - { - // Casting the return value of a function is bad practice (apparently) - double budget = round((1.0 / R_GetFramerateCap()) * I_GetPrecisePrecision()); - capbudget = (precise_t) budget; - } - bool ranwipe = false; I_UpdateTime(cv_timescale.value); @@ -965,47 +1129,11 @@ void D_SRB2Loop(void) } #endif - // Fully completed frame made. - finishprecise = I_GetPreciseTime(); - - // Use the time before sleep for frameskip calculations: - // post-sleep time is literally being intentionally wasted - deltasecs = (double)((INT64)(finishprecise - enterprecise)) / I_GetPrecisePrecision(); - deltatics = deltasecs * NEWTICRATE; - - // If time spent this game loop exceeds a single tic, - // it's probably because of rendering. - // - // Skip rendering the next frame, up to a limit of 3 - // frames before a frame is rendered no matter what. - // // Wipes run an inner loop and artificially increase // the measured time. - if (!ranwipe && frameskip < 3 && deltatics > 1.0) - { - frameskip++; - } - else - { + if (ranwipe) frameskip = 0; - } - - if (!singletics) - { - INT64 elapsed = (INT64)(finishprecise - enterprecise); - - // in the case of "match refresh rate" + vsync, don't sleep at all - const boolean vsync_with_match_refresh = cv_vidwait.value && cv_fpscap.value == 0; - - if (elapsed > 0 && (INT64)capbudget > elapsed && !vsync_with_match_refresh) - { - I_SleepDuration(capbudget - (finishprecise - enterprecise)); - } - } - // Capture the time once more to get the real delta time. - finishprecise = I_GetPreciseTime(); - deltasecs = (double)((INT64)(finishprecise - enterprecise)) / I_GetPrecisePrecision(); - deltatics = deltasecs * NEWTICRATE; + deltatics = D_EndFrame(enterprecise, !ranwipe ? &frameskip : NULL); } } diff --git a/src/d_main.h b/src/d_main.h index 5210c76be..a9749408a 100644 --- a/src/d_main.h +++ b/src/d_main.h @@ -61,6 +61,16 @@ extern char srb2path[256]; //Alam: SRB2's Home // the infinite loop of D_SRB2Loop() called from win_main for windows version void D_SRB2Loop(void) FUNCNORETURN; +typedef enum +{ + WIPELOOP_RUNWIPE, + WIPELOOP_TITLECARD, + WIPELOOP_ENCORE, + WIPELOOP_TITLEBLACK, +} wipelooptype_t; + +void D_WipeLoop(wipelooptype_t type, UINT8 wipetype, boolean drawMenu); + // // D_SRB2Main() // Not a globally visible function, just included for source reference, diff --git a/src/f_finale.c b/src/f_finale.c index e8de3b581..0c648bc36 100644 --- a/src/f_finale.c +++ b/src/f_finale.c @@ -431,34 +431,7 @@ void F_IntroTicker(void) } // Stay on black for a bit. =) - { - tic_t nowtime, quittime, lasttime; - nowtime = lasttime = I_GetTime(); - quittime = nowtime + NEWTICRATE; // Shortened the quit time, used to be 2 seconds - while (quittime > nowtime) - { - while (!((nowtime = I_GetTime()) - lasttime)) - { - I_Sleep(cv_sleep.value); - I_UpdateTime(cv_timescale.value); - } - lasttime = nowtime; - - I_OsPolling(); - I_UpdateNoBlit(); -#ifdef HAVE_THREADS - I_lock_mutex(&m_menu_mutex); -#endif - M_Drawer(); // menu is drawn even on top of wipes -#ifdef HAVE_THREADS - I_unlock_mutex(m_menu_mutex); -#endif - I_FinishUpdate(); // Update the screen with the image Tails 06-19-2001 - - if (moviemode) // make sure we save frames for the white hold too - M_SaveFrame(); - } - } + D_WipeLoop(WIPELOOP_TITLEBLACK, 0, false); D_StartTitle(); return; diff --git a/src/f_finale.h b/src/f_finale.h index 4c8da0b23..516a0caf3 100644 --- a/src/f_finale.h +++ b/src/f_finale.h @@ -145,12 +145,12 @@ void F_MenuPresTicker(boolean run); extern boolean WipeInAction; extern boolean WipeStageTitle; -extern INT32 lastwipetic; - // Don't know where else to place this constant // But this file seems appropriate #define PRELEVELTIME TICRATE // frames in tics +fademask_t *F_GetFadeMask(UINT8 masknum, UINT8 scrnnum); +void F_DoWipe(fademask_t *fademask); void F_WipeStartScreen(void); void F_WipeEndScreen(void); void F_RunWipe(UINT8 wipetype, boolean drawMenu); diff --git a/src/f_wipe.c b/src/f_wipe.c index 0497ae497..f52ca8d55 100644 --- a/src/f_wipe.c +++ b/src/f_wipe.c @@ -43,12 +43,12 @@ #define NOWIPE // do not enable wipe image post processing for ARM, SH and MIPS CPUs #endif -typedef struct fademask_s { +struct fademask_t { UINT8* mask; UINT16 width, height; size_t size; fixed_t xscale, yscale; -} fademask_t; +}; UINT8 wipedefs[NUMWIPEDEFS] = { 99, // wipe_credits_intermediate (0) @@ -85,7 +85,6 @@ UINT8 wipedefs[NUMWIPEDEFS] = { boolean WipeInAction = false; boolean WipeStageTitle = false; -INT32 lastwipetic = 0; #ifndef NOWIPE @@ -101,7 +100,7 @@ static fixed_t paldiv; * \param lump Lump name to get data from * \return fademask_t for lump */ -static fademask_t *F_GetFadeMask(UINT8 masknum, UINT8 scrnnum) { +fademask_t *F_GetFadeMask(UINT8 masknum, UINT8 scrnnum) { static char lumpname[9] = "FADEmmss"; static fademask_t fm = {NULL,0,0,0,0,0}; lumpnum_t lumpnum; @@ -186,7 +185,7 @@ static fademask_t *F_GetFadeMask(UINT8 masknum, UINT8 scrnnum) { * * \param fademask pixels to change */ -static void F_DoWipe(fademask_t *fademask) +void F_DoWipe(fademask_t *fademask) { // Software mask wipe -- optimized; though it might not look like it! // Okay, to save you wondering *how* this is more optimized than the simpler @@ -360,62 +359,14 @@ void F_RunWipe(UINT8 wipetype, boolean drawMenu) (void)wipetype; (void)drawMenu; #else - tic_t nowtime; - UINT8 wipeframe = 0; - fademask_t *fmask; - paldiv = FixedDiv(257< Date: Fri, 28 Mar 2025 23:32:11 +0100 Subject: [PATCH 02/15] Fix bad wipe timing for dedi servers on level load --- src/d_main.cpp | 2 +- src/p_setup.c | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/d_main.cpp b/src/d_main.cpp index 67f6f2e81..c40103d56 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -892,7 +892,7 @@ void D_WipeLoop(wipelooptype_t type, UINT8 wipetype, boolean drawMenu) } #endif - if (type == WIPELOOP_TITLECARD) + if (rendermode != render_none && type == WIPELOOP_TITLECARD) ST_preLevelTitleCardDrawer(); D_WipeDraw(drawMenu); diff --git a/src/p_setup.c b/src/p_setup.c index 5013b1d43..e833a0fb1 100644 --- a/src/p_setup.c +++ b/src/p_setup.c @@ -9012,12 +9012,14 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) levelloading = false; } - if (rendermode != render_none && reloadinggamestate == false) + if (reloadinggamestate == false) { - - R_ResetViewInterpolation(0); - R_ResetViewInterpolation(0); - R_UpdateMobjInterpolators(); + if (rendermode != render_none) + { + R_ResetViewInterpolation(0); + R_ResetViewInterpolation(0); + R_UpdateMobjInterpolators(); + } // Title card! G_StartTitleCard(); @@ -9026,6 +9028,10 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate) if (WipeStageTitle && ranspecialwipe != 2 && fromnetsave == false) { G_PreLevelTitleCard(); + + // don't do a fade-in if we ran the title card, it trips up dedis! + // normal clients never see the fade-in, and it causes a very brief lag spike + wipegamestate = gamestate; } } From 05f08ef5cb0990b458a935d94631e01d49152098 Mon Sep 17 00:00:00 2001 From: GenericHeroGuy Date: Sat, 29 Mar 2025 00:04:16 +0100 Subject: [PATCH 03/15] Set WipeInAction in D_WipeLoop, don't run scoreboard/cecho/etc during wipes --- src/d_main.cpp | 4 ++++ src/f_wipe.c | 3 --- src/hu_stuff.c | 6 ++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/d_main.cpp b/src/d_main.cpp index c40103d56..adbe747cd 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -849,6 +849,8 @@ void D_WipeLoop(wipelooptype_t type, UINT8 wipetype, boolean drawMenu) if (gamestate != GS_TITLESCREEN && gamestate != GS_TIMEATTACK) drawMenu = true; + WipeInAction = true; + // lastwipetic should either be 0 or the tic we last wiped // on for fade-to-black while (nowtime < endtime) @@ -906,6 +908,8 @@ void D_WipeLoop(wipelooptype_t type, UINT8 wipetype, boolean drawMenu) delta = D_EndFrame(enterprecise, NULL); lastwipetic = nowtime; } + + WipeInAction = false; } // ========================================================================= diff --git a/src/f_wipe.c b/src/f_wipe.c index f52ca8d55..a048be0f6 100644 --- a/src/f_wipe.c +++ b/src/f_wipe.c @@ -362,12 +362,9 @@ void F_RunWipe(UINT8 wipetype, boolean drawMenu) paldiv = FixedDiv(257< Date: Sat, 29 Mar 2025 00:20:32 +0100 Subject: [PATCH 04/15] Properly lock out menu inputs during GS_TIMEATTACK wipes --- src/d_main.cpp | 19 ++++++++++--------- src/f_finale.h | 2 +- src/f_wipe.c | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/d_main.cpp b/src/d_main.cpp index adbe747cd..9cdfdd510 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -197,7 +197,7 @@ void D_ProcessEvents(void) { event_t *ev; - boolean eaten; + boolean eaten = false; // i have to reset this somewhere or else your camera just glides away! for (size_t i = 0; i < 4; i++) @@ -225,15 +225,16 @@ void D_ProcessEvents(void) } // Menu input -#ifdef HAVE_THREADS - I_lock_mutex(&m_menu_mutex); -#endif + if (WipeInAction < 2) { - eaten = M_Responder(ev); - } #ifdef HAVE_THREADS - I_unlock_mutex(m_menu_mutex); + I_lock_mutex(&m_menu_mutex); #endif + eaten = M_Responder(ev); +#ifdef HAVE_THREADS + I_unlock_mutex(m_menu_mutex); +#endif + } if (eaten) continue; // menu ate the event @@ -849,7 +850,7 @@ void D_WipeLoop(wipelooptype_t type, UINT8 wipetype, boolean drawMenu) if (gamestate != GS_TITLESCREEN && gamestate != GS_TIMEATTACK) drawMenu = true; - WipeInAction = true; + WipeInAction = 1 + !drawMenu; // lastwipetic should either be 0 or the tic we last wiped // on for fade-to-black @@ -909,7 +910,7 @@ void D_WipeLoop(wipelooptype_t type, UINT8 wipetype, boolean drawMenu) lastwipetic = nowtime; } - WipeInAction = false; + WipeInAction = 0; } // ========================================================================= diff --git a/src/f_finale.h b/src/f_finale.h index 516a0caf3..0aa6b8001 100644 --- a/src/f_finale.h +++ b/src/f_finale.h @@ -142,7 +142,7 @@ void F_MenuPresTicker(boolean run); // WIPE // -extern boolean WipeInAction; +extern UINT8 WipeInAction; extern boolean WipeStageTitle; // Don't know where else to place this constant diff --git a/src/f_wipe.c b/src/f_wipe.c index a048be0f6..9dc414237 100644 --- a/src/f_wipe.c +++ b/src/f_wipe.c @@ -83,7 +83,7 @@ UINT8 wipedefs[NUMWIPEDEFS] = { // SCREEN WIPE PACKAGE //-------------------------------------------------------------------------- -boolean WipeInAction = false; +UINT8 WipeInAction = 0; boolean WipeStageTitle = false; #ifndef NOWIPE From 6a45300f8fdefb7f49de5e981f3d754a453466d9 Mon Sep 17 00:00:00 2001 From: GenericHeroGuy Date: Sat, 29 Mar 2025 00:52:31 +0100 Subject: [PATCH 05/15] Allow menu in GS_TITLESCREEN fadein --- src/d_main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/d_main.cpp b/src/d_main.cpp index 9cdfdd510..01f332187 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -674,7 +674,7 @@ static bool D_Display(void) { F_WipeEndScreen(); - F_RunWipe(wipetypepost, gamestate != GS_TIMEATTACK && gamestate != GS_TITLESCREEN); + F_RunWipe(wipetypepost, gamestate != GS_TIMEATTACK); ranwipe = true; } From 93712044cf7c8b30c3961949cafc8ec6cac11f4a Mon Sep 17 00:00:00 2001 From: GenericHeroGuy Date: Sat, 29 Mar 2025 01:12:56 +0100 Subject: [PATCH 06/15] No opening menus during wipes Unfortunate, but this has some nasty side effects, like letting you carry the pause menu into the title screen, or crashing your game by using any TA pause menu options during a wipe --- src/m_menu.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/m_menu.c b/src/m_menu.c index 772846fc0..8136955c5 100644 --- a/src/m_menu.c +++ b/src/m_menu.c @@ -1469,7 +1469,7 @@ boolean M_Responder(event_t *ev) return true; case KEY_F4: // Sound Volume - if (modeattacking) + if (modeattacking || WipeInAction) return true; M_StartControlPanel(); M_EnterMenu(MN_OP_MAIN, true, 0); @@ -1478,7 +1478,7 @@ boolean M_Responder(event_t *ev) return true; case KEY_F5: // Video Mode - if (modeattacking) + if (modeattacking || WipeInAction) return true; M_StartControlPanel(); M_EnterMenu(MN_OP_MAIN, true, 0); @@ -1490,7 +1490,7 @@ boolean M_Responder(event_t *ev) return true; case KEY_F7: // Options - if (modeattacking) + if (modeattacking || WipeInAction) return true; M_StartControlPanel(); M_EnterMenu(MN_OP_MAIN, true, 0); @@ -1744,7 +1744,7 @@ void M_Drawer(void) void M_StartControlPanel(void) { // intro might call this repeatedly - if (menustack[0]) + if (menustack[0] || WipeInAction) { CON_ToggleOff(); // move away console return; From 241794b67045c4a8f6400a4ee1d9dffe1ec2efe8 Mon Sep 17 00:00:00 2001 From: GenericHeroGuy Date: Thu, 7 Aug 2025 02:09:43 +0200 Subject: [PATCH 07/15] Fix menu replays causing event loop recursion --- src/d_main.cpp | 9 +++++++++ src/g_demo.c | 1 + src/m_menu.c | 22 +++++++++------------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/d_main.cpp b/src/d_main.cpp index 01f332187..c467ee126 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -189,6 +189,8 @@ UINT8 ctrldown = 0; // 0x1 left, 0x2 right UINT8 altdown = 0; // 0x1 left, 0x2 right boolean capslock = 0; // gee i wonder what this does. +static boolean recursioncheck = false; + // // D_ProcessEvents // Send all the events of the given timestamp down the responder chain @@ -199,6 +201,11 @@ void D_ProcessEvents(void) boolean eaten = false; + if (recursioncheck == true) + I_Error("D_ProcessEvents recursion detected"); + + recursioncheck = true; + // i have to reset this somewhere or else your camera just glides away! for (size_t i = 0; i < 4; i++) gamekeydown[0][KEY_MOUSEMOVE + i] = 0; @@ -263,6 +270,8 @@ void D_ProcessEvents(void) G_Responder(ev); } + + recursioncheck = false; } static void D_RenderLevel(void) diff --git a/src/g_demo.c b/src/g_demo.c index c1f9f0b77..28c789245 100644 --- a/src/g_demo.c +++ b/src/g_demo.c @@ -3398,6 +3398,7 @@ void G_DoPlayDemo(char *defdemoname) boolean skiperrors = false; #endif + M_ClearMenus(true); G_InitDemoRewind(); // No demo name means we're restarting the current demo diff --git a/src/m_menu.c b/src/m_menu.c index 8136955c5..4315db894 100644 --- a/src/m_menu.c +++ b/src/m_menu.c @@ -1848,7 +1848,9 @@ void M_ClearMenus(boolean callexitmenufunc) currentMenu->quitroutine(0); // Save the config file. I'm sick of crashing the game later and losing all my changes! - COM_BufAddText(va("saveconfig \"%s\" -silent\n", configfile)); + char buf[sizeof(configfile) + 50]; + sprintf(buf, "saveconfig \"%s\" -silent\n", configfile); + COM_BufAddText(buf); if (currentMenu->exitwipe >= 0) { @@ -4220,11 +4222,9 @@ INT32 MR_HutStartReplay(INT32 choice) { (void)choice; - M_ClearMenus(false); - demo.loadfiles = M_IsItemOn(MN_MISC_REPLAYSTART, "LOADWATCH"); - demo.ignorefiles = !M_IsItemOn(MN_MISC_REPLAYSTART, "LOADWATCH"); - - G_DoPlayDemo(demolist[dir_on[menudepthleft]].filepath); + COM_BufAddText(va("playdemo %s %s", + demolist[dir_on[menudepthleft]].filepath + strlen(srb2home) + 1, // dumb hack + M_IsItemOn(MN_MISC_REPLAYSTART, "LOADWATCH") ? "-addfiles" : "-force")); return true; } @@ -5733,9 +5733,7 @@ INT32 MR_ReplayStaff(INT32 choice) if (l == LUMPERROR) return false; - M_ClearMenus(true); - demo.loadfiles = false; demo.ignorefiles = true; // Just assume that record attack replays have the files needed - G_DoPlayDemo(va("%sS%02u",G_BuildMapName(cv_nextmap.value),cv_dummystaff.value)); + COM_BufAddText(va("playdemo %sS%02u -force", G_BuildMapName(cv_nextmap.value), cv_dummystaff.value)); return true; } @@ -5768,8 +5766,6 @@ INT32 MR_ReplayTimeAttack(INT32 arg) { const char *which; char *gamemode = M_AppendGametypeAndModName(); - M_ClearMenus(true); - demo.loadfiles = false; demo.ignorefiles = true; // Just assume that record attack replays have the files needed switch(arg) { default: @@ -5784,12 +5780,12 @@ INT32 MR_ReplayTimeAttack(INT32 arg) break; case 3: // guest // srb2/replay/main/map01-guest.lmp - G_DoPlayDemo(va("%s"PATHSEP"media"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s-guest.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), gamemode)); + COM_BufAddText(va("playdemo media"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s-guest.lmp -force", timeattackfolder, G_BuildMapName(cv_nextmap.value), gamemode)); Z_Free(gamemode); return true; } // srb2/replay/main/map01-sonic-time-best.lmp - G_DoPlayDemo(va("%s"PATHSEP"media"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s-%s-%s.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), skins[cv_chooseskin.value].name, gamemode, which)); + COM_BufAddText(va("playdemo media"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s-%s-%s.lmp -force", timeattackfolder, G_BuildMapName(cv_nextmap.value), skins[cv_chooseskin.value].name, gamemode, which)); Z_Free(gamemode); return true; } From d6edfb430322e3e4f60efba7780ffed88659d9e9 Mon Sep 17 00:00:00 2001 From: GenericHeroGuy Date: Fri, 8 Aug 2025 22:13:29 +0200 Subject: [PATCH 08/15] Don't allow menu routines to run during wipes This sucks, but there are just far too many possibilities to segfault... Instead, any input that would run a routine is buffered until the wipe ends --- src/g_demo.c | 2 +- src/m_menu.c | 55 +++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/g_demo.c b/src/g_demo.c index 28c789245..76552ebb1 100644 --- a/src/g_demo.c +++ b/src/g_demo.c @@ -3957,7 +3957,7 @@ void G_AddGhost(char *defdemoname) return; } - if (header.numplayers == 0) + if (buffer[header.endofs] == DEMOMARKER || header.numplayers == 0) { CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Replay is empty.\n"), pdemoname); Z_Free(pdemoname); diff --git a/src/m_menu.c b/src/m_menu.c index 4315db894..502e9b197 100644 --- a/src/m_menu.c +++ b/src/m_menu.c @@ -1187,7 +1187,7 @@ static boolean M_ChangeStringCvar(INT32 choice) // lock out further input in a tic when important buttons are pressed // (in other words -- stop bullshit happening by mashing buttons in fades) -static boolean noFurtherInput = false; +static INT32 noFurtherInput = 0; static void Command_Manual_f(void) { @@ -1438,7 +1438,7 @@ boolean M_Responder(event_t *ev) if (messagebox.routine) messagebox.routine(ch); messagebox.active = false; - noFurtherInput = true; + noFurtherInput = -1; } } else @@ -1453,7 +1453,7 @@ boolean M_Responder(event_t *ev) // F-Keys if (!menustack[0]) { - noFurtherInput = true; + noFurtherInput = -1; switch (ch) { @@ -1519,13 +1519,15 @@ boolean M_Responder(event_t *ev) M_StartControlPanel(); return true; } - noFurtherInput = false; // turns out we didn't care + noFurtherInput = 0; // turns out we didn't care return false; } // Handle menuitems which need a specific key handling if (currentMenu->keyhandler) { + if (WipeInAction) + goto wipebuffer; if (shiftdown && ch >= 32 && ch <= 127) ch = shiftxform[ch]; if (currentMenu->keyhandler(ch)) @@ -1537,8 +1539,14 @@ boolean M_Responder(event_t *ev) // G: nevermind we have combi menuitems now menuitem_t *item = currentMenu->numitems ? ¤tMenu->menuitems[itemOn] : NULL; - if (item && item->cvar && !item->cvar->PossibleValue && M_ChangeStringCvar(ch)) - return true; + // string cvars accept any keyboard input + if (item && item->cvar && !item->cvar->PossibleValue) + { + if (WipeInAction && item->cvar->flags & CV_CALL) + goto wipebuffer; + if (M_ChangeStringCvar(ch)) + return true; + } if (menustack[0] == MN_PLAYBACK && !con_destlines) { @@ -1557,6 +1565,19 @@ boolean M_Responder(event_t *ev) } } + // anything that might call a routine needs to be buffered until the wipe finishes + if (WipeInAction && ( + ((ch == KEY_LEFTARROW || ch == KEY_RIGHTARROW) && ((item->cvar && item->cvar->flags & CV_CALL) || item->status & IT_ARROWS)) + || (ch == KEY_ENTER && !item->submenu) + || (ch == KEY_ESCAPE && currentMenu->quitroutine) + || (ch == KEY_BACKSPACE && item->cvar))) + { +wipebuffer: + noFurtherInput = ch; + S_StartSound(NULL, sfx_kc50); + return true; + } + INT16 oldItemOn = itemOn; // prevent infinite loop // Keys usable within menu @@ -1603,7 +1624,7 @@ boolean M_Responder(event_t *ev) if (!item) return true; - noFurtherInput = true; + noFurtherInput = -1; currentMenu->lastOn = itemOn; if (menustack[0] == MN_PLAYBACK) @@ -1636,7 +1657,7 @@ boolean M_Responder(event_t *ev) return true; case KEY_ESCAPE: - noFurtherInput = true; + noFurtherInput = -1; currentMenu->lastOn = itemOn; //If we entered the game search menu, but didn't enter a game, @@ -1962,8 +1983,18 @@ boolean M_MouseNeeded(void) // void M_Ticker(void) { - // reset input trigger - noFurtherInput = false; + // send buffered input from wipe? + if (noFurtherInput > 0 && !WipeInAction) + { + event_t fake; + fake.device = 0; + fake.type = ev_keydown; + fake.data1 = noFurtherInput; + D_PostEvent(&fake); + noFurtherInput = 0; + } + else if (noFurtherInput == -1 || !WipeInAction) + noFurtherInput = 0; if (dedicated) return; @@ -2697,7 +2728,9 @@ void MD_DrawGenericMenu(void) // DRAW THE SKULL CURSOR if (M_ItemSelectable(¤tMenu->menuitems[itemOn])) - V_DrawScaledPatch(cursorx, cursory, 0, W_CachePatchName("M_CURSOR", PU_CACHE)); + V_DrawScaledPatch(cursorx, cursory, (noFurtherInput > 0 ? V_TRANSLUCENT : 0), W_CachePatchName("M_CURSOR", PU_CACHE)); + if (noFurtherInput > 0) + V_DrawSmallString(cursorx, cursory+3, 0, "WAIT"); x = currentMenu->x - 20 + currentMenu->cursoroffset; if (cliptop) From 6c24f4d70c815824fe000a524cccbe159ca6cf94 Mon Sep 17 00:00:00 2001 From: GenericHeroGuy Date: Fri, 8 Aug 2025 23:24:16 +0200 Subject: [PATCH 09/15] Properly fix empty demo crash --- src/g_demo.c | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/g_demo.c b/src/g_demo.c index 76552ebb1..ffca19191 100644 --- a/src/g_demo.c +++ b/src/g_demo.c @@ -276,6 +276,7 @@ typedef struct UINT8 numplayers; demoplayer_t *playerdata; + boolean empty; UINT32 endofs; } demoheader_t; @@ -669,6 +670,7 @@ static headerstatus_e G_ReadDemoHeader(UINT8 *dp, demoheader_t *header) header->mapmusrng = 0; header->numlaps = 0; header->raflags = 0; + header->empty = false; if (memcmp(dp, DEMOHEADER, 12)) return HEADER_BADMAGIC; @@ -687,7 +689,6 @@ static headerstatus_e G_ReadDemoHeader(UINT8 *dp, demoheader_t *header) raflag = false; break; - case 0x0001: // SRB2Kart 1.0.x (only staff ghosts supported) oldkart = kart = true; raflag = false; @@ -847,10 +848,16 @@ skipfiles: if (!kart) header->mapmusrng = READUINT8(dp); - // Sigh ... it's an empty demo. - if (*dp == DEMOMARKER || oldkart) + if (oldkart) goto end; + // Sigh ... it's an empty demo. + if (*dp == DEMOMARKER) + { + header->empty = true; + goto end; + } + // Load players that were in-game when the map started UINT8 playernum; header->playerdata = NULL; @@ -901,6 +908,10 @@ skipfiles: plr->followitem = !kart ? READUINT32(dp) : MT_NULL; } + // Sigh ... it's an empty demo. Again. + if (*dp == DEMOMARKER) + header->empty = true; + end: header->endofs = dp - startdp; return HEADER_OK; @@ -3658,7 +3669,7 @@ void G_DoPlayDemo(char *defdemoname) mapmusrng = header.mapmusrng; // Sigh ... it's an empty demo. - if (header.numplayers == 0) + if (header.empty) { snprintf(msg, 1024, M_GetText("%s contains no data to be played.\n"), pdemoname); CONS_Alert(CONS_ERROR, "%s", msg); @@ -3957,7 +3968,7 @@ void G_AddGhost(char *defdemoname) return; } - if (buffer[header.endofs] == DEMOMARKER || header.numplayers == 0) + if (header.empty) { CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Replay is empty.\n"), pdemoname); Z_Free(pdemoname); From 9b6c104cfcf4c61fb32910e0cb98c26693785a86 Mon Sep 17 00:00:00 2001 From: GenericHeroGuy Date: Sat, 9 Aug 2025 00:33:40 +0200 Subject: [PATCH 10/15] An impromptu cleanup of G_DoPlayDemo I found three memory leaks, and decided I was gonna do more than fix the broken title screen when demo loading fails in the RA menu --- src/g_demo.c | 253 +++++++++++++++++++-------------------------------- 1 file changed, 92 insertions(+), 161 deletions(-) diff --git a/src/g_demo.c b/src/g_demo.c index ffca19191..a573e2b51 100644 --- a/src/g_demo.c +++ b/src/g_demo.c @@ -3401,9 +3401,7 @@ void G_DoPlayDemo(char *defdemoname) lumpnum_t l; char *n,*pdemoname; char msg[1024]; - - boolean spectator, bot; - UINT8 slots[MAXPLAYERS], kartspeed[MAXPLAYERS], kartweight[MAXPLAYERS], numslots = 0; + UINT8 pnum; #if defined(SKIPERRORS) && !defined(DEVELOP) boolean skiperrors = false; @@ -3411,6 +3409,7 @@ void G_DoPlayDemo(char *defdemoname) M_ClearMenus(true); G_InitDemoRewind(); + gameaction = ga_nothing; // No demo name means we're restarting the current demo if (defdemoname == NULL) @@ -3437,10 +3436,7 @@ void G_DoPlayDemo(char *defdemoname) if (P_SaveBufferFromFile(&demobuf, defdemoname) == false) { snprintf(msg, 1024, M_GetText("Failed to read file '%s'.\n"), defdemoname); - CONS_Alert(CONS_ERROR, "%s", msg); - gameaction = ga_nothing; - M_StartMessage(msg, NULL, MM_NOTHING); - return; + goto lumperror; } } // load demo resource from WAD @@ -3452,11 +3448,7 @@ void G_DoPlayDemo(char *defdemoname) if ((l = W_CheckNumForName(defdemoname)) == LUMPERROR) { snprintf(msg, 1024, M_GetText("Failed to read lump '%s'.\n"), defdemoname); - CONS_Alert(CONS_ERROR, "%s", msg); - Z_Free(pdemoname); - gameaction = ga_nothing; - M_StartMessage(msg, NULL, MM_NOTHING); - return; + goto lumperror; } P_SaveBufferFromLump(&demobuf, l); @@ -3482,11 +3474,7 @@ void G_DoPlayDemo(char *defdemoname) if (mapnum >= nummapheaders || mapheaderinfo[mapnum]->lumpnum == LUMPERROR) { snprintf(msg, 1024, M_GetText("Failed to read lump '%s (couldn't find map %s)'.\n"), defdemoname, mapname); - CONS_Alert(CONS_ERROR, "%s", msg); - Z_Free(pdemoname); - gameaction = ga_nothing; - M_StartMessage(msg, NULL, MM_NOTHING); - return; + goto lumperror; } vRes = vres_GetMap(mapheaderinfo[mapnum]->lumpnum); @@ -3495,11 +3483,7 @@ void G_DoPlayDemo(char *defdemoname) if (vLump == NULL) { snprintf(msg, 1024, M_GetText("Failed to read lump '%s (couldn't find lump %s in %s)'.\n"), defdemoname, pdemoname, mapname); - CONS_Alert(CONS_ERROR, "%s", msg); - Z_Free(pdemoname); - gameaction = ga_nothing; - M_StartMessage(msg, NULL, MM_NOTHING); - return; + goto lumperror; } P_SaveBufferAlloc(&demobuf, vLump->size); @@ -3514,7 +3498,6 @@ void G_DoPlayDemo(char *defdemoname) } // read demo header - gameaction = ga_nothing; demo.playback = true; demo.buffer = &demobuf; @@ -3528,33 +3511,15 @@ void G_DoPlayDemo(char *defdemoname) case HEADER_BADMAGIC: snprintf(msg, 1024, M_GetText("%s is not a SRB2Kart replay file.\n"), pdemoname); - CONS_Alert(CONS_ERROR, "%s", msg); - Z_Free(pdemoname); - P_SaveBufferFree(&demobuf); - demo.playback = false; - demo.title = false; - M_StartMessage(msg, NULL, MM_NOTHING); - return; + goto headererror; case HEADER_BADVERSION: snprintf(msg, 1024, M_GetText("%s is an incompatible replay format and cannot be played.\n"), pdemoname); - CONS_Alert(CONS_ERROR, "%s", msg); - Z_Free(pdemoname); - P_SaveBufferFree(&demobuf); - demo.playback = false; - demo.title = false; - M_StartMessage(msg, NULL, MM_NOTHING); - return; + goto headererror; case HEADER_BADFORMAT: snprintf(msg, 1024, M_GetText("%s is the wrong type of recording and cannot be played.\n"), pdemoname); - CONS_Alert(CONS_ERROR, "%s", msg); - Z_Free(pdemoname); - P_SaveBufferFree(&demobuf); - demo.playback = false; - demo.title = false; - M_StartMessage(msg, NULL, MM_NOTHING); - return; + goto headererror; } demo.version = header.demoversion; @@ -3576,54 +3541,68 @@ void G_DoPlayDemo(char *defdemoname) G_LoadDemoExtraFiles(&header); else if (demo.ignorefiles) ;//G_SkipDemoExtraFiles(&demobuf.p); - else + else switch (G_CheckDemoExtraFiles(&header, false)) { - UINT8 error = G_CheckDemoExtraFiles(&header, false); + case DFILE_ERROR_NOTLOADED: + snprintf(msg, 1024, + M_GetText("Required files for this demo are not loaded.\n\nUse\n\"playdemo %s -addfiles\"\nto load them and play the demo.\n"), + pdemoname); + goto error; - if (error) + case DFILE_ERROR_OUTOFORDER: + snprintf(msg, 1024, + M_GetText("Required files for this demo are loaded out of order.\n\nUse\n\"playdemo %s -force\"\nto play the demo anyway.\n"), + pdemoname); + goto error; + + case DFILE_ERROR_INCOMPLETEOUTOFORDER: + snprintf(msg, 1024, + M_GetText("Required files for this demo are not loaded, and some are out of order.\n\nUse\n\"playdemo %s -addfiles\"\nto load needed files and play the demo.\n"), + pdemoname); + goto error; + + case DFILE_ERROR_CANNOTLOAD: + snprintf(msg, 1024, + M_GetText("Required files for this demo cannot be loaded.\n\nUse\n\"playdemo %s -force\"\nto play the demo anyway.\n"), + pdemoname); + goto error; + + case DFILE_ERROR_EXTRAFILES: + snprintf(msg, 1024, + M_GetText("You have additional files loaded beyond the demo's file list.\n\nUse\n\"playdemo %s -force\"\nto play the demo anyway.\n"), + pdemoname); + goto error; + } + + // ...*map* not loaded? + if (!gamemap || (gamemap > nummapheaders) || !mapheaderinfo[gamemap-1] || mapheaderinfo[gamemap-1]->lumpnum == LUMPERROR) + { + snprintf(msg, 1024, M_GetText("%s features a course that is not currently loaded.\n"), pdemoname); + goto error; + } + + // Sigh ... it's an empty demo. + if (header.empty) + { + snprintf(msg, 1024, M_GetText("%s contains no data to be played.\n"), pdemoname); + goto error; + } + + // extra checks for RA replays + if (demoflags & DF_ATTACKMASK) + { + const char *reason = NULL; + if (header.numplayers != 1) + reason = "multiple players"; + else if (header.playerdata[0].flags & DEMO_SPECTATOR) + reason = "spectators"; + else if (header.playerdata[0].flags & DEMO_BOT) + reason = "bots"; + + if (reason) { - switch (error) - { - case DFILE_ERROR_NOTLOADED: - snprintf(msg, 1024, - M_GetText("Required files for this demo are not loaded.\n\nUse\n\"playdemo %s -addfiles\"\nto load them and play the demo.\n"), - pdemoname); - break; - - case DFILE_ERROR_OUTOFORDER: - snprintf(msg, 1024, - M_GetText("Required files for this demo are loaded out of order.\n\nUse\n\"playdemo %s -force\"\nto play the demo anyway.\n"), - pdemoname); - break; - - case DFILE_ERROR_INCOMPLETEOUTOFORDER: - snprintf(msg, 1024, - M_GetText("Required files for this demo are not loaded, and some are out of order.\n\nUse\n\"playdemo %s -addfiles\"\nto load needed files and play the demo.\n"), - pdemoname); - break; - - case DFILE_ERROR_CANNOTLOAD: - snprintf(msg, 1024, - M_GetText("Required files for this demo cannot be loaded.\n\nUse\n\"playdemo %s -force\"\nto play the demo anyway.\n"), - pdemoname); - break; - - case DFILE_ERROR_EXTRAFILES: - snprintf(msg, 1024, - M_GetText("You have additional files loaded beyond the demo's file list.\n\nUse\n\"playdemo %s -force\"\nto play the demo anyway.\n"), - pdemoname); - break; - } - - CONS_Alert(CONS_ERROR, "%s", msg); - Z_Free(pdemoname); - P_SaveBufferFree(&demobuf); - G_FreeDemoHeader(&header); - demo.playback = false; - demo.title = false; - if (!CON_Ready()) // In the console they'll just see the notice there! No point pulling them out. - M_StartMessage(msg, NULL, MM_NOTHING); - return; + snprintf(msg, 1024, M_GetText("%s is a Record Attack replay with %s, and is thus invalid.\n"), pdemoname, reason); + goto error; } } @@ -3639,20 +3618,6 @@ void G_DoPlayDemo(char *defdemoname) if (modeattacking != ATTACKING_TIME && modeattacking != ATTACKING_ITEMBREAK) modeattacking = ATTACKING_NONE; // is this really necessary? - // ...*map* not loaded? - if (!gamemap || (gamemap > nummapheaders) || !mapheaderinfo[gamemap-1] || mapheaderinfo[gamemap-1]->lumpnum == LUMPERROR) - { - snprintf(msg, 1024, M_GetText("%s features a course that is not currently loaded.\n"), pdemoname); - CONS_Alert(CONS_ERROR, "%s", msg); - Z_Free(pdemoname); - P_SaveBufferFree(&demobuf); - G_FreeDemoHeader(&header); - demo.playback = false; - demo.title = false; - M_StartMessage(msg, NULL, MM_NOTHING); - return; - } - // net var data CV_LoadDemoVars(&header); @@ -3668,20 +3633,6 @@ void G_DoPlayDemo(char *defdemoname) // Load "mapmusrng" used for altmusic selection mapmusrng = header.mapmusrng; - // Sigh ... it's an empty demo. - if (header.empty) - { - snprintf(msg, 1024, M_GetText("%s contains no data to be played.\n"), pdemoname); - CONS_Alert(CONS_ERROR, "%s", msg); - Z_Free(pdemoname); - P_SaveBufferFree(&demobuf); - G_FreeDemoHeader(&header); - demo.playback = false; - demo.title = false; - M_StartMessage(msg, NULL, MM_NOTHING); - return; - } - Z_Free(pdemoname); memset(&oldcmd,0,sizeof(oldcmd)); @@ -3711,48 +3662,16 @@ void G_DoPlayDemo(char *defdemoname) memset(camera,0,sizeof(camera)); // reset freecam // Load players that were in-game when the map started - for (UINT8 pnum = 0; pnum < header.numplayers; pnum++) + for (pnum = 0; pnum < header.numplayers; pnum++) { demoplayer_t *plr = &header.playerdata[pnum]; UINT8 p = plr->playernum; - spectator = !!(plr->flags & DEMO_SPECTATOR); - bot = !!(plr->flags & DEMO_BOT); - - if ((spectator || bot)) - { - if (modeattacking) - { - snprintf(msg, 1024, M_GetText("%s is a Record Attack replay with %s, and is thus invalid.\n"), pdemoname, (bot ? "bots" : "spectators")); - CONS_Alert(CONS_ERROR, "%s", msg); - Z_Free(pdemoname); - P_SaveBufferFree(&demobuf); - demo.playback = false; - demo.title = false; - M_StartMessage(msg, NULL, MM_NOTHING); - return; - } - } - - slots[numslots] = p; - numslots++; - - if (modeattacking && numslots > 1) - { - snprintf(msg, 1024, M_GetText("%s is a Record Attack replay with multiple players, and is thus invalid.\n"), pdemoname); - CONS_Alert(CONS_ERROR, "%s", msg); - Z_Free(pdemoname); - P_SaveBufferFree(&demobuf); - demo.playback = false; - demo.title = false; - M_StartMessage(msg, NULL, MM_NOTHING); - return; - } if (!playeringame[displayplayers[0]] || players[displayplayers[0]].spectator) displayplayers[0] = consoleplayer = serverplayer = p; G_AddPlayer(p, p); - players[p].spectator = spectator; + players[p].spectator = !!(plr->flags & DEMO_SPECTATOR); if (plr->flags & DEMO_KICKSTART) players[p].pflags |= PF_KICKSTARTACCEL; @@ -3766,7 +3685,7 @@ void G_DoPlayDemo(char *defdemoname) K_UpdateShrinkCheat(&players[p]); - if ((players[p].bot = bot) == true) + if ((players[p].bot = !!(plr->flags & DEMO_BOT)) == true) { players[p].botvars.difficulty = plr->bot.difficulty; players[p].botvars.diffincrease = plr->bot.diffincrease; // needed to avoid having to duplicate logic @@ -3809,12 +3728,10 @@ void G_DoPlayDemo(char *defdemoname) // Power Levels clientpowerlevels[p][gametype == GT_BATTLE ? PWRLV_BATTLE : PWRLV_RACE] = plr->powerlevel; - // Kart stats, temporarily - kartspeed[p] = plr->kartspeed; - kartweight[p] = plr->kartweight; + // Kart stats (set later) if (stricmp(skins[players[p].skin].name, plr->skin) != 0) - FindClosestSkinForStats(p, kartspeed[p], kartweight[p]); + FindClosestSkinForStats(p, plr->kartspeed, plr->kartweight); // Followitem players[p].followitem = plr->followitem; @@ -3836,10 +3753,10 @@ void G_DoPlayDemo(char *defdemoname) if (demo.title) { splitscreen = M_RandomKey(6)-1; - splitscreen = min(min(3, numslots-1), splitscreen); // Bias toward 1p and 4p views + splitscreen = min(min(3, header.numplayers-1), splitscreen); // Bias toward 1p and 4p views for (i = 0; i <= splitscreen; i++) - G_ResetView(i+1, slots[M_RandomKey(numslots)], false); + G_ResetView(i+1, header.playerdata[M_RandomKey(header.numplayers)].playernum, false); } R_ExecuteSetViewSize(); @@ -3847,19 +3764,33 @@ void G_DoPlayDemo(char *defdemoname) P_SetRandSeed(header.randseed); G_InitNew(demoflags & DF_ENCORE, gamemap, true, true, false); // Doesn't matter whether you reset or not here, given changes to resetplayer. - for (i = 0; i < MAXPLAYERS; i++) + for (pnum = 0; pnum < header.numplayers; pnum++) { // oldghost init doesn't work here, players aren't immediately spawned anymore // Set saved attribute values // No cheat checking here, because even if they ARE wrong... // it would only break the replay if we clipped them. - players[i].kartspeed = kartspeed[i]; - players[i].kartweight = kartweight[i]; + demoplayer_t *plr = &header.playerdata[pnum]; + players[plr->playernum].kartspeed = plr->kartspeed; + players[plr->playernum].kartweight = plr->kartweight; } demo.deferstart = true; G_FreeDemoHeader(&header); + return; + +error: + G_FreeDemoHeader(&header); +headererror: + P_SaveBufferFree(&demobuf); +lumperror: + CONS_Alert(CONS_ERROR, "%s", msg); + Z_Free(pdemoname); + demo.playback = false; + demo.title = false; + if (!CON_Ready()) // In the console they'll just see the notice there! No point pulling them out. + M_StartMessage(msg, NULL, MM_NOTHING); } void G_SetupDemoPlayer(INT32 i) From 04ae82b5bc9bacda89a8307a3e835a1c6b935a3a Mon Sep 17 00:00:00 2001 From: GenericHeroGuy Date: Sat, 9 Aug 2025 01:30:00 +0200 Subject: [PATCH 11/15] Fix wipebuffer logic, reallow menu during wipes, fix pause menu carrying --- src/d_clisrv.c | 3 +++ src/m_menu.c | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/d_clisrv.c b/src/d_clisrv.c index 5075ab50d..4546aa1fc 100644 --- a/src/d_clisrv.c +++ b/src/d_clisrv.c @@ -2682,6 +2682,9 @@ void CL_Reset(void) if (demo.recording) G_CheckDemoStatus(); + // don't carry menus into the title screen (or wherever we're going) + M_ClearMenus(true); + // reset client/server code DEBFILE(va("\n-=-=-=-=-=-=-= Client reset =-=-=-=-=-=-=-\n\n")); diff --git a/src/m_menu.c b/src/m_menu.c index 502e9b197..ba3c10ac5 100644 --- a/src/m_menu.c +++ b/src/m_menu.c @@ -1435,6 +1435,8 @@ boolean M_Responder(event_t *ev) { if (ch == ' ' || ch == 'n' || ch == 'y' || ch == KEY_ESCAPE || ch == KEY_ENTER) { + if (WipeInAction) + goto wipebuffer; if (messagebox.routine) messagebox.routine(ch); messagebox.active = false; @@ -1568,9 +1570,9 @@ boolean M_Responder(event_t *ev) // anything that might call a routine needs to be buffered until the wipe finishes if (WipeInAction && ( ((ch == KEY_LEFTARROW || ch == KEY_RIGHTARROW) && ((item->cvar && item->cvar->flags & CV_CALL) || item->status & IT_ARROWS)) - || (ch == KEY_ENTER && !item->submenu) + || (ch == KEY_ENTER && ((item->submenu && menudefs[item->submenu].enterroutine) || item->routine || (item->cvar && item->cvar->flags & CV_CALL))) || (ch == KEY_ESCAPE && currentMenu->quitroutine) - || (ch == KEY_BACKSPACE && item->cvar))) + || (ch == KEY_BACKSPACE && item->cvar && item->cvar->flags & CV_CALL))) { wipebuffer: noFurtherInput = ch; @@ -1765,7 +1767,7 @@ void M_Drawer(void) void M_StartControlPanel(void) { // intro might call this repeatedly - if (menustack[0] || WipeInAction) + if (menustack[0]) { CON_ToggleOff(); // move away console return; From d70cdbe4a0b7a68fa7835267fbf09dfe2e3fbf95 Mon Sep 17 00:00:00 2001 From: GenericHeroGuy Date: Sat, 9 Aug 2025 02:45:51 +0200 Subject: [PATCH 12/15] Fix renderdeltatics exploding after wipes (and a warning) --- src/d_main.cpp | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/d_main.cpp b/src/d_main.cpp index c467ee126..589e0456b 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -400,9 +400,8 @@ gamestate_t wipegamestate = GS_LEVEL; INT16 wipetypepre = -1; INT16 wipetypepost = -1; -static bool D_Display(void) +static void D_Display(void) { - bool ranwipe = false; boolean forcerefresh = false; static boolean wipe = false; INT32 wipedefindex = 0; @@ -413,7 +412,7 @@ static bool D_Display(void) if (!dedicated) { if (nodrawers) - return false; // for comparative timing/profiling + return; // for comparative timing/profiling // Lactozilla: Switching renderers works by checking // if the game has to do it right when the frame @@ -483,7 +482,6 @@ static bool D_Display(void) F_WipeColorFill(31); F_WipeEndScreen(); F_RunWipe(wipetypepre, gamestate != GS_TIMEATTACK && gamestate != GS_TITLESCREEN); - ranwipe = true; } if (gamestate != GS_LEVEL && rendermode != render_none) @@ -497,7 +495,6 @@ static bool D_Display(void) else //dedicated servers { F_RunWipe(wipedefs[wipedefindex], gamestate != GS_TIMEATTACK && gamestate != GS_TITLESCREEN); - ranwipe = true; wipegamestate = gamestate; } @@ -507,7 +504,7 @@ static bool D_Display(void) wipetypepre = -1; if (dedicated) //bail out after wipe logic - return false; + return; // Catch runaway clipping rectangles. V_ClearClipRect(); @@ -684,7 +681,6 @@ static bool D_Display(void) F_WipeEndScreen(); F_RunWipe(wipetypepost, gamestate != GS_TIMEATTACK); - ranwipe = true; } // reset counters so timedemo doesn't count the wipe duration @@ -742,8 +738,6 @@ static bool D_Display(void) I_FinishUpdate(); // page flip or blit buffer ps_swaptime = I_GetPreciseTime() - ps_swaptime; } - - return ranwipe; } static void D_WipeTick(boolean menu) @@ -838,12 +832,14 @@ static double D_EndFrame(precise_t enterprecise, int *frameskip) } static INT32 endtimes[] = { - [WIPELOOP_RUNWIPE] = UINT8_MAX, - [WIPELOOP_TITLECARD] = PRELEVELTIME*NEWTICRATERATIO, - [WIPELOOP_ENCORE] = 3*TICRATE/2, - [WIPELOOP_TITLEBLACK] = NEWTICRATE, // Shortened the quit time, used to be 2 seconds + UINT8_MAX, // WIPELOOP_RUNWIPE + PRELEVELTIME*NEWTICRATERATIO, // WIPELOOP_TITLECARD + 3*TICRATE/2, // WIPELOOP_ENCORE + NEWTICRATE, // WIPELOOP_TITLEBLACK }; +static bool ranwipe = false; + // one single function for all the extra main loops in this god-forsaken codebase // should make it easier to fold these back into D_SRB2Loop at some point... void D_WipeLoop(wipelooptype_t type, UINT8 wipetype, boolean drawMenu) @@ -919,6 +915,7 @@ void D_WipeLoop(wipelooptype_t type, UINT8 wipetype, boolean drawMenu) lastwipetic = nowtime; } + ranwipe = true; WipeInAction = 0; } @@ -989,8 +986,6 @@ void D_SRB2Loop(void) g_dc = {}; Z_Frame_Reset(); - bool ranwipe = false; - I_UpdateTime(cv_timescale.value); if (lastwipetic) @@ -1104,7 +1099,7 @@ void D_SRB2Loop(void) { if (!frameskip) { - ranwipe = D_Display(); + D_Display(); } else if (!dedicated && frameskip) { @@ -1143,11 +1138,16 @@ void D_SRB2Loop(void) } #endif + deltatics = D_EndFrame(enterprecise, &frameskip); + // Wipes run an inner loop and artificially increase // the measured time. if (ranwipe) + { + deltatics = 35.0 / R_GetFramerateCap(); frameskip = 0; - deltatics = D_EndFrame(enterprecise, !ranwipe ? &frameskip : NULL); + ranwipe = false; + } } } From 5dca43633febacad3c4498217475f0032bcdcb5d Mon Sep 17 00:00:00 2001 From: GenericHeroGuy Date: Sat, 9 Aug 2025 22:00:49 +0200 Subject: [PATCH 13/15] Fix comparing wipegamestate with FORCEWIPE Fun fact: the signedness of an enumeration is up to the implementation! --- src/d_main.cpp | 1 + src/f_finale.h | 3 --- src/g_game.c | 1 + src/g_state.h | 1 + 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/d_main.cpp b/src/d_main.cpp index 589e0456b..bb1498024 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -596,6 +596,7 @@ static void D_Display(void) } case GS_DEDICATEDSERVER: case GS_NULL: + case FORCEWIPE: break; } diff --git a/src/f_finale.h b/src/f_finale.h index 0aa6b8001..471b74568 100644 --- a/src/f_finale.h +++ b/src/f_finale.h @@ -26,9 +26,6 @@ extern "C" { // // FINALE // -// HACK for menu fading while titlemapinaction; skips the level check -#define FORCEWIPE -2 - // Called by main loop. boolean F_IntroResponder(event_t *ev); diff --git a/src/g_game.c b/src/g_game.c index 5222167d8..d65a5e6fd 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -2277,6 +2277,7 @@ void G_Ticker(boolean run) case GS_DEDICATEDSERVER: case GS_NULL: + case FORCEWIPE: break; // do nothing } diff --git a/src/g_state.h b/src/g_state.h index 94f2414bb..678ba0a5d 100644 --- a/src/g_state.h +++ b/src/g_state.h @@ -22,6 +22,7 @@ extern "C" { // the current state of the game typedef enum { + FORCEWIPE = -2, // HACK for menu fading while titlemapinaction; skips the level check GS_NULL = 0, // At beginning. // Fadable gamestates From 875b747d01fd67454fb8dba0988084b50000de71 Mon Sep 17 00:00:00 2001 From: GenericHeroGuy Date: Sat, 9 Aug 2025 23:16:20 +0200 Subject: [PATCH 14/15] Add a routine whitelist for wipe buffering in menus --- src/m_menu.c | 64 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/src/m_menu.c b/src/m_menu.c index ba3c10ac5..a8bf85a10 100644 --- a/src/m_menu.c +++ b/src/m_menu.c @@ -1256,6 +1256,30 @@ static boolean M_DemoBinds(INT32 ch) return true; } +// if the menu is being used during a wipe, running routines can be dangerous! +// buffer a single input, then wait for the wipe to end +static boolean M_WipeBuffer(INT32 ch, menufunc_f *routine) +{ + if (!WipeInAction) + return false; + + // these routines are allowed to run during wipes + // solely to make the menus less annoying to use + if (routine == NULL + || (routine == MR_SelectableClearMenus && !currentMenu->quitroutine) + || routine == MR_Options + || routine == MR_SetupMultiPlayer + ) + return false; + + noFurtherInput = ch; + S_StartSound(NULL, sfx_kc50); + return true; +} + +// use this when routine being NULL isn't a free pass +static INT32 MR_Dummy(INT32 ch) { return true; } + // // M_Responder // @@ -1435,8 +1459,8 @@ boolean M_Responder(event_t *ev) { if (ch == ' ' || ch == 'n' || ch == 'y' || ch == KEY_ESCAPE || ch == KEY_ENTER) { - if (WipeInAction) - goto wipebuffer; + if (M_WipeBuffer(ch, MR_Dummy)) + return true; if (messagebox.routine) messagebox.routine(ch); messagebox.active = false; @@ -1528,8 +1552,8 @@ boolean M_Responder(event_t *ev) // Handle menuitems which need a specific key handling if (currentMenu->keyhandler) { - if (WipeInAction) - goto wipebuffer; + if (M_WipeBuffer(ch, currentMenu->keyhandler)) + return true; if (shiftdown && ch >= 32 && ch <= 127) ch = shiftxform[ch]; if (currentMenu->keyhandler(ch)) @@ -1544,8 +1568,8 @@ boolean M_Responder(event_t *ev) // string cvars accept any keyboard input if (item && item->cvar && !item->cvar->PossibleValue) { - if (WipeInAction && item->cvar->flags & CV_CALL) - goto wipebuffer; + if (item->cvar->flags & CV_CALL && M_WipeBuffer(ch, MR_Dummy)) + return true; if (M_ChangeStringCvar(ch)) return true; } @@ -1567,19 +1591,6 @@ boolean M_Responder(event_t *ev) } } - // anything that might call a routine needs to be buffered until the wipe finishes - if (WipeInAction && ( - ((ch == KEY_LEFTARROW || ch == KEY_RIGHTARROW) && ((item->cvar && item->cvar->flags & CV_CALL) || item->status & IT_ARROWS)) - || (ch == KEY_ENTER && ((item->submenu && menudefs[item->submenu].enterroutine) || item->routine || (item->cvar && item->cvar->flags & CV_CALL))) - || (ch == KEY_ESCAPE && currentMenu->quitroutine) - || (ch == KEY_BACKSPACE && item->cvar && item->cvar->flags & CV_CALL))) - { -wipebuffer: - noFurtherInput = ch; - S_StartSound(NULL, sfx_kc50); - return true; - } - INT16 oldItemOn = itemOn; // prevent infinite loop // Keys usable within menu @@ -1612,6 +1623,9 @@ wipebuffer: if (!item || !(item->cvar || item->status & IT_ARROWS)) return true; + if (M_WipeBuffer(ch, item->cvar ? (item->cvar->flags & CV_CALL ? MR_Dummy : NULL) : item->routine)) + return true; + if (menustack[0] != MN_OP_SOUND || itemOn > 3) S_StartSound(NULL, sfx_menu1); @@ -1643,16 +1657,22 @@ wipebuffer: if (item->submenu) { + if (M_WipeBuffer(ch, menudefs[item->submenu].enterroutine)) + return true; S_StartSound(NULL, sfx_menu1); M_EnterMenu(item->submenu, true, argument); } else if (item->routine) { + if (M_WipeBuffer(ch, item->routine)) + return true; S_StartSound(NULL, sfx_menu1); item->routine(argument); } else if (item->cvar && item->cvar->PossibleValue) // not for string cvars! { + if (item->cvar->flags & CV_CALL && M_WipeBuffer(ch, MR_Dummy)) + return true; S_StartSound(NULL, sfx_menu1); M_ChangeCvar(item, 1); // right arrow } @@ -1662,6 +1682,9 @@ wipebuffer: noFurtherInput = -1; currentMenu->lastOn = itemOn; + if (M_WipeBuffer(ch, currentMenu->quitroutine)) + return true; + //If we entered the game search menu, but didn't enter a game, //make sure the game doesn't still think we're in a netgame. if (!Playing() && netgame && multiplayer) @@ -1682,6 +1705,9 @@ wipebuffer: || item->cvar == &cv_dummymultiplayer) return true; + if (item->cvar->flags & CV_CALL && M_WipeBuffer(ch, MR_Dummy)) + return true; + if (menustack[0] != MN_OP_SOUND || itemOn > 3) S_StartSound(NULL, sfx_menu1); M_ResetCvar(item); From 3bc2cd6c6eeafd7926a2ea69d02e98e5ddc818ec Mon Sep 17 00:00:00 2001 From: GenericHeroGuy Date: Sat, 9 Aug 2025 23:18:42 +0200 Subject: [PATCH 15/15] Make dedis run post wipes, fix pre wipe logic --- src/d_main.cpp | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/d_main.cpp b/src/d_main.cpp index bb1498024..56c8cb5b0 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -471,20 +471,23 @@ static void D_Display(void) if (wipetypepre < 0 || !F_WipeExists(wipetypepre)) wipetypepre = wipedefs[wipedefindex]; - if (rendermode != render_none) + // Fade to black first + if ((wipegamestate == FORCEWIPE || + !(gamestate == GS_LEVEL || (gamestate == GS_TITLESCREEN && titlemapinaction))) // fades to black on its own timing, always + && wipetypepre != UINT8_MAX) { - // Fade to black first - if ((wipegamestate == FORCEWIPE || - !(gamestate == GS_LEVEL || (gamestate == GS_TITLESCREEN && titlemapinaction))) // fades to black on its own timing, always - && wipetypepre != UINT8_MAX) + if (rendermode != render_none) { F_WipeStartScreen(); F_WipeColorFill(31); F_WipeEndScreen(); - F_RunWipe(wipetypepre, gamestate != GS_TIMEATTACK && gamestate != GS_TITLESCREEN); } + F_RunWipe(wipetypepre, gamestate != GS_TIMEATTACK && gamestate != GS_TITLESCREEN); + } - if (gamestate != GS_LEVEL && rendermode != render_none) + if (rendermode != render_none) + { + if (gamestate != GS_LEVEL) { V_SetPaletteLump("PLAYPAL"); // Reset the palette R_ReInitColormaps(0, NULL, 0, false); @@ -492,19 +495,14 @@ static void D_Display(void) F_WipeStartScreen(); } - else //dedicated servers - { - F_RunWipe(wipedefs[wipedefindex], gamestate != GS_TIMEATTACK && gamestate != GS_TITLESCREEN); + else wipegamestate = gamestate; - } - - wipetypepre = -1; } - else - wipetypepre = -1; + + wipetypepre = -1; if (dedicated) //bail out after wipe logic - return; + goto dedipostwipe; // WAIT! don't forget about post wipes! // Catch runaway clipping rectangles. V_ClearClipRect(); @@ -668,6 +666,7 @@ static void D_Display(void) // // wipe update // +dedipostwipe: if (wipe && wipetypepost != INT16_MAX) { // note: moved up here because NetUpdate does input changes @@ -678,11 +677,9 @@ static void D_Display(void) wipetypepost = wipedefs[wipedefindex]; if (rendermode != render_none) - { F_WipeEndScreen(); - F_RunWipe(wipetypepost, gamestate != GS_TIMEATTACK); - } + F_RunWipe(wipetypepost, gamestate != GS_TIMEATTACK); // reset counters so timedemo doesn't count the wipe duration if (demo.timing) @@ -690,11 +687,12 @@ static void D_Display(void) framecount = 0; demostarttime = I_GetTime(); } - - wipetypepost = -1; } - else - wipetypepost = -1; + + wipetypepost = -1; + + if (dedicated) + return; // NOW we can bail NetUpdate(); // send out any new accumulation