From 19ef96351a99d61adcb89c73099baf33bf56d00d Mon Sep 17 00:00:00 2001 From: toaster Date: Sat, 8 Apr 2023 20:42:27 +0100 Subject: [PATCH 01/17] roundqueue A general purpose system that permits cacheing of GP progression in one place, but which permits future expansion and brings Online GP a little closer to reality. - Stores a bunch of levels, gametypes, encore state, and restricted-by-rank-ness in sequence. - Initialised on GP cup select. - FUTURE WORK: Open to being initialised by other methods - Digests its way through that sequence as maps are completed. - Stores round number instead of `grandprixinfo`. - Map commands as sent over the wire have been adjusted. - Sends round number and size of/position in roundqueue. - Now figures out GP Event Type from gametype. - Can be swung in the direction of a Special Stage with a hint flag. - This hint flag replaces "fromlevelselect", which was functionally vestigal. --- src/d_clisrv.c | 2 +- src/d_main.c | 11 +- src/d_netcmd.c | 132 +++++++-------- src/d_netcmd.h | 2 +- src/doomstat.h | 3 +- src/g_demo.c | 2 +- src/g_game.c | 272 ++++++++++++++++++++----------- src/g_game.h | 23 ++- src/hu_stuff.c | 12 +- src/k_grandprix.c | 20 ++- src/k_grandprix.h | 1 - src/k_menufunc.c | 1 - src/menus/transient/cup-select.c | 21 ++- src/p_saveg.c | 63 ++++++- src/st_stuff.c | 42 ++--- src/typedef.h | 1 + 16 files changed, 375 insertions(+), 233 deletions(-) diff --git a/src/d_clisrv.c b/src/d_clisrv.c index 37bb6323b..5b3e4093d 100644 --- a/src/d_clisrv.c +++ b/src/d_clisrv.c @@ -6086,7 +6086,7 @@ boolean TryRunTics(tic_t realtics) { COM_BufTicker(); if (mapchangepending) - D_MapChange(-1, 0, encoremode, false, 2, false, fromlevelselect); // finish the map change + D_MapChange(-1, 0, encoremode, false, 2, false, forcespecialstage); // finish the map change } NetUpdate(); diff --git a/src/d_main.c b/src/d_main.c index c96ff0dc9..7cd1ad2c9 100644 --- a/src/d_main.c +++ b/src/d_main.c @@ -1021,8 +1021,9 @@ void D_ClearState(void) modeattacking = ATTACKING_NONE; marathonmode = 0; - // Reset GP + // Reset GP and roundqueue memset(&grandprixinfo, 0, sizeof(struct grandprixinfo)); + memset(&roundqueue, 0, sizeof(struct roundqueue)); // empty maptol so mario/etc sounds don't play in sound test when they shouldn't maptol = 0; @@ -1740,13 +1741,12 @@ void D_SRB2Main(void) // Start up a "minor" grand prix session memset(&grandprixinfo, 0, sizeof(struct grandprixinfo)); + memset(&roundqueue, 0, sizeof(struct roundqueue)); grandprixinfo.gamespeed = KARTSPEED_NORMAL; - grandprixinfo.encore = false; grandprixinfo.masterbots = false; grandprixinfo.gp = true; - grandprixinfo.roundnum = 0; grandprixinfo.cup = NULL; grandprixinfo.wonround = false; @@ -1997,11 +1997,6 @@ void D_SRB2Main(void) D_GameTypeChanged(j); } - if (gametyperules & (GTR_BOSS|GTR_CATCHER)) - grandprixinfo.eventmode = GPEVENT_SPECIAL; - else if (gametype != GT_RACE) - grandprixinfo.eventmode = GPEVENT_BONUS; - multiplayer = true; } diff --git a/src/d_netcmd.c b/src/d_netcmd.c index 27cdc9042..9493efefe 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -567,6 +567,8 @@ INT16 numgametypes = GT_FIRSTFREESLOT; boolean forceresetplayers = false; boolean deferencoremode = false; +boolean forcespecialstage = false; + UINT8 splitscreen = 0; INT32 adminplayers[MAXPLAYERS]; @@ -2541,42 +2543,27 @@ INT32 mapchangepending = 0; * 63 old events will get reexecuted, with ridiculous results. Just don't do * it (without setting delay to 1, which is the current solution). * - * \param mapnum Map number to change to. - * \param gametype Gametype to switch to. - * \param pencoremode Is this 'Encore Mode'? - * \param resetplayers 1 to reset player scores and lives and such, 0 not to. - * \param delay Determines how the function will be executed: 0 to do - * it all right now (must not be done from a menu), 1 to - * do step one and prepare step two, 2 to do step two. - * \param skipprecutscene To skip the precutscence or not? + * \param mapnum Map number to change to. + * \param gametype Gametype to switch to. + * \param pencoremode Is this 'Encore Mode'? + * \param presetplayers 1 to reset player scores and lives and such, 0 not to. + * \param delay Determines how the function will be executed: 0 to do + * it all right now (must not be done from a menu), 1 to + * do step one and prepare step two, 2 to do step two. + * \param skipprecutscene To skip the precutscence or not? + * \param pforcespecialstage For certain contexts, forces a special stage. * \sa D_GameTypeChanged, Command_Map_f * \author Graue */ -void D_MapChange(INT32 mapnum, INT32 newgametype, boolean pencoremode, boolean resetplayers, INT32 delay, boolean skipprecutscene, boolean FLS) +void D_MapChange(INT32 mapnum, INT32 newgametype, boolean pencoremode, boolean presetplayers, INT32 delay, boolean skipprecutscene, boolean pforcespecialstage) { - static char buf[2+MAX_WADPATH+1+4]; + static char buf[1+1+1+1+1+2+4]; static char *buf_p = buf; // The supplied data are assumed to be good. I_Assert(delay >= 0 && delay <= 2); - /* - if (mapnum != -1) - { - CV_SetValue(&cv_nextmap, mapnum); - } - */ - - CONS_Debug(DBG_GAMELOGIC, "Map change: mapnum=%d gametype=%d pencoremode=%d resetplayers=%d delay=%d skipprecutscene=%d\n", - mapnum, newgametype, pencoremode, resetplayers, delay, skipprecutscene); - - if ((netgame || multiplayer) && (grandprixinfo.gp != false)) - FLS = false; - - // Too lazy to change the input value for every instance of this function....... - if (grandprixinfo.gp == true) - { - pencoremode = grandprixinfo.encore; - } + CONS_Debug(DBG_GAMELOGIC, "Map change: mapnum=%d gametype=%d pencoremode=%d presetplayers=%d delay=%d skipprecutscene=%d pforcespecialstage = %d\n", + mapnum, newgametype, pencoremode, presetplayers, delay, skipprecutscene, pforcespecialstage); if (delay != 2) { @@ -2585,14 +2572,19 @@ void D_MapChange(INT32 mapnum, INT32 newgametype, boolean pencoremode, boolean r buf_p = buf; if (pencoremode) flags |= 1; - if (!resetplayers) + if (presetplayers) flags |= 1<<1; if (skipprecutscene) flags |= 1<<2; - if (FLS) + if (pforcespecialstage) flags |= 1<<3; WRITEUINT8(buf_p, flags); + // roundqueue state + WRITEUINT8(buf_p, roundqueue.position); + WRITEUINT8(buf_p, roundqueue.size); + WRITEUINT8(buf_p, roundqueue.roundnum); + // new gametype value WRITEUINT8(buf_p, newgametype); @@ -2784,6 +2776,7 @@ static void Command_Map_f(void) size_t option_skill; const char *gametypename; boolean newresetplayers; + boolean newforcespecialstage; boolean usingcheats; boolean ischeating; @@ -2809,6 +2802,7 @@ static void Command_Map_f(void) option_encore = COM_CheckPartialParm("-e"); option_skill = COM_CheckPartialParm("-s"); newresetplayers = ! COM_CheckParm("-noresetplayers"); + newforcespecialstage = COM_CheckParm("-forcespecialstage"); usingcheats = CV_CheatsEnabled(); ischeating = (!(netgame || multiplayer)) || (!newresetplayers); @@ -2946,7 +2940,7 @@ static void Command_Map_f(void) // don't use a gametype the map doesn't support if (cht_debug || option_force || cv_skipmapcheck.value) { - fromlevelselect = false; // The player wants us to trek on anyway. Do so. + // The player wants us to trek on anyway. Do so. } else { @@ -2961,12 +2955,6 @@ static void Command_Map_f(void) Z_Free(mapname); return; } - else - { - fromlevelselect = - ( netgame || multiplayer ) && - grandprixinfo.gp != false; - } } if (!(netgame || multiplayer)) @@ -3011,29 +2999,16 @@ static void Command_Map_f(void) } } - grandprixinfo.encore = newencoremode; - grandprixinfo.gp = true; - grandprixinfo.roundnum = 0; - grandprixinfo.cup = NULL; grandprixinfo.wonround = false; - grandprixinfo.initalize = true; - - grandprixinfo.eventmode = GPEVENT_NONE; - - if (gametypes[newgametype]->rules & (GTR_BOSS|GTR_CATCHER)) - { - grandprixinfo.eventmode = GPEVENT_SPECIAL; - } - else if (newgametype != GT_RACE) - { - grandprixinfo.eventmode = GPEVENT_BONUS; - } if (!Playing()) { UINT8 ssplayers = cv_splitplayers.value-1; + grandprixinfo.cup = NULL; + grandprixinfo.initalize = true; + multiplayer = true; restoreMenu = NULL; @@ -3050,7 +3025,7 @@ static void Command_Map_f(void) } } - D_MapChange(newmapnum, newgametype, newencoremode, newresetplayers, 0, false, fromlevelselect); + D_MapChange(newmapnum, newgametype, newencoremode, newresetplayers, 0, false, newforcespecialstage); Z_Free(realmapname); } @@ -3065,8 +3040,8 @@ static void Command_Map_f(void) static void Got_Mapcmd(UINT8 **cp, INT32 playernum) { UINT8 flags; - INT32 resetplayer = 1, lastgametype; - UINT8 skipprecutscene, FLS; + INT32 presetplayer = 1, lastgametype; + UINT8 skipprecutscene, pforcespecialstage; boolean pencoremode; INT16 mapnumber; @@ -3087,7 +3062,15 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum) pencoremode = ((flags & 1) != 0); - resetplayer = ((flags & (1<<1)) == 0); + presetplayer = ((flags & (1<<1)) != 0); + + skipprecutscene = ((flags & (1<<2)) != 0); + + pforcespecialstage = ((flags & (1<<3)) != 0); + + roundqueue.position = READUINT8(*cp); + roundqueue.size = READUINT8(*cp); + roundqueue.roundnum = READUINT8(*cp); lastgametype = gametype; gametype = READUINT8(*cp); @@ -3101,35 +3084,46 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum) if (!(gametyperules & GTR_ENCORE)) pencoremode = false; - skipprecutscene = ((flags & (1<<2)) != 0); - - FLS = ((flags & (1<<3)) != 0); - mapnumber = READINT16(*cp); + // Handle some Grand Prix state. + if (grandprixinfo.gp) + { + boolean caughtretry = (gametype == lastgametype + && mapnumber == gamemap); + if (pforcespecialstage // Forced. + || (caughtretry && grandprixinfo.eventmode == GPEVENT_SPECIAL) // Catch retries of forced. + || (gametyperules & (GTR_BOSS|GTR_CATCHER))) // Conventional rules. + { + grandprixinfo.eventmode = GPEVENT_SPECIAL; + } + else if (gametype != GT_RACE) + { + grandprixinfo.eventmode = GPEVENT_BONUS; + } + else + { + grandprixinfo.eventmode = GPEVENT_NONE; + } + } + if (netgame) P_ClearRandom(READUINT32(*cp)); if (!skipprecutscene) { DEBFILE(va("Warping to %s [resetplayer=%d lastgametype=%d gametype=%d cpnd=%d]\n", - G_BuildMapName(mapnumber), resetplayer, lastgametype, gametype, chmappending)); + G_BuildMapName(mapnumber), presetplayer, lastgametype, gametype, chmappending)); CON_LogMessage(M_GetText("Speeding off to level...\n")); } if (demo.playback && !demo.timing) precache = false; - if (resetplayer && !FLS) - { - emeralds = 0; - memset(&luabanks, 0, sizeof(luabanks)); - } - demo.savemode = (cv_recordmultiplayerdemos.value == 2) ? DSM_WILLAUTOSAVE : DSM_NOTSAVING; demo.savebutton = 0; - G_InitNew(pencoremode, mapnumber, resetplayer, skipprecutscene, FLS); + G_InitNew(pencoremode, mapnumber, presetplayer, skipprecutscene); if (demo.playback && !demo.timing) precache = true; if (demo.timing) diff --git a/src/d_netcmd.h b/src/d_netcmd.h index 8cab1e10f..908c6aec7 100644 --- a/src/d_netcmd.h +++ b/src/d_netcmd.h @@ -239,7 +239,7 @@ void D_SendPlayerConfig(UINT8 n); void Command_ExitGame_f(void); void Command_Retry_f(void); void D_GameTypeChanged(INT32 lastgametype); // not a real _OnChange function anymore -void D_MapChange(INT32 pmapnum, INT32 pgametype, boolean pencoremode, boolean presetplayers, INT32 pdelay, boolean pskipprecutscene, boolean pfromlevelselect); +void D_MapChange(INT32 pmapnum, INT32 pgametype, boolean pencoremode, boolean presetplayers, INT32 pdelay, boolean pskipprecutscene, boolean pforcespecialstage); void D_SetupVote(void); void D_ModifyClientVote(UINT8 player, SINT8 voted); void D_PickVote(void); diff --git a/src/doomstat.h b/src/doomstat.h index a16e5baa8..72a3a8c71 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -182,8 +182,7 @@ extern boolean multiplayer; extern UINT8 splitscreen; extern int r_splitscreen; -extern boolean fromlevelselect; -extern boolean forceresetplayers, deferencoremode; +extern boolean forceresetplayers, deferencoremode, forcespecialstage; // ======================================== // Internal parameters for sound rendering. diff --git a/src/g_demo.c b/src/g_demo.c index da617188a..f5e9bcfdd 100644 --- a/src/g_demo.c +++ b/src/g_demo.c @@ -3463,7 +3463,7 @@ void G_DoPlayDemo(char *defdemoname) P_SetRandSeed(i, randseed[i]); } - G_InitNew(demoflags & DF_ENCORE, gamemap, true, true, false); // Doesn't matter whether you reset or not here, given changes to resetplayer. + G_InitNew(demoflags & DF_ENCORE, gamemap, true, true); // Doesn't matter whether you reset or not here, given changes to resetplayer. for (i = 0; i < numslots; i++) { diff --git a/src/g_game.c b/src/g_game.c index f5173e642..16ef27cbe 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -3988,117 +3988,201 @@ static void G_HandleSaveLevel(void) } } +// Next map apparatus +struct roundqueue roundqueue; + +void G_MapIntoRoundQueue(UINT16 map, UINT8 setgametype, boolean setencore, boolean rankrestricted) +{ + if (roundqueue.size >= ROUNDQUEUE_MAX) + { + CONS_Alert(CONS_ERROR, "G_MapIntoRoundQueue: Unable to add map beyond %u\n", roundqueue.size); + return; + } + + roundqueue.entries[roundqueue.size].mapnum = map; + roundqueue.entries[roundqueue.size].gametype = setgametype; + roundqueue.entries[roundqueue.size].encore = setencore; + roundqueue.entries[roundqueue.size].rankrestricted = rankrestricted; + + roundqueue.size++; +} + +void G_GPCupIntoRoundQueue(cupheader_t *cup, UINT8 setgametype, boolean setencore) +{ + UINT8 i, levelindex = 0, bonusindex = 0; + UINT8 bonusmodulo = (cup->numlevels+1)/(cup->numbonus+1); + UINT16 cupLevelNum; + + // Levels are added to the queue in the following pattern. + // For 5 Race rounds and 2 Bonus rounds, the most common case: + // race - race - BONUS - race - race - BONUS - race + // The system is flexible enough to permit other arrangements. + // However, we just want to keep the pacing even & consistent. + while (levelindex < cup->numlevels) + { + // Fill like two or three Race maps. + for (i = 0; i < bonusmodulo; i++) + { + cupLevelNum = cup->cachedlevels[levelindex]; + + if (cupLevelNum >= nummapheaders) + { + // For invalid Race maps, we keep the pacing by going to TEST RUN. + // It transparently lets the user know something is wrong. + cupLevelNum = 0; + } + + G_MapIntoRoundQueue( + cupLevelNum, + setgametype, + setencore, // *probably* correct + false + ); + + levelindex++; + if (levelindex >= cup->numlevels) + break; + } + + // Attempt to add an interstitial Bonus round. + if (levelindex < cup->numlevels + && bonusindex < cup->numbonus) + { + cupLevelNum = cup->cachedlevels[CUPCACHE_BONUS + bonusindex]; + + if (cupLevelNum < nummapheaders) + { + // In the case of Bonus rounds, we simply skip invalid maps. + G_MapIntoRoundQueue( + cupLevelNum, + G_GuessGametypeByTOL(mapheaderinfo[cupLevelNum]->typeoflevel), + setencore, // if this isn't correct, Got_Mapcmd will fix it + false + ); + } + + bonusindex++; + } + } + + // ...but there's one last trick up our sleeves. + // At the end of the Cup is a Rank-restricted treat. + // So we append it to the end of the roundqueue. + // (as long as it exists, of course!) + cupLevelNum = cup->cachedlevels[CUPCACHE_SPECIAL]; + if (cupLevelNum < nummapheaders) + { + G_MapIntoRoundQueue( + cupLevelNum, + G_GuessGametypeByTOL(mapheaderinfo[cupLevelNum]->typeoflevel), + setencore, // if this isn't correct, Got_Mapcmd will fix it + true // Rank-restricted! + ); + } + + if (roundqueue.size == 0) + { + I_Error("G_CupToRoundQueue: roundqueue size is 0 after population!?"); + } +} + static void G_GetNextMap(void) { INT32 i; + boolean setalready = false; + + if (!server) + { + // Server is authoriative, not you + return; + } + + deferencoremode = (cv_kartencore.value == 1); + forceresetplayers = forcespecialstage = false; // go to next level // nextmap is 0-based, unlike gamemap if (nextmapoverride != 0) { nextmap = (INT16)(nextmapoverride-1); + setalready = true; } - else if (grandprixinfo.gp == true) + else if (roundqueue.size > 0) { - if (grandprixinfo.roundnum == 0 || grandprixinfo.cup == NULL) // Single session + boolean permitrank = false; + if (grandprixinfo.gp == true + && grandprixinfo.gamespeed >= KARTSPEED_NORMAL) { - nextmap = prevmap; // Same map + // On A rank pace? Then you get a chance for S rank! + permitrank = (K_CalculateGPGrade(&grandprixinfo.rank) >= GRADE_A); + } + + while (roundqueue.position < roundqueue.size + && (roundqueue.entries[roundqueue.position].mapnum >= nummapheaders + || (permitrank == false && roundqueue.entries[roundqueue.position].rankrestricted == true))) + { + // Skip all restricted queue entries. + roundqueue.position++; + } + + if (roundqueue.position < roundqueue.size) + { + // The next entry in the queue is valid; set it as nextmap! + nextmap = roundqueue.entries[roundqueue.position].mapnum; + deferencoremode = roundqueue.entries[roundqueue.position].encore; + + // And we handle gametype changes, too. + if (roundqueue.entries[roundqueue.position].gametype != gametype) + { + INT32 lastgametype = gametype; + G_SetGametype(roundqueue.entries[roundqueue.position].gametype); + D_GameTypeChanged(lastgametype); + } + + // On entering roundqueue mode, kill the non-PWR between-round scores. + // This makes it viable as a future tournament mode base. + if (roundqueue.position == 0) + { + forceresetplayers = true; + } + + // Handle primary queue position update. + roundqueue.position++; + if (grandprixinfo.gp == false || gametype == roundqueue.entries[0].gametype) + { + roundqueue.roundnum++; + } + + setalready = true; } else { - INT32 lastgametype = gametype, newgametype = GT_RACE; - // 5 levels, 2 bonus stages: after rounds 2 and 4 (but flexible enough to accomodate other solutions) - UINT8 bonusmodulo = (grandprixinfo.cup->numlevels+1)/(grandprixinfo.cup->numbonus+1); - UINT8 bonusindex = (grandprixinfo.roundnum / bonusmodulo) - 1; + // Wipe the queue info. + memset(&roundqueue, 0, sizeof(struct roundqueue)); - // If we're in a GP event, don't immediately follow it up with another. - // I also suspect this will not work with online GP so I'm gonna prevent it right now. - // The server might have to communicate eventmode (alongside other GP data) in XD_MAP later. - if (netgame) - ; - else if (grandprixinfo.eventmode != GPEVENT_NONE) + if (grandprixinfo.gp == true) { - grandprixinfo.eventmode = GPEVENT_NONE; - - G_SetGametype(GT_RACE); - if (gametype != lastgametype) - D_GameTypeChanged(lastgametype); - } - // Special stage - else if (grandprixinfo.roundnum >= grandprixinfo.cup->numlevels) - { - gp_rank_e grade = K_CalculateGPGrade(&grandprixinfo.rank); - - if (grade >= GRADE_A && grandprixinfo.gamespeed >= KARTSPEED_NORMAL) // On A rank pace? Then you get a chance for S rank! - { - const INT32 cupLevelNum = grandprixinfo.cup->cachedlevels[CUPCACHE_SPECIAL]; - if (cupLevelNum < nummapheaders && mapheaderinfo[cupLevelNum]) - { - grandprixinfo.eventmode = GPEVENT_SPECIAL; - nextmap = cupLevelNum; - newgametype = G_GuessGametypeByTOL(mapheaderinfo[cupLevelNum]->typeoflevel); - - if (gamedata->everseenspecial == false) - { - gamedata->everseenspecial = true; - M_UpdateUnlockablesAndExtraEmblems(true, true); - G_SaveGameData(); - } - } - } - } - else if ((grandprixinfo.cup->numbonus > 0) - && (grandprixinfo.roundnum % bonusmodulo) == 0 - && bonusindex < MAXBONUSLIST) - { - // todo any other condition? - { - const INT32 cupLevelNum = grandprixinfo.cup->cachedlevels[CUPCACHE_BONUS + bonusindex]; - if (cupLevelNum < nummapheaders && mapheaderinfo[cupLevelNum]) - { - grandprixinfo.eventmode = GPEVENT_BONUS; - nextmap = cupLevelNum; - newgametype = G_GuessGametypeByTOL(mapheaderinfo[cupLevelNum]->typeoflevel); - } - } - } - - if (newgametype == -1) - { - // Don't permit invalid changes. - grandprixinfo.eventmode = GPEVENT_NONE; - newgametype = gametype; - } - - if (grandprixinfo.eventmode != GPEVENT_NONE) - { - G_SetGametype(newgametype); - if (gametype != lastgametype) - D_GameTypeChanged(lastgametype); - } - else if (grandprixinfo.roundnum >= grandprixinfo.cup->numlevels) // On final map - { - nextmap = NEXTMAP_CEREMONY; // ceremonymap + // In GP, we're now ready to go to the ceremony. + nextmap = NEXTMAP_CEREMONY; + setalready = true; } else { - // Proceed to next map - const INT32 cupLevelNum = grandprixinfo.cup->cachedlevels[grandprixinfo.roundnum]; - - if (cupLevelNum < nummapheaders && mapheaderinfo[cupLevelNum]) - { - nextmap = cupLevelNum; - } - else - { - nextmap = 0; // Prevent uninitialised use -- go to TEST RUN, it's very obvious - } - - grandprixinfo.roundnum++; + // On exiting roundqueue mode, kill the non-PWR between-round scores. + // This prevents future tournament winners from carrying their wins out. + forceresetplayers = true; } } } - else + else if (grandprixinfo.gp == true) + { + // Fast And Rapid Testing + // this codepath is exclusively accessible through console/command line + nextmap = prevmap; + setalready = true; + } + + if (setalready == false) { UINT32 tolflag = G_TOLFlag(gametype); register INT16 cm; @@ -4242,8 +4326,6 @@ static void G_GetNextMap(void) if (!spec) #endif //#if 0 lastmap = nextmap; - - deferencoremode = (cv_kartencore.value == 1); } // @@ -4417,8 +4499,6 @@ void G_NextLevel(void) return; } - forceresetplayers = false; - gameaction = ga_worlddone; } @@ -4433,7 +4513,7 @@ static void G_DoWorldDone(void) forceresetplayers, 0, false, - false); + forcespecialstage); } gameaction = ga_nothing; @@ -5406,14 +5486,12 @@ void G_DeferedInitNew(boolean pencoremode, INT32 map, INT32 pickedchar, UINT8 ss // This is the map command interpretation something like Command_Map_f // // called at: map cmd execution, doloadgame, doplaydemo -void G_InitNew(UINT8 pencoremode, INT32 map, boolean resetplayer, boolean skipprecutscene, boolean FLS) +void G_InitNew(UINT8 pencoremode, INT32 map, boolean resetplayer, boolean skipprecutscene) { const char * mapname = G_BuildMapName(map); INT32 i; - (void)FLS; - if (paused) { paused = false; @@ -5440,7 +5518,7 @@ void G_InitNew(UINT8 pencoremode, INT32 map, boolean resetplayer, boolean skippr players[i].roundscore = 0; - if (resetplayer && !(multiplayer && demo.playback)) // SRB2Kart + if (resetplayer && !demo.playback) // SRB2Kart { players[i].lives = 3; players[i].xtralife = 0; diff --git a/src/g_game.h b/src/g_game.h index 9fe875c09..da957937a 100644 --- a/src/g_game.h +++ b/src/g_game.h @@ -53,6 +53,27 @@ typedef enum NEXTMAP_SPECIAL = NEXTMAP_INVALID } nextmapspecial_t; +#define ROUNDQUEUE_MAX 10 // sane max? maybe make dynamically allocated later + +struct roundentry_t +{ + UINT16 mapnum; // Map number at this position + UINT8 gametype; // Gametype we want to play this in + boolean encore; // Whether this will be flipped + boolean rankrestricted; // For grand prix progression +}; + +extern struct roundqueue +{ + UINT8 roundnum; // Visible number on HUD + UINT8 position; // Head position in the round queue + UINT8 size; // Number of entries in the round queue + roundentry_t entries[ROUNDQUEUE_MAX]; // Entries in the round queue +} roundqueue; + +void G_MapIntoRoundQueue(UINT16 map, UINT8 setgametype, boolean setencore, boolean rankrestricted); +void G_GPCupIntoRoundQueue(cupheader_t *cup, UINT8 setgametype, boolean setencore); + extern INT32 gameovertics; extern UINT8 ammoremovaltics; extern tic_t timeinmap; // Ticker for time spent in level (used for levelcard display) @@ -117,7 +138,7 @@ void G_ChangePlayerReferences(mobj_t *oldmo, mobj_t *newmo); void G_DoReborn(INT32 playernum); void G_PlayerReborn(INT32 player, boolean betweenmaps); void G_InitNew(UINT8 pencoremode, INT32 map, boolean resetplayer, - boolean skipprecutscene, boolean FLS); + boolean skipprecutscene); char *G_BuildMapTitle(INT32 mapnum); struct searchdim diff --git a/src/hu_stuff.c b/src/hu_stuff.c index e5eb0aed9..5b028662d 100644 --- a/src/hu_stuff.c +++ b/src/hu_stuff.c @@ -2482,24 +2482,26 @@ static void HU_DrawRankings(void) V_DrawString(4, 188, hilicol|V_SNAPTOBOTTOM|V_SNAPTOLEFT, gametypes[gametype]->name); // Left hand side - if (grandprixinfo.gp == true) + if (grandprixinfo.gp == true && grandprixinfo.eventmode != GPEVENT_NONE) { const char *roundstr = NULL; V_DrawCenteredString(64, 8, 0, "ROUND"); switch (grandprixinfo.eventmode) { - case GPEVENT_BONUS: - roundstr = "BONUS"; - break; case GPEVENT_SPECIAL: roundstr = "SPECIAL"; break; default: - roundstr = va("%d", grandprixinfo.roundnum); + roundstr = "BONUS"; break; } V_DrawCenteredString(64, 16, hilicol, roundstr); } + else if (roundqueue.size > 0) + { + V_DrawCenteredString(64, 8, 0, "ROUND"); + V_DrawCenteredString(64, 16, hilicol, va("%d", roundqueue.roundnum)); + } else if ((gametyperules & GTR_TIMELIMIT) && timelimitintics > 0) { UINT32 timeval = (timelimitintics + starttime + 1 - leveltime); diff --git a/src/k_grandprix.c b/src/k_grandprix.c index 73cc0d27b..52c82d7a3 100644 --- a/src/k_grandprix.c +++ b/src/k_grandprix.c @@ -283,10 +283,10 @@ static INT16 K_RivalScore(player_t *bot) UINT8 lowestdifficulty = MAXBOTDIFFICULTY; UINT8 i; - if (grandprixinfo.cup != NULL) + if (grandprixinfo.cup != NULL && roundqueue.size > 0) { - roundnum = grandprixinfo.roundnum; - roundsleft = grandprixinfo.cup->numlevels - grandprixinfo.roundnum; + roundnum = roundqueue.roundnum; + roundsleft = grandprixinfo.cup->numlevels - roundnum; } for (i = 0; i < MAXPLAYERS; i++) @@ -523,8 +523,10 @@ void K_RetireBots(void) UINT8 i; if (grandprixinfo.gp == true - && ((grandprixinfo.cup != NULL && grandprixinfo.roundnum >= grandprixinfo.cup->numlevels) - || grandprixinfo.eventmode != GPEVENT_NONE)) + && (grandprixinfo.eventmode != GPEVENT_NONE + || (grandprixinfo.cup != NULL + && roundqueue.size > 0 + && roundqueue.roundnum >= grandprixinfo.cup->numlevels))) { // No replacement. return; @@ -566,7 +568,11 @@ void K_RetireBots(void) else { const UINT8 startingdifficulty = K_BotStartingDifficulty(grandprixinfo.gamespeed); - newDifficulty = startingdifficulty - 4 + grandprixinfo.roundnum; + newDifficulty = startingdifficulty - 4; + if (roundqueue.size > 0) + { + newDifficulty += roundqueue.roundnum; + } } if (newDifficulty > MAXBOTDIFFICULTY) @@ -697,7 +703,7 @@ void K_PlayerLoseLife(player_t *player) --------------------------------------------------*/ boolean K_CanChangeRules(boolean allowdemos) { - if (grandprixinfo.gp == true /*&& grandprixinfo.roundnum > 0*/) + if (grandprixinfo.gp == true) { // Don't cheat the rules of the GP! return false; diff --git a/src/k_grandprix.h b/src/k_grandprix.h index 891efc85c..ba19bdb61 100644 --- a/src/k_grandprix.h +++ b/src/k_grandprix.h @@ -31,7 +31,6 @@ typedef enum extern struct grandprixinfo { boolean gp; ///< If true, then we are in a Grand Prix. - UINT8 roundnum; ///< Round number. If 0, this is a single session from the warp command. cupheader_t *cup; ///< Which cup are we playing? UINT8 gamespeed; ///< Copy of gamespeed, just to make sure you can't cheat it with cvars boolean encore; ///< Ditto, but for encore mode diff --git a/src/k_menufunc.c b/src/k_menufunc.c index 7c798915a..fd17477eb 100644 --- a/src/k_menufunc.c +++ b/src/k_menufunc.c @@ -32,7 +32,6 @@ I_mutex k_menu_mutex; #endif boolean menuactive = false; -boolean fromlevelselect = false; // current menudef menu_t *currentMenu = &MAIN_ProfilesDef; diff --git a/src/menus/transient/cup-select.c b/src/menus/transient/cup-select.c index f4f97ed29..8c2ed8fe1 100644 --- a/src/menus/transient/cup-select.c +++ b/src/menus/transient/cup-select.c @@ -104,7 +104,6 @@ void M_CupSelectHandler(INT32 choice) if (cupgrid.grandprix == true) { - INT32 levelNum; UINT8 ssplayers = cv_splitplayers.value-1; S_StartSound(NULL, sfx_s3k63); @@ -130,13 +129,15 @@ void M_CupSelectHandler(INT32 choice) grandprixinfo.gamespeed = min(KARTSPEED_HARD, cv_dummygpdifficulty.value); grandprixinfo.masterbots = (cv_dummygpdifficulty.value == 3); - grandprixinfo.encore = (boolean)cv_dummygpencore.value; - - grandprixinfo.cup = newcup; grandprixinfo.gp = true; - grandprixinfo.roundnum = 1; grandprixinfo.initalize = true; + grandprixinfo.cup = newcup; + + // Populate the roundqueue + memset(&roundqueue, 0, sizeof(struct roundqueue)); + G_GPCupIntoRoundQueue(newcup, levellist.newgametype, (boolean)cv_dummygpencore.value); + roundqueue.position = roundqueue.roundnum = 1; paused = false; @@ -146,16 +147,14 @@ void M_CupSelectHandler(INT32 choice) SV_StartSinglePlayerServer(levellist.newgametype, levellist.netgame); } - levelNum = grandprixinfo.cup->cachedlevels[0]; - D_MapChange( - levelNum + 1, - GT_RACE, - grandprixinfo.encore, + roundqueue.entries[0].mapnum + 1, + roundqueue.entries[0].gametype, + roundqueue.entries[0].encore, true, 1, false, - false + roundqueue.entries[0].rankrestricted ); M_ClearMenus(true); diff --git a/src/p_saveg.c b/src/p_saveg.c index d66ea3f20..536d241dc 100644 --- a/src/p_saveg.c +++ b/src/p_saveg.c @@ -49,15 +49,16 @@ savedata_t savedata; // Block UINT32s to attempt to ensure that the correct data is // being sent and received -#define ARCHIVEBLOCK_MISC 0x7FEEDEED -#define ARCHIVEBLOCK_PLAYERS 0x7F448008 -#define ARCHIVEBLOCK_PARTIES 0x7F87AF0C -#define ARCHIVEBLOCK_WORLD 0x7F8C08C0 -#define ARCHIVEBLOCK_POBJS 0x7F928546 -#define ARCHIVEBLOCK_THINKERS 0x7F37037C -#define ARCHIVEBLOCK_SPECIALS 0x7F228378 -#define ARCHIVEBLOCK_WAYPOINTS 0x7F46498F -#define ARCHIVEBLOCK_RNG 0x7FAAB5BD +#define ARCHIVEBLOCK_MISC 0x7FEEDEED +#define ARCHIVEBLOCK_PLAYERS 0x7F448008 +#define ARCHIVEBLOCK_PARTIES 0x7F87AF0C +#define ARCHIVEBLOCK_ROUNDQUEUE 0x7F721331 +#define ARCHIVEBLOCK_WORLD 0x7F8C08C0 +#define ARCHIVEBLOCK_POBJS 0x7F928546 +#define ARCHIVEBLOCK_THINKERS 0x7F37037C +#define ARCHIVEBLOCK_SPECIALS 0x7F228378 +#define ARCHIVEBLOCK_WAYPOINTS 0x7F46498F +#define ARCHIVEBLOCK_RNG 0x7FAAB5BD // Note: This cannot be bigger // than an UINT16 @@ -932,6 +933,48 @@ static void P_NetUnArchiveParties(savebuffer_t *save) } } +static void P_NetArchiveRoundQueue(savebuffer_t *save) +{ + UINT8 i; + WRITEUINT32(save->p, ARCHIVEBLOCK_ROUNDQUEUE); + + WRITEUINT8(save->p, roundqueue.position); + WRITEUINT8(save->p, roundqueue.size); + WRITEUINT8(save->p, roundqueue.roundnum); + + for (i = 0; i < roundqueue.size; i++) + { + //WRITEUINT16(save->p, roundqueue.entries[i].mapnum); + /* NOPE! Clients do not need to know what is in the roundqueue. + This disincentivises cheaty clients in future tournament environments. + ~toast 080423 */ + WRITEUINT8(save->p, roundqueue.entries[i].gametype); + WRITEUINT8(save->p, (UINT8)roundqueue.entries[i].encore); + WRITEUINT8(save->p, (UINT8)roundqueue.entries[i].rankrestricted); + } +} + +static void P_NetUnArchiveRoundQueue(savebuffer_t *save) +{ + UINT8 i; + if (READUINT32(save->p) != ARCHIVEBLOCK_ROUNDQUEUE) + I_Error("Bad $$$.sav at archive block Parties"); + + memset(&roundqueue, 0, sizeof(struct roundqueue)); + + roundqueue.position = READUINT8(save->p); + roundqueue.size = READUINT8(save->p); + roundqueue.roundnum = READUINT8(save->p); + + for (i = 0; i < roundqueue.size; i++) + { + roundqueue.entries[i].mapnum = 0; // TEST RUN -- dummy, has to be < nummapheaders + roundqueue.entries[i].gametype = READUINT8(save->p); + roundqueue.entries[i].encore = (boolean)READUINT8(save->p); + roundqueue.entries[i].rankrestricted = (boolean)READUINT8(save->p); + } +} + /// /// Colormaps /// @@ -5400,6 +5443,7 @@ void P_SaveNetGame(savebuffer_t *save, boolean resending) P_NetArchivePlayers(save); P_NetArchiveParties(save); + P_NetArchiveRoundQueue(save); if (gamestate == GS_LEVEL) { @@ -5450,6 +5494,7 @@ boolean P_LoadNetGame(savebuffer_t *save, boolean reloading) P_NetUnArchivePlayers(save); P_NetUnArchiveParties(save); + P_NetUnArchiveRoundQueue(save); if (gamestate == GS_LEVEL) { diff --git a/src/st_stuff.c b/src/st_stuff.c index 5dde5c826..9604d66a8 100644 --- a/src/st_stuff.c +++ b/src/st_stuff.c @@ -696,7 +696,7 @@ void ST_runTitleCard(void) { boolean run = !(paused || P_AutoPause()); INT32 auxticker; - boolean gp = (marathonmode || (grandprixinfo.gp && grandprixinfo.roundnum)); + boolean doroundicon = (marathonmode || roundqueue.size > 0); if (!G_IsTitleCardAvailable()) return; @@ -818,7 +818,7 @@ void ST_runTitleCard(void) roundnumy = eggy1; // split both halves of the egg, but only do that in grand prix! - if (gp && lt_ticker > TTANIMTHRESHOLD + TICRATE/2) + if (doroundicon && lt_ticker > TTANIMTHRESHOLD + TICRATE/2) { auxticker = (INT32)lt_ticker - (TTANIMTHRESHOLD + TICRATE/2); @@ -881,7 +881,7 @@ void ST_drawTitleCard(void) char *lvlttl = mapheaderinfo[gamemap-1]->lvlttl; char *zonttl = mapheaderinfo[gamemap-1]->zonttl; // SRB2kart UINT8 actnum = mapheaderinfo[gamemap-1]->actnum; - boolean gp = (marathonmode || (grandprixinfo.gp && grandprixinfo.roundnum)); + boolean doroundicon = (marathonmode || roundqueue.size > 0); INT32 acttimer; fixed_t actscale; @@ -1011,9 +1011,9 @@ void ST_drawTitleCard(void) // Draw ROUND bar, scroll it downwards. V_DrawFixedPatch(roundx*FRACUNIT, ((-32) + (lt_ticker%32))*FRACUNIT, FRACUNIT, V_SNAPTOTOP|V_SNAPTOLEFT, tcroundbar, NULL); // Draw ROUND text - if (gp) + if (doroundicon) V_DrawFixedPatch((roundx+10)*FRACUNIT, roundy*FRACUNIT, FRACUNIT, V_SNAPTOTOP|V_SNAPTOLEFT, - ((grandprixinfo.gp && grandprixinfo.eventmode) ? tcbonus : tcround), + ((grandprixinfo.gp && grandprixinfo.eventmode != GPEVENT_NONE) ? tcbonus : tcround), NULL); // round num background @@ -1032,25 +1032,29 @@ void ST_drawTitleCard(void) } // If possible, draw round number/icon - if (gp) + if (doroundicon) { patch_t *roundico = NULL; if (marathonmode) ; // TODO: Ruby - else switch (grandprixinfo.eventmode) + else if (grandprixinfo.gp == true && grandprixinfo.eventmode != GPEVENT_NONE) { - case GPEVENT_BONUS: - roundico = tcroundbonus; // TODO don't show capsule if we have other bonus types - break; - /*case GPEVENT_SPECIAL: - ; // TODO: Emerald/mount - break;*/ - case GPEVENT_NONE: - if (grandprixinfo.roundnum > 0 && grandprixinfo.roundnum < 11) // Check boundaries JUST IN CASE. - roundico = tcroundnum[grandprixinfo.roundnum-1]; - break; - default: - break; + switch (grandprixinfo.eventmode) + { + case GPEVENT_BONUS: + roundico = tcroundbonus; // TODO don't show capsule if we have other bonus types + break; + /*case GPEVENT_SPECIAL: + ; // TODO: Emerald/mount + break;*/ + default: + break; + } + } + else if (roundqueue.size > 0) + { + if (roundqueue.roundnum > 0 && roundqueue.roundnum < 11) // We DEFINITELY need to check boundaries. + roundico = tcroundnum[roundqueue.roundnum-1]; } if (roundico) diff --git a/src/typedef.h b/src/typedef.h index 2683bff70..416fd36f9 100644 --- a/src/typedef.h +++ b/src/typedef.h @@ -135,6 +135,7 @@ TYPEDEF (menudemo_t); TYPEDEF (demoghost); // g_game.h +TYPEDEF (roundentry_t); TYPEDEF (mapsearchfreq_t); // hu_stuff.h From a08683e8197b42212aa817ee6446cc3cfd7dac9d Mon Sep 17 00:00:00 2001 From: toaster Date: Mon, 10 Apr 2023 13:33:58 +0100 Subject: [PATCH 02/17] Improved round-queue state net communication - Only send from the server when an update to roundqueue state is relevant - Perform sanity checking on reciept - Initialise when map command is sent with roundqueue size greater than the client's - Correct gametype/encore state on reciept - Only permit from the server, forbid admin clients from providing it on penalty of kick --- src/d_netcmd.c | 61 ++++++++++++++++++++++++++------ src/g_game.c | 3 ++ src/g_game.h | 1 + src/menus/transient/cup-select.c | 1 + 4 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/d_netcmd.c b/src/d_netcmd.c index 9493efefe..ae08f0e42 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -2578,12 +2578,18 @@ void D_MapChange(INT32 mapnum, INT32 newgametype, boolean pencoremode, boolean p flags |= 1<<2; if (pforcespecialstage) flags |= 1<<3; + if (roundqueue.netcommunicate) + flags |= 1<<4; WRITEUINT8(buf_p, flags); - // roundqueue state - WRITEUINT8(buf_p, roundqueue.position); - WRITEUINT8(buf_p, roundqueue.size); - WRITEUINT8(buf_p, roundqueue.roundnum); + if (roundqueue.netcommunicate) + { + // roundqueue state + WRITEUINT8(buf_p, roundqueue.position); + WRITEUINT8(buf_p, roundqueue.size); + WRITEUINT8(buf_p, roundqueue.roundnum); + roundqueue.netcommunicate = false; + } // new gametype value WRITEUINT8(buf_p, newgametype); @@ -3042,7 +3048,7 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum) UINT8 flags; INT32 presetplayer = 1, lastgametype; UINT8 skipprecutscene, pforcespecialstage; - boolean pencoremode; + boolean pencoremode, hasroundqueuedata; INT16 mapnumber; forceresetplayers = deferencoremode = false; @@ -3055,9 +3061,6 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum) return; } - if (chmappending) - chmappending--; - flags = READUINT8(*cp); pencoremode = ((flags & 1) != 0); @@ -3068,9 +3071,38 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum) pforcespecialstage = ((flags & (1<<3)) != 0); - roundqueue.position = READUINT8(*cp); - roundqueue.size = READUINT8(*cp); - roundqueue.roundnum = READUINT8(*cp); + hasroundqueuedata = ((flags & (1<<4)) != 0); + + if (hasroundqueuedata) + { + UINT8 position = READUINT8(*cp); + UINT8 size = READUINT8(*cp); + UINT8 roundnum = READUINT8(*cp); + + if (playernum != serverplayer // Clients, even admin clients, don't have full roundqueue data + || position > size // Sanity check A (intentionally not a >= comparison) + || size > ROUNDQUEUE_MAX) // Sanity Check B (ditto) + { + CONS_Alert(CONS_WARNING, M_GetText("Illegal round-queue data received from %s\n"), player_names[playernum]); + if (server) + SendKick(playernum, KICK_MSG_CON_FAIL); + return; + } + + roundqueue.position = position; + while (roundqueue.size < size) + { + // We wipe rather than provide full data to prevent bloating the packet, + // and because only this data is necessary for sync. ~toast 100423 + memset(&roundqueue.entries[roundqueue.size], 0, sizeof(roundentry_t)); + roundqueue.size++; + } + roundqueue.roundnum = roundnum; // no sanity checking required, server is authoriative + } + + // No more kicks below this line, we can now start modifying state beyond this function. + if (chmappending) + chmappending--; lastgametype = gametype; gametype = READUINT8(*cp); @@ -3081,6 +3113,13 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum) else if (gametype != lastgametype) D_GameTypeChanged(lastgametype); // emulate consvar_t behavior for gametype + if (hasroundqueuedata && roundqueue.position > 0 && roundqueue.size > 0) + { + // ...we can evaluate CURRENT specifics for roundqueue data, though. + roundqueue.entries[roundqueue.position-1].gametype = gametype; + roundqueue.entries[roundqueue.position-1].encore = pencoremode; + } + if (!(gametyperules & GTR_ENCORE)) pencoremode = false; diff --git a/src/g_game.c b/src/g_game.c index 16ef27cbe..9f273ca49 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -4173,6 +4173,9 @@ static void G_GetNextMap(void) forceresetplayers = true; } } + + // Make sure the next D_MapChange sends updated roundqueue state. + roundqueue.netcommunicate = true; } else if (grandprixinfo.gp == true) { diff --git a/src/g_game.h b/src/g_game.h index da957937a..e568ab49a 100644 --- a/src/g_game.h +++ b/src/g_game.h @@ -68,6 +68,7 @@ extern struct roundqueue UINT8 roundnum; // Visible number on HUD UINT8 position; // Head position in the round queue UINT8 size; // Number of entries in the round queue + boolean netcommunicate; // As server, should we net-communicate this in XD_MAP? roundentry_t entries[ROUNDQUEUE_MAX]; // Entries in the round queue } roundqueue; diff --git a/src/menus/transient/cup-select.c b/src/menus/transient/cup-select.c index 8c2ed8fe1..a83d5f2c9 100644 --- a/src/menus/transient/cup-select.c +++ b/src/menus/transient/cup-select.c @@ -138,6 +138,7 @@ void M_CupSelectHandler(INT32 choice) memset(&roundqueue, 0, sizeof(struct roundqueue)); G_GPCupIntoRoundQueue(newcup, levellist.newgametype, (boolean)cv_dummygpencore.value); roundqueue.position = roundqueue.roundnum = 1; + roundqueue.netcommunicate = true; // relevant for future Online GP paused = false; From 5751ecdcd1da69fb8456c0120e2fcbc8e164f89d Mon Sep 17 00:00:00 2001 From: toaster Date: Mon, 10 Apr 2023 13:35:59 +0100 Subject: [PATCH 03/17] Special stage access-related fixes - Fix GPEVENT_SPECIAL forcing via rank restriction in round queue - Re-implements gamedata->everseenspecial --- src/d_netcmd.c | 8 ++++++++ src/g_game.c | 3 +++ 2 files changed, 11 insertions(+) diff --git a/src/d_netcmd.c b/src/d_netcmd.c index ae08f0e42..a1321b4dc 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -3135,6 +3135,14 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum) || (gametyperules & (GTR_BOSS|GTR_CATCHER))) // Conventional rules. { grandprixinfo.eventmode = GPEVENT_SPECIAL; + + if (pforcespecialstage == true && gamedata->everseenspecial == false) + { + gamedata->everseenspecial = true; + // No need to do anything else here -- P_LoadLevel will get this for us! + //M_UpdateUnlockablesAndExtraEmblems(true, true); + //gamedata->deferredsave = true; + } } else if (gametype != GT_RACE) { diff --git a/src/g_game.c b/src/g_game.c index 9f273ca49..8c5be2de7 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -4139,6 +4139,9 @@ static void G_GetNextMap(void) D_GameTypeChanged(lastgametype); } + // Is this special..? + forcespecialstage = roundqueue.entries[roundqueue.position].rankrestricted; + // On entering roundqueue mode, kill the non-PWR between-round scores. // This makes it viable as a future tournament mode base. if (roundqueue.position == 0) From 487ec6db7423e61a1338d9e54a7dc266ac2bea7c Mon Sep 17 00:00:00 2001 From: toaster Date: Mon, 10 Apr 2023 13:36:35 +0100 Subject: [PATCH 04/17] K_UsingPowerLevels: Disable PWR if a roundqueue is in action --- src/k_pwrlv.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/k_pwrlv.c b/src/k_pwrlv.c index 0f234a504..ffab53778 100644 --- a/src/k_pwrlv.c +++ b/src/k_pwrlv.c @@ -39,7 +39,7 @@ SINT8 K_UsingPowerLevels(void) { SINT8 pt = PWRLV_DISABLED; - if (!cv_kartusepwrlv.value || !(netgame || (demo.playback && demo.netgame)) || grandprixinfo.gp == true) + if (!cv_kartusepwrlv.value || !(netgame || (demo.playback && demo.netgame)) || grandprixinfo.gp == true || roundqueue.size > 0) { return PWRLV_DISABLED; } From 48665e5ee35bf8bd45cad14de7580b3f10cefea9 Mon Sep 17 00:00:00 2001 From: toaster Date: Mon, 10 Apr 2023 13:37:49 +0100 Subject: [PATCH 05/17] [pink dog voice] THAT'S not archive block Parties!! --- src/p_saveg.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/p_saveg.c b/src/p_saveg.c index 536d241dc..5f6329612 100644 --- a/src/p_saveg.c +++ b/src/p_saveg.c @@ -958,7 +958,7 @@ static void P_NetUnArchiveRoundQueue(savebuffer_t *save) { UINT8 i; if (READUINT32(save->p) != ARCHIVEBLOCK_ROUNDQUEUE) - I_Error("Bad $$$.sav at archive block Parties"); + I_Error("Bad $$$.sav at archive block Round-queue"); memset(&roundqueue, 0, sizeof(struct roundqueue)); From b8c858e00b05e63d050400a2ddb46cac5f252c0a Mon Sep 17 00:00:00 2001 From: toaster Date: Mon, 10 Apr 2023 17:21:32 +0100 Subject: [PATCH 06/17] Don't halt the server over malformed roundqueue data --- src/d_netcmd.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/d_netcmd.c b/src/d_netcmd.c index a1321b4dc..57de439a3 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -3084,7 +3084,7 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum) || size > ROUNDQUEUE_MAX) // Sanity Check B (ditto) { CONS_Alert(CONS_WARNING, M_GetText("Illegal round-queue data received from %s\n"), player_names[playernum]); - if (server) + if (server && playernum != serverplayer) SendKick(playernum, KICK_MSG_CON_FAIL); return; } From 85897190e0e62ff671bd961ecae79e0028f3dd10 Mon Sep 17 00:00:00 2001 From: toaster Date: Mon, 10 Apr 2023 20:42:45 +0100 Subject: [PATCH 07/17] GetGametypeParm Both Command_Mapf and Command_RandomMap take gametype information, so let's seperate it into one generic function --- src/d_netcmd.c | 164 +++++++++++++++++++------------------------------ 1 file changed, 62 insertions(+), 102 deletions(-) diff --git a/src/d_netcmd.c b/src/d_netcmd.c index 57de439a3..ec87bc331 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -2773,6 +2773,64 @@ ConcatCommandArgv (int start, int end) // // Largely rewritten by James. // + +static INT32 GetGametypeParm(size_t option_gametype) +{ + const char *gametypename; + INT32 newgametype; + + if (COM_Argc() < option_gametype + 2)/* no argument after? */ + { + CONS_Alert(CONS_ERROR, + "No gametype name follows parameter '%s'.\n", + COM_Argv(option_gametype)); + return -1; + } + + // new gametype value + // use current one by default + gametypename = COM_Argv(option_gametype + 1); + + newgametype = G_GetGametypeByName(gametypename); + + if (newgametype == -1) // reached end of the list with no match + { + /* Did they give us a gametype number? That's okay too! */ + if (isdigit(gametypename[0])) + { + INT16 d = atoi(gametypename); + if (d >= 0 && d < numgametypes) + newgametype = d; + else + { + CONS_Alert(CONS_ERROR, + "Gametype number %d is out of range. Use a number between" + " 0 and %d inclusive. ...Or just use the name. :v\n", + d, + numgametypes-1); + return -1; + } + } + else + { + CONS_Alert(CONS_ERROR, + "'%s' is not a valid gametype.\n", + gametypename); + return -1; + } + } + + if (Playing() && netgame && (gametypes[newgametype]->rules & GTR_FORBIDMP)) + { + CONS_Alert(CONS_ERROR, + "'%s' is not a net-compatible gametype.\n", + gametypename); + return -1; + } + + return newgametype; +} + static void Command_Map_f(void) { size_t first_option; @@ -2780,7 +2838,6 @@ static void Command_Map_f(void) size_t option_gametype; size_t option_encore; size_t option_skill; - const char *gametypename; boolean newresetplayers; boolean newforcespecialstage; @@ -2795,8 +2852,6 @@ static void Command_Map_f(void) INT32 newgametype = gametype; boolean newencoremode = (cv_kartencore.value == 1); - INT32 d; - if (client && !IsPlayerAdmin(consoleplayer)) { CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n")); @@ -2813,17 +2868,6 @@ static void Command_Map_f(void) usingcheats = CV_CheatsEnabled(); ischeating = (!(netgame || multiplayer)) || (!newresetplayers); - if (option_gametype) - { - if (COM_Argc() < option_gametype + 2)/* no argument after? */ - { - CONS_Alert(CONS_ERROR, - "No gametype name follows parameter '%s'.\n", - COM_Argv(option_gametype)); - return; - } - } - if (!( first_option = COM_FirstOption() )) first_option = COM_Argc(); @@ -2863,46 +2907,9 @@ static void Command_Map_f(void) // use current one by default if (option_gametype) { - gametypename = COM_Argv(option_gametype + 1); - - newgametype = G_GetGametypeByName(gametypename); - - if (newgametype == -1) // reached end of the list with no match + newgametype = GetGametypeParm(option_gametype); + if (newgametype == -1) { - /* Did they give us a gametype number? That's okay too! */ - if (isdigit(gametypename[0])) - { - d = atoi(gametypename); - if (d >= 0 && d < numgametypes) - newgametype = d; - else - { - CONS_Alert(CONS_ERROR, - "Gametype number %d is out of range. Use a number between" - " 0 and %d inclusive. ...Or just use the name. :v\n", - d, - numgametypes-1); - Z_Free(realmapname); - Z_Free(mapname); - return; - } - } - else - { - CONS_Alert(CONS_ERROR, - "'%s' is not a valid gametype.\n", - gametypename); - Z_Free(realmapname); - Z_Free(mapname); - return; - } - } - - if (Playing() && netgame && (gametypes[newgametype]->rules & GTR_FORBIDMP)) - { - CONS_Alert(CONS_ERROR, - "'%s' is not a net-compatible gametype.\n", - gametypename); Z_Free(realmapname); Z_Free(mapname); return; @@ -3204,56 +3211,9 @@ static void Command_RandomMap(void) if ((option_gametype = COM_CheckPartialParm("-g"))) { - const char *gametypename; - - if (COM_Argc() < option_gametype + 2)/* no argument after? */ - { - CONS_Alert(CONS_ERROR, - "No gametype name follows parameter '%s'.\n", - COM_Argv(option_gametype)); + newgametype = GetGametypeParm(option_gametype); + if (newgametype == -1) return; - } - - // new gametype value - // use current one by default - gametypename = COM_Argv(option_gametype + 1); - - newgametype = G_GetGametypeByName(gametypename); - - if (newgametype == -1) // reached end of the list with no match - { - /* Did they give us a gametype number? That's okay too! */ - if (isdigit(gametypename[0])) - { - INT16 d = atoi(gametypename); - if (d >= 0 && d < numgametypes) - newgametype = d; - else - { - CONS_Alert(CONS_ERROR, - "Gametype number %d is out of range. Use a number between" - " 0 and %d inclusive. ...Or just use the name. :v\n", - d, - numgametypes-1); - return; - } - } - else - { - CONS_Alert(CONS_ERROR, - "'%s' is not a valid gametype.\n", - gametypename); - return; - } - } - - if (Playing() && netgame && (gametypes[newgametype]->rules & GTR_FORBIDMP)) - { - CONS_Alert(CONS_ERROR, - "'%s' is not a net-compatible gametype.\n", - gametypename); - return; - } } // TODO: Handle singleplayer conditions. From 371c52d45731a4dc242f48db03f103471cc49d79 Mon Sep 17 00:00:00 2001 From: toaster Date: Mon, 10 Apr 2023 23:06:21 +0100 Subject: [PATCH 08/17] XD_MAP: Clean up associated functions and typings - Free `mapname` at successful completion of Command_Map_f - Maps are UINT16, not INT16 or INT32 - Remove [MAP]code component of help message as it hasn't been relevant since long map markers --- src/d_netcmd.c | 11 ++++++----- src/d_netcmd.h | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/d_netcmd.c b/src/d_netcmd.c index ec87bc331..e1eca3d62 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -2555,7 +2555,7 @@ INT32 mapchangepending = 0; * \sa D_GameTypeChanged, Command_Map_f * \author Graue */ -void D_MapChange(INT32 mapnum, INT32 newgametype, boolean pencoremode, boolean presetplayers, INT32 delay, boolean skipprecutscene, boolean pforcespecialstage) +void D_MapChange(UINT16 mapnum, INT32 newgametype, boolean pencoremode, boolean presetplayers, INT32 delay, boolean skipprecutscene, boolean pforcespecialstage) { static char buf[1+1+1+1+1+2+4]; static char *buf_p = buf; @@ -2594,7 +2594,7 @@ void D_MapChange(INT32 mapnum, INT32 newgametype, boolean pencoremode, boolean p // new gametype value WRITEUINT8(buf_p, newgametype); - WRITEINT16(buf_p, mapnum); + WRITEUINT16(buf_p, mapnum); } if (delay == 1) @@ -2874,7 +2874,7 @@ static void Command_Map_f(void) if (first_option < 2) { /* I'm going over the fucking lines and I DON'T CAREEEEE */ - CONS_Printf("map [-gametype ] [-force]:\n"); + CONS_Printf("map [-gametype ] [-force]:\n"); CONS_Printf(M_GetText( "Warp to a map, by its name, two character code, with optional \"MAP\" prefix, or by its number (though why would you).\n" "All parameters are case-insensitive and may be abbreviated.\n")); @@ -3041,6 +3041,7 @@ static void Command_Map_f(void) D_MapChange(newmapnum, newgametype, newencoremode, newresetplayers, 0, false, newforcespecialstage); Z_Free(realmapname); + Z_Free(mapname); } /** Receives a map command and changes the map. @@ -3056,7 +3057,7 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum) INT32 presetplayer = 1, lastgametype; UINT8 skipprecutscene, pforcespecialstage; boolean pencoremode, hasroundqueuedata; - INT16 mapnumber; + UINT16 mapnumber; forceresetplayers = deferencoremode = false; @@ -3130,7 +3131,7 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum) if (!(gametyperules & GTR_ENCORE)) pencoremode = false; - mapnumber = READINT16(*cp); + mapnumber = READUINT16(*cp); // Handle some Grand Prix state. if (grandprixinfo.gp) diff --git a/src/d_netcmd.h b/src/d_netcmd.h index 908c6aec7..b765cf28e 100644 --- a/src/d_netcmd.h +++ b/src/d_netcmd.h @@ -239,7 +239,7 @@ void D_SendPlayerConfig(UINT8 n); void Command_ExitGame_f(void); void Command_Retry_f(void); void D_GameTypeChanged(INT32 lastgametype); // not a real _OnChange function anymore -void D_MapChange(INT32 pmapnum, INT32 pgametype, boolean pencoremode, boolean presetplayers, INT32 pdelay, boolean pskipprecutscene, boolean pforcespecialstage); +void D_MapChange(UINT16 pmapnum, INT32 pgametype, boolean pencoremode, boolean presetplayers, INT32 pdelay, boolean pskipprecutscene, boolean pforcespecialstage); void D_SetupVote(void); void D_ModifyClientVote(UINT8 player, SINT8 voted); void D_PickVote(void); From a1065d9036ebf841faaa96dfe039245eb493a293 Mon Sep 17 00:00:00 2001 From: toaster Date: Mon, 10 Apr 2023 23:10:47 +0100 Subject: [PATCH 09/17] G_MapIntoRoundQueue: Seperate into a general append function *and* a specific placement function. A surprise tool that will help us later --- src/g_game.c | 16 +++++++++++----- src/g_game.h | 1 + 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/g_game.c b/src/g_game.c index 8c5be2de7..cb5f728a5 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -3991,6 +3991,16 @@ static void G_HandleSaveLevel(void) // Next map apparatus struct roundqueue roundqueue; +void G_MapSlipIntoRoundQueue(UINT8 position, UINT16 map, UINT8 setgametype, boolean setencore, boolean rankrestricted) +{ + I_Assert(position < ROUNDQUEUE_MAX); + + roundqueue.entries[position].mapnum = map; + roundqueue.entries[position].gametype = setgametype; + roundqueue.entries[position].encore = setencore; + roundqueue.entries[position].rankrestricted = rankrestricted; +} + void G_MapIntoRoundQueue(UINT16 map, UINT8 setgametype, boolean setencore, boolean rankrestricted) { if (roundqueue.size >= ROUNDQUEUE_MAX) @@ -3999,11 +4009,7 @@ void G_MapIntoRoundQueue(UINT16 map, UINT8 setgametype, boolean setencore, boole return; } - roundqueue.entries[roundqueue.size].mapnum = map; - roundqueue.entries[roundqueue.size].gametype = setgametype; - roundqueue.entries[roundqueue.size].encore = setencore; - roundqueue.entries[roundqueue.size].rankrestricted = rankrestricted; - + G_MapSlipIntoRoundQueue(roundqueue.size, map, setgametype, setencore, rankrestricted); roundqueue.size++; } diff --git a/src/g_game.h b/src/g_game.h index e568ab49a..e2540a117 100644 --- a/src/g_game.h +++ b/src/g_game.h @@ -72,6 +72,7 @@ extern struct roundqueue roundentry_t entries[ROUNDQUEUE_MAX]; // Entries in the round queue } roundqueue; +void G_MapSlipIntoRoundQueue(UINT8 position, UINT16 map, UINT8 setgametype, boolean setencore, boolean rankrestricted); void G_MapIntoRoundQueue(UINT16 map, UINT8 setgametype, boolean setencore, boolean rankrestricted); void G_GPCupIntoRoundQueue(cupheader_t *cup, UINT8 setgametype, boolean setencore); From 6bca2a700337d762e1087de9fc1e6969e6acdb3d Mon Sep 17 00:00:00 2001 From: toaster Date: Mon, 10 Apr 2023 23:41:58 +0100 Subject: [PATCH 10/17] Console command `queuemap` - Full valid format: queuemap [name/num] -gametype [name] -encore -force - Server is fully authoriative about the order of maps in the round-queue - Server sends XD_MAPQUEUE (which contains gametype, encore, and ordering) - Admin clients have to send XD_REQMAPQUEUE (which contains gametype, encore, and mapnum) - Servers spit out a processed XD_MAPQUEUE on reciept - Done this way just in case an XD_MAPQUEUE is not recieved and has to be resent, guarantees ordering - Will create a UI for this post-launch, this is primarily for testing but may be useful for user-ran tournaments --- src/d_netcmd.c | 254 +++++++++++++++++++++++++++++++++++++++++++++++++ src/d_netcmd.h | 2 + 2 files changed, 256 insertions(+) diff --git a/src/d_netcmd.c b/src/d_netcmd.c index e1eca3d62..9ba16f5f7 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -102,6 +102,8 @@ static void Got_DiscordInfo(UINT8 **cp, INT32 playernum); static void Got_ScheduleTaskcmd(UINT8 **cp, INT32 playernum); static void Got_ScheduleClearcmd(UINT8 **cp, INT32 playernum); static void Got_Automatecmd(UINT8 **cp, INT32 playernum); +static void Got_RequestMapQueuecmd(UINT8 **cp, INT32 playernum); +static void Got_MapQueuecmd(UINT8 **cp, INT32 playernum); static void Got_Cheat(UINT8 **cp, INT32 playernum); static void PointLimit_OnChange(void); @@ -162,6 +164,7 @@ static void Command_StopMovie_f(void); static void Command_Map_f(void); static void Command_RandomMap(void); static void Command_RestartLevel(void); +static void Command_QueueMap_f(void); static void Command_ResetCamera_f(void); static void Command_View_f (void); @@ -632,6 +635,8 @@ const char *netxcmdnames[MAXNETXCMD - 1] = "SCHEDULETASK", // XD_SCHEDULETASK "SCHEDULECLEAR", // XD_SCHEDULECLEAR "AUTOMATE", // XD_AUTOMATE + "REQMAPQUEUE", // XD_REQMAPQUEUE + "MAPQUEUE", // XD_MAPQUEUE }; // ========================================================================= @@ -681,6 +686,8 @@ void D_RegisterServerCommands(void) RegisterNetXCmd(XD_SCHEDULETASK, Got_ScheduleTaskcmd); RegisterNetXCmd(XD_SCHEDULECLEAR, Got_ScheduleClearcmd); RegisterNetXCmd(XD_AUTOMATE, Got_Automatecmd); + RegisterNetXCmd(XD_REQMAPQUEUE, Got_RequestMapQueuecmd); + RegisterNetXCmd(XD_MAPQUEUE, Got_MapQueuecmd); RegisterNetXCmd(XD_CHEAT, Got_Cheat); @@ -703,6 +710,7 @@ void D_RegisterServerCommands(void) COM_AddCommand("map", Command_Map_f); COM_AddCommand("randommap", Command_RandomMap); COM_AddCommand("restartlevel", Command_RestartLevel); + COM_AddCommand("queuemap", Command_QueueMap_f); COM_AddCommand("exitgame", Command_ExitGame_f); COM_AddCommand("retry", Command_Retry_f); @@ -3271,6 +3279,252 @@ static void Command_RestartLevel(void) D_MapChange(gamemap, gametype, newencore, false, 0, false, false); } +static void Command_QueueMap_f(void) +{ + size_t first_option; + size_t option_force; + size_t option_gametype; + size_t option_encore; + + boolean usingcheats; + boolean ischeating; + + INT32 newmapnum; + + char * mapname; + char *realmapname = NULL; + + INT32 newgametype = gametype; + boolean newencoremode = (cv_kartencore.value == 1); + + if (!Playing()) + { + CONS_Printf(M_GetText("Levels can only be queued in-game.\n")); + return; + } + + if (client && !IsPlayerAdmin(consoleplayer)) + { + CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n")); + return; + } + + if (roundqueue.size >= ROUNDQUEUE_MAX) + { + CONS_Printf(M_GetText("Round queue is currently full.\n")); + return; + } + + option_force = COM_CheckPartialParm("-f"); + option_gametype = COM_CheckPartialParm("-g"); + option_encore = COM_CheckPartialParm("-e"); + + usingcheats = CV_CheatsEnabled(); + ischeating = (!(netgame || multiplayer)); + + if (!( first_option = COM_FirstOption() )) + first_option = COM_Argc(); + + if (first_option < 2) + { + /* I'm going over the fucking lines and I DON'T CAREEEEE */ + CONS_Printf("queuemap [-gametype ] [-force]:\n"); + CONS_Printf(M_GetText( + "Queue up a map by its name, or by its number (though why would you).\n" + "All parameters are case-insensitive and may be abbreviated.\n")); + return; + } + + mapname = ConcatCommandArgv(1, first_option); + + newmapnum = G_FindMapByNameOrCode(mapname, &realmapname); + + if (newmapnum == 0) + { + CONS_Alert(CONS_ERROR, M_GetText("Could not find any map described as '%s'.\n"), mapname); + Z_Free(mapname); + return; + } + + if (!K_CanChangeRules(false) || (/*newmapnum != 1 &&*/ M_MapLocked(newmapnum))) + { + ischeating = true; + } + + if (ischeating && !usingcheats) + { + CONS_Printf(M_GetText("Cheats must be enabled.\n")); + return; + } + + // new gametype value + // use current one by default + if (option_gametype) + { + newgametype = GetGametypeParm(option_gametype); + if (newgametype == -1) + { + Z_Free(realmapname); + Z_Free(mapname); + return; + } + } + + // new encoremode value + if (option_encore) + { + newencoremode = !newencoremode; + + if (!M_SecretUnlocked(SECRET_ENCORE, false) && newencoremode == true && !usingcheats) + { + CONS_Alert(CONS_NOTICE, M_GetText("You haven't unlocked Encore Mode yet!\n")); + Z_Free(realmapname); + Z_Free(mapname); + return; + } + } + + // don't use a gametype the map doesn't support + if (cht_debug || option_force || cv_skipmapcheck.value) + { + // The player wants us to trek on anyway. Do so. + } + else + { + // G_TOLFlag handles both multiplayer gametype and ignores it for !multiplayer + if (!( + mapheaderinfo[newmapnum-1] && + mapheaderinfo[newmapnum-1]->typeoflevel & G_TOLFlag(newgametype) + )) + { + CONS_Alert(CONS_WARNING, M_GetText("%s (%s) doesn't support %s mode!\n(Use -force to override)\n"), realmapname, G_BuildMapName(newmapnum), gametypes[newgametype]->name); + Z_Free(realmapname); + Z_Free(mapname); + return; + } + } + + { + static char buf[1+1+2]; + static char *buf_p = buf; + + UINT8 flags = 0; + + CONS_Debug(DBG_GAMELOGIC, "Map queue: mapnum=%d newgametype=%d newencoremode=%d\n", + newmapnum-1, newgametype, newencoremode); + + buf_p = buf; + + if (newencoremode) + flags |= 1; + + WRITEUINT8(buf_p, flags); + WRITEUINT8(buf_p, newgametype); + + if (client) + { + WRITEUINT16(buf_p, (newmapnum-1)); + SendNetXCmd(XD_REQMAPQUEUE, buf, buf_p - buf); + } + else + { + WRITEUINT8(buf_p, roundqueue.size); + SendNetXCmd(XD_MAPQUEUE, buf, buf_p - buf); + + G_MapIntoRoundQueue(newmapnum-1, newgametype, newencoremode, false); + } + } + + Z_Free(realmapname); + Z_Free(mapname); +} + +static void Got_RequestMapQueuecmd(UINT8 **cp, INT32 playernum) +{ + UINT8 flags, setgametype; + boolean setencore; + UINT16 mapnumber; + + flags = READUINT8(*cp); + + setencore = ((flags & 1) != 0); + + setgametype = READUINT8(*cp); + + mapnumber = READUINT16(*cp); + + if (!IsPlayerAdmin(playernum)) + { + CONS_Alert(CONS_WARNING, M_GetText("Illegal request map queue command received from %s\n"), player_names[playernum]); + if (server && playernum != serverplayer) + SendKick(playernum, KICK_MSG_CON_FAIL); + return; + } + + if (roundqueue.size >= ROUNDQUEUE_MAX) + { + CONS_Alert(CONS_ERROR, "queuemap: Unable to add map beyond %u\n", roundqueue.size); + return; + } + + if (client) + return; + + { + static char buf[1+1+1]; + static char *buf_p = buf; + + buf_p = buf; + + WRITEUINT8(buf_p, flags); + WRITEUINT8(buf_p, setgametype); + WRITEUINT8(buf_p, roundqueue.size); + + SendNetXCmd(XD_MAPQUEUE, buf, buf_p - buf); + } + + G_MapIntoRoundQueue(mapnumber, setgametype, setencore, false); +} + +static void Got_MapQueuecmd(UINT8 **cp, INT32 playernum) +{ + UINT8 flags, setgametype, queueposition; + boolean setencore; + + flags = READUINT8(*cp); + + setencore = ((flags & 1) != 0); + + setgametype = READUINT8(*cp); + + queueposition = READUINT8(*cp); + + if (playernum != serverplayer) + { + CONS_Alert(CONS_WARNING, M_GetText("Illegal map queue command received from %s\n"), player_names[playernum]); + if (server) + SendKick(playernum, KICK_MSG_CON_FAIL); + return; + } + + if (queueposition >= ROUNDQUEUE_MAX) + { + CONS_Alert(CONS_ERROR, "queuemap: Unable to add map beyond %u\n", roundqueue.size); + return; + } + + if (server) + return; + + while (roundqueue.size <= queueposition) + { + memset(&roundqueue.entries[roundqueue.size], 0, sizeof(roundentry_t)); + roundqueue.size++; + } + + G_MapSlipIntoRoundQueue(queueposition, 0, setgametype, setencore, false); +} + static void Command_Pause(void) { UINT8 buf[2]; diff --git a/src/d_netcmd.h b/src/d_netcmd.h index b765cf28e..879361db2 100644 --- a/src/d_netcmd.h +++ b/src/d_netcmd.h @@ -181,6 +181,8 @@ typedef enum XD_SCHEDULETASK, // 36 XD_SCHEDULECLEAR, // 37 XD_AUTOMATE, // 38 + XD_REQMAPQUEUE, // 39 + XD_MAPQUEUE, // 40 MAXNETXCMD } netxcmd_t; From 7d1e1a578e20e3b23b984a22d8931f00b927cb41 Mon Sep 17 00:00:00 2001 From: toaster Date: Tue, 11 Apr 2023 00:03:13 +0100 Subject: [PATCH 11/17] (Will merge conflict with new intermission) Do not show advancemap string if roundqueue (or nextmapoverride) will have effect next round --- src/y_inter.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/y_inter.c b/src/y_inter.c index 884466261..3636834e2 100644 --- a/src/y_inter.c +++ b/src/y_inter.c @@ -539,6 +539,9 @@ skiptallydrawer: if (demo.playback) string = va("Replay ends in %d", tickdown); + else if ((nextmapoverride != 0) + || (roundqueue.size > 0 && roundqueue.position < roundqueue.size)) + string = va("Next starts in %d", tickdown); else string = va("%s starts in %d", cv_advancemap.string, tickdown); From 47ecc6370bfe8ca888281e3573c59e588df5f26b Mon Sep 17 00:00:00 2001 From: toaster Date: Tue, 11 Apr 2023 12:40:34 +0100 Subject: [PATCH 12/17] K_UsingPowerLevels: Clean up significantly - Comment each restriction/enablement - Fix the roundqueue PWR disabling to only occour when the roundqueue is in action, as opposed to when it has any entries at all --- src/k_pwrlv.c | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/k_pwrlv.c b/src/k_pwrlv.c index ffab53778..a93445184 100644 --- a/src/k_pwrlv.c +++ b/src/k_pwrlv.c @@ -37,23 +37,38 @@ consvar_t cv_debugencorevote = CVAR_INIT ("debugencorevote", "Off", CV_CHEAT|CV_ SINT8 K_UsingPowerLevels(void) { - SINT8 pt = PWRLV_DISABLED; - - if (!cv_kartusepwrlv.value || !(netgame || (demo.playback && demo.netgame)) || grandprixinfo.gp == true || roundqueue.size > 0) + if (!cv_kartusepwrlv.value) { + // Explicitly forbidden. + return PWRLV_DISABLED; + } + + if (!(netgame || (demo.playback && demo.netgame))) + { + // Servers only. + return PWRLV_DISABLED; + } + + if (roundqueue.size > 0 && roundqueue.position > 0) + { + // When explicit progression is in place, we're going by different rules. return PWRLV_DISABLED; } if (gametype == GT_RACE) { - pt = PWRLV_RACE; - } - else if (gametype == GT_BATTLE) - { - pt = PWRLV_BATTLE; + // Race PWR. + return PWRLV_RACE; } - return pt; + if (gametype == GT_BATTLE) + { + // Battle PWR. + return PWRLV_BATTLE; + } + + // We do not support PWR for custom gametypes at this moment in time. + return PWRLV_DISABLED; } void K_ClearClientPowerLevels(void) From 4f271aec24fb861d9f6f6a950cb445889ed33c3f Mon Sep 17 00:00:00 2001 From: toaster Date: Tue, 11 Apr 2023 12:49:14 +0100 Subject: [PATCH 13/17] Handle_MapQueueSend All XD_MAPQUEUE/XD_REQMAPQUEUE packets are now handled in one function, to guarantee requests are made consistently. --- src/d_netcmd.c | 77 ++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 44 deletions(-) diff --git a/src/d_netcmd.c b/src/d_netcmd.c index 9ba16f5f7..9129c8cc5 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -3279,6 +3279,37 @@ static void Command_RestartLevel(void) D_MapChange(gamemap, gametype, newencore, false, 0, false, false); } +static void Handle_MapQueueSend(UINT16 newmapnum, UINT8 newgametype, boolean newencoremode) +{ + static char buf[1+1+2]; + static char *buf_p = buf; + + UINT8 flags = 0; + + CONS_Debug(DBG_GAMELOGIC, "Map queue: mapnum=%d newgametype=%d newencoremode=%d\n", + newmapnum, newgametype, newencoremode); + + buf_p = buf; + + if (newencoremode) + flags |= 1; + + WRITEUINT8(buf_p, flags); + WRITEUINT8(buf_p, newgametype); + + if (client) + { + WRITEUINT16(buf_p, newmapnum); + SendNetXCmd(XD_REQMAPQUEUE, buf, buf_p - buf); + return; + } + + WRITEUINT8(buf_p, roundqueue.size); + SendNetXCmd(XD_MAPQUEUE, buf, buf_p - buf); + + G_MapIntoRoundQueue(newmapnum, newgametype, newencoremode, false); +} + static void Command_QueueMap_f(void) { size_t first_option; @@ -3404,36 +3435,7 @@ static void Command_QueueMap_f(void) } } - { - static char buf[1+1+2]; - static char *buf_p = buf; - - UINT8 flags = 0; - - CONS_Debug(DBG_GAMELOGIC, "Map queue: mapnum=%d newgametype=%d newencoremode=%d\n", - newmapnum-1, newgametype, newencoremode); - - buf_p = buf; - - if (newencoremode) - flags |= 1; - - WRITEUINT8(buf_p, flags); - WRITEUINT8(buf_p, newgametype); - - if (client) - { - WRITEUINT16(buf_p, (newmapnum-1)); - SendNetXCmd(XD_REQMAPQUEUE, buf, buf_p - buf); - } - else - { - WRITEUINT8(buf_p, roundqueue.size); - SendNetXCmd(XD_MAPQUEUE, buf, buf_p - buf); - - G_MapIntoRoundQueue(newmapnum-1, newgametype, newencoremode, false); - } - } + Handle_MapQueueSend(newmapnum-1, newgametype, newencoremode); Z_Free(realmapname); Z_Free(mapname); @@ -3470,20 +3472,7 @@ static void Got_RequestMapQueuecmd(UINT8 **cp, INT32 playernum) if (client) return; - { - static char buf[1+1+1]; - static char *buf_p = buf; - - buf_p = buf; - - WRITEUINT8(buf_p, flags); - WRITEUINT8(buf_p, setgametype); - WRITEUINT8(buf_p, roundqueue.size); - - SendNetXCmd(XD_MAPQUEUE, buf, buf_p - buf); - } - - G_MapIntoRoundQueue(mapnumber, setgametype, setencore, false); + Handle_MapQueueSend(mapnumber, setgametype, setencore); } static void Got_MapQueuecmd(UINT8 **cp, INT32 playernum) From f1ca263c4029e708b19da89c7ba5a2344b4ea132 Mon Sep 17 00:00:00 2001 From: toaster Date: Tue, 11 Apr 2023 13:06:37 +0100 Subject: [PATCH 14/17] Promote gametypes in XD_MAP, XD_MAPQUEUE and XD_REQMAPQUEUE to UINT16 Since new-voting repermitted custom gametypes to go beyond the bounds of a UINT8, here's some extra anti-footgun protection. --- src/d_netcmd.c | 25 +++++++++++++------------ src/g_game.h | 2 +- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/d_netcmd.c b/src/d_netcmd.c index 9129c8cc5..3c310f014 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -2600,7 +2600,7 @@ void D_MapChange(UINT16 mapnum, INT32 newgametype, boolean pencoremode, boolean } // new gametype value - WRITEUINT8(buf_p, newgametype); + WRITEUINT16(buf_p, newgametype); WRITEUINT16(buf_p, mapnum); } @@ -3062,10 +3062,10 @@ static void Command_Map_f(void) static void Got_Mapcmd(UINT8 **cp, INT32 playernum) { UINT8 flags; - INT32 presetplayer = 1, lastgametype; + INT32 presetplayer = 1; UINT8 skipprecutscene, pforcespecialstage; boolean pencoremode, hasroundqueuedata; - UINT16 mapnumber; + UINT16 mapnumber, lastgametype; forceresetplayers = deferencoremode = false; @@ -3121,7 +3121,7 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum) chmappending--; lastgametype = gametype; - gametype = READUINT8(*cp); + gametype = READUINT16(*cp); G_SetGametype(gametype); // I fear putting that macro as an argument if (gametype < 0 || gametype >= numgametypes) @@ -3279,9 +3279,9 @@ static void Command_RestartLevel(void) D_MapChange(gamemap, gametype, newencore, false, 0, false, false); } -static void Handle_MapQueueSend(UINT16 newmapnum, UINT8 newgametype, boolean newencoremode) +static void Handle_MapQueueSend(UINT16 newmapnum, UINT16 newgametype, boolean newencoremode) { - static char buf[1+1+2]; + static char buf[1+2+2]; static char *buf_p = buf; UINT8 flags = 0; @@ -3295,7 +3295,7 @@ static void Handle_MapQueueSend(UINT16 newmapnum, UINT8 newgametype, boolean new flags |= 1; WRITEUINT8(buf_p, flags); - WRITEUINT8(buf_p, newgametype); + WRITEUINT16(buf_p, newgametype); if (client) { @@ -3443,15 +3443,15 @@ static void Command_QueueMap_f(void) static void Got_RequestMapQueuecmd(UINT8 **cp, INT32 playernum) { - UINT8 flags, setgametype; + UINT8 flags; boolean setencore; - UINT16 mapnumber; + UINT16 mapnumber, setgametype; flags = READUINT8(*cp); setencore = ((flags & 1) != 0); - setgametype = READUINT8(*cp); + setgametype = READUINT16(*cp); mapnumber = READUINT16(*cp); @@ -3477,14 +3477,15 @@ static void Got_RequestMapQueuecmd(UINT8 **cp, INT32 playernum) static void Got_MapQueuecmd(UINT8 **cp, INT32 playernum) { - UINT8 flags, setgametype, queueposition; + UINT8 flags, queueposition; boolean setencore; + UINT16 setgametype; flags = READUINT8(*cp); setencore = ((flags & 1) != 0); - setgametype = READUINT8(*cp); + setgametype = READUINT16(*cp); queueposition = READUINT8(*cp); diff --git a/src/g_game.h b/src/g_game.h index e2540a117..c071a6b3e 100644 --- a/src/g_game.h +++ b/src/g_game.h @@ -58,7 +58,7 @@ typedef enum struct roundentry_t { UINT16 mapnum; // Map number at this position - UINT8 gametype; // Gametype we want to play this in + UINT16 gametype; // Gametype we want to play this in boolean encore; // Whether this will be flipped boolean rankrestricted; // For grand prix progression }; From d4de130fe002d1b3f03a62729b9a6858af3ac504 Mon Sep 17 00:00:00 2001 From: toaster Date: Tue, 11 Apr 2023 13:20:25 +0100 Subject: [PATCH 15/17] More sanity checks for skipping restricted queue entries - GTR_FORBIDMP in netgames - mapheader doesn't exist --- src/g_game.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/g_game.c b/src/g_game.c index cb5f728a5..29edf7bd6 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -4125,6 +4125,8 @@ static void G_GetNextMap(void) while (roundqueue.position < roundqueue.size && (roundqueue.entries[roundqueue.position].mapnum >= nummapheaders + || mapheaderinfo[roundqueue.entries[roundqueue.position].mapnum] == NULL + || (netgame && (gametypes[roundqueue.entries[roundqueue.position].gametype]->rules & GTR_FORBIDMP)) || (permitrank == false && roundqueue.entries[roundqueue.position].rankrestricted == true))) { // Skip all restricted queue entries. From 9b0c82d5dc693b5fdd14fef5e2ef7e429bf76051 Mon Sep 17 00:00:00 2001 From: toaster Date: Mon, 17 Apr 2023 20:16:30 +0100 Subject: [PATCH 16/17] Got_MapQueuecmd: Add a success message for adding a map to the round queue (visible only to admins) --- src/d_netcmd.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/d_netcmd.c b/src/d_netcmd.c index 998557f92..59fddaf99 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -3474,6 +3474,11 @@ static void Got_MapQueuecmd(UINT8 **cp, INT32 playernum) } G_MapSlipIntoRoundQueue(queueposition, 0, setgametype, setencore, false); + + if (!IsPlayerAdmin(playernum)) + return; + + CONS_Printf("queuemap: A map was successfully added to the round queue (position %u)\n", queueposition); } static void Command_Pause(void) From c48283bd240346ce1e72eebe8b288c4eefeaeb63 Mon Sep 17 00:00:00 2001 From: toaster Date: Tue, 18 Apr 2023 09:56:14 +0100 Subject: [PATCH 17/17] Fix showing ROUND with invalid number for queueing a map during voting --- src/st_stuff.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/st_stuff.c b/src/st_stuff.c index 9604d66a8..81cd693da 100644 --- a/src/st_stuff.c +++ b/src/st_stuff.c @@ -696,7 +696,7 @@ void ST_runTitleCard(void) { boolean run = !(paused || P_AutoPause()); INT32 auxticker; - boolean doroundicon = (marathonmode || roundqueue.size > 0); + boolean doroundicon = (marathonmode || (roundqueue.size > 0 && roundqueue.position > 0)); if (!G_IsTitleCardAvailable()) return; @@ -881,7 +881,7 @@ void ST_drawTitleCard(void) char *lvlttl = mapheaderinfo[gamemap-1]->lvlttl; char *zonttl = mapheaderinfo[gamemap-1]->zonttl; // SRB2kart UINT8 actnum = mapheaderinfo[gamemap-1]->actnum; - boolean doroundicon = (marathonmode || roundqueue.size > 0); + boolean doroundicon = (marathonmode || (roundqueue.size > 0 && roundqueue.position > 0)); INT32 acttimer; fixed_t actscale;