/* =========================================================================== Doom 3 BFG Edition GPL Source Code Copyright (C) 1993-2012 id Software LLC, a ZeniMax Media company. This file is part of the Doom 3 BFG Edition GPL Source Code ("Doom 3 BFG Edition Source Code"). Doom 3 BFG Edition Source Code is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Doom 3 BFG Edition Source Code 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 Doom 3 BFG Edition Source Code. If not, see . In addition, the Doom 3 BFG Edition Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Doom 3 BFG Edition Source Code. If not, please request a copy in writing from id Software at the address below. If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA. =========================================================================== */ #pragma hdrstop #include "../../idlib/precompiled.h" #include "../sys_session_local.h" #include "../sys_savegame.h" idCVar savegame_winInduceDelay( "savegame_winInduceDelay", "0", CVAR_INTEGER, "on windows, this is a delay induced before any file operation occurs" ); extern idCVar fs_savepath; extern idCVar saveGame_checksum; extern idCVar savegame_error; #define SAVEGAME_SENTINAL 0x12358932 /* ======================== void Sys_ExecuteSavegameCommandAsync ======================== */ void Sys_ExecuteSavegameCommandAsyncImpl( idSaveLoadParms * savegameParms ) { assert( savegameParms != NULL ); session->GetSaveGameManager().GetSaveGameThread().data.saveLoadParms = savegameParms; if ( session->GetSaveGameManager().GetSaveGameThread().GetThreadHandle() == 0 ) { session->GetSaveGameManager().GetSaveGameThread().StartWorkerThread( "Savegame", CORE_ANY ); } session->GetSaveGameManager().GetSaveGameThread().SignalWork(); } /* ======================== idLocalUser * GetLocalUserFromUserId ======================== */ idLocalUserWin * GetLocalUserFromSaveParms( const saveGameThreadArgs_t & data ) { if ( ( data.saveLoadParms != NULL) && ( data.saveLoadParms->inputDeviceId >= 0 ) ) { idLocalUser * user = session->GetSignInManager().GetLocalUserByInputDevice( data.saveLoadParms->inputDeviceId ); if ( user != NULL ) { idLocalUserWin * userWin = static_cast< idLocalUserWin * >( user ); if ( userWin != NULL && data.saveLoadParms->userId == idStr::Hash( userWin->GetGamerTag() ) ) { return userWin; } } } return NULL; } /* ======================== idSaveGameThread::SaveGame ======================== */ int idSaveGameThread::Save() { idLocalUserWin * user = GetLocalUserFromSaveParms( data ); if ( user == NULL ) { data.saveLoadParms->errorCode = SAVEGAME_E_INVALID_USER; return -1; } idSaveLoadParms * callback = data.saveLoadParms; idStr saveFolder = "savegame"; saveFolder.AppendPath( callback->directory ); // Check for the required storage space. int64 requiredSizeBytes = 0; { for ( int i = 0; i < callback->files.Num(); i++ ) { idFile_SaveGame * file = callback->files[i]; requiredSizeBytes += ( file->Length() + sizeof( unsigned int ) ); // uint for checksum if ( file->type == SAVEGAMEFILE_PIPELINED ) { requiredSizeBytes += MIN_SAVEGAME_SIZE_BYTES; } } } int ret = ERROR_SUCCESS; // Check size of previous files if needed // ALL THE FILES RIGHT NOW---- could use pattern later... idStrList filesToDelete; if ( ( callback->mode & SAVEGAME_MBF_DELETE_FILES ) && !callback->cancelled ) { if ( fileSystem->IsFolder( saveFolder.c_str(), "fs_savePath" ) == FOLDER_YES ) { idFileList * files = fileSystem->ListFilesTree( saveFolder.c_str(), "*.*" ); for ( int i = 0; i < files->GetNumFiles(); i++ ) { requiredSizeBytes -= fileSystem->GetFileLength( files->GetFile( i ) ); filesToDelete.Append( files->GetFile( i ) ); } fileSystem->FreeFileList( files ); } } // Inform user about size required if necessary if ( requiredSizeBytes > 0 && !callback->cancelled ) { user->StorageSizeAvailable( requiredSizeBytes, callback->requiredSpaceInBytes ); if ( callback->requiredSpaceInBytes > 0 ) { // check to make sure savepath actually exists before erroring idStr directory = fs_savepath.GetString(); directory += "\\"; // so it doesn't think the last part is a file and ignores in the directory creation fileSystem->CreateOSPath( directory ); // we can't actually check FileExists in production builds, so just try to create it user->StorageSizeAvailable( requiredSizeBytes, callback->requiredSpaceInBytes ); if ( callback->requiredSpaceInBytes > 0 ) { callback->errorCode = SAVEGAME_E_INSUFFICIENT_ROOM; // safe to return, haven't written any files yet return -1; } } } // Delete all previous files if needed // ALL THE FILES RIGHT NOW---- could use pattern later... for ( int i = 0; i < filesToDelete.Num() && !callback->cancelled; i++ ) { fileSystem->RemoveFile( filesToDelete[i].c_str() ); } // Save the raw files. for ( int i = 0; i < callback->files.Num() && ret == ERROR_SUCCESS && !callback->cancelled; i++ ) { idFile_SaveGame * file = callback->files[i]; idStr fileName = saveFolder; fileName.AppendPath( file->GetName() ); idStr tempFileName = va( "%s.temp", fileName.c_str() ); idFile * outputFile = fileSystem->OpenFileWrite( tempFileName, "fs_savePath" ); if ( outputFile == NULL ) { idLib::Warning( "[%s]: Couldn't open file for writing, %s. Error = %08x", __FUNCTION__, tempFileName.c_str(), GetLastError() ); file->error = true; callback->errorCode = SAVEGAME_E_UNKNOWN; ret = -1; continue; } if ( ( file->type & SAVEGAMEFILE_PIPELINED ) != 0 ) { idFile_SaveGamePipelined * inputFile = dynamic_cast< idFile_SaveGamePipelined * >( file ); assert( inputFile != NULL ); blockForIO_t block; while ( inputFile->NextWriteBlock( & block ) ) { if ( (size_t)outputFile->Write( block.data, block.bytes ) != block.bytes ) { idLib::Warning( "[%s]: Write failed. Error = %08x", __FUNCTION__, GetLastError() ); file->error = true; callback->errorCode = SAVEGAME_E_INSUFFICIENT_ROOM; ret = -1; break; } } } else { if ( ( file->type & SAVEGAMEFILE_BINARY ) || ( file->type & SAVEGAMEFILE_COMPRESSED ) ) { if ( saveGame_checksum.GetBool() ) { unsigned int checksum = MD5_BlockChecksum( file->GetDataPtr(), file->Length() ); size_t size = outputFile->WriteBig( checksum ); if ( size != sizeof( checksum ) ) { idLib::Warning( "[%s]: Write failed. Error = %08x", __FUNCTION__, GetLastError() ); file->error = true; callback->errorCode = SAVEGAME_E_INSUFFICIENT_ROOM; ret = -1; } } } size_t size = outputFile->Write( file->GetDataPtr(), file->Length() ); if ( size != (size_t)file->Length() ) { idLib::Warning( "[%s]: Write failed. Error = %08x", __FUNCTION__, GetLastError() ); file->error = true; callback->errorCode = SAVEGAME_E_INSUFFICIENT_ROOM; ret = -1; } else { idLib::PrintfIf( saveGame_verbose.GetBool(), "Saved %s (%s)\n", fileName.c_str(), outputFile->GetFullPath() ); } } delete outputFile; if ( ret == ERROR_SUCCESS ) { // Remove the old file if ( !fileSystem->RenameFile( tempFileName, fileName, "fs_savePath" ) ) { idLib::Warning( "Could not start to rename temporary file %s to %s.", tempFileName.c_str(), fileName.c_str() ); } } else { fileSystem->RemoveFile( tempFileName ); idLib::Warning( "Invalid write to temporary file %s.", tempFileName.c_str() ); } } if ( data.saveLoadParms->cancelled ) { data.saveLoadParms->errorCode = SAVEGAME_E_CANCELLED; } // Removed because it seemed a bit drastic #if 0 // If there is an error, delete the partially saved folder if ( callback->errorCode != SAVEGAME_E_NONE ) { if ( fileSystem->IsFolder( saveFolder, "fs_savePath" ) == FOLDER_YES ) { idFileList * files = fileSystem->ListFilesTree( saveFolder, "/|*" ); for ( int i = 0; i < files->GetNumFiles(); i++ ) { fileSystem->RemoveFile( files->GetFile( i ) ); } fileSystem->FreeFileList( files ); fileSystem->RemoveDir( saveFolder ); } } #endif return ret; } /* ======================== idSessionLocal::LoadGame ======================== */ int idSaveGameThread::Load() { idSaveLoadParms * callback = data.saveLoadParms; idStr saveFolder = "savegame"; saveFolder.AppendPath( callback->directory ); if ( fileSystem->IsFolder( saveFolder, "fs_savePath" ) != FOLDER_YES ) { callback->errorCode = SAVEGAME_E_FOLDER_NOT_FOUND; return -1; } int ret = ERROR_SUCCESS; for ( int i = 0; i < callback->files.Num() && ret == ERROR_SUCCESS && !callback->cancelled; i++ ) { idFile_SaveGame * file = callback->files[i]; idStr filename = saveFolder; filename.AppendPath( file->GetName() ); idFile * inputFile = fileSystem->OpenFileRead( filename.c_str() ); if ( inputFile == NULL ) { file->error = true; if ( !( file->type & SAVEGAMEFILE_OPTIONAL ) ) { callback->errorCode = SAVEGAME_E_CORRUPTED; ret = -1; } continue; } if ( ( file->type & SAVEGAMEFILE_PIPELINED ) != 0 ) { idFile_SaveGamePipelined * outputFile = dynamic_cast< idFile_SaveGamePipelined * >( file ); assert( outputFile != NULL ); size_t lastReadBytes = 0; blockForIO_t block; while ( outputFile->NextReadBlock( &block, lastReadBytes ) && !callback->cancelled ) { lastReadBytes = inputFile->Read( block.data, block.bytes ); if ( lastReadBytes != block.bytes ) { // Notify end-of-file to the save game file which will cause all reads on the // other end of the pipeline to return zero bytes after the pipeline is drained. outputFile->NextReadBlock( NULL, lastReadBytes ); break; } } } else { size_t size = inputFile->Length(); unsigned int originalChecksum = 0; if ( ( file->type & SAVEGAMEFILE_BINARY ) != 0 || ( file->type & SAVEGAMEFILE_COMPRESSED ) != 0 ) { if ( saveGame_checksum.GetBool() ) { if ( size >= sizeof( originalChecksum ) ) { inputFile->ReadBig( originalChecksum ); size -= sizeof( originalChecksum ); } } } file->SetLength( size ); size_t sizeRead = inputFile->Read( (void *)file->GetDataPtr(), size ); if ( sizeRead != size ) { file->error = true; callback->errorCode = SAVEGAME_E_CORRUPTED; ret = -1; } if ( ( file->type & SAVEGAMEFILE_BINARY ) != 0 || ( file->type & SAVEGAMEFILE_COMPRESSED ) != 0 ) { if ( saveGame_checksum.GetBool() ) { unsigned int checksum = MD5_BlockChecksum( file->GetDataPtr(), file->Length() ); if ( checksum != originalChecksum ) { file->error = true; callback->errorCode = SAVEGAME_E_CORRUPTED; ret = -1; } } } } delete inputFile; } if ( data.saveLoadParms->cancelled ) { data.saveLoadParms->errorCode = SAVEGAME_E_CANCELLED; } return ret; } /* ======================== idSaveGameThread::Delete This deletes a complete savegame directory ======================== */ int idSaveGameThread::Delete() { idSaveLoadParms * callback = data.saveLoadParms; idStr saveFolder = "savegame"; saveFolder.AppendPath( callback->directory ); int ret = ERROR_SUCCESS; if ( fileSystem->IsFolder( saveFolder, "fs_savePath" ) == FOLDER_YES ) { idFileList * files = fileSystem->ListFilesTree( saveFolder, "/|*" ); for ( int i = 0; i < files->GetNumFiles() && !callback->cancelled; i++ ) { fileSystem->RemoveFile( files->GetFile( i ) ); } fileSystem->FreeFileList( files ); fileSystem->RemoveDir( saveFolder ); } else { callback->errorCode = SAVEGAME_E_FOLDER_NOT_FOUND; ret = -1; } if ( data.saveLoadParms->cancelled ) { data.saveLoadParms->errorCode = SAVEGAME_E_CANCELLED; } return ret; } /* ======================== idSaveGameThread::Enumerate ======================== */ int idSaveGameThread::Enumerate() { idSaveLoadParms * callback = data.saveLoadParms; idStr saveFolder = "savegame"; callback->detailList.Clear(); int ret = ERROR_SUCCESS; if ( fileSystem->IsFolder( saveFolder, "fs_savePath" ) == FOLDER_YES ) { idFileList * files = fileSystem->ListFilesTree( saveFolder, SAVEGAME_DETAILS_FILENAME ); const idStrList & fileList = files->GetList(); for ( int i = 0; i < fileList.Num() && !callback->cancelled; i++ ) { idSaveGameDetails * details = callback->detailList.Alloc(); // We have more folders on disk than we have room in our save detail list, stop trying to read them in and continue with what we have if ( details == NULL ) { break; } idStr directory = fileList[i]; idFile * file = fileSystem->OpenFileRead( directory.c_str() ); if ( file != NULL ) { // Read the DETAIL file for the enumerated data if ( callback->mode & SAVEGAME_MBF_READ_DETAILS ) { if ( !SavegameReadDetailsFromFile( file, *details ) ) { details->damaged = true; ret = -1; } } // Use the date from the directory WIN32_FILE_ATTRIBUTE_DATA attrData; BOOL attrRet = GetFileAttributesEx( file->GetFullPath(), GetFileExInfoStandard, &attrData ); delete file; if ( attrRet == TRUE ) { FILETIME lastWriteTime = attrData.ftLastWriteTime; const ULONGLONG second = 10000000L; // One second = 10,000,000 * 100 nsec SYSTEMTIME base_st = { 1970, 1, 0, 1, 0, 0, 0, 0 }; ULARGE_INTEGER itime; FILETIME base_ft; BOOL success = SystemTimeToFileTime( &base_st, &base_ft ); itime.QuadPart = ((ULARGE_INTEGER *)&lastWriteTime)->QuadPart; if ( success ) { itime.QuadPart -= ((ULARGE_INTEGER *)&base_ft)->QuadPart; } else { // Hard coded number of 100-nanosecond units from 1/1/1601 to 1/1/1970 itime.QuadPart -= 116444736000000000LL; } itime.QuadPart /= second; details->date = itime.QuadPart; } } else { details->damaged = true; } // populate the game details struct directory = directory.StripFilename(); details->slotName = directory.c_str() + saveFolder.Length() + 1; // Strip off the prefix too // JDC: I hit this all the time assert( fileSystem->IsFolder( directory.c_str(), "fs_savePath" ) == FOLDER_YES ); } fileSystem->FreeFileList( files ); } else { callback->errorCode = SAVEGAME_E_FOLDER_NOT_FOUND; ret = -3; } if ( data.saveLoadParms->cancelled ) { data.saveLoadParms->errorCode = SAVEGAME_E_CANCELLED; } return ret; } /* ======================== idSaveGameThread::EnumerateFiles ======================== */ int idSaveGameThread::EnumerateFiles() { idSaveLoadParms * callback = data.saveLoadParms; idStr folder = "savegame"; folder.AppendPath( callback->directory ); callback->files.Clear(); int ret = ERROR_SUCCESS; if ( fileSystem->IsFolder( folder, "fs_savePath" ) == FOLDER_YES ) { // get listing of all the files, but filter out below idFileList * files = fileSystem->ListFilesTree( folder, "*.*" ); // look for the instance pattern for ( int i = 0; i < files->GetNumFiles() && ret == 0 && !callback->cancelled; i++ ) { idStr fullFilename = files->GetFile( i ); idStr filename = fullFilename; filename.StripPath(); if ( filename.IcmpPrefix( callback->pattern ) != 0 ) { continue; } if ( !callback->postPattern.IsEmpty() && filename.Right( callback->postPattern.Length() ).IcmpPrefix( callback->postPattern ) != 0 ) { continue; } // Read the DETAIL file for the enumerated data if ( callback->mode & SAVEGAME_MBF_READ_DETAILS ) { idSaveGameDetails & details = callback->description; idFile * uncompressed = fileSystem->OpenFileRead( fullFilename.c_str() ); if ( uncompressed == NULL ) { details.damaged = true; } else { if ( !SavegameReadDetailsFromFile( uncompressed, details ) ) { ret = -1; } delete uncompressed; } // populate the game details struct details.slotName = callback->directory; assert( fileSystem->IsFolder( details.slotName, "fs_savePath" ) == FOLDER_YES ); } idFile_SaveGame * file = new (TAG_SAVEGAMES) idFile_SaveGame( filename, SAVEGAMEFILE_AUTO_DELETE ); callback->files.Append( file ); } fileSystem->FreeFileList( files ); } else { callback->errorCode = SAVEGAME_E_FOLDER_NOT_FOUND; ret = -3; } if ( data.saveLoadParms->cancelled ) { data.saveLoadParms->errorCode = SAVEGAME_E_CANCELLED; } return ret; } /* ======================== idSaveGameThread::DeleteFiles ======================== */ int idSaveGameThread::DeleteFiles() { idSaveLoadParms * callback = data.saveLoadParms; idStr folder = "savegame"; folder.AppendPath( callback->directory ); // delete the explicitly requested files first for ( int j = 0; j < callback->files.Num() && !callback->cancelled; ++j ) { idFile_SaveGame * file = callback->files[j]; idStr fullpath = folder; fullpath.AppendPath( file->GetName() ); fileSystem->RemoveFile( fullpath ); } int ret = ERROR_SUCCESS; if ( fileSystem->IsFolder( folder, "fs_savePath" ) == FOLDER_YES ) { // get listing of all the files, but filter out below idFileList * files = fileSystem->ListFilesTree( folder, "*.*" ); // look for the instance pattern for ( int i = 0; i < files->GetNumFiles() && !callback->cancelled; i++ ) { idStr filename = files->GetFile( i ); filename.StripPath(); // If there are post/pre patterns to match, make sure we adhere to the patterns if ( callback->pattern.IsEmpty() || ( filename.IcmpPrefix( callback->pattern ) != 0 ) ) { continue; } if ( callback->postPattern.IsEmpty() || ( filename.Right( callback->postPattern.Length() ).IcmpPrefix( callback->postPattern ) != 0 ) ) { continue; } fileSystem->RemoveFile( files->GetFile( i ) ); } fileSystem->FreeFileList( files ); } else { callback->errorCode = SAVEGAME_E_FOLDER_NOT_FOUND; ret = -3; } if ( data.saveLoadParms->cancelled ) { data.saveLoadParms->errorCode = SAVEGAME_E_CANCELLED; } return ret; } /* ======================== idSaveGameThread::DeleteAll This deletes all savegame directories ======================== */ int idSaveGameThread::DeleteAll() { idSaveLoadParms * callback = data.saveLoadParms; idStr saveFolder = "savegame"; int ret = ERROR_SUCCESS; if ( fileSystem->IsFolder( saveFolder, "fs_savePath" ) == FOLDER_YES ) { idFileList * files = fileSystem->ListFilesTree( saveFolder, "/|*" ); // remove directories after files for ( int i = 0; i < files->GetNumFiles() && !callback->cancelled; i++ ) { // contained files should always be first if ( fileSystem->IsFolder( files->GetFile( i ), "fs_savePath" ) == FOLDER_YES ) { fileSystem->RemoveDir( files->GetFile( i ) ); } else { fileSystem->RemoveFile( files->GetFile( i ) ); } } fileSystem->FreeFileList( files ); } else { callback->errorCode = SAVEGAME_E_FOLDER_NOT_FOUND; ret = -3; } if ( data.saveLoadParms->cancelled ) { data.saveLoadParms->errorCode = SAVEGAME_E_CANCELLED; } return ret; } /* ======================== idSaveGameThread::Run ======================== */ int idSaveGameThread::Run() { int ret = ERROR_SUCCESS; try { idLocalUserWin * user = GetLocalUserFromSaveParms( data ); if ( user != NULL && !user->IsStorageDeviceAvailable() ) { data.saveLoadParms->errorCode = SAVEGAME_E_UNABLE_TO_SELECT_STORAGE_DEVICE; } if ( savegame_winInduceDelay.GetInteger() > 0 ) { Sys_Sleep( savegame_winInduceDelay.GetInteger() ); } if ( data.saveLoadParms->mode & SAVEGAME_MBF_SAVE ) { ret = Save(); } else if ( data.saveLoadParms->mode & SAVEGAME_MBF_LOAD ) { ret = Load(); } else if ( data.saveLoadParms->mode & SAVEGAME_MBF_ENUMERATE ) { ret = Enumerate(); } else if ( data.saveLoadParms->mode & SAVEGAME_MBF_DELETE_FOLDER ) { ret = Delete(); } else if ( data.saveLoadParms->mode & SAVEGAME_MBF_DELETE_ALL_FOLDERS ) { ret = DeleteAll(); } else if ( data.saveLoadParms->mode & SAVEGAME_MBF_DELETE_FILES ) { ret = DeleteFiles(); } else if ( data.saveLoadParms->mode & SAVEGAME_MBF_ENUMERATE_FILES ) { ret = EnumerateFiles(); } // if something failed and no one set an error code, do it now. if ( ret != 0 && data.saveLoadParms->errorCode == SAVEGAME_E_NONE ) { data.saveLoadParms->errorCode = SAVEGAME_E_UNKNOWN; } } catch ( ... ) { // if anything horrible happens, leave it up to the savegame processors to handle in PostProcess(). data.saveLoadParms->errorCode = SAVEGAME_E_UNKNOWN; } // Make sure to cancel any save game file pipelines. if ( data.saveLoadParms->errorCode != SAVEGAME_E_NONE ) { data.saveLoadParms->CancelSaveGameFilePipelines(); } // Override error if cvar set if ( savegame_error.GetInteger() != 0 ) { data.saveLoadParms->errorCode = (saveGameError_t)savegame_error.GetInteger(); } // Tell the waiting caller that we are done data.saveLoadParms->callbackSignal.Raise(); return ret; } /* ======================== Sys_SaveGameCheck ======================== */ void Sys_SaveGameCheck( bool & exists, bool & autosaveExists ) { exists = false; autosaveExists = false; const idStr autosaveFolderStr = AddSaveFolderPrefix( SAVEGAME_AUTOSAVE_FOLDER, idSaveGameManager::PACKAGE_GAME ); const char * autosaveFolder = autosaveFolderStr.c_str(); const char * saveFolder = "savegame"; if ( fileSystem->IsFolder( saveFolder, "fs_savePath" ) == FOLDER_YES ) { idFileList * files = fileSystem->ListFiles( saveFolder, "/" ); const idStrList & fileList = files->GetList(); idLib::PrintfIf( saveGame_verbose.GetBool(), "found %d savegames\n", fileList.Num() ); for ( int i = 0; i < fileList.Num(); i++ ) { const char * directory = va( "%s/%s", saveFolder, fileList[i].c_str() ); if ( fileSystem->IsFolder( directory, "fs_savePath" ) == FOLDER_YES ) { exists = true; idLib::PrintfIf( saveGame_verbose.GetBool(), "found savegame: %s\n", fileList[i].c_str() ); if ( idStr::Icmp( fileList[i].c_str(), autosaveFolder ) == 0 ) { autosaveExists = true; break; } } } fileSystem->FreeFileList( files ); } }