From eb7e84b96142995929b65512c2e2f469a87dbf0c Mon Sep 17 00:00:00 2001 From: Sally Coolatta Date: Fri, 10 Jun 2022 11:19:17 -0400 Subject: [PATCH] Automate commands - `automate_set ` to set a command to run each time an event triggers. - Currently implemented events are "roundstart", "intermissionstart", and "votestart", all of the ones from HOSTMOD. - Turn `automate` off to disable this feature entirely. Because of the new safer way this is implemented (in HOSTMOD, this just calls some console aliases), this is turned on by default instead of off. - This is set up in a way to facilitate adding more automation events very easily, if desired. --- src/d_clisrv.c | 3 + src/d_netcmd.c | 192 ++++++++++++++++++++++++++++++++++++++++++++++++- src/d_netcmd.h | 13 ++++ src/g_game.c | 10 +++ src/y_inter.c | 3 + 5 files changed, 219 insertions(+), 2 deletions(-) diff --git a/src/d_clisrv.c b/src/d_clisrv.c index e60a9a549..2a6cf1fde 100644 --- a/src/d_clisrv.c +++ b/src/d_clisrv.c @@ -2002,6 +2002,7 @@ static void CL_ConnectToServer(void) ClearAdminPlayers(); Schedule_Clear(); + Automate_Clear(); K_ClearClientPowerLevels(); pnumnodes = 1; @@ -3145,6 +3146,7 @@ void SV_ResetServer(void) ClearAdminPlayers(); Schedule_Clear(); + Automate_Clear(); K_ClearClientPowerLevels(); memset(splitscreen_invitations, -1, sizeof splitscreen_invitations); @@ -3233,6 +3235,7 @@ void D_QuitNetGame(void) D_CloseConnection(); ClearAdminPlayers(); Schedule_Clear(); + Automate_Clear(); K_ClearClientPowerLevels(); DEBFILE("===========================================================================\n" diff --git a/src/d_netcmd.c b/src/d_netcmd.c index f5976997b..416f94ace 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -100,6 +100,7 @@ static void Got_Clearscores(UINT8 **cp, INT32 playernum); 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 PointLimit_OnChange(void); static void TimeLimit_OnChange(void); @@ -229,6 +230,8 @@ static void Command_Schedule_Add(void); static void Command_Schedule_Clear(void); static void Command_Schedule_List(void); +static void Command_Automate_Set(void); + // ========================================================================= // CLIENT VARIABLES // ========================================================================= @@ -531,6 +534,8 @@ consvar_t cv_director = CVAR_INIT ("director", "Off", 0, CV_OnOff, NULL); consvar_t cv_schedule = CVAR_INIT ("schedule", "On", CV_NETVAR|CV_CALL, CV_OnOff, Schedule_OnChange); +consvar_t cv_automate = CVAR_INIT ("automate", "On", CV_NETVAR, CV_OnOff, NULL); + char timedemo_name[256]; boolean timedemo_csv; char timedemo_csv_id[256]; @@ -546,11 +551,21 @@ UINT8 splitscreen = 0; boolean circuitmap = false; INT32 adminplayers[MAXPLAYERS]; -// Scheduled cvars. +// Scheduled commands. scheduleTask_t **schedule = NULL; size_t schedule_size = 0; size_t schedule_len = 0; +// Automation commands +char *automate_commands[AEV__MAX]; + +const char *automate_names[AEV__MAX] = +{ + "RoundStart", // AEV_ROUNDSTART + "IntermissionStart", // AEV_INTERMISSIONSTART + "VoteStart" // AEV_VOTESTART +}; + /// \warning Keep this up-to-date if you add/remove/rename net text commands const char *netxcmdnames[MAXNETXCMD - 1] = { @@ -592,7 +607,8 @@ const char *netxcmdnames[MAXNETXCMD - 1] = "DISCORD", // XD_DISCORD "PLAYSOUND", // XD_PLAYSOUND "SCHEDULETASK", // XD_SCHEDULETASK - "SCHEDULECLEAR" // XD_SCHEDULECLEAR + "SCHEDULECLEAR", // XD_SCHEDULECLEAR + "AUTOMATE" // XD_AUTOMATE }; // ========================================================================= @@ -649,6 +665,7 @@ void D_RegisterServerCommands(void) RegisterNetXCmd(XD_SCHEDULETASK, Got_ScheduleTaskcmd); RegisterNetXCmd(XD_SCHEDULECLEAR, Got_ScheduleClearcmd); + RegisterNetXCmd(XD_AUTOMATE, Got_Automatecmd); // Remote Administration COM_AddCommand("password", Command_Changepassword_f); @@ -711,6 +728,8 @@ void D_RegisterServerCommands(void) COM_AddCommand("schedule_clear", Command_Schedule_Clear); COM_AddCommand("schedule_list", Command_Schedule_List); + COM_AddCommand("automate_set", Command_Automate_Set); + // for master server connection AddMServCommands(); @@ -783,6 +802,7 @@ void D_RegisterServerCommands(void) CV_RegisterVar(&cv_director); CV_RegisterVar(&cv_schedule); + CV_RegisterVar(&cv_automate); CV_RegisterVar(&cv_dummyconsvar); @@ -3985,6 +4005,82 @@ void Schedule_Clear(void) schedule = NULL; } +void Automate_Set(automateEvents_t type, const char *command) +{ + if (!server) + { + // Since there's no list command or anything for this, + // we don't need this code to run for anyone but the server. + return; + } + + if (automate_commands[type] != NULL) + { + // Free the old command. + Z_Free(automate_commands[type]); + } + + if (command == NULL || strlen(command) == 0) + { + // Remove the command. + automate_commands[type] = NULL; + } + else + { + // New command. + automate_commands[type] = Z_StrDup(command); + } +} + +void Automate_Run(automateEvents_t type) +{ + if (!server) + { + // Only the server should be doing this. + return; + } + +#ifdef PARANOIA + if (type >= AEV__MAX) + { + // Shouldn't happen. + I_Error("Attempted to run invalid automation type."); + return; + } +#endif + + if (!cv_automate.value) + { + // We don't want to run automation. + return; + } + + if (automate_commands[type] == NULL) + { + // No command to run. + return; + } + + CONS_Printf( + "Running %s automate command \"" "\x82" "%s" "\x80" "\"...\n", + automate_names[type], + automate_commands[type] + ); + + COM_BufAddText(automate_commands[type]); + COM_BufAddText("\n"); +} + +void Automate_Clear(void) +{ + size_t i; + + for (i = 0; i < AEV__MAX; i++) + { + Automate_Set(i, NULL); + } +} + static void Command_MotD_f(void) { size_t i, j; @@ -5210,6 +5306,49 @@ static void Got_ScheduleClearcmd(UINT8 **cp, INT32 playernum) } } +static void Got_Automatecmd(UINT8 **cp, INT32 playernum) +{ + UINT8 eventID; + char command[MAXTEXTCMD]; + + eventID = READUINT8(*cp); + READSTRING(*cp, command); + + if ( + (playernum != serverplayer && !IsPlayerAdmin(playernum)) + || (eventID >= AEV__MAX) + ) + { + CONS_Alert(CONS_WARNING, + M_GetText ("Illegal automate received from %s\n"), + player_names[playernum]); + if (server) + SendKick(playernum, KICK_MSG_CON_FAIL); + return; + } + + Automate_Set(eventID, command); + + if (server || consoleplayer == playernum) + { + if (command == NULL || strlen(command) == 0) + { + CONS_Printf( + "Removed the %s automate command.\n", + automate_names[eventID] + ); + } + else + { + CONS_Printf( + "Set the %s automate command to \"" "\x82" "%s" "\x80" "\".\n", + automate_names[eventID], + command + ); + } + } +} + /** Prints the number of displayplayers[0]. * * \todo Possibly remove this; it was useful for debugging at one point. @@ -5554,6 +5693,55 @@ static void Command_Schedule_List(void) } } +static void Command_Automate_Set(void) +{ + UINT8 buf[MAXTEXTCMD]; + UINT8 *buf_p = buf; + + size_t ac; + + const char *event; + size_t eventID; + + const char *command; + + if (!(server || IsPlayerAdmin(consoleplayer))) + { + CONS_Printf("Only the server or a remote admin can use this.\n"); + return; + } + + ac = COM_Argc(); + if (ac < 3) + { + CONS_Printf("automate_set : sets the command to run each time a event triggers\n"); + return; + } + + event = COM_Argv(1); + + for (eventID = 0; eventID < AEV__MAX; eventID++) + { + if (strcasecmp(event, automate_names[eventID]) == 0) + { + break; + } + } + + if (eventID == AEV__MAX) + { + CONS_Printf("Unknown event type \"%s\".\n", event); + return; + } + + command = COM_Argv(2); + + WRITEUINT8(buf_p, eventID); + WRITESTRING(buf_p, command); + + SendNetXCmd(XD_AUTOMATE, buf, buf_p - buf); +} + /** Makes a change to ::cv_forceskin take effect immediately. * * \sa Command_SetForcedSkin_f, cv_forceskin, forcedskin diff --git a/src/d_netcmd.h b/src/d_netcmd.h index ff3b0be9c..ddcba960e 100644 --- a/src/d_netcmd.h +++ b/src/d_netcmd.h @@ -163,6 +163,7 @@ typedef enum XD_PLAYSOUND, // 35 XD_SCHEDULETASK, // 36 XD_SCHEDULECLEAR, // 37 + XD_AUTOMATE, // 38 MAXNETXCMD } netxcmd_t; @@ -247,6 +248,18 @@ void Schedule_Insert(scheduleTask_t *addTask); void Schedule_Add(INT16 basetime, INT16 timeleft, const char *command); void Schedule_Clear(void); +typedef enum +{ + AEV_ROUNDSTART, + AEV_INTERMISSIONSTART, + AEV_VOTESTART, + AEV__MAX +} automateEvents_t; + +void Automate_Run(automateEvents_t type); +void Automate_Set(automateEvents_t type, const char *command); +void Automate_Clear(void); + // used for the player setup menu UINT8 CanChangeSkin(INT32 playernum); diff --git a/src/g_game.c b/src/g_game.c index f3e0e4f1d..31fe9876e 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -1335,6 +1335,7 @@ static void weaponPrefChange4(void) void G_DoLoadLevel(boolean resetplayer) { INT32 i, j; + boolean doAutomate = false; // Make sure objectplace is OFF when you first start the level! OP_ResetObjectplace(); @@ -1364,6 +1365,10 @@ void G_DoLoadLevel(boolean resetplayer) else titlemapinaction = TITLEMAP_OFF; + // Doing this matches HOSTMOD behavior. + // Is that desired? IDK + doAutomate = (gamestate != GS_LEVEL); + G_SetGamestate(GS_LEVEL); I_UpdateMouseGrab(); @@ -1405,6 +1410,11 @@ void G_DoLoadLevel(boolean resetplayer) CON_ClearHUD(); server_lagless = cv_lagless.value; + + if (doAutomate == true) + { + Automate_Run(AEV_ROUNDSTART); + } } // diff --git a/src/y_inter.c b/src/y_inter.c index d66a82dcb..3a19360a9 100644 --- a/src/y_inter.c +++ b/src/y_inter.c @@ -1023,6 +1023,8 @@ void Y_StartIntermission(void) usetile = useinterpic = false; usebuffer = true; } + + Automate_Run(AEV_INTERMISSIONSTART); } // ====== @@ -1685,6 +1687,7 @@ void Y_StartVote(void) } voteclient.loaded = true; + Automate_Run(AEV_VOTESTART); } //