/* =========================================================================== Copyright (C) 2000 - 2013, Raven Software, Inc. Copyright (C) 2001 - 2013, Activision, Inc. Copyright (C) 2013 - 2015, OpenJK contributors This file is part of the OpenJK source code. OpenJK is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . =========================================================================== */ // Filename:- sv_savegame.cpp #include #include "../server/exe_headers.h" #define JPEG_IMAGE_QUALITY 95 //#define USE_LAST_SAVE_FROM_THIS_MAP // enable this if you want to use the last explicity-loaded savegame from this map // when respawning after dying, else it'll just load "auto" regardless // (EF1 behaviour). I should maybe time/date check them though? #include "server.h" #include "../qcommon/stringed_ingame.h" #include "../game/statindex.h" #include "../game/weapons.h" #include "../game/g_items.h" #include #include "qcommon/ojk_saved_game.h" #include "qcommon/ojk_saved_game_helper.h" static char saveGameComment[iSG_COMMENT_SIZE]; //#define SG_PROFILE // enable for debug save stats if you want int giSaveGameVersion; // filled in when a savegame file is opened SavedGameJustLoaded_e eSavedGameJustLoaded = eNO; char sLastSaveFileLoaded[MAX_QPATH]={0}; #ifdef JK2_MODE #define iSG_MAPCMD_SIZE (MAX_TOKEN_CHARS) #else #define iSG_MAPCMD_SIZE (MAX_QPATH) #endif // JK2_MODE static char *SG_GetSaveGameMapName(const char *psPathlessBaseName); #ifdef SG_PROFILE class CChid { private: int m_iCount; int m_iSize; public: CChid() { m_iCount = 0; m_iSize = 0; } void Add(int iLength) { m_iCount++; m_iSize += iLength; } int GetCount() { return m_iCount; } int GetSize() { return m_iSize; } }; typedef map CChidInfo_t; CChidInfo_t save_info; #endif static const char *GetString_FailedToOpenSaveGame(const char *psFilename, qboolean bOpen) { static char sTemp[256]; strcpy(sTemp,S_COLOR_RED); #ifdef JK2_MODE const char *psReference = bOpen ? "MENUS3_FAILED_TO_OPEN_SAVEGAME" : "MENUS3_FAILED_TO_CREATE_SAVEGAME"; #else const char *psReference = bOpen ? "MENUS_FAILED_TO_OPEN_SAVEGAME" : "MENUS3_FAILED_TO_CREATE_SAVEGAME"; #endif Q_strncpyz(sTemp + strlen(sTemp), va( SE_GetString(psReference), psFilename),sizeof(sTemp)); strcat(sTemp,"\n"); return sTemp; } void SG_WipeSavegame( const char* psPathlessBaseName) { ojk::SavedGame::remove( psPathlessBaseName); } // called from the ERR_DROP stuff just in case the error occured during loading of a saved game, because if // we didn't do this then we'd run out of quake file handles after the 8th load fail... // void SG_Shutdown() { ojk::SavedGame& saved_game = ojk::SavedGame::get_instance(); saved_game.close(); eSavedGameJustLoaded = eNO; // important to do this if we ERR_DROP during loading, else next map you load after // a bad save-file you'll arrive at dead :-) // and this bit stops people messing up the laoder by repeatedly stabbing at the load key during loads... // extern qboolean gbAlreadyDoingLoad; gbAlreadyDoingLoad = qfalse; } void SV_WipeGame_f(void) { if (Cmd_Argc() != 2) { Com_Printf (S_COLOR_RED "USAGE: wipe \n"); return; } if (!Q_stricmp (Cmd_Argv(1), "auto") ) { Com_Printf (S_COLOR_RED "Can't wipe 'auto'\n"); return; } SG_WipeSavegame(Cmd_Argv(1)); // Com_Printf("%s has been wiped\n", Cmd_Argv(1)); // wurde gelöscht in german, but we've only got one string // Com_Printf("Ok\n"); // no localization of this } /* // Store given string in saveGameComment for later use when game is // actually saved */ void SG_StoreSaveGameComment(const char *sComment) { memmove(saveGameComment,sComment,iSG_COMMENT_SIZE); } qboolean SV_TryLoadTransition( const char *mapname ) { char *psFilename = va( "hub/%s", mapname ); Com_Printf (S_COLOR_CYAN "Restoring game \"%s\"...\n", psFilename); if ( !SG_ReadSavegame( psFilename ) ) {//couldn't load a savegame return qfalse; } #ifdef JK2_MODE Com_Printf (S_COLOR_CYAN "Done.\n"); #else Com_Printf (S_COLOR_CYAN "%s.\n",SE_GetString("MENUS_DONE")); #endif return qtrue; } qboolean gbAlreadyDoingLoad = qfalse; void SV_LoadGame_f(void) { if (gbAlreadyDoingLoad) { Com_DPrintf ("( Already loading, ignoring extra 'load' commands... )\n"); return; } // // check server is running // // // if ( !com_sv_running->integer ) // { // Com_Printf( "Server is not running\n" ); // return; // } if (Cmd_Argc() != 2) { Com_Printf ("USAGE: load \n"); return; } const char *psFilename = Cmd_Argv(1); if (strstr (psFilename, "..") || strstr (psFilename, "/") || strstr (psFilename, "\\") ) { Com_Printf (S_COLOR_RED "Bad loadgame name.\n"); return; } if (!Q_stricmp (psFilename, "current")) { Com_Printf (S_COLOR_RED "Can't load from \"current\"\n"); return; } // special case, if doing a respawn then check that the available auto-save (if any) is from the same map // as we're currently on (if in a map at all), if so, load that "auto", else re-load the last-loaded file... // if (!Q_stricmp(psFilename, "*respawn")) { psFilename = "auto"; // default to standard respawn behaviour // see if there's a last-loaded file to even check against as regards loading... // if ( sLastSaveFileLoaded[0] ) { const char *psServerInfo = sv.configstrings[CS_SERVERINFO]; const char *psMapName = Info_ValueForKey( psServerInfo, "mapname" ); char *psMapNameOfAutoSave = SG_GetSaveGameMapName("auto"); if ( !Q_stricmp(psMapName,"_brig") ) {//if you're in the brig and there is no autosave, load the last loaded savegame if ( !psMapNameOfAutoSave ) { psFilename = sLastSaveFileLoaded; } } else { #ifdef USE_LAST_SAVE_FROM_THIS_MAP // if the map name within the name of the last save file we explicitly loaded is the same // as the current map, then use that... // char *psMapNameOfLastSaveFileLoaded = SG_GetSaveGameMapName(sLastSaveFileLoaded); if (!Q_stricmp(psMapName,psMapNameOfLastSaveFileLoaded))) { psFilename = sLastSaveFileLoaded; } else #endif if (!(psMapName && psMapNameOfAutoSave && !Q_stricmp(psMapName,psMapNameOfAutoSave))) { // either there's no auto file, or it's from a different map to the one we've just died on... // psFilename = sLastSaveFileLoaded; } } } //default will continue to load auto } #ifdef JK2_MODE Com_Printf (S_COLOR_CYAN "Loading game \"%s\"...\n", psFilename); #else Com_Printf (S_COLOR_CYAN "%s\n",va(SE_GetString("MENUS_LOADING_MAPNAME"), psFilename)); #endif gbAlreadyDoingLoad = qtrue; if (!SG_ReadSavegame(psFilename)) { gbAlreadyDoingLoad = qfalse; // do NOT do this here now, need to wait until client spawn, unless the load failed. } else { #ifdef JK2_MODE Com_Printf (S_COLOR_CYAN "Done.\n"); #else Com_Printf (S_COLOR_CYAN "%s.\n",SE_GetString("MENUS_DONE")); #endif } } qboolean SG_GameAllowedToSaveHere(qboolean inCamera); //JLF notes // save game will be in charge of creating a new directory void SV_SaveGame_f(void) { // check server is running // if ( !com_sv_running->integer ) { Com_Printf( S_COLOR_RED "Server is not running\n" ); return; } if (sv.state != SS_GAME) { Com_Printf (S_COLOR_RED "You must be in a game to save.\n"); return; } // check args... // if ( Cmd_Argc() != 2 ) { Com_Printf( "USAGE: save \n" ); return; } if (svs.clients[0].frames[svs.clients[0].netchan.outgoingSequence & PACKET_MASK].ps.stats[STAT_HEALTH] <= 0) { #ifdef JK2_MODE Com_Printf (S_COLOR_RED "\nCan't savegame while dead!\n"); #else Com_Printf (S_COLOR_RED "\n%s\n", SE_GetString("SP_INGAME_CANT_SAVE_DEAD")); #endif return; } //this check catches deaths even the instant you die, like during a slo-mo death! gentity_t *svent; svent = SV_GentityNum(0); if (svent->client->stats[STAT_HEALTH]<=0) { #ifdef JK2_MODE Com_Printf (S_COLOR_RED "\nCan't savegame while dead!\n"); #else Com_Printf (S_COLOR_RED "\n%s\n", SE_GetString("SP_INGAME_CANT_SAVE_DEAD")); #endif return; } const char *psFilename = Cmd_Argv(1); char filename[MAX_QPATH] = {0}; Q_strncpyz(filename, psFilename, sizeof(filename)); if (!Q_stricmp (filename, "current")) { Com_Printf (S_COLOR_RED "Can't save to 'current'\n"); return; } if (strstr (filename, "..") || strstr (filename, "/") || strstr (filename, "\\") ) { Com_Printf (S_COLOR_RED "Bad savegame name.\n"); return; } if (!SG_GameAllowedToSaveHere(qfalse)) //full check return; // this prevents people saving via quick-save now during cinematics. #ifdef JK2_MODE if ( !Q_stricmp (filename, "quik*") || !Q_stricmp (filename, "auto*") ) { SCR_PrecacheScreenshot(); if ( filename[4]=='*' ) filename[4]=0; //remove the * SG_StoreSaveGameComment(""); // clear previous comment/description, which will force time/date comment. } #else if ( !Q_stricmp (filename, "auto") ) { SG_StoreSaveGameComment(""); // clear previous comment/description, which will force time/date comment. } #endif #ifdef JK2_MODE Com_Printf (S_COLOR_CYAN "Saving game \"%s\"...\n", filename); #else Com_Printf (S_COLOR_CYAN "%s \"%s\"...\n", SE_GetString("CON_TEXT_SAVING_GAME"), filename); #endif if (SG_WriteSavegame(filename, qfalse)) { #ifdef JK2_MODE Com_Printf (S_COLOR_CYAN "Done.\n"); #else Com_Printf (S_COLOR_CYAN "%s.\n",SE_GetString("MENUS_DONE")); #endif } else { #ifdef JK2_MODE Com_Printf (S_COLOR_RED "Failed.\n"); #else Com_Printf (S_COLOR_RED "%s.\n",SE_GetString("MENUS_FAILED_TO_OPEN_SAVEGAME")); #endif } } //--------------- static void WriteGame(qboolean autosave) { ojk::SavedGameHelper saved_game( &ojk::SavedGame::get_instance()); saved_game.write_chunk( INT_ID('G', 'A', 'M', 'E'), autosave); if (autosave) { // write out player ammo level, health, etc... // extern void SV_Player_EndOfLevelSave(void); SV_Player_EndOfLevelSave(); // this sets up the various cvars needed, so we can then write them to disk // char s[MAX_STRING_CHARS]; // write health/armour etc... // memset(s,0,sizeof(s)); Cvar_VariableStringBuffer( sCVARNAME_PLAYERSAVE, s, sizeof(s) ); saved_game.write_chunk( INT_ID('C', 'V', 'S', 'V'), s); // write ammo... // memset(s,0,sizeof(s)); Cvar_VariableStringBuffer( "playerammo", s, sizeof(s) ); saved_game.write_chunk( INT_ID('A', 'M', 'M', 'O'), s); // write inventory... // memset(s,0,sizeof(s)); Cvar_VariableStringBuffer( "playerinv", s, sizeof(s) ); saved_game.write_chunk( INT_ID('I', 'V', 'T', 'Y'), s); // the new JK2 stuff - force powers, etc... // memset(s,0,sizeof(s)); Cvar_VariableStringBuffer( "playerfplvl", s, sizeof(s) ); saved_game.write_chunk( INT_ID('F', 'P', 'L', 'V'), s); } } static qboolean ReadGame (void) { qboolean qbAutoSave = qfalse; ojk::SavedGameHelper saved_game( &ojk::SavedGame::get_instance()); saved_game.read_chunk( INT_ID('G', 'A', 'M', 'E'), qbAutoSave); if (qbAutoSave) { char s[MAX_STRING_CHARS]={0}; // read health/armour etc... // memset(s,0,sizeof(s)); saved_game.read_chunk( INT_ID('C', 'V', 'S', 'V'), s); Cvar_Set( sCVARNAME_PLAYERSAVE, s ); // read ammo... // memset(s,0,sizeof(s)); saved_game.read_chunk( INT_ID('A', 'M', 'M', 'O'), s); Cvar_Set( "playerammo", s); // read inventory... // memset(s,0,sizeof(s)); saved_game.read_chunk( INT_ID('I', 'V', 'T', 'Y'), s); Cvar_Set( "playerinv", s); // read force powers... // memset(s,0,sizeof(s)); saved_game.read_chunk( INT_ID('F', 'P', 'L', 'V'), s); Cvar_Set( "playerfplvl", s ); } return qbAutoSave; } //--------------- // write all CVAR_SAVEGAME cvars // these will be things like model, name, ... // extern cvar_t *cvar_vars; // I know this is really unpleasant, but I need access for scanning/writing latched cvars during save games void SG_WriteCvars(void) { cvar_t *var; int iCount = 0; ojk::SavedGameHelper saved_game( &ojk::SavedGame::get_instance()); // count the cvars... // for (var = cvar_vars; var; var = var->next) { #ifdef JK2_MODE if (!(var->flags & (CVAR_SAVEGAME|CVAR_USERINFO))) #else if (!(var->flags & CVAR_SAVEGAME)) #endif { continue; } iCount++; } // store count... // saved_game.write_chunk( INT_ID('C', 'V', 'C', 'N'), iCount); // write 'em... // for (var = cvar_vars; var; var = var->next) { #ifdef JK2_MODE if (!(var->flags & (CVAR_SAVEGAME|CVAR_USERINFO))) #else if (!(var->flags & CVAR_SAVEGAME)) #endif { continue; } saved_game.write_chunk( INT_ID('C', 'V', 'A', 'R'), var->name, static_cast(strlen(var->name) + 1)); saved_game.write_chunk( INT_ID('V', 'A', 'L', 'U'), var->string, static_cast(strlen(var->string) + 1)); } } void SG_ReadCvars() { int iCount = 0; std::string psName; const char* psValue; ojk::SavedGameHelper saved_game( &ojk::SavedGame::get_instance()); saved_game.read_chunk( INT_ID('C', 'V', 'C', 'N'), iCount); for (int i = 0; i < iCount; ++i) { saved_game.read_chunk( INT_ID('C', 'V', 'A', 'R')); psName = reinterpret_cast( saved_game.get_buffer_data()); saved_game.read_chunk( INT_ID('V', 'A', 'L', 'U')); psValue = reinterpret_cast( saved_game.get_buffer_data()); ::Cvar_Set(psName.c_str(), psValue); } } void SG_WriteServerConfigStrings() { ojk::SavedGameHelper saved_game( &ojk::SavedGame::get_instance()); int iCount = 0; int i; // not in FOR statement in case compiler goes weird by reg-optimising it then failing to get the address later // count how many non-blank server strings there are... // for ( i=0; i { iCount++; } } } saved_game.write_chunk( INT_ID('C', 'S', 'C', 'N'), iCount); // now write 'em... // for (i=0; i( INT_ID('C', 'S', 'I', 'N'), i); saved_game.write_chunk( INT_ID('C', 'S', 'D', 'A'), ::sv.configstrings[i], static_cast(strlen(::sv.configstrings[i]) + 1)); } } } } void SG_ReadServerConfigStrings( void ) { // trash the whole table... // for (int i=0; i( INT_ID('C', 'S', 'C', 'N'), iCount); Com_DPrintf( "Reading %d configstrings...\n",iCount); for (int i = 0; i( INT_ID('C', 'S', 'I', 'N'), iIndex); saved_game.read_chunk( INT_ID('C', 'S', 'D', 'A')); psName = reinterpret_cast( saved_game.get_buffer_data()); Com_DPrintf( "Cfg str %d = %s\n",iIndex, psName); //sv.configstrings[iIndex] = psName; SV_SetConfigstring(iIndex, psName); } } static unsigned int SG_UnixTimestamp ( const time_t& t ) { return static_cast(t); } static void SG_WriteComment(qboolean qbAutosave, const char *psMapName) { ojk::SavedGameHelper saved_game( &ojk::SavedGame::get_instance()); char sComment[iSG_COMMENT_SIZE]; if ( qbAutosave || !*saveGameComment) { Com_sprintf( sComment, sizeof(sComment), "---> %s", psMapName ); } else { Q_strncpyz(sComment,saveGameComment, sizeof(sComment)); } saved_game.write_chunk( INT_ID('C', 'O', 'M', 'M'), sComment); // Add Date/Time/Map stamp unsigned int timestamp = SG_UnixTimestamp (time (NULL)); saved_game.write_chunk( INT_ID('C', 'M', 'T', 'M'), timestamp); Com_DPrintf("Saving: current (%s)\n", sComment); } static time_t SG_GetTime ( unsigned int timestamp ) { return static_cast(timestamp); } // Test to see if the given file name is in the save game directory // then grab the comment if it's there // int SG_GetSaveGameComment( const char* psPathlessBaseName, char* sComment, char* sMapName) { int ret = 0; ojk::SavedGame& saved_game = ojk::SavedGame::get_instance(); ojk::SavedGameHelper sgh( &ojk::SavedGame::get_instance()); if (!saved_game.open( psPathlessBaseName)) { return 0; } bool is_succeed = true; // Read description // is_succeed = sgh.try_read_chunk( INT_ID('C', 'O', 'M', 'M')); if (is_succeed) { if (sComment) { if (sgh.get_buffer_size() == iSG_COMMENT_SIZE) { std::uninitialized_copy_n( static_cast(sgh.get_buffer_data()), iSG_COMMENT_SIZE, sComment); } else { sComment[0] = '\0'; } } } // Read timestamp // time_t tFileTime = ::SG_GetTime(0); if (is_succeed) { unsigned int fileTime = 0; is_succeed = sgh.try_read_chunk( INT_ID('C', 'M', 'T', 'M'), fileTime); if (is_succeed) { tFileTime = ::SG_GetTime( fileTime); } } #ifdef JK2_MODE // Read screenshot // if (is_succeed) { size_t iScreenShotLength; is_succeed = sgh.try_read_chunk( INT_ID('S', 'H', 'L', 'N'), iScreenShotLength); } if (is_succeed) { is_succeed = sgh.try_read_chunk( INT_ID('S', 'H', 'O', 'T')); } #endif // Read mapname // if (is_succeed) { is_succeed = sgh.try_read_chunk( INT_ID('M', 'P', 'C', 'M')); if (is_succeed) { if (sMapName) { if (sgh.get_buffer_size() == iSG_MAPCMD_SIZE) { std::uninitialized_copy_n( static_cast(sgh.get_buffer_data()), iSG_MAPCMD_SIZE, sMapName); } else { sMapName[0] = '\0'; } } } } ret = tFileTime; saved_game.close(); return ret; } // read the mapname field from the supplied savegame file // // returns NULL if not found // static char *SG_GetSaveGameMapName(const char *psPathlessBaseName) { static char sMapName[iSG_MAPCMD_SIZE]={0}; char *psReturn = NULL; if (SG_GetSaveGameComment(psPathlessBaseName, NULL, sMapName)) { psReturn = sMapName; } return psReturn; } // pass in qtrue to set as loading screen, else pass in pvDest to read it into there... // #ifdef JK2_MODE static bool SG_ReadScreenshot( bool set_as_loading_screen, void* screenshot_ptr) { bool is_succeed = true; ojk::SavedGameHelper saved_game( &ojk::SavedGame::get_instance()); // get JPG screenshot data length... // size_t screenshot_length = 0; is_succeed = saved_game.try_read_chunk( INT_ID('S', 'H', 'L', 'N'), screenshot_length); // // alloc enough space plus extra 4K for sloppy JPG-decode reader to not do memory access violation... // byte* jpeg_data = nullptr; if (is_succeed) { jpeg_data = static_cast(::Z_Malloc( static_cast(screenshot_length + 4096), TAG_TEMP_WORKSPACE, qfalse)); } // // now read the JPG data... // if (is_succeed) { is_succeed = saved_game.try_read_chunk( INT_ID('S', 'H', 'O', 'T'), jpeg_data, static_cast(screenshot_length)); } // // decompress JPG data... // byte* image = NULL; int width; int height; if (is_succeed) { ::re.LoadJPGFromBuffer( jpeg_data, screenshot_length, &image, &width, &height); // // if the loaded image is the same size as the game is expecting, then copy it to supplied arg (if present)... // if (width == SG_SCR_WIDTH && height == SG_SCR_HEIGHT) { if (screenshot_ptr) { ::memcpy( screenshot_ptr, image, SG_SCR_WIDTH * SG_SCR_HEIGHT * 4); } if (set_as_loading_screen) { ::SCR_SetScreenshot( image, SG_SCR_WIDTH, SG_SCR_HEIGHT); } } else { is_succeed = false; } } if (jpeg_data) { ::Z_Free(jpeg_data); } if (image) { ::Z_Free(image); } return is_succeed; } // Gets the savegame screenshot // qboolean SG_GetSaveImage( const char* base_name, void* image_ptr) { if (!base_name) { return qfalse; } ojk::SavedGame& saved_game = ojk::SavedGame::get_instance(); if (!saved_game.open(base_name)) { return qfalse; } bool is_succeed = true; ojk::SavedGameHelper sgh( &saved_game); is_succeed = sgh.try_read_chunk( INT_ID('C', 'O', 'M', 'M')); if (is_succeed) { is_succeed = sgh.try_read_chunk( INT_ID('C', 'M', 'T', 'M')); } if (is_succeed) { is_succeed = SG_ReadScreenshot( false, image_ptr); } saved_game.close(); return is_succeed ? qtrue : qfalse; } static void SG_WriteScreenshot(qboolean qbAutosave, const char *psMapName) { ojk::SavedGameHelper saved_game( &ojk::SavedGame::get_instance()); byte *pbRawScreenShot = NULL; byte *byBlank = NULL; if( qbAutosave ) { // try to read a levelshot (any valid TGA/JPG etc named the same as the map)... // int iWidth = SG_SCR_WIDTH; int iHeight= SG_SCR_HEIGHT; const size_t bySize = SG_SCR_WIDTH * SG_SCR_HEIGHT * 4; byte *src, *dst; byBlank = new byte[bySize]; pbRawScreenShot = SCR_TempRawImage_ReadFromFile(va("levelshots/%s.tga",psMapName), &iWidth, &iHeight, byBlank, qtrue); // qtrue = vert flip if (pbRawScreenShot) { for (int y = 0; y < iHeight; y++) { for (int x = 0; x < iWidth; x++) { src = pbRawScreenShot + 4 * (y * iWidth + x); dst = pbRawScreenShot + 3 * (y * iWidth + x); dst[0] = src[0]; dst[1] = src[1]; dst[2] = src[2]; } } } } if (!pbRawScreenShot) { pbRawScreenShot = SCR_GetScreenshot(0); } size_t iJPGDataSize = 0; size_t bufSize = SG_SCR_WIDTH * SG_SCR_HEIGHT * 3; byte *pJPGData = (byte *)Z_Malloc( static_cast(bufSize), TAG_TEMP_WORKSPACE, qfalse, 4 ); #ifdef JK2_MODE bool flip_vertical = true; #else bool flip_vertical = false; #endif // JK2_MODE iJPGDataSize = re.SaveJPGToBuffer(pJPGData, bufSize, JPEG_IMAGE_QUALITY, SG_SCR_WIDTH, SG_SCR_HEIGHT, pbRawScreenShot, 0, flip_vertical ); if ( qbAutosave ) delete[] byBlank; saved_game.write_chunk( INT_ID('S', 'H', 'L', 'N'), iJPGDataSize); saved_game.write_chunk( INT_ID('S', 'H', 'O', 'T'), pJPGData, static_cast(iJPGDataSize)); Z_Free(pJPGData); SCR_TempRawImage_CleanUp(); } #endif qboolean SG_GameAllowedToSaveHere(qboolean inCamera) { if (!inCamera) { if ( !com_sv_running || !com_sv_running->integer ) { return qfalse; // Com_Printf( S_COLOR_RED "Server is not running\n" ); } if (CL_IsRunningInGameCinematic()) { return qfalse; //nope, not during a video } if (sv.state != SS_GAME) { return qfalse; // Com_Printf (S_COLOR_RED "You must be in a game to save.\n"); } //No savegames from "_" maps if ( !sv_mapname || (sv_mapname->string != NULL && sv_mapname->string[0] == '_') ) { return qfalse; // Com_Printf (S_COLOR_RED "Cannot save on holodeck or brig.\n"); } if (svs.clients[0].frames[svs.clients[0].netchan.outgoingSequence & PACKET_MASK].ps.stats[STAT_HEALTH] <= 0) { return qfalse; // Com_Printf (S_COLOR_RED "\nCan't savegame while dead!\n"); } } if (!ge) return inCamera; // only happens when called to test if inCamera return ge->GameAllowedToSaveHere(); } qboolean SG_WriteSavegame(const char *psPathlessBaseName, qboolean qbAutosave) { if (!qbAutosave && !SG_GameAllowedToSaveHere(qfalse)) //full check return qfalse; // this prevents people saving via quick-save now during cinematics int iPrevTestSave = sv_testsave->integer; sv_testsave->integer = 0; // Write out server data... // const char *psServerInfo = sv.configstrings[CS_SERVERINFO]; const char *psMapName = Info_ValueForKey( psServerInfo, "mapname" ); //JLF #ifdef JK2_MODE if ( !strcmp("quik",psPathlessBaseName)) #else if ( !strcmp("quick",psPathlessBaseName)) #endif { SG_StoreSaveGameComment(va("--> %s <--",psMapName)); } ojk::SavedGame& saved_game = ojk::SavedGame::get_instance(); if(!saved_game.create( "current" )) { Com_Printf (GetString_FailedToOpenSaveGame("current",qfalse));//S_COLOR_RED "Failed to create savegame\n"); SG_WipeSavegame( "current" ); sv_testsave->integer = iPrevTestSave; return qfalse; } //END JLF ojk::SavedGameHelper sgh( &saved_game); char sMapCmd[iSG_MAPCMD_SIZE]={0}; Q_strncpyz( sMapCmd,psMapName, sizeof(sMapCmd)); // need as array rather than ptr because const strlen needed for MPCM chunk SG_WriteComment(qbAutosave, sMapCmd); #ifdef JK2_MODE SG_WriteScreenshot(qbAutosave, sMapCmd); #endif sgh.write_chunk( INT_ID('M', 'P', 'C', 'M'), sMapCmd); SG_WriteCvars(); WriteGame (qbAutosave); // Write out all the level data... // if (!qbAutosave) { sgh.write_chunk( INT_ID('T', 'I', 'M', 'E'), ::sv.time); sgh.write_chunk( INT_ID('T', 'I', 'M', 'R'), ::sv.timeResidual); CM_WritePortalState(); SG_WriteServerConfigStrings(); } ge->WriteLevel(qbAutosave); // always done now, but ent saver only does player if auto bool is_write_failed = saved_game.is_failed(); saved_game.close(); if (is_write_failed) { Com_Printf (GetString_FailedToOpenSaveGame("current",qfalse));//S_COLOR_RED "Failed to write savegame!\n"); SG_WipeSavegame( "current" ); sv_testsave->integer = iPrevTestSave; return qfalse; } ojk::SavedGame::rename( "current", psPathlessBaseName); sv_testsave->integer = iPrevTestSave; return qtrue; } qboolean SG_ReadSavegame( const char* psPathlessBaseName) { char sComment[iSG_COMMENT_SIZE]; char sMapCmd[iSG_MAPCMD_SIZE]; #ifdef JK2_MODE Cvar_Set( "cg_missionstatusscreen", "0"); #endif ojk::SavedGame& saved_game = ojk::SavedGame::get_instance(); ojk::SavedGameHelper sgh( &saved_game); const int iPrevTestSave = ::sv_testsave->integer; ojk::ScopeGuard scope_guard( [&]() { ::sv_testsave->integer = 0; }, [&]() { saved_game.close(); ::sv_testsave->integer = iPrevTestSave; } ); if (!saved_game.open(psPathlessBaseName)) { //S_COLOR_RED "Failed to open savegame \"%s\"\n", psPathlessBaseName); ::Com_Printf( ::GetString_FailedToOpenSaveGame( psPathlessBaseName, qtrue)); return qfalse; } // this check isn't really necessary, but it reminds me that these two strings may actually be the same physical one. // if (psPathlessBaseName != sLastSaveFileLoaded) { ::Q_strncpyz( ::sLastSaveFileLoaded, psPathlessBaseName, sizeof(sLastSaveFileLoaded)); } // Read in all the server data... // sgh.read_chunk( INT_ID('C', 'O', 'M', 'M'), sComment); ::Com_DPrintf( "Reading: %s\n", sComment); sgh.read_chunk( INT_ID('C', 'M', 'T', 'M')); #ifdef JK2_MODE ::SG_ReadScreenshot( true, nullptr); #endif sgh.read_chunk( INT_ID('M', 'P', 'C', 'M'), sMapCmd); ::SG_ReadCvars(); // read game state const qboolean qbAutosave = ::ReadGame(); ::eSavedGameJustLoaded = (qbAutosave ? eAUTO : eFULL); // note that this also trashes the whole G_Alloc pool as well (of course) ::SV_SpawnServer( sMapCmd, eForceReload_NOTHING, (::eSavedGameJustLoaded != eFULL ? qtrue : qfalse)); // read in all the level data... // if (!qbAutosave) { sgh.read_chunk( INT_ID('T', 'I', 'M', 'E'), ::sv.time); sgh.read_chunk( INT_ID('T', 'I', 'M', 'R'), ::sv.timeResidual); ::CM_ReadPortalState(); ::SG_ReadServerConfigStrings(); } // always done now, but ent reader only does player if auto ::ge->ReadLevel( qbAutosave, qbLoadTransition); return qtrue; } void SG_TestSave(void) { if (sv_testsave->integer && sv.state == SS_GAME) { WriteGame(qfalse); ge->WriteLevel(qfalse); } }