Arty has proposed merging lp:~widelands-dev/widelands/robust-file-saving into 
lp:widelands with 
lp:~widelands-dev/widelands/bug-1753230-working-with-tempfiles as a 
prerequisite.

Requested reviews:
  Widelands Developers (widelands-dev)

For more details, see:
https://code.launchpad.net/~widelands-dev/widelands/robust-file-saving/+merge/358473
-- 
Your team Widelands Developers is requested to review the proposed merge of 
lp:~widelands-dev/widelands/robust-file-saving into lp:widelands.
=== modified file 'src/editor/CMakeLists.txt'
--- src/editor/CMakeLists.txt	2018-07-19 16:43:14 +0000
+++ src/editor/CMakeLists.txt	2018-11-08 02:11:06 +0000
@@ -99,6 +99,7 @@
     logic
     logic_constants
     logic_filesystem_constants
+    logic_generic_save_handler
     logic_tribe_basic_info
     logic_widelands_geometry
     map_io

=== modified file 'src/editor/ui_menus/main_menu_save_map.cc'
--- src/editor/ui_menus/main_menu_save_map.cc	2018-11-08 02:11:06 +0000
+++ src/editor/ui_menus/main_menu_save_map.cc	2018-11-08 02:11:06 +0000
@@ -29,6 +29,7 @@
 
 #include "base/i18n.h"
 #include "base/macros.h"
+#include "base/time_string.h"
 #include "base/wexception.h"
 #include "editor/editorinteractive.h"
 #include "editor/ui_menus/main_menu_map_options.h"
@@ -37,6 +38,7 @@
 #include "io/filesystem/layered_filesystem.h"
 #include "io/filesystem/zip_filesystem.h"
 #include "logic/filesystem_constants.h"
+#include "logic/generic_save_handler.h"
 #include "map_io/map_saver.h"
 #include "map_io/widelands_map_loader.h"
 #include "ui_basic/messagebox.h"
@@ -119,7 +121,8 @@
  * Called when the ok button was pressed or a file in list was double clicked.
  */
 void MainMenuSaveMap::clicked_ok() {
-	assert(ok_.enabled());
+	if (!ok_.enabled())
+		return;
 	std::string filename = editbox_->text();
 	std::string complete_filename;
 
@@ -243,15 +246,12 @@
 
 /**
  * Save the map in the current directory with
- * the current filename
+ * the given filename
  *
  * returns true if dialog should close, false if it
  * should stay open
  */
 bool MainMenuSaveMap::save_map(std::string filename, bool binary) {
-	//  Make sure that the current directory exists and is writeable.
-	g_fs->ensure_directory_exists(curdir_);
-
 	// Trim it for preceding/trailing whitespaces in user input
 	boost::trim(filename);
 
@@ -272,18 +272,6 @@
 		if (mbox.run<UI::Panel::Returncodes>() == UI::Panel::Returncodes::kBack)
 			return false;
 	}
-	
-	//  Try deleting file (if it exists). If it fails, give a message and let the player choose a new name.
-	try {
-		g_fs->fs_unlink(complete_filename);
-	} catch (const std::exception& e) {
-		const std::string s =
-		   (boost::format(_("File ‘%s.tmp’ could not be deleted.")) % FileSystem::fs_filename(filename.c_str())).str()
-		   + " " + _("Try saving under a different name!");
-		UI::WLMessageBox mbox(&eia(), _("Error Saving Map!"), s, UI::WLMessageBox::MBoxType::kOk);
-		mbox.run<UI::Panel::Returncodes>();
-		return false;
-	}
 
 	Widelands::EditorGameBase& egbase = eia().egbase();
 	Widelands::Map* map = egbase.mutable_map();
@@ -301,23 +289,34 @@
 	} else {
 		map->delete_tag("artifacts");
 	}
-	
-	//  Try saving.
-	try {
-		std::unique_ptr<FileSystem> fs(
-		   g_fs->create_sub_file_system(complete_filename, binary ? FileSystem::ZIP : FileSystem::DIR));
-		Widelands::MapSaver* wms = new Widelands::MapSaver(*fs, egbase);
-		wms->save();
-		delete wms;
-		fs.reset();
-	} catch (const std::exception& e) {
-		std::string s = _("Error Saving Map!\nSaved map file may be corrupt!\n\nReason "
-		                  "given:\n");
-		s += e.what();
-		UI::WLMessageBox mbox(&eia(), _("Error Saving Map!"), s, UI::WLMessageBox::MBoxType::kOk);
-		mbox.run<UI::Panel::Returncodes>();
+
+	// Try saving the map.
+	GenericSaveHandler gsh(
+		[&egbase](FileSystem& fs) {
+			Widelands::MapSaver wms(fs, egbase);
+			wms.save();
+		},
+		complete_filename,
+		binary ? FileSystem::ZIP : FileSystem::DIR
+	);
+	uint32_t error = gsh.save();
+	if (error == GenericSaveHandler::kSuccess ||
+	    error == GenericSaveHandler::kDeletingBackupFailed) {
+			// No need to bother the player if only the temporary backup couldn't be deleted.
+			// Automatic cleanup will try to deal with it later.
+		egbase.get_ibase()->log_message(_("Map saved"));
+		return true;
 	}
 
-	die();
+	std::string msg = gsh.localized_formatted_result_message();
+	UI::WLMessageBox mbox(this, _("Error Saving Map!"), msg, UI::WLMessageBox::MBoxType::kOk);
+	mbox.run<UI::Panel::Returncodes>();
+
+	// If only the backup failed (likely just because of a file lock),
+	// then leave the dialog open for the player to try with a new filename.
+	if (error == GenericSaveHandler::kBackupFailed)
+	  return false;
+
+	// In the other error cases close the dialog.
 	return true;
 }

=== modified file 'src/io/filesystem/disk_filesystem.cc'
--- src/io/filesystem/disk_filesystem.cc	2018-04-07 16:59:00 +0000
+++ src/io/filesystem/disk_filesystem.cc	2018-11-08 02:11:06 +0000
@@ -167,15 +167,13 @@
 }
 
 /**
- * Create a sub filesystem out of this filesystem
+ * Make a sub filesystem out of this filesystem
  */
 FileSystem* RealFSImpl::make_sub_file_system(const std::string& path) {
 	FileSystemPath fspath(canonicalize_name(path));
 
 	if (!fspath.exists_) {
-		throw wexception("RealFSImpl: unable to create sub filesystem, path does not exist for '%s'"
-		                 " in directory '%s'",
-		                 fspath.c_str(), directory_.c_str());
+		throw FileNotFoundError("RealFSImpl::make_sub_file_system", fspath);
 	}
 
 	if (fspath.is_directory_)
@@ -190,10 +188,7 @@
 FileSystem* RealFSImpl::create_sub_file_system(const std::string& path, Type const fs) {
 	FileSystemPath fspath(canonicalize_name(path));
 	if (fspath.exists_)
-		throw wexception(
-		   "path '%s'' already exists in directory '%s', can not create a filesystem from it",
-		   path.c_str(), directory_.c_str());
-
+		throw FileError("RealFSImpl::create_sub_file_system", fspath, "path already exists, cannot create new filesystem from it");
 	if (fs == FileSystem::DIR) {
 		ensure_directory_exists(path);
 		return new RealFSImpl(fspath);
@@ -221,20 +216,22 @@
 void RealFSImpl::unlink_file(const std::string& file) {
 	FileSystemPath fspath(canonicalize_name(file));
 	if (!fspath.exists_) {
-		throw wexception(
-		   "RealFSImpl: unable to unlink file, path does not exist for '%s' in directory '%s'",
-		   fspath.c_str(), directory_.c_str());
+		throw FileNotFoundError("RealFSImpl::unlink_file", fspath);
 	}
 	if (fspath.is_directory_) {
-		throw wexception(
-		   "RealFSImpl: unable to unlink file, path '%s' in directory '%s' is a directory",
-		   fspath.c_str(), directory_.c_str());
+		throw FileTypeError("RealFSImpl::unlink_file", fspath, "path is a directory");
 	}
-
 #ifndef _WIN32
-	unlink(fspath.c_str());
+	if (unlink(fspath.c_str()) == -1)
+		throw FileError("RealFSImpl::unlink_file", fspath, strerror(errno));
 #else
-	DeleteFile(fspath.c_str());
+	// Note: We could possibly replace this with _unlink()
+	//       which would then also work with errno instead of GetLastError(),
+	//       but I am not sure if this is available (and works properly)
+	//       on all windows platforms or only with Visual Studio.
+	if (!DeleteFile(fspath.c_str()))
+		throw FileError("RealFSImpl::unlink_file", fspath, std::string("file error (Windows error code ")+std::to_string(GetLastError())+")");
+		// TODO(Arty): generate proper system message from GetLastError() via FormatMessage
 #endif
 }
 
@@ -244,14 +241,10 @@
 void RealFSImpl::unlink_directory(const std::string& file) {
 	FileSystemPath fspath(canonicalize_name(file));
 	if (!fspath.exists_) {
-		throw wexception("RealFSImpl: unable to unlink directory, path does not exist for '%s'"
-		                 " in directory '%s'",
-		                 fspath.c_str(), directory_.c_str());
+		throw FileNotFoundError("RealFSImpl::unlink_directory", fspath);
 	}
 	if (!fspath.is_directory_) {
-		throw wexception("RealFSImpl: unable to unlink directory, path '%s' in directory '%s'"
-		                 " is not a directory",
-		                 fspath.c_str(), directory_.c_str());
+		throw FileTypeError("RealFSImpl::unlink_directory", fspath, "path is not a directory");
 	}
 
 	FilenameSet files = list_directory(file);
@@ -268,14 +261,18 @@
 			unlink_file(*pname);
 	}
 
-// NOTE: this might fail if this directory contains CVS dir,
-// so no error checking here
+// NOTE: this might fail if this directory contains CVS dir
 #ifndef _WIN32
-	rmdir(fspath.c_str());
+	if (rmdir(fspath.c_str()) == -1)
+		throw FileError("RealFSImpl::unlink_directory", fspath, strerror(errno));
 #else
+	// Note: We could possibly replace this with _rmdir()
+	//       which would then also work with errno instead of GetLastError(),
+	//       but I am not sure if this is available (and works properly)
+	//       on all windows platforms or only with Visual Studio.
 	if (!RemoveDirectory(fspath.c_str()))
-		throw wexception(
-		   "'%s' could not be deleted in directory '%s'.", fspath.c_str(), directory_.c_str());
+		throw FileError("RealFSImpl::unlink_directory", fspath, std::string("file error (Windows error code ")+std::to_string(GetLastError())+")");
+		// TODO(Arty): generate proper system message from GetLastError() via FormatMessage
 #endif
 }
 
@@ -292,25 +289,20 @@
 	boost::replace_all(clean_dirname, "\\", "/");
 #endif
 
-	try {
-		std::string::size_type it = 0;
-		while (it < clean_dirname.size()) {
-			it = clean_dirname.find('/', it);
-
-			FileSystemPath fspath(canonicalize_name(clean_dirname.substr(0, it)));
-			if (fspath.exists_ && !fspath.is_directory_)
-				throw wexception("'%s' in directory '%s' exists and is not a directory",
-				                 clean_dirname.substr(0, it).c_str(), directory_.c_str());
-			if (!fspath.exists_)
-				make_directory(clean_dirname.substr(0, it));
-
-			if (it == std::string::npos)
-				break;
-			++it;
-		}
-	} catch (const std::exception& e) {
-		throw wexception("RealFSImpl::ensure_directory_exists(%s) in directory '%s': %s",
-		                 clean_dirname.c_str(), directory_.c_str(), e.what());
+	std::string::size_type it = 0;
+	while (it < clean_dirname.size()) {
+		it = clean_dirname.find('/', it);
+
+		FileSystemPath fspath(canonicalize_name(clean_dirname.substr(0, it)));
+		if (fspath.exists_ && !fspath.is_directory_)
+			throw FileTypeError("RealFSImpl::ensure_directory_exists",
+			   fspath, "path is not a directory");
+		if (!fspath.exists_)
+			make_directory(clean_dirname.substr(0, it));
+
+		if (it == std::string::npos)
+			break;
+		++it;
 	}
 }
 
@@ -318,33 +310,37 @@
  * Create this directory, throw an error if it already exists or
  * if a file is in the way or if the creation fails.
  *
- * Pleas note, this function does not honor parents,
+ * Please note, this function does not honor parents,
  * make_directory("onedir/otherdir/onemoredir") will fail
  * if either onedir or otherdir is missing
  */
 void RealFSImpl::make_directory(const std::string& dirname) {
 	FileSystemPath fspath(canonicalize_name(dirname));
 	if (fspath.exists_)
-		throw wexception("a file/directory with the name '%s' already exists in directory '%s'",
-		                 dirname.c_str(), directory_.c_str());
+		throw FileError("RealFSImpl::make_directory", fspath, "a file or directory with that name already exists");
 
-	if
-#ifdef _WIN32
-	   (!CreateDirectory(fspath.c_str(), NULL))
+#ifndef _WIN32
+	if (!mkdir(fspath.c_str(), 0x1FF) == -1)
+		throw FileError("RealFSImpl::make_directory", fspath, strerror(errno));
 #else
-	   (mkdir(fspath.c_str(), 0x1FF) == -1)
+	// Note: We could possibly replace this with _mkdir()
+	//       which would then also work with errno instead of GetLastError(),
+	//       but I am not sure if this is available (and works properly)
+	//       on all windows platforms or only with Visual Studio.
+	if (!CreateDirectory(fspath.c_str(), NULL))
+		throw FileError("RealFSImpl::make_directory", fspath, std::string("file error (Windows error code ")+std::to_string(GetLastError())+")");
+		// TODO(Arty): generate proper system message from GetLastError() via FormatMessage
 #endif
-		throw DirectoryCannotCreateError("RealFSImpl::make_directory", dirname, strerror(errno));
 }
 
 /**
- * Read the given file into alloced memory; called by FileRead::open.
+ * Read the given file into allocated memory; called by FileRead::open.
  * Throws an exception if the file couldn't be opened.
  */
 void* RealFSImpl::load(const std::string& fname, size_t& length) {
 	const std::string fullname = canonicalize_name(fname);
 	if (is_directory(fullname)) {
-		throw FileError("RealFSImpl::load", fullname);
+		throw FileTypeError("RealFSImpl::load", fullname, "path is a directory");
 	}
 
 	FILE* file = nullptr;
@@ -353,7 +349,7 @@
 	try {
 		file = fopen(fullname.c_str(), "rb");
 		if (!file)
-			throw FileError("RealFSImpl::load", fullname);
+			throw FileError("RealFSImpl::load", fullname, "could not open file for reading");
 
 		// determine the size of the file (rather quirky, but it doesn't require
 		// potentially unportable functions)
@@ -371,6 +367,9 @@
 
 		// allocate a buffer and read the entire file into it
 		data = malloc(size + 1);  //  TODO(unknown): memory leak!
+		if (!data)
+			throw wexception("RealFSImpl::load: memory allocation failed for reading file %s (%s) with size %" PRIuS "",
+			                 fname.c_str(), fullname.c_str(), size);
 		int result = fread(data, size, 1, file);
 		if (size && (result != 1)) {
 			throw wexception("RealFSImpl::load: read failed for %s (%s) with size %" PRIuS "",
@@ -383,10 +382,10 @@
 
 		length = size;
 	} catch (...) {
-		if (file) {
+		if (file)
 			fclose(file);
-		}
-		free(data);
+		if (data)
+			free(data);
 		throw;
 	}
 
@@ -410,7 +409,7 @@
 
 	FILE* const f = fopen(fullname.c_str(), append ? "a" : "wb");
 	if (!f)
-		throw wexception("could not open %s (%s) for writing", fname.c_str(), fullname.c_str());
+		throw FileError("RealFSImpl::write", fullname, "could not open file for writing");
 
 	size_t const c = fwrite(data, length, 1, f);
 	fclose(f);
@@ -424,8 +423,7 @@
 	const std::string fullname1 = canonicalize_name(old_name);
 	const std::string fullname2 = canonicalize_name(new_name);
 	if (rename(fullname1.c_str(), fullname2.c_str()) != 0)
-		throw wexception("DiskFileSystem: unable to rename %s to %s: %s", fullname1.c_str(),
-		                 fullname2.c_str(), strerror(errno));
+		throw FileError("RealFSImpl::fs_rename", fullname1, std::string("unable to rename file to ") + fullname2 + ", " + strerror(errno));
 }
 
 /*****************************************************************************
@@ -439,7 +437,7 @@
 struct RealFSStreamRead : public StreamRead {
 	explicit RealFSStreamRead(const std::string& fname) : file_(fopen(fname.c_str(), "rb")) {
 		if (!file_)
-			throw wexception("could not open %s for reading", fname.c_str());
+			throw FileError("RealFSStreamRead::RealFSStreamRead", fname, "could not open file for reading");
 	}
 
 	~RealFSStreamRead() override {
@@ -477,7 +475,7 @@
 	explicit RealFSStreamWrite(const std::string& fname) : filename_(fname) {
 		file_ = fopen(fname.c_str(), "wb");
 		if (!file_)
-			throw wexception("could not open %s for writing", fname.c_str());
+			throw FileError("RealFSStreamWrite::RealFSStreamWrite", fname, "could not open file for writing");
 	}
 
 	~RealFSStreamWrite() override {

=== modified file 'src/io/filesystem/filesystem.cc'
--- src/io/filesystem/filesystem.cc	2018-07-12 04:41:20 +0000
+++ src/io/filesystem/filesystem.cc	2018-11-08 02:11:06 +0000
@@ -306,7 +306,7 @@
 	std::list<std::string>::iterator i;
 
 #ifdef _WIN32
-	// remove all slashes with backslashes so following can work.
+	// replace all slashes with backslashes so following can work.
 	for (uint32_t j = 0; j < path.size(); ++j) {
 		if (path[j] == '/')
 			path[j] = '\\';
@@ -463,7 +463,7 @@
 		fs.ensure_directory_exists(".widelands");
 		fs.fs_unlink(".widelands");
 		return true;
-	} catch (...) {
+	} catch (const FileError& e) {
 		log("Directory %s is not writeable - next try\n", path);
 	}
 

=== modified file 'src/io/filesystem/zip_filesystem.cc'
--- src/io/filesystem/zip_filesystem.cc	2018-11-08 02:11:06 +0000
+++ src/io/filesystem/zip_filesystem.cc	2018-11-08 02:11:06 +0000
@@ -268,12 +268,14 @@
 	if (!file_exists(path)) {
 		throw wexception(
 		   "ZipFilesystem::make_sub_file_system: The path '%s' does not exist in zip file '%s'.",
-		   path.c_str(), zip_file_->path().c_str());
+		   (basedir_in_zip_file_.empty() ? path : basedir_in_zip_file_ + "/" + path).c_str(),
+		   zip_file_->path().c_str());
 	}
 	if (!is_directory(path)) {
-		throw wexception("ZipFilesystem::make_sub_file_system: "
-		                 "The path '%s' needs to be a directory in zip file '%s'.",
-		                 path.c_str(), zip_file_->path().c_str());
+		throw wexception(
+		   "ZipFilesystem::make_sub_file_system: The path '%s' needs to be a directory in zip file '%s'.",
+		   (basedir_in_zip_file_.empty()?path:basedir_in_zip_file_+"/"+path).c_str(),
+		   zip_file_->path().c_str());
 	}
 
 	std::string localpath = path;
@@ -291,7 +293,10 @@
 // see Filesystem::create
 FileSystem* ZipFilesystem::create_sub_file_system(const std::string& path, Type const type) {
 	if (file_exists(path)) {
-		throw wexception("ZipFilesystem::create_sub_file_system: Sub file system already exists.");
+		throw wexception(
+		   "ZipFilesystem::create_sub_file_system: Path '%s' already exists in zip file %s.",
+		   (basedir_in_zip_file_.empty() ? path : basedir_in_zip_file_ + "/" + path).c_str(),
+		   zip_file_->path().c_str());
 	}
 
 	if (type != FileSystem::DIR)
@@ -438,8 +443,9 @@
 		   "ZipFilesystem::write", complete_filename,
 		   (boost::format("in path '%s'', Error") % zip_file_->path() % strerror(errno)).str());
 	default:
-		throw FileError("ZipFilesystem::write", complete_filename,
-		                (boost::format("in path '%s'") % zip_file_->path()).str());
+		throw FileError(
+		   "ZipFilesystem::write", complete_filename,
+		   (boost::format("in path '%s'") % zip_file_->path()).str());
 	}
 
 	zipCloseFileInZip(zip_file_->write_handle());

=== modified file 'src/logic/CMakeLists.txt'
--- src/logic/CMakeLists.txt	2018-08-24 07:12:20 +0000
+++ src/logic/CMakeLists.txt	2018-11-08 02:11:06 +0000
@@ -7,7 +7,6 @@
     base_i18n
 )
 
-
 wl_library(logic_widelands_geometry
   SRCS
     widelands_geometry.cc
@@ -81,6 +80,7 @@
     filesystem_constants.h
     filesystem_constants.cc
 )
+
 wl_library(logic_tribe_basic_info
   SRCS
     map_objects/tribes/tribe_basic_info.cc
@@ -93,6 +93,14 @@
 
 )
 
+wl_library(logic_generic_save_handler
+  SRCS
+    generic_save_handler.h
+    generic_save_handler.cc
+  DEPENDS
+    io_filesystem
+)
+
 wl_library(logic
   SRCS
     ai_dna_handler.cc
@@ -281,6 +289,7 @@
     logic_filesystem_constants
     logic_game_controller
     logic_game_settings
+    logic_generic_save_handler
     logic_tribe_basic_info
     logic_widelands_geometry
     map_io

=== modified file 'src/logic/editor_game_base.cc'
--- src/logic/editor_game_base.cc	2018-11-08 02:11:06 +0000
+++ src/logic/editor_game_base.cc	2018-11-08 02:11:06 +0000
@@ -8,7 +8,7 @@
  *
  * This program is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
-rnrnrn * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * 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
@@ -120,7 +120,7 @@
 		int suffix;
 		for (suffix = 0; suffix <= 9; suffix++)
 		{
-			complete_filename = filename + "-" + std::to_string(suffix) + ".tmp";
+			complete_filename = filename + "-" + std::to_string(suffix) + kTempFileExtension;
 			if (!g_fs->file_exists(complete_filename))
 			  break;
 		}
@@ -136,7 +136,7 @@
 	Widelands::MapSaver* wms = new Widelands::MapSaver(*tmp_fs_, *this);
 	wms->save();
 	delete wms;
-	
+
 	// swap map fs
 	std::unique_ptr<FileSystem> mapfs(tmp_fs_->make_sub_file_system("."));
 	map_.swap_filesystem(mapfs);

=== modified file 'src/logic/editor_game_base.h'
--- src/logic/editor_game_base.h	2018-11-08 02:11:06 +0000
+++ src/logic/editor_game_base.h	2018-11-08 02:11:06 +0000
@@ -254,7 +254,7 @@
 	std::unique_ptr<Tribes> tribes_;
 	std::unique_ptr<InteractiveBase> ibase_;
 	Map map_;
-	
+
 	/// Even after a map is fully loaded, some static data (images, scripts)
 	/// will still be read from a filesystem whenever a map/game is saved.
 	/// To avoid potential filesystem conflicts when (pre)loading/saving/deleting
@@ -262,7 +262,7 @@
 	/// a temporary file (in a special dir) is created for such data.
 	std::unique_ptr<FileSystem> tmp_fs_;
 	void delete_tempfile();
-	void create_tempfile_and_save_mapdata(FileSystem::Type const type);
+	void create_tempfile_and_save_mapdata(FileSystem::Type type);
 
 	DISALLOW_COPY_AND_ASSIGN(EditorGameBase);
 };

=== modified file 'src/logic/filesystem_constants.h'
--- src/logic/filesystem_constants.h	2018-11-08 02:11:06 +0000
+++ src/logic/filesystem_constants.h	2018-11-08 02:11:06 +0000
@@ -45,6 +45,11 @@
 // We delete (accidentally remaining) temp files older than a week
 constexpr double kTempFilesKeepAroundTime = 7 * 24 * 60 * 60;
 
+/// Filesystem names for for temporary backup when overwriting files during saving
+const std::string kTempBackupExtension = ".tmp";
+// We delete (accidentally remaining) temp backup files older than a day
+constexpr double kTempBackupsKeepAroundTime = 24 * 60 * 60;
+
 /// Filesystem names and timeouts for replays
 const std::string kReplayDir = "replays";
 const std::string kReplayExtension = ".wrpl";

=== added file 'src/logic/generic_save_handler.cc'
--- src/logic/generic_save_handler.cc	1970-01-01 00:00:00 +0000
+++ src/logic/generic_save_handler.cc	2018-11-08 02:11:06 +0000
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2002-2018 by the Widelands Development Team
+ *
+ * This program 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 2
+ * of the License, or (at your option) any later version.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ *
+ */
+
+#include "logic/generic_save_handler.h"
+
+#include <memory>
+#include <string>
+
+#include <boost/format.hpp>
+
+#include "base/i18n.h"
+#include "base/log.h"
+#include "base/time_string.h"
+#include "base/wexception.h"
+#include "io/filesystem/filesystem.h"
+#include "io/filesystem/filesystem_exceptions.h"
+#include "io/filesystem/layered_filesystem.h"
+#include "logic/filesystem_constants.h"
+
+void GenericSaveHandler::clear_errors() {
+	error_ = kSuccess;
+	for (uint32_t errbit = 0; errbit < maxErrors; errbit++) {
+		error_msg_[errbit].clear();
+	}
+	backup_filename_.clear();
+	return;
+}
+
+void GenericSaveHandler::make_backup() {
+	std::string backup_filename_base = dir_ + g_fs->file_separator() + timestring() + "_" + filename_;
+	backup_filename_ = backup_filename_base + kTempBackupExtension;
+
+	// If a file with that name already exists, then try a few name modifications.
+	if (g_fs->file_exists(backup_filename_))
+	{
+		int suffix;
+		for (suffix = 0; suffix <= 9; suffix++)
+		{
+			backup_filename_ = backup_filename_base + "-" + std::to_string(suffix) + kTempBackupExtension;
+			if (!g_fs->file_exists(backup_filename_))
+			  break;
+		}
+		if (suffix > 9) {
+			error_ |= kBackupFailed;
+			error_msg_[bitBackupFailed] = (boost::format("GenericSaveHandler::make_backup: %s: "
+			   "for all considered filenames a file already existed (last filename tried was %s)\n")
+				 % complete_filename_.c_str() % backup_filename_).str();
+			log("%s", error_msg_[bitBackupFailed].c_str());
+			return;
+		}
+	}
+
+	// Try to rename file.
+	try {
+		g_fs->fs_rename(complete_filename_, backup_filename_);
+	} catch (const FileError& e) {
+			error_ |= kBackupFailed;
+			error_msg_[bitBackupFailed] = (boost::format("GenericSaveHandler::make_backup: file %s "
+			   "could not be renamed: %s\n") % complete_filename_.c_str() % backup_filename_).str();
+			log("%s", error_msg_[bitBackupFailed].c_str());
+		return;
+	}
+
+	return;
+}
+
+void GenericSaveHandler::save_file() {
+	// Write data to file/dir.
+	try {
+		std::unique_ptr<FileSystem> fs(g_fs->create_sub_file_system(complete_filename_, type_));
+		do_save_(*fs);
+	} catch (const std::exception& e) {
+		error_ |= kSavingDataFailed;
+		error_msg_[bitSavingDataFailed] = (boost::format("GenericSaveHandler::save_file: "
+		   "data could not be written to file %s: %s\n") % complete_filename_.c_str() % e.what()).str();
+		log("%s", error_msg_[bitSavingDataFailed].c_str());
+	}
+
+	if (error_ & kSavingDataFailed) {
+		// Delete remnants of the failed save attempt.
+		if (g_fs->file_exists(complete_filename_)) {
+			try {
+				g_fs->fs_unlink(complete_filename_);
+			} catch (const FileError& e) {
+				error_ |= kCorruptFileLeft;
+				error_msg_[bitCorruptFileLeft] = (boost::format("GenericSaveHandler::save_file: possibly "
+				   "corrupt file %s could not be deleted: %s\n") % complete_filename_.c_str() % e.what()).str();
+				log("%s", error_msg_[bitCorruptFileLeft].c_str());
+			}
+		}
+	}
+	return;
+}
+
+uint32_t GenericSaveHandler::save() {
+	try { // everything additionally in one big try block to catch any unexpected errors
+		clear_errors();
+
+		//  Make sure that the current directory exists and is writeable.
+		try {
+			g_fs->ensure_directory_exists(dir_);
+		} catch (const FileError& e) {
+			error_ |= kCreatingDirFailed;
+			error_msg_[bitCreatingDirFailed] = (boost::format("GenericSaveHandler::save: "
+			   "directory %s could not be created: %s\n") % dir_.c_str() % e.what()).str();
+			log("%s", error_msg_[bitCreatingDirFailed].c_str());
+			return error_;
+		}
+
+		// Make a backup if file already exists.
+		if (g_fs->file_exists(complete_filename_)) {
+			make_backup();
+		}
+		if (error_) {
+		  return error_;
+		}
+
+		// Write data to file/dir.
+		save_file();
+
+		// Restore or delete backup if one was made.
+		if (!backup_filename_.empty())
+		{
+			if (!error_) {
+				// Delete backup.
+				try {
+					g_fs->fs_unlink(backup_filename_);
+				} catch (const FileError& e) {
+					error_ |= kDeletingBackupFailed;
+					error_msg_[bitDeletingBackupFailed] = (boost::format("GenericSaveHandler::save: "
+					   "backup file %s could not be deleted: %s\n") % backup_filename_.c_str() % e.what()).str();
+					log("%s", error_msg_[bitDeletingBackupFailed].c_str());
+				}
+
+			} else {
+				if (error_ & kCorruptFileLeft) {
+					error_ |= kRestoringBackupFailed;
+					error_msg_[bitRestoringBackupFailed] = (boost::format("GenericSaveHandler::save: "
+					   "file %s could not be restored from backup %s: file still exists\n")
+						 % complete_filename_.c_str() % backup_filename_.c_str()).str();
+					log("%s", error_msg_[bitRestoringBackupFailed].c_str());
+				}
+				else {
+					// Restore backup.
+					try {
+						g_fs->fs_rename(backup_filename_, complete_filename_);
+					} catch (const FileError& e) {
+						error_ |= kRestoringBackupFailed;
+						error_msg_[bitRestoringBackupFailed] = (boost::format("GenericSaveHandler::save: "
+						   "file %s could not be restored from backup %s: %s\n") % backup_filename_.c_str()
+						   % backup_filename_.c_str() % e.what()).str();
+						log("%s", error_msg_[bitRestoringBackupFailed].c_str());
+					}
+				}
+			}
+		}
+
+	} catch (const std::exception& e) {
+		error_ |= kUnexpectedError;
+		error_msg_[bitUnexpectedError] = (boost::format("GenericSaveHandler::save: "
+		   "unknown error: %s\n") % e.what()).str();
+		log("%s", error_msg_[bitUnexpectedError].c_str());
+	}
+
+	return error_;
+}
+
+std::string GenericSaveHandler::error_message(uint32_t error_mask) {
+	error_mask &= error_;
+	std::string err_msg;
+	for (uint32_t errind = 0; errind < maxErrors; errind++) {
+		if ((error_mask >> errind) & 1) {
+			err_msg += error_msg_[errind];
+		}
+	}
+	return err_msg;
+}
+
+std::string GenericSaveHandler::localized_formatted_result_message() {
+	std::string msg;
+
+	if (error_ == kSuccess || error_ == kDeletingBackupFailed) {
+		// no need to mention a failed backup deletion
+		return _("File successfully saved!");
+	}
+
+	if (error_ == kCreatingDirFailed) {
+		return
+		   (boost::format(_("Directory ‘%s’ could not be created!"))
+		   % dir_).str();
+	}
+
+	if (error_ == kBackupFailed) {
+		return
+		   (boost::format(_("File ‘%s’ could not be removed!"))
+		   % complete_filename_.c_str()).str() + "\n"
+			 + _("Try saving under a different name!");
+	}
+
+	// from here on multiple errors might have occurred
+	if (error_ & kSavingDataFailed) {
+		msg =
+		   (boost::format(_("Error writing data to file ‘%s’!"))
+		   % complete_filename_.c_str()).str();
+	}
+
+	if (error_ & kCorruptFileLeft) {
+		if (!msg.empty()) msg += "\n";
+		msg += (boost::format(_("Saved file may be corrupt!"))).str();
+	}
+
+	if (error_ & kRestoringBackupFailed) {
+		if (!msg.empty()) msg += "\n";
+		msg +=
+		   (boost::format(_("File ‘%s’ could not be restored!"))
+		   % complete_filename_.c_str()).str() + "\n" +
+		   (boost::format(_("Backup file ‘%s’ will be available for some time."))
+			 % backup_filename_.c_str()).str();
+	}
+
+	if (!backup_filename_.empty() &&
+	    (error_ & kSavingDataFailed) &&
+	    !(error_ & kCorruptFileLeft) &&
+	    !(error_ & kRestoringBackupFailed)) {
+		if (!msg.empty()) msg += "\n";
+		msg +=
+		   (boost::format(_("File ‘%s’ was restored from backup."))
+		   % complete_filename_.c_str()).str();
+	}
+
+	if (error_ & kUnexpectedError) {
+		if (!msg.empty()) msg += "\n";
+		msg +=
+		   (boost::format(_("An unexpected error occurred:" ))).str()
+		   + "\n" + error_msg_[bitUnexpectedError];
+	}
+
+	return msg;
+}

=== added file 'src/logic/generic_save_handler.h'
--- src/logic/generic_save_handler.h	1970-01-01 00:00:00 +0000
+++ src/logic/generic_save_handler.h	2018-11-08 02:11:06 +0000
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2002-2018 by the Widelands Development Team
+ *
+ * This program 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 2
+ * of the License, or (at your option) any later version.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ *
+ */
+
+#ifndef WL_LOGIC_GENERIC_SAVE_HANDLER_H
+#define WL_LOGIC_GENERIC_SAVE_HANDLER_H
+
+#include <functional>
+#include <string>
+
+#include <stdint.h>
+
+#include "io/filesystem/filesystem.h"
+
+// just some constants for convenience
+namespace {
+	enum ErrorBitIndex : uint32_t {
+		bitCreatingDirFailed = 0,
+		bitBackupFailed,
+		bitSavingDataFailed,
+		bitCorruptFileLeft,
+		bitDeletingBackupFailed,
+		bitRestoringBackupFailed,
+		bitUnexpectedError,
+		maxErrors
+	};
+}
+
+/**
+ * Class that provides handling all errors for generic file saving.
+ * It stores error codes and error messages.
+ * It can also generate an overview message aimed at a human user.
+ *
+ * The saving routine (what actually writes data to a file system) must be provided.
+ */
+// TODO(Arty): Possibly make it more generic, allowing to provide options whether
+// to overwrite, whether to make backups; maybe allow to provide a naming scheme, etc.
+class GenericSaveHandler {
+public:
+	// error constants; usable as bit masks
+	static constexpr uint32_t kSuccess = 0;
+	static constexpr uint32_t kCreatingDirFailed = uint32_t(1) << bitCreatingDirFailed;
+	static constexpr uint32_t kBackupFailed = uint32_t(1) << bitBackupFailed;
+	static constexpr uint32_t kSavingDataFailed = uint32_t(1) << bitSavingDataFailed;
+	static constexpr uint32_t kCorruptFileLeft = uint32_t(1) << bitCorruptFileLeft;
+	static constexpr uint32_t kDeletingBackupFailed = uint32_t(1) << bitDeletingBackupFailed;
+	static constexpr uint32_t kRestoringBackupFailed = uint32_t(1) << bitRestoringBackupFailed;
+	static constexpr uint32_t kUnexpectedError = uint32_t(1) << bitUnexpectedError;
+	static constexpr uint32_t kAllErrors = (1 << maxErrors) - 1;
+
+	explicit GenericSaveHandler(
+	   std::function<void(FileSystem&)> do_save, // function which actually saves data to the filesystem
+	   std::string complete_filename,
+	   FileSystem::Type type)
+   : do_save_(do_save),
+     complete_filename_(complete_filename),
+     dir_(FileSystem::fs_dirname(complete_filename.c_str())),
+     filename_(FileSystem::fs_filename(complete_filename.c_str())),
+     type_(type),
+     error_(kSuccess) {};
+
+	/**
+	 * Tries to save a file.
+	 * If the file already exists, it will be overwritten but a temporary backup is made
+	 * which will be restored if saving fails and deleted otherwise.
+	 * Catches ALL errors.
+	 * Error messages for all errors are written to the log but also stored.
+	 * Stores and returns an error code (bit mask of all occurred errors).
+	 */
+	uint32_t save();
+
+	// returns the stored error code (from the last saving operation)
+	uint32_t error() { return error_; };
+
+	// Returns the combination of error_messages (of occurred errors) specified by a bit mask.
+	std::string error_message(uint32_t error_mask = kAllErrors);
+
+	// Generates a localized formatted message describing
+	// the result of the saving attempt.
+	// Aimed to be sufficiently informative for a human user.
+	std::string localized_formatted_result_message();
+
+private:
+	std::function<void(FileSystem&)> do_save_;
+	std::string complete_filename_;
+	std::string dir_;
+	std::string filename_;
+	FileSystem::Type type_;
+
+	// Backup filename is automatically generated when saving but is also
+	// stored for generating messages containing backup-related things.
+	std::string backup_filename_;
+	uint32_t error_;
+	std::string error_msg_[maxErrors];
+
+	void clear_errors();
+
+	// Finds a suitable backup filename and tries to rename a file.
+	// Stores an errorcode and error message (if applicable).
+	void make_backup();
+
+	// Saves a file. Assumes file doesn't exist.
+	// Stores an errorcode and error message (if applicable).
+	void save_file();
+};
+
+
+#endif  // end of include guard: WL_LOGIC_GENERIC_SAVE_HANDLER_H

=== modified file 'src/logic/save_handler.cc'
--- src/logic/save_handler.cc	2018-08-26 18:22:27 +0000
+++ src/logic/save_handler.cc	2018-11-08 02:11:06 +0000
@@ -32,14 +32,13 @@
 #include "base/wexception.h"
 #include "game_io/game_saver.h"
 #include "io/filesystem/filesystem.h"
+#include "io/filesystem/filesystem_exceptions.h"
 #include "logic/filesystem_constants.h"
 #include "logic/game.h"
 #include "logic/game_controller.h"
+#include "logic/generic_save_handler.h"
 #include "wui/interactive_base.h"
 
-// The actual work of saving is done by the GameSaver
-using Widelands::GameSaver;
-
 SaveHandler::SaveHandler()
    : next_save_realtime_(0),
      initialized_(false),
@@ -52,34 +51,59 @@
      number_of_rolls_(5) {
 }
 
-void SaveHandler::roll_save_files(const std::string& filename) {
-
-	int32_t rolls = number_of_rolls_;
-	log("Autosave: Rolling savefiles (count): %d\n", rolls);
-	rolls--;
-	std::string filename_previous =
-	   create_file_name(kSaveDir, (boost::format("%s_%02d") % filename % rolls).str());
-	if (rolls > 0 && g_fs->file_exists(filename_previous)) {
-		g_fs->fs_unlink(filename_previous);  // Delete last of the rolling files
-		log("Autosave: Deleted %s\n", filename_previous.c_str());
-	}
+bool SaveHandler::roll_save_files(const std::string& filename, std::string* const error) {
+	int32_t rolls = 0;
+	std::string filename_previous;
+
+	// only roll the smallest necessary number of files
+	while (rolls < number_of_rolls_) {
+		filename_previous =
+			   create_file_name(kSaveDir, (boost::format("%s_%02d") % filename % rolls).str());
+		if (!g_fs->file_exists(filename_previous))
+		  break;
+		rolls++;
+	}
+	if (rolls < number_of_rolls_) {
+		// There is a file missing in the sequence; no need to delete any file.
+		log("Autosave: Rolling savefiles (count): %d of %d\n", rolls, number_of_rolls_);
+	}
+	else {
+		log("Autosave: Rolling savefiles (count): %d\n", rolls);
+		rolls--;
+		filename_previous =
+		   create_file_name(kSaveDir, (boost::format("%s_%02d") % filename % rolls).str());
+		if (rolls > 0) {
+			try {
+				g_fs->fs_unlink(filename_previous);  // Delete last of the rolling files
+				log("Autosave: Deleted %s\n", filename_previous.c_str());
+			} catch (const FileError& e) {
+				log("Autosave: Unable to delete file %s: %s\n", filename_previous.c_str(), e.what());
+				if (error) {
+					*error =
+					   (boost::format("Autosave: Unable to delete file %s: %s\n")
+					   % filename_previous.c_str() % e.what()).str();
+				}
+				return false;
+			}
+		}
+	}
+
 	rolls--;
 	while (rolls >= 0) {
 		const std::string filename_next =
 		   create_file_name(kSaveDir, (boost::format("%s_%02d") % filename % rolls).str());
-		if (g_fs->file_exists(filename_next)) {
-			try {
-				g_fs->fs_rename(
-				   filename_next, filename_previous);  // e.g. wl_autosave_08 -> wl_autosave_09
-				log("Autosave: Rolled %s to %s\n", filename_next.c_str(), filename_previous.c_str());
-			} catch (const std::exception& e) {
-				log("Autosave: Unable to rename file %s to %s: %s\n", filename_previous.c_str(),
-				    filename_next.c_str(), e.what());
-			}
+		try {
+			g_fs->fs_rename(filename_next, filename_previous);  // e.g. wl_autosave_08 -> wl_autosave_09
+			log("Autosave: Rolled %s to %s\n", filename_next.c_str(), filename_previous.c_str());
+		} catch (const FileError& e) {
+			log("Autosave: Unable to roll file %s to %s: %s\n", filename_previous.c_str(),
+			    filename_next.c_str(), e.what());
+			return false;
 		}
 		filename_previous = filename_next;
 		rolls--;
 	}
+	return true;
 }
 
 /**
@@ -106,43 +130,6 @@
 }
 
 /**
- * If saving fails restore the backup file.
- *
- * @return true when save was a success.
- */
-bool SaveHandler::save_and_handle_error(Widelands::Game& game,
-                                        const std::string& complete_filename,
-                                        const std::string& backup_filename) {
-	std::string error;
-	bool result = save_game(game, complete_filename, &error);
-	if (!result) {
-		log("Autosave: ERROR! - %s\n", error.c_str());
-		game.get_ibase()->log_message(_("Saving failed!"));
-
-		// if backup file was created, move it back
-		if (backup_filename.length() > 0) {
-			if (g_fs->file_exists(complete_filename)) {
-				g_fs->fs_unlink(complete_filename);
-			}
-			g_fs->fs_rename(backup_filename, complete_filename);
-		}
-		// Wait 30 seconds until next save try
-		next_save_realtime_ = SDL_GetTicks() + 30000;
-	} else {
-		// if backup file was created, time to remove it
-		if (backup_filename.length() > 0 && g_fs->file_exists(backup_filename)) {
-			g_fs->fs_unlink(backup_filename);
-		}
-
-		// Count save interval from end of save.
-		// This prevents us from going into endless autosave cycles if the save should take longer
-		// than the autosave interval.
-		next_save_realtime_ = SDL_GetTicks() + autosave_interval_in_ms_;
-	}
-	return result;
-}
-
-/**
  * Check if autosave is needed and allowed or save was requested by user.
  */
 void SaveHandler::think(Widelands::Game& game) {
@@ -155,6 +142,9 @@
 
 	// Are we saving now?
 	if (saving_next_tick_ || save_requested_) {
+		saving_next_tick_ = false;
+		bool save_success = true;
+		std::string error;
 		std::string filename = autosave_filename_;
 		if (save_requested_) {
 			// Requested by user
@@ -166,59 +156,34 @@
 			save_filename_ = "";
 		} else {
 			// Autosave ...
-			roll_save_files(filename);
-			filename = (boost::format("%s_00") % autosave_filename_).str();
-			log("Autosave: saving as %s\n", filename.c_str());
-		}
-
-		// Saving now
-		std::string complete_filename = create_file_name(kSaveDir, filename);
-		std::string backup_filename;
-
-		// always overwrite a file
-		if (g_fs->file_exists(complete_filename)) {
-			filename += "2";
-			backup_filename = create_file_name(kSaveDir, filename);
-			if (g_fs->file_exists(backup_filename)) {
-				g_fs->fs_unlink(backup_filename);
-			}
-			if (save_requested_) {
-				// Exceptions (e.g. no file permissions) will be handled in UI
-				g_fs->fs_rename(complete_filename, backup_filename);
-			} else {
-				// We're autosaving, try to handle file permissions issues
-				try {
-					g_fs->fs_rename(complete_filename, backup_filename);
-				} catch (const std::exception& e) {
-					log("Autosave: Unable to rename file %s to %s: %s\n", complete_filename.c_str(),
-					    backup_filename.c_str(), e.what());
-					// See if we can find a file that doesn't exist and save to that
-					int current_autosave = 0;
-					do {
-						filename = create_file_name(
-						   kSaveDir,
-						   (boost::format("%s_0%d") % autosave_filename_ % (++current_autosave)).str());
-					} while (current_autosave < 9 && g_fs->file_exists(filename));
-					complete_filename = filename;
-					log("Autosave: saving as %s instead\n", complete_filename.c_str());
-					try {
-						g_fs->fs_rename(complete_filename, backup_filename);
-					} catch (const std::exception&) {
-						log("Autosave failed, skipping this interval\n");
-						saving_next_tick_ = check_next_tick(game, realtime);
-						return;
-					}
-				}
-			}
-		}
-
-		if (!save_and_handle_error(game, complete_filename, backup_filename)) {
+			save_success = roll_save_files(filename, &error);
+			if (save_success) {
+				filename = (boost::format("%s_00") % autosave_filename_).str();
+				log("Autosave: saving as %s\n", filename.c_str());
+			}
+		}
+
+		if (save_success) {
+			// Saving now (always overwrite file)
+			std::string complete_filename = create_file_name(kSaveDir, filename);
+			save_success = save_game(game, complete_filename, &error);
+		}
+		if (!save_success) {
+			log("Autosave: ERROR! - %s\n", error.c_str());
+			game.get_ibase()->log_message(_("Saving failed!"));
+
+			// Wait 30 seconds until next save try
+			next_save_realtime_ = SDL_GetTicks() + 30000;
 			return;
 		}
 
+		// Count save interval from end of save.
+		// This prevents us from going into endless autosave cycles if the save should take longer
+		// than the autosave interval.
+		next_save_realtime_ = SDL_GetTicks() + autosave_interval_in_ms_;
+
 		log("Autosave: save took %d ms\n", SDL_GetTicks() - realtime);
 		game.get_ibase()->log_message(_("Game saved"));
-		saving_next_tick_ = false;
 	} else {
 		saving_next_tick_ = check_next_tick(game, realtime);
 	}
@@ -262,10 +227,13 @@
 	return complete_filename;
 }
 
+
 /*
  * Save the game using the GameSaver.
  *
- * Will copy text of exceptions to error string.
+ * Overwrites file if it exists.
+ *
+ * Will copy text of errors to error string.
  *
  * returns true if saved, false in case some error occured.
  */
@@ -274,22 +242,26 @@
                             std::string* const error) {
 	ScopedTimer save_timer("SaveHandler::save_game() took %ums");
 
-	// Make sure that the base directory exists
-	g_fs->ensure_directory_exists(kSaveDir);
-
-	// Make a filesystem out of this
-	std::unique_ptr<FileSystem> fs;
-	fs.reset(g_fs->create_sub_file_system(complete_filename, fs_type_));
-
-	bool result = true;
-	GameSaver gs(*fs, game);
-	try {
-		gs.save();
-	} catch (const std::exception& e) {
-		if (error)
-			*error = e.what();
-		result = false;
-	}
-
-	return result;
+	// save game via the GenericSaveHandler
+	GenericSaveHandler gsh(
+		[&game](FileSystem& fs) {
+			Widelands::GameSaver gs(fs, game);
+			gs.save();
+		},
+		complete_filename,
+		fs_type_
+	);
+	gsh.save();
+
+	// Ignore it if only the temporary backup wasn't deleted
+	// but save was successfull otherwise
+	if (gsh.error() == GenericSaveHandler::kSuccess ||
+	    gsh.error() == GenericSaveHandler::kDeletingBackupFailed) {
+		return true;
+	}
+
+	if (error) {
+		*error = gsh.error_message();
+	}
+	return false;
 }

=== modified file 'src/logic/save_handler.h'
--- src/logic/save_handler.h	2018-04-07 16:59:00 +0000
+++ src/logic/save_handler.h	2018-11-08 02:11:06 +0000
@@ -42,6 +42,8 @@
 
 	void think(Widelands::Game&);
 	std::string create_file_name(const std::string& dir, const std::string& filename) const;
+
+	// saves the game, overwrites file, handles errors
 	bool save_game(Widelands::Game&, const std::string& filename, std::string* error = nullptr);
 
 	const std::string get_cur_filename() {
@@ -82,11 +84,8 @@
 	int32_t number_of_rolls_;  // For rolling file update
 
 	void initialize(uint32_t gametime);
-	void roll_save_files(const std::string& filename);
+	bool roll_save_files(const std::string& filename, std::string* error);
 	bool check_next_tick(Widelands::Game& game, uint32_t realtime);
-	bool save_and_handle_error(Widelands::Game& game,
-	                           const std::string& complete_filename,
-	                           const std::string& backup_filename);
 };
 
 #endif  // end of include guard: WL_LOGIC_SAVE_HANDLER_H

=== modified file 'src/wlapplication.cc'
--- src/wlapplication.cc	2018-11-08 02:11:06 +0000
+++ src/wlapplication.cc	2018-11-08 02:11:06 +0000
@@ -58,6 +58,7 @@
 #include "graphic/text_constants.h"
 #include "helper.h"
 #include "io/filesystem/disk_filesystem.h"
+#include "io/filesystem/filesystem_exceptions.h"
 #include "io/filesystem/layered_filesystem.h"
 #include "logic/ai_dna_handler.h"
 #include "logic/filesystem_constants.h"
@@ -215,7 +216,7 @@
 // Returns true if 'filename' was autogenerated, i.e. if 'extract_creation_day' can return a date
 // and it is old enough to be deleted.
 bool is_autogenerated_and_expired(const std::string& filename,
-                                  const double keep_time = kReplayKeepAroundTime) {
+                                  const double keep_time) {
 	tm tfile;
 	if (!extract_creation_day(filename, &tfile)) {
 		return false;
@@ -340,6 +341,7 @@
 	cleanup_replays();
 	cleanup_ai_files();
 	cleanup_temp_files();
+	cleanup_temp_backups();
 
 	Section& config = g_options.pull_section("global");
 
@@ -1464,22 +1466,16 @@
 void WLApplication::cleanup_replays() {
 	for (const std::string& filename :
 	     filter(g_fs->list_directory(kReplayDir), [](const std::string& fn) {
-		     return boost::ends_with(
-		        fn, (boost::format("%s%s") % kReplayExtension % kSyncstreamExtension).str());
-		  })) {
-		if (is_autogenerated_and_expired(filename)) {
-			log("Delete syncstream %s\n", filename.c_str());
-			g_fs->fs_unlink(filename);
-		}
-	}
-
-	for (const std::string& filename :
-	     filter(g_fs->list_directory(kReplayDir),
-	            [](const std::string& fn) { return boost::ends_with(fn, kReplayExtension); })) {
-		if (is_autogenerated_and_expired(filename)) {
+	       return boost::ends_with(fn, kReplayExtension) ||
+	         boost::ends_with(fn, kReplayExtension + kSavegameExtension) ||
+	         boost::ends_with(fn, kReplayExtension + kSyncstreamExtension); })) {
+		if (is_autogenerated_and_expired(filename, kReplayKeepAroundTime)) {
 			log("Deleting replay %s\n", filename.c_str());
-			g_fs->fs_unlink(filename);
-			g_fs->fs_unlink(filename + kSavegameExtension);
+			try {
+				g_fs->fs_unlink(filename);
+			} catch (const FileError& e) {
+				log("WLApplication::cleanup_replays: File %s couldn't be deleted.\n", filename.c_str());
+			}
 		}
 	}
 }
@@ -1494,7 +1490,11 @@
 		  })) {
 		if (is_autogenerated_and_expired(filename, kAIFilesKeepAroundTime)) {
 			log("Deleting generated ai file: %s\n", filename.c_str());
-			g_fs->fs_unlink(filename);
+			try {
+				g_fs->fs_unlink(filename);
+			} catch (const FileError& e) {
+				log("WLApplication::cleanup_ai_files: File %s couldn't be deleted.\n", filename.c_str());
+			}
 		}
 	}
 }
@@ -1509,9 +1509,51 @@
 		  })) {
 		if (is_autogenerated_and_expired(filename, kTempFilesKeepAroundTime)) {
 			log("Deleting old temp file: %s\n", filename.c_str());
-			g_fs->fs_unlink(filename);
-		}
-	}
+			try {
+				g_fs->fs_unlink(filename);
+			} catch (const FileError& e) {
+				log("WLApplication::cleanup_temp_files: File %s couldn't be deleted.\n", filename.c_str());
+			}
+		}
+	}
+}
+
+/**
+ * Recursively delete temporary backup files in a given directory
+ */
+void WLApplication::cleanup_temp_backups(std::string dir) {
+	for (const std::string& filename :
+	     filter(g_fs->list_directory(dir), [](const std::string& fn) {
+		     return boost::ends_with(fn, kTempBackupExtension);
+		  })) {
+		if (is_autogenerated_and_expired(filename, kTempBackupsKeepAroundTime)) {
+			log("Deleting old temp backup file: %s\n", filename.c_str());
+			try {
+				g_fs->fs_unlink(filename);
+			} catch (const FileError& e) {
+				log("WLApplication::cleanup_temp_backups: File %s couldn't be deleted.\n", filename.c_str());
+			}
+		}
+	}
+	// recursively delete in subdirs
+	for (const std::string& dirname :
+	     filter(g_fs->list_directory(dir), [](const std::string& fn) {
+		     return g_fs->is_directory(fn) &&
+	         // avoid searching within savegames/maps/backups that were created as directories instead of zipfiles
+	         !boost::ends_with(fn, kSavegameExtension) &&
+	         !boost::ends_with(fn, kWidelandsMapExtension) &&
+	         !boost::ends_with(fn, kTempBackupExtension);
+		  })) {
+		cleanup_temp_backups(dirname);
+	}
+}
+
+/**
+ * Delete old temporary backup files that might still lurk around (game crashes etc.)
+ */
+void WLApplication::cleanup_temp_backups() {
+	cleanup_temp_backups(kSaveDir);
+	cleanup_temp_backups(kMapsDir);
 }
 
 bool WLApplication::redirect_output(std::string path) {

=== modified file 'src/wlapplication.h'
--- src/wlapplication.h	2018-11-08 02:11:06 +0000
+++ src/wlapplication.h	2018-11-08 02:11:06 +0000
@@ -213,10 +213,10 @@
 	void setup_homedir();
 
 	void cleanup_replays();
-
 	void cleanup_ai_files();
-
 	void cleanup_temp_files();
+	void cleanup_temp_backups(std::string dir);
+	void cleanup_temp_backups();
 
 	bool redirect_output(std::string path = "");
 

=== modified file 'src/wui/game_main_menu_save_game.cc'
--- src/wui/game_main_menu_save_game.cc	2018-10-26 07:09:29 +0000
+++ src/wui/game_main_menu_save_game.cc	2018-11-08 02:11:06 +0000
@@ -21,6 +21,7 @@
 
 #include <memory>
 
+#include <boost/algorithm/string.hpp>
 #include <boost/format.hpp>
 
 #include "base/i18n.h"
@@ -32,6 +33,7 @@
 #include "logic/filesystem_constants.h"
 #include "logic/game.h"
 #include "logic/game_controller.h"
+#include "logic/generic_save_handler.h"
 #include "logic/playersmanager.h"
 #include "ui_basic/messagebox.h"
 #include "wui/interactive_gamebase.h"
@@ -153,63 +155,16 @@
 	}
 }
 
-static void dosave(InteractiveGameBase& igbase, const std::string& complete_filename) {
-	Widelands::Game& game = igbase.game();
-
-	std::string error;
-	if (!game.save_handler().save_game(game, complete_filename, &error)) {
-		std::string s = _("Game Saving Error!\nSaved game file may be corrupt!\n\n"
-		                  "Reason given:\n");
-		s += error;
-		UI::WLMessageBox mbox(&igbase, _("Save Game Error!"), s, UI::WLMessageBox::MBoxType::kOk);
-		mbox.run<UI::Panel::Returncodes>();
-	}
-	game.save_handler().set_current_filename(complete_filename);
-}
-
-struct SaveWarnMessageBox : public UI::WLMessageBox {
-	SaveWarnMessageBox(GameMainMenuSaveGame& parent, const std::string& filename)
-	   : UI::WLMessageBox(&parent,
-	                      _("Save Game Error!"),
-	                      (boost::format(_("A file with the name ‘%s’ already exists. Overwrite?")) %
-	                       FileSystem::fs_filename(filename.c_str()))
-	                         .str(),
-	                      MBoxType::kOkCancel),
-	     filename_(filename) {
-	}
-
-	GameMainMenuSaveGame& menu_save_game() {
-		return dynamic_cast<GameMainMenuSaveGame&>(*get_parent());
-	}
-
-	void clicked_ok() override {
-		g_fs->fs_unlink(filename_);
-		dosave(menu_save_game().igbase(), filename_);
-		menu_save_game().die();
-	}
-
-	void clicked_back() override {
-		die();
-	}
-
-private:
-	std::string const filename_;
-};
-
 void GameMainMenuSaveGame::ok() {
-	if (filename_editbox_.text().empty()) {
+	if (!ok_.enabled()) {
 		return;
 	}
 
-	std::string const complete_filename =
-	   igbase().game().save_handler().create_file_name(curdir_, filename_editbox_.text());
-
-	//  Check if file exists. If it does, show a warning.
-	if (g_fs->file_exists(complete_filename)) {
-		new SaveWarnMessageBox(*this, complete_filename);
+	std::string filename = filename_editbox_.text();
+	if (save_game(filename, !g_options.pull_section("global").get_bool("nozip", false))) {
+		die();
 	} else {
-		dosave(igbase(), complete_filename);
-		die();
+		load_or_save_.table_.focus();
 	}
 }
 
@@ -244,3 +199,67 @@
 	}
 	igbase().game().game_controller()->set_paused(paused);
 }
+
+/**
+ * Save the game in the Savegame directory with
+ * the given filename
+ *
+ * returns true if dialog should close, false if it
+ * should stay open
+ */
+bool GameMainMenuSaveGame::save_game(std::string filename, bool binary) {
+	// Trim it for preceding/trailing whitespaces in user input
+	boost::trim(filename);
+
+	//  OK, first check if the extension matches (ignoring case).
+	if (!boost::iends_with(filename, kSavegameExtension))
+		filename += kSavegameExtension;
+
+	//  Append directory name.
+	const std::string complete_filename = curdir_ + g_fs->file_separator() + filename;
+
+	//  Check if file exists. If so, show a warning.
+	if (g_fs->file_exists(complete_filename)) {
+		const std::string s =
+		   (boost::format(_("A file with the name ‘%s’ already exists. Overwrite?")) %
+		    FileSystem::fs_filename(filename.c_str()))
+		      .str();
+		UI::WLMessageBox mbox(this, _("Error Saving Game!"), s, UI::WLMessageBox::MBoxType::kOkCancel);
+		if (mbox.run<UI::Panel::Returncodes>() == UI::Panel::Returncodes::kBack)
+			return false;
+	}
+
+	// Try saving the game.
+	Widelands::Game& game = igbase().game();
+	GenericSaveHandler gsh(
+		[&game](FileSystem& fs) {
+			Widelands::GameSaver gs(fs, game);
+			gs.save();
+		},
+		complete_filename,
+		binary ? FileSystem::ZIP : FileSystem::DIR
+	);
+	uint32_t error = gsh.save();
+	if (error == GenericSaveHandler::kSuccess ||
+	    error == GenericSaveHandler::kDeletingBackupFailed) {
+			// No need to bother the player if only the temporary backup couldn't be deleted.
+			// Automatic cleanup will try to deal with it later.
+		game.save_handler().set_current_filename(complete_filename);
+		igbase().log_message(_("Game saved"));
+		return true;
+	}
+
+	// Show player an error message.
+	std::string msg = gsh.localized_formatted_result_message();
+	UI::WLMessageBox mbox(this, _("Error Saving Game!"), msg, UI::WLMessageBox::MBoxType::kOk);
+	mbox.run<UI::Panel::Returncodes>();
+
+	// If only the backup failed (likely just because of a file lock),
+	// then leave the dialog open for the player to try with a new filename.
+	if (error == GenericSaveHandler::kBackupFailed)
+	  return false;
+
+	// In the other error cases close the dialog.
+	igbase().log_message(_("Saving failed!"));
+	return true;
+}

=== modified file 'src/wui/game_main_menu_save_game.h'
--- src/wui/game_main_menu_save_game.h	2018-05-23 04:40:43 +0000
+++ src/wui/game_main_menu_save_game.h	2018-11-08 02:11:06 +0000
@@ -60,6 +60,8 @@
 	/// Called when the OK button is clicked or the Return key pressed in the edit box.
 	void ok();
 
+	bool save_game(std::string filename, bool binary);
+
 	/// Pause/unpause the game
 	void pause_game(bool paused);
 

_______________________________________________
Mailing list: https://launchpad.net/~widelands-dev
Post to     : widelands-dev@lists.launchpad.net
Unsubscribe : https://launchpad.net/~widelands-dev
More help   : https://help.launchpad.net/ListHelp

Reply via email to