Browse Source

Allow changing default directories in Windows

George King 6 months ago
parent
commit
a08e923934
1 changed files with 78 additions and 212 deletions
  1. 78 212
      lib/VCMIDirs.cpp

+ 78 - 212
lib/VCMIDirs.cpp

@@ -10,6 +10,7 @@
 
 #include "StdInc.h"
 #include "VCMIDirs.h"
+#include "../lib/json/JsonNode.h"
 
 #ifdef VCMI_IOS
 #include "iOS_utils.h"
@@ -23,7 +24,7 @@ namespace bfs = boost::filesystem;
 
 bfs::path IVCMIDirs::userLogsPath() const { return userCachePath(); }
 
-bfs::path IVCMIDirs::userSavePath() const { return userDataPath() / "Saves"; }
+bfs::path IVCMIDirs::userSavePath() const { return userDataPath() / "saves"; }
 
 bfs::path IVCMIDirs::userExtractedPath() const { return userCachePath() / "extracted"; }
 
@@ -74,76 +75,6 @@ void IVCMIDirs::init()
 
 #include <windows.h>
 #include <shlobj.h>
-#include <shellapi.h>
-
-// Generates script file named _temp.bat in 'to' directory and runs it
-// Script will:
-// - Wait util 'exeName' ends.
-// - Copy all files from 'from' to 'to'
-// - Ask user to replace files existed in 'to'.
-// - Run 'exeName'
-// - Delete itself.
-bool StartBatchCopyDataProgram(
-	const bfs::path& from, const bfs::path& to, const bfs::path& exeName,
-	const bfs::path& currentPath = bfs::current_path())
-{
-	static const char base[] =
-		"@echo off"												"\n"
-		"echo Preparing to move VCMI data system."				"\n"
-
-		":CLIENT_RUNNING_LOOP"									"\n"
-		"TASKLIST | FIND /I %1% > nul"							"\n"
-		"IF ERRORLEVEL 1 ("										"\n"
-			"GOTO CLIENT_NOT_RUNNING"							"\n"
-		") ELSE ("												"\n"
-			"echo %1% is still running..."						"\n"
-			"echo Waiting until process ends..."				"\n"
-			"ping 1.1.1.1 -n 1 -w 3000 > nul"					"\n" // Sleep ~3 seconds. I love Windows :)
-			"goto :CLIENT_RUNNING_LOOP"							"\n"
-		")"														"\n"
-
-		":CLIENT_NOT_RUNNING"									"\n"
-		"echo %1% turned off..."								"\n"
-		"echo Attempt to move data."							"\n"
-		"echo From: %2%"										"\n"
-		"echo To: %4%"											"\n"
-		"echo Please resolve any conflicts..."					"\n"
-		"move /-Y %3% %4%"										"\n" // Move all files from %3% to %4%.
-																	 // /-Y ask what to do when file exists in %4%
-		":REMOVE_OLD_DIR"										"\n"
-		"rd %2% || rem"											"\n" // Remove empty directory. Sets error flag if fail.
-		"IF ERRORLEVEL 145 ("									"\n" // Directory not empty
-			"echo Directory %2% is not empty."					"\n"
-			"echo Please move rest of files manually now."		"\n"
-			"pause"												"\n" // Press any key to continue...
-			"goto REMOVE_OLD_DIR"								"\n"
-		")"														"\n"
-		"echo Game data updated successfully."					"\n"
-		"echo Please update your shortcuts."					"\n"
-		"echo Press any key to start a game . . ."				"\n"
-		"pause > nul"											"\n"
-		"%5%"													"\n"
-		"del \"%%~f0\"&exit"									"\n" // Script deletes itself
-		;
-
-	const auto startGameString =
-		bfs::equivalent(currentPath, from) ?
-		(boost::format("start \"\" %1%") % (to / exeName)) :						// Start game in new path.
-		(boost::format("start \"\" /D %1% %2%") % currentPath % (to / exeName));	// Start game in 'currentPath"
-
-	const bfs::path bathFilename = to / "_temp.bat";
-	bfs::ofstream bathFile(bathFilename, bfs::ofstream::trunc | bfs::ofstream::out);
-	if (!bathFile.is_open())
-		return false;
-	bathFile << (boost::format(base) % exeName % from % (from / "*.*") % to % startGameString.str()).str();
-	bathFile.close();
-
-	std::system(("start \"Updating VCMI data\" /D \"" + to.string() + "\" \"" + bathFilename.string() + '\"').c_str());
-	// start won't block std::system
-	// /D start bat in other directory insteand of current directory.
-
-	return true;
-}
 
 class VCMIDirsWIN32 final : public IVCMIDirs
 {
@@ -151,6 +82,8 @@ class VCMIDirsWIN32 final : public IVCMIDirs
 		bfs::path userDataPath() const override;
 		bfs::path userCachePath() const override;
 		bfs::path userConfigPath() const override;
+		bfs::path userLogsPath() const override;
+		bfs::path userSavePath() const override;
 
 		std::vector<bfs::path> dataPaths() const override;
 
@@ -164,178 +97,111 @@ class VCMIDirsWIN32 final : public IVCMIDirs
 		std::string libraryName(const std::string& basename) const override;
 
 		void init() override;
+
 	protected:
-		bfs::path oldUserDataPath() const;
-		bfs::path oldUserSavePath() const;
+		mutable std::optional<JsonNode> dirsConfig;
+		void loadDirsJsonIfNeeded() const;
+		bfs::path getPathFromConfigOrDefault(const std::string& key, const std::function<bfs::path()>& fallbackFunc) const;
+		std::wstring utf8ToWstring(const std::string& str) const;
 };
 
-void VCMIDirsWIN32::init()
+std::wstring VCMIDirsWIN32::utf8ToWstring(const std::string& str) const
 {
-	// Call base (init dirs)
-	IVCMIDirs::init();
-
-	// Moves one directory (from) contents to another directory (to)
-	// Shows user the "moving file dialog" and ask to resolve conflits.
-	// If necessary updates current directory.
-	auto moveDirIfExists = [](const bfs::path& from, const bfs::path& to) -> bool
+	std::wstring result;
+	int size_needed = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, nullptr, 0);
+	if (size_needed > 0)
 	{
-		if (!bfs::is_directory(from))
-			return true; // Nothing to do here. Flies away.
-
-		if (bfs::is_empty(from))
-		{
-			if (bfs::current_path() == from)
-				bfs::current_path(to);
+		result.resize(size_needed - 1);
+		MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, &result[0], size_needed);
+	}
+	return result;
+}
 
-			bfs::remove(from);
-			return true; // Nothing to do here. Flies away.
-		}
+void VCMIDirsWIN32::loadDirsJsonIfNeeded() const
+{
+	if (dirsConfig.has_value())
+		return;
 
-		if (!bfs::is_directory(to))
-		{
-			// IVCMIDirs::init() should create all destination directories.
-			// TODO: Log fact, that we shouldn't be here.
-			bfs::create_directories(to);
-		}
+	wchar_t currentPath[MAX_PATH];
+	GetModuleFileNameW(nullptr, currentPath, MAX_PATH);
 
-		// Why the hell path strings should be end with double null :/
-		auto makeDoubleNulled = [](const bfs::path& path) -> std::unique_ptr<wchar_t[]>
-		{
-			const std::wstring& pathStr = path.native();
-			std::unique_ptr<wchar_t[]> result(new wchar_t[pathStr.length() + 2]);
-
-			size_t i = 0;
-			for (const wchar_t ch : pathStr)
-				result[i++] = ch;
-			result[i++] = L'\0';
-			result[i++] = L'\0';
-
-			return result;
-		};
-
-		auto fromDNulled = makeDoubleNulled(from / L"*.*");
-		auto toDNulled = makeDoubleNulled(to);
-
-		SHFILEOPSTRUCTW fileOp;
-		fileOp.hwnd = GetConsoleWindow();
-		fileOp.wFunc = FO_MOVE;
-		fileOp.pFrom = fromDNulled.get();
-		fileOp.pTo = toDNulled.get();
-		fileOp.fFlags = 0;
-		fileOp.hNameMappings = nullptr;
-		fileOp.lpszProgressTitle = nullptr;
-
-		const int errorCode = SHFileOperationW(&fileOp);
-		if (errorCode != 0) // TODO: Log error. User should try to move files.
-			return false;
-		else if (fileOp.fAnyOperationsAborted) // TODO: Log warn. User aborted operation. User should move files.
-			return false;
-		else if (!bfs::is_empty(from)) // TODO: Log warn. Some files not moved. User should try to move files.
-			return false;
-
-		if (bfs::current_path() == from)
-			bfs::current_path(to);
-
-		// TODO: Log fact that we moved files successfully.
-		bfs::remove(from);
-		return true;
-	};
+	auto configPath = bfs::path(currentPath).parent_path() / "config" / "dirs.json";
 
-	// Retrieves the fully qualified path for the file that contains the specified module.
-	// The module must have been loaded by the current process.
-	// If this parameter is nullptr, retrieves the path of the executable file of the current process.
-	auto getModulePath = [](HMODULE hModule) -> bfs::path
+	if (!bfs::exists(configPath))
 	{
-		wchar_t exePathW[MAX_PATH];
-		DWORD nSize = GetModuleFileNameW(hModule, exePathW, MAX_PATH);
-		DWORD error = GetLastError();
-		// WARN: Windows XP don't set ERROR_INSUFFICIENT_BUFFER error.
-		if (nSize != 0 && error != ERROR_INSUFFICIENT_BUFFER)
-			return bfs::path(std::wstring(exePathW, nSize));
-		// TODO: Error handling
-		return bfs::path();
-	};
+		dirsConfig = std::nullopt;
+		return;
+	}
 
-	// Moves one directory contents to another directory
-	// Shows user the "moving file dialog" and ask to resolve conflicts.
-	// It takes into account that 'from' path can contain current executable.
-	// If necessary closes program and starts update script.
-	auto advancedMoveDirIfExists = [getModulePath, moveDirIfExists](const bfs::path& from, const bfs::path& to) -> bool
+	std::ifstream in(configPath.string(), std::ios::binary);
+	if (!in)
 	{
-		const bfs::path executablePath = getModulePath(nullptr);
-
-		// VCMI can't determine executable path.
-		// Use standard way to move directory and exit function.
-		if (executablePath.empty())
-			return moveDirIfExists(from, to);
+		dirsConfig = std::nullopt;
+		return;
+	}
 
-		const bfs::path executableName = executablePath.filename();
+	std::string buffer((std::istreambuf_iterator<char>(in)), {});
+	dirsConfig = JsonNode(reinterpret_cast<const std::byte*>(buffer.data()), buffer.size(), configPath.string());
+}
 
-		// Current executabl isn't in 'from' path.
-		// Use standard way to move directory and exit function.
-		if (!bfs::equivalent(executablePath, from / executableName))
-			return moveDirIfExists(from, to);
+bfs::path VCMIDirsWIN32::getPathFromConfigOrDefault(const std::string& key, const std::function<bfs::path()>& fallbackFunc) const
+{
+	loadDirsJsonIfNeeded();
 
-		// Try standard way to move directory.
-		// I don't know how other systems, but Windows 8.1 allow to move running executable.
-		if (moveDirIfExists(from, to))
-			return true;
+	if (!dirsConfig || !dirsConfig->isStruct())
+		return fallbackFunc();
 
-		// Start copying script and exit program.
-		if (StartBatchCopyDataProgram(from, to, executableName))
-			exit(ERROR_SUCCESS);
+	const auto& structMap = dirsConfig->Struct();
+	auto it = structMap.find(key);
+	if (it == structMap.end() || !it->second.isString())
+		return fallbackFunc();
 
-		// Everything failed :C
-		return false;
-	};
+	std::wstring raw = utf8ToWstring(it->second.String());
+	wchar_t expanded[MAX_PATH];
+	if (ExpandEnvironmentStringsW(raw.c_str(), expanded, MAX_PATH))
+		return bfs::path(expanded);
+	else
+		return bfs::path(raw);
+}
 
-	moveDirIfExists(oldUserSavePath(), userSavePath());
-	advancedMoveDirIfExists(oldUserDataPath(), userDataPath());
+void VCMIDirsWIN32::init()
+{
+	// Call base (init dirs)
+	IVCMIDirs::init();
 }
 
-bfs::path VCMIDirsWIN32::userDataPath() const
+static bfs::path getDefaultUserDataPath()
 {
 	wchar_t profileDir[MAX_PATH];
-
-	if (SHGetSpecialFolderPathW(nullptr, profileDir, CSIDL_MYDOCUMENTS, FALSE) != FALSE)
+	if (SHGetSpecialFolderPathW(nullptr, profileDir, CSIDL_MYDOCUMENTS, FALSE))
 		return bfs::path(profileDir) / "My Games" / "vcmi";
+	return bfs::path(".");
+}
 
-	return ".";
+bfs::path VCMIDirsWIN32::userDataPath() const
+{
+	return getPathFromConfigOrDefault("userDataPath", [this] { return getDefaultUserDataPath(); });
 }
 
-bfs::path VCMIDirsWIN32::oldUserDataPath() const
+bfs::path VCMIDirsWIN32::userCachePath() const
 {
-	wchar_t profileDir[MAX_PATH];
+	return getPathFromConfigOrDefault("userCachePath", [this] { return userDataPath() / "cache"; });
+}
 
-	if (SHGetSpecialFolderPathW(nullptr, profileDir, CSIDL_PROFILE, FALSE) == FALSE) // WinAPI way failed
-	{
-#if defined(_MSC_VER) && _MSC_VER >= 1700
-		wchar_t* buffer;
-		size_t bufferSize;
-		errno_t result = _wdupenv_s(&buffer, &bufferSize, L"userprofile");
-		if (result == 0)
-		{
-			bfs::path result(std::wstring(buffer, bufferSize));
-			free(buffer);
-			return result;
-		}
-#else
-		const char* profileDirA;
-		if ((profileDirA = std::getenv("userprofile"))) // STL way succeed
-			return bfs::path(profileDirA) / "vcmi";
-#endif
-		else
-			return "."; // Every thing failed, return current directory.
-	}
-	else
-		return bfs::path(profileDir) / "vcmi";
+bfs::path VCMIDirsWIN32::userConfigPath() const
+{
+	return getPathFromConfigOrDefault("userConfigPath", [this] { return userDataPath() / "config"; });
+}
 
-	//return dataPaths()[0] ???;
+bfs::path VCMIDirsWIN32::userLogsPath() const
+{
+	return getPathFromConfigOrDefault("userLogsPath", [this] { return userDataPath() / "logs"; });
 }
-bfs::path VCMIDirsWIN32::oldUserSavePath() const { return userDataPath() / "Games"; }
 
-bfs::path VCMIDirsWIN32::userCachePath() const { return userDataPath(); }
-bfs::path VCMIDirsWIN32::userConfigPath() const { return userDataPath() / "config"; }
+bfs::path VCMIDirsWIN32::userSavePath() const
+{
+	return getPathFromConfigOrDefault("userSavePath", [this] { return userDataPath() / "saves"; });
+}
 
 std::vector<bfs::path> VCMIDirsWIN32::dataPaths() const
 {