From 04697e94f33b6ce7367e749acceb75d8b1c1adbe Mon Sep 17 00:00:00 2001 From: toaster Date: Mon, 10 Apr 2023 23:41:58 +0100 Subject: [PATCH] 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 | 347 ++++++++++++++++++++++++++++++++++++++++++++----- src/d_netcmd.h | 2 + src/st_stuff.c | 4 +- 3 files changed, 319 insertions(+), 34 deletions(-) diff --git a/src/d_netcmd.c b/src/d_netcmd.c index a0dfea2b1..05558e21c 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -106,6 +106,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 Command_ScoreboardAdd(void); static void Command_ScoreboardClear(void); @@ -190,6 +192,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); @@ -942,6 +945,8 @@ const char *netxcmdnames[MAXNETXCMD - 1] = "SCHEDULECLEAR", // XD_SCHEDULECLEAR "AUTOMATE", // XD_AUTOMATE "CHEAT", // XD_CHEAT + "REQMAPQUEUE", // XD_REQMAPQUEUE + "MAPQUEUE", // XD_MAPQUEUE }; // ========================================================================= @@ -991,6 +996,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); COM_AddCommand("scoreboard_addline", Command_ScoreboardAdd); @@ -1016,6 +1023,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); @@ -3562,6 +3570,64 @@ static char *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; @@ -3663,39 +3729,10 @@ 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 gametype.\n", - gametypename); - Z_Free(realmapname); - Z_Free(mapname); - return; - } + return; } } else if (!Playing()) @@ -4086,6 +4123,252 @@ static void Command_RestartLevel(void) D_MapChange(gamemap, gametype, encoremode, 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) && 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 a5a041e25..2924fc2cf 100644 --- a/src/d_netcmd.h +++ b/src/d_netcmd.h @@ -360,6 +360,8 @@ typedef enum XD_SCHEDULECLEAR, // 36 XD_AUTOMATE, // 37 XD_CHEAT, // 38 + XD_REQMAPQUEUE, // 39 + XD_MAPQUEUE, // 40 MAXNETXCMD } netxcmd_t; diff --git a/src/st_stuff.c b/src/st_stuff.c index 3ebe0e56b..517d47e61 100644 --- a/src/st_stuff.c +++ b/src/st_stuff.c @@ -778,8 +778,8 @@ void ST_drawTitleCard(void) V_DrawLevelTitle(ttlnumxpos+12, bary+6, V_SNAPTOBOTTOM, actnum); // TODO: a bit more elaborate? - if (grandprixinfo.gp) - V_DrawCenteredString(BASEVIDWIDTH/2 + sub, bary+24, V_SNAPTOBOTTOM|V_YELLOWMAP, va("ROUND %d", grandprixinfo.roundnum)); + if (roundqueue.size > 0) + V_DrawCenteredString(BASEVIDWIDTH/2 + sub, bary+24, V_SNAPTOBOTTOM|V_YELLOWMAP, va("ROUND %d", roundqueue.roundnum)); luahook: if (renderisnewtic)