From cf9a9097b9b6a647990f1b1654540b9d33ee3435 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Wed, 2 Jul 2025 10:52:51 -0400 Subject: [PATCH] Fix up saving in multiplayer By default allow only settings controllers to save the game. Use actual file names to help prevent possible save file overriding as savexx is unreliable online. Prevent quicksave behavior from working with the rotator. Force a unique netgame subfolder for multiplayer saves to remove the ability to override singleplayer saves. Send over the host's -loadgame argument to make loading easier (will not override the guest's -loadgame in case they need a special file name). --- src/common/menu/savegamemanager.cpp | 55 +++++++++++++++++-- .../platform/posix/unix/i_specialpaths.cpp | 7 ++- src/common/platform/win32/i_specialpaths.cpp | 4 ++ src/console/c_cmds.cpp | 2 +- src/d_net.cpp | 36 +++++++++++- src/g_game.cpp | 13 +++++ src/menu/doommenu.cpp | 2 +- 7 files changed, 110 insertions(+), 9 deletions(-) diff --git a/src/common/menu/savegamemanager.cpp b/src/common/menu/savegamemanager.cpp index efb5a02af..f436ba8a3 100644 --- a/src/common/menu/savegamemanager.cpp +++ b/src/common/menu/savegamemanager.cpp @@ -53,6 +53,8 @@ CVAR(String, save_dir, "", CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_SYSTEM_ONLY); FString SavegameFolder; CVAR(Int, save_sort_order, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) +extern bool netgame; + //============================================================================= // // Save data maintenance @@ -264,12 +266,47 @@ void FSavegameManagerBase::DoSave(int Selected, const char *savegamestring) FString filename; int i; - for (i = 0;; ++i) + if (netgame) { - filename = BuildSaveName("save", i); - if (!FileExists(filename)) + // For netgames it's usually a bad idea to use the default savexx names, so instead + // sanitize the description to use as a name. + filename = savegamestring; + FixPathSeperator(filename); + bool failed = false; + if (filename[0] == '/') { - break; + Printf("saving to an absolute path is not allowed\n"); + failed = true; + } + else if (filename.IndexOf("..") >= 0) + { + Printf("'..' not allowed in file names\n"); + failed = true; + } +#ifdef _WIN32 + // block all invalid characters for Windows file names + else if (filename.IndexOfAny(":?*<>|") >= 0) + { + Printf("file name contains invalid characters\n"); + failed = true; + } +#endif + if (failed) + { + M_ClearMenus(); + return; + } + filename = G_BuildSaveName(filename.GetChars()); + } + else + { + for (i = 0;; ++i) + { + filename = BuildSaveName("save", i); + if (!FileExists(filename)) + { + break; + } } } PerformSaveGame(filename.GetChars(), savegamestring); @@ -561,7 +598,15 @@ FString G_GetSavegamesFolder() FString name; bool usefilter; - if (const char* const dir = Args->CheckValue("-savedir")) + // Always use the netgame folder for multiplayer games to prevent any singleplayer saves + // from being overridden by someone else. Also makes it easier for everyone to load from + // it. + if (netgame) + { + name = M_GetSavegamesPath(); + usefilter = true; + } + else if (const char* const dir = Args->CheckValue("-savedir")) { name = dir; usefilter = false; //-savedir specifies an absolute save directory path. diff --git a/src/common/platform/posix/unix/i_specialpaths.cpp b/src/common/platform/posix/unix/i_specialpaths.cpp index 112e92ee7..c06e27b67 100644 --- a/src/common/platform/posix/unix/i_specialpaths.cpp +++ b/src/common/platform/posix/unix/i_specialpaths.cpp @@ -42,6 +42,8 @@ #include "version.h" // for GAMENAME +extern bool netgame; + FString GetUserFile (const char *file) { @@ -191,7 +193,10 @@ FString M_GetScreenshotsPath() FString M_GetSavegamesPath() { - return NicePath("$HOME/" GAME_DIR "/savegames/"); + FString pName = "$HOME/" GAME_DIR "/savegames/"; + if (netgame) + pName << "netgame/"; + return NicePath(pName.GetChars()); } //=========================================================================== diff --git a/src/common/platform/win32/i_specialpaths.cpp b/src/common/platform/win32/i_specialpaths.cpp index 8862024b7..c7f53c24a 100644 --- a/src/common/platform/win32/i_specialpaths.cpp +++ b/src/common/platform/win32/i_specialpaths.cpp @@ -51,6 +51,8 @@ static int isportable = -1; +extern bool netgame; + //=========================================================================== // // IsProgramDirectoryWritable @@ -377,6 +379,8 @@ FString M_GetSavegamesPath() path = GetKnownFolder(-1, FOLDERID_SavedGames, true); path << "/" GAMENAME "/"; } + if (netgame) + path << "NetGame/"; return path; } diff --git a/src/console/c_cmds.cpp b/src/console/c_cmds.cpp index 9b5e8f612..1d45a1130 100644 --- a/src/console/c_cmds.cpp +++ b/src/console/c_cmds.cpp @@ -713,7 +713,7 @@ UNSAFE_CCMD(save) Printf("saving to an absolute path is not allowed\n"); return; } - if (fname.IndexOf("..") > 0) + if (fname.IndexOf("..") >= 0) { Printf("'..' not allowed in file names\n"); return; diff --git a/src/d_net.cpp b/src/d_net.cpp index 7d7f08587..90dbd83b2 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -159,6 +159,7 @@ CVAR(Bool, vid_lowerinbackground, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) CVAR(Bool, net_ticbalance, false, CVAR_SERVERINFO | CVAR_NOSAVE) // Currently deprecated, but may be brought back later. CVAR(Bool, net_extratic, false, CVAR_SERVERINFO | CVAR_NOSAVE) +CVAR(Bool, net_limitsaves, true, CVAR_SERVERINFO | CVAR_NOSAVE) CVAR(Bool, net_repeatableactioncooldown, true, CVAR_SERVERINFO | CVAR_NOSAVE) CVAR(Bool, net_limitconversations, false, CVAR_SERVERINFO | CVAR_NOSAVE) CUSTOM_CVAR(Int, net_disablepause, 0, CVAR_SERVERINFO | CVAR_NOSAVE) @@ -1818,7 +1819,21 @@ size_t Net_SetGameInfo(uint8_t*& stream) WriteString(startmap.GetChars(), &stream); WriteInt32(rngseed, &stream); C_WriteCVars(&stream, CVAR_SERVERINFO, true); - return stream - start; + + auto load = Args->CheckValue("-loadgame"); + if (load != nullptr) + { + stream[0] = true; + const size_t len = strlen(load) + 1; + memcpy(&stream[1], load, len); + stream += len; + } + else + { + stream[0] = false; + } + + return (stream + 1) - start; } size_t Net_ReadGameInfo(uint8_t*& stream) @@ -1827,6 +1842,25 @@ size_t Net_ReadGameInfo(uint8_t*& stream) startmap = ReadStringConst(&stream); rngseed = ReadInt32(&stream); C_ReadCVars(&stream); + + if (stream[0]) + { + ++stream; + const size_t len = strlen((char*)stream) + 1; + // Don't override the existing argument in case they need to use + // a custom savefile name. + if (!Args->CheckParm("-loadgame")) + { + Args->AppendArg("-loadgame"); + Args->AppendArg((char*)stream); + } + stream += len; + } + else + { + ++stream; + } + return stream - start; } diff --git a/src/g_game.cpp b/src/g_game.cpp index 808d8ba15..558a592e1 100644 --- a/src/g_game.cpp +++ b/src/g_game.cpp @@ -128,6 +128,7 @@ CVAR (Bool, enablescriptscreenshot, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG); CVAR (Bool, cl_restartondeath, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG); EXTERN_CVAR (Float, con_midtime); EXTERN_CVAR(Int, net_disablepause) +EXTERN_CVAR(Bool, net_limitsaves) //========================================================================== // @@ -2161,6 +2162,10 @@ void G_SaveGame (const char *filename, const char *description) { Printf ("%s\n", GStrings.GetString("TXT_SPPLAYERDEAD")); } + else if (netgame && net_limitsaves && !players[consoleplayer].settings_controller) + { + Printf("Only settings controllers can save the game\n"); + } else { savegamefile = filename; @@ -2196,6 +2201,10 @@ CUSTOM_CVAR (Int, quicksaverotationcount, 4, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) void G_DoAutoSave () { + // Never autosave in netgames since you can't load this properly anyway. + if (netgame) + return; + FString description; FString file; // Keep up to four autosaves at a time @@ -2231,6 +2240,10 @@ void G_DoAutoSave () void G_DoQuickSave () { + // Never quicksave in netgames since you can't load this properly anyway. + if (netgame) + return; + FString description; FString file; // Keeps a rotating set of quicksaves diff --git a/src/menu/doommenu.cpp b/src/menu/doommenu.cpp index 5718bd6b0..e21f977db 100644 --- a/src/menu/doommenu.cpp +++ b/src/menu/doommenu.cpp @@ -477,7 +477,7 @@ CCMD (quicksave) return; // If the quick save rotation is enabled, it handles the save slot. - if (quicksaverotation) + if (!netgame && quicksaverotation) { G_DoQuickSave(); return;