diff --git a/source/graphics/tests/test_MeshManager.h b/source/graphics/tests/test_MeshManager.h index 3f21e59130..554292f86d 100644 --- a/source/graphics/tests/test_MeshManager.h +++ b/source/graphics/tests/test_MeshManager.h @@ -28,8 +28,8 @@ #include "ps/CLogger.h" #include "ps/Pyrogenesis.h" -static fs::path MOD_PATH(psLogPath()/"../data/mods/_test.mesh"); -static fs::path CACHE_PATH(psLogPath()/"../data/_testcache"); +static fs::path MOD_PATH(fs::path(psLogDir())/"../data/mods/_test.mesh"); +static fs::path CACHE_PATH(fs::path(psLogDir())/"../data/_testcache"); const char* srcDAE = "collada/sphere.dae"; const char* srcPMD = "collada/sphere.pmd"; diff --git a/source/lib/file/vfs/vfs.cpp b/source/lib/file/vfs/vfs.cpp index d15be6240b..3730af6157 100644 --- a/source/lib/file/vfs/vfs.cpp +++ b/source/lib/file/vfs/vfs.cpp @@ -42,7 +42,7 @@ public: virtual LibError Mount(const VfsPath& mountPoint, const fs::path& path, size_t flags /* = 0 */, size_t priority /* = 0 */) { debug_assert(vfs_path_IsDirectory(mountPoint)); - // note: mounting subdirectories is now allowed. + fs::create_directories(path); VfsDirectory* directory; CHECK_ERR(vfs_Lookup(mountPoint, &m_rootDirectory, directory, 0, VFS_LOOKUP_ADD|VFS_LOOKUP_CREATE)); diff --git a/source/lib/sysdep/os/win/wutil.cpp b/source/lib/sysdep/os/win/wutil.cpp index 6f23432272..0930106314 100644 --- a/source/lib/sysdep/os/win/wutil.cpp +++ b/source/lib/sysdep/os/win/wutil.cpp @@ -30,6 +30,9 @@ #include "win.h" #include "winit.h" +#include // SHGetFolderPath + + WINIT_REGISTER_EARLY_INIT(wutil_Init); WINIT_REGISTER_LATE_SHUTDOWN(wutil_Shutdown); @@ -232,19 +235,37 @@ bool wutil_HasCommandLineArgument(const char* arg) char win_sys_dir[MAX_PATH+1]; char win_exe_dir[MAX_PATH+1]; +char win_appdata_dir[MAX_PATH+1]; static void GetDirectories() { - GetSystemDirectory(win_sys_dir, sizeof(win_sys_dir)); + WinScopedPreserveLastError s; - const DWORD len = GetModuleFileName(GetModuleHandle(0), win_exe_dir, MAX_PATH); - debug_assert(len != 0); - // strip EXE filename and trailing slash - char* slash = strrchr(win_exe_dir, '\\'); - if(slash) - *slash = '\0'; - else - debug_assert(0); // directory name invalid?! + // system directory + { + const UINT charsWritten = GetSystemDirectory(win_sys_dir, ARRAY_SIZE(win_sys_dir)); + debug_assert(charsWritten != 0); + } + + // executable's directory + { + const DWORD len = GetModuleFileName(GetModuleHandle(0), win_exe_dir, ARRAY_SIZE(win_exe_dir)); + debug_assert(len != 0); + // strip EXE filename and trailing slash + char* slash = strrchr(win_exe_dir, '\\'); + if(slash) + *slash = '\0'; + else + debug_assert(0); // directory name invalid?! + } + + // application data + { + HWND hwnd = 0; // ignored unless a dial-up connection is needed to access the folder + HANDLE token = 0; + const HRESULT ret = SHGetFolderPath(hwnd, CSIDL_APPDATA, token, 0, win_appdata_dir); + debug_assert(SUCCEEDED(ret)); + } } diff --git a/source/lib/sysdep/os/win/wutil.h b/source/lib/sysdep/os/win/wutil.h index c97e312c71..262028516e 100644 --- a/source/lib/sysdep/os/win/wutil.h +++ b/source/lib/sysdep/os/win/wutil.h @@ -99,6 +99,7 @@ public: WinScopedPreserveLastError() : m_lastError(GetLastError()) { + SetLastError(0); } ~WinScopedPreserveLastError() @@ -143,9 +144,10 @@ extern bool wutil_HasCommandLineArgument(const char* arg); // directories // -// neither of these end in a slash. +// none of these end with a slash. extern char win_sys_dir[MAX_PATH+1]; extern char win_exe_dir[MAX_PATH+1]; +extern char win_appdata_dir[MAX_PATH+1]; // diff --git a/source/network/NetLog.cpp b/source/network/NetLog.cpp index c8cb50ef61..9f83eff994 100644 --- a/source/network/NetLog.cpp +++ b/source/network/NetLog.cpp @@ -258,7 +258,7 @@ CNetLogFileSink::CNetLogFileSink( void ) CNetLogger::GetStringTime( time ); // Make relative path - fs::path path(psLogPath()/"net_log"); + fs::path path(fs::path(psLogDir())/"net_log"); path /= time+".txt"; m_FileName = path.external_file_string(); m_Append = true; diff --git a/source/ps/CLogger.cpp b/source/ps/CLogger.cpp index 18eee81adf..4f0a39ab26 100644 --- a/source/ps/CLogger.cpp +++ b/source/ps/CLogger.cpp @@ -54,10 +54,10 @@ const char* html_footer = ""; CLogger::CLogger() { - fs::path mainlogPath(psLogPath()/"mainlog.html"); + fs::path mainlogPath(fs::path(psLogDir())/"mainlog.html"); m_MainLog = new std::ofstream(mainlogPath.external_file_string().c_str(), std::ofstream::out | std::ofstream::trunc); - fs::path interestinglogPath(psLogPath()/"interestinglog.html"); + fs::path interestinglogPath(fs::path(psLogDir())/"interestinglog.html"); m_InterestingLog = new std::ofstream(interestinglogPath.external_file_string().c_str(), std::ofstream::out | std::ofstream::trunc); m_OwnsStreams = true; diff --git a/source/ps/ConfigDB.cpp b/source/ps/ConfigDB.cpp index 27693bcba6..372ed39b80 100644 --- a/source/ps/ConfigDB.cpp +++ b/source/ps/ConfigDB.cpp @@ -274,7 +274,7 @@ bool CConfigDB::Reload(EConfigNamespace ns) shared_ptr buffer; size_t buflen; { // Handle missing files quietly - if (g_VFS->GetFileInfo(m_ConfigFile[ns], NULL) == ERR::VFS_FILE_NOT_FOUND) + if (g_VFS->GetFileInfo(m_ConfigFile[ns], NULL) < 0) { LOG(CLogger::Warning, LOG_CATEGORY, "Cannot find config file \"%s\" - ignoring", m_ConfigFile[ns].c_str()); return false; diff --git a/source/ps/GameSetup/GameSetup.cpp b/source/ps/GameSetup/GameSetup.cpp index 7365bbdec1..cc87896194 100644 --- a/source/ps/GameSetup/GameSetup.cpp +++ b/source/ps/GameSetup/GameSetup.cpp @@ -17,6 +17,9 @@ #include "precompiled.h" +#if OS_WIN +#include "lib/sysdep/os/win/wutil.h" +#endif #include "lib/external_libraries/sdl.h" #include "lib/ogl.h" #include "lib/timer.h" @@ -88,6 +91,7 @@ #include "network/NetServer.h" #include "network/NetClient.h" +#include "ps/Pyrogenesis.h" // psSetLogDir #include "ps/GameSetup/Atlas.h" #include "ps/GameSetup/GameSetup.h" #include "ps/GameSetup/Config.h" @@ -551,63 +555,154 @@ static size_t ChooseCacheSize() return 96*MiB; } -fs::path BinariesDir(const CStr& argv0) + +class Paths { - // get full path to executable - char pathname[PATH_MAX]; - // .. first try safe, but system-dependent version - if(sys_get_executable_name(pathname, PATH_MAX) < 0) +public: + Paths(const CStr& argv0, const char* subdirectoryName) { - // .. failed; use argv[0] - errno = 0; - if(!realpath(argv0.c_str(), pathname)) - WARN_ERR(LibError_from_errno(false)); + m_root = Root(argv0); + m_rdata = m_root/"data"; + + // everything is a subdirectory of the root + if(!subdirectoryName) + { + m_data = m_rdata; + m_config = m_data/"config"; + m_cache = m_data/"cache"; + m_logs = m_root/"logs"; + } + else + { +#if OS_WIN + const fs::path appdata(fs::path(win_appdata_dir)/subdirectoryName); + m_data = appdata/"data"; + m_config = appdata/"config"; + m_cache = appdata/"cache"; + m_logs = appdata/"logs"; +#else + const char* envHome = getenv("HOME"); + debug_assert(envHome); + const fs::path home(envHome); + m_data = XDG_Path("XDG_DATA_HOME", home/".local/share")/subdirectoryName; + m_config = XDG_Path("XDG_CONFIG_HOME", home/".config")/subdirectoryName; + m_cache = XDG_Path("XDG_CACHE_HOME", home/".cache")/subdirectoryName; + m_logs = m_config/"logs"; +#endif + } } - // make sure it's valid - errno = 0; - if(access(pathname, X_OK) < 0) - WARN_ERR(LibError_from_errno(false)); + const fs::path& Root() const + { + return m_root; + } - // strip executable name - char* name = (char*)path_name_only(pathname); - *name = '\0'; + const fs::path& RData() const + { + return m_rdata; + } - return fs::path(pathname); -} + const fs::path& Data() const + { + return m_data; + } + + const fs::path& Config() const + { + return m_config; + } + + const fs::path& Cache() const + { + return m_cache; + } + + const fs::path& Logs() const + { + return m_logs; + } + +private: + static fs::path Root(const CStr& argv0) + { + // get full path to executable + char pathname[PATH_MAX]; + // .. first try safe, but system-dependent version + if(sys_get_executable_name(pathname, PATH_MAX) < 0) + { + // .. failed; use argv[0] + errno = 0; + if(!realpath(argv0.c_str(), pathname)) + WARN_ERR(LibError_from_errno(false)); + } + + // make sure it's valid + errno = 0; + if(access(pathname, X_OK) < 0) + WARN_ERR(LibError_from_errno(false)); + + fs::path path(pathname); + for(size_t i = 0; i < 3; i++) // remove "system/name.exe" + path.remove_leaf(); + return path; + } + + static fs::path XDG_Path(const char* envname, const fs::path& home, const fs::path& defaultPath) + { + const char* path = getenv(envname); + if(path) + { + if(path[0] != '/') // relative to $HOME + return home/path; + return fs::path(path); + } + return defaultPath; + } + + // read-only directories, fixed paths relative to executable + fs::path m_root; + fs::path m_rdata; + + // writable directories + fs::path m_data; + fs::path m_config; + fs::path m_cache; + fs::path m_logs; // special-cased in single-root-folder installations +}; static void InitVfs(const CmdLineArgs& args) { TIMER("InitVfs"); + const char* subdirectory = args.Has("writableRoot")? 0 : "0ad"; + const Paths paths(args.GetArg0(), subdirectory); + + fs::path logs(paths.Logs()); + fs::create_directories(logs); + psSetLogDir(logs.string().c_str()); + const size_t cacheSize = ChooseCacheSize(); g_VFS = CreateVfs(cacheSize); - const fs::path binariesDir(BinariesDir(args.GetArg0())); - g_VFS->Mount("screenshots/", binariesDir/"../data/screenshots"); - g_VFS->Mount("config/", binariesDir/"../data/config"); - g_VFS->Mount("profiles/", binariesDir/"../data/profiles"); - - // rationale: - // - this is in a separate real directory so that it can later be moved - // to $APPDATA to allow running without Admin access. - // - we mount as archivable so that all files will be added to archive. - // even though we write out XMBs here, they will eventually be read, - // so putting them in an archive boosts performance. - g_VFS->Mount("cache/", binariesDir/"../data/cache", VFS_MOUNT_ARCHIVABLE); + g_VFS->Mount("screenshots/", paths.Data()/"screenshots"); + g_VFS->Mount("config/", paths.RData()/"config"); + g_VFS->Mount("profiles/", paths.Config()/"profiles"); + g_VFS->Mount("cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); // (adding XMBs to archive speeds up subsequent reads) std::vector mods = args.GetMultiple("mod"); mods.push_back("public"); if(!args.Has("onlyPublicFiles")) mods.push_back("internal"); + fs::path modArchivePath(paths.Cache()/"mods"); + fs::path modLoosePath(paths.RData()/"mods"); for (size_t i = 0; i < mods.size(); ++i) { - CStr path = "mods/" + mods[i]; size_t priority = i; const int flags = VFS_MOUNT_WATCH|VFS_MOUNT_ARCHIVABLE; - g_VFS->Mount("", (binariesDir/"../data")/path, flags, priority); + g_VFS->Mount("", modLoosePath/mods[i], flags, priority); + g_VFS->Mount("", modArchivePath/mods[i], flags, priority); } // don't try g_VFS->Display yet: SDL_Init hasn't yet redirected stdout @@ -916,7 +1011,7 @@ void Init(const CmdLineArgs& args, int flags) hooks.translate = psTranslate; hooks.translate_free = psTranslateFree; hooks.bundle_logs = psBundleLogs; - hooks.get_log_dir = psGetLogDir; + hooks.get_log_dir = psLogDir; app_hooks_update(&hooks); // Set up the console early, so that debugging diff --git a/source/ps/ProfileViewer.cpp b/source/ps/ProfileViewer.cpp index 7e07d506af..4df93a5c36 100644 --- a/source/ps/ProfileViewer.cpp +++ b/source/ps/ProfileViewer.cpp @@ -428,7 +428,7 @@ void CProfileViewer::SaveToFile() { // Open the file. (It will be closed when the CProfileViewer // destructor is called.) - fs::path path(psLogPath()/"profile.txt"); + fs::path path(fs::path(psLogDir())/"profile.txt"); m->outputStream.open(path.external_file_string().c_str(), std::ofstream::out | std::ofstream::trunc); if (m->outputStream.fail()) diff --git a/source/ps/Pyrogenesis.cpp b/source/ps/Pyrogenesis.cpp index 55c40bc7af..e533a91dfd 100644 --- a/source/ps/Pyrogenesis.cpp +++ b/source/ps/Pyrogenesis.cpp @@ -93,35 +93,25 @@ void psBundleLogs(FILE* f) fwprintf(f, L"SVN Revision: %s\n\n", svn_revision); fwprintf(f, L"System info:\n\n"); - fs::path path1(psLogPath()/"system_info.txt"); + fs::path path1(fs::path(psLogDir())/"system_info.txt"); AppendAsciiFile(f, path1.external_file_string().c_str()); fwprintf(f, L"\n\n====================================\n\n"); fwprintf(f, L"Main log:\n\n"); - fs::path path2(psLogPath()/"mainlog.html"); + fs::path path2(fs::path(psLogDir())/"mainlog.html"); AppendAsciiFile(f, path2.external_file_string().c_str()); fwprintf(f, L"\n\n====================================\n\n"); } -const char* psGetLogDir() +static char logDir[PATH_MAX]; + +void psSetLogDir(const char* path) { - static char N_log_dir[PATH_MAX]; - ONCE(\ - char N_exe_name[PATH_MAX];\ - (void)sys_get_executable_name(N_exe_name, ARRAY_SIZE(N_exe_name));\ - /* strip app name (we only want its path) */\ - path_strip_fn(N_exe_name);\ - (void)path_append(N_log_dir, N_exe_name, "../logs/"); - ); - return N_log_dir; + path_copy(logDir, path); } - -fs::path psLogPath() +const char* psLogDir() { - char exePathname[PATH_MAX]; - (void)sys_get_executable_name(exePathname, ARRAY_SIZE(exePathname)); - path_strip_fn(exePathname); - return fs::path(exePathname)/"../logs/"; + return logDir; } diff --git a/source/ps/Pyrogenesis.h b/source/ps/Pyrogenesis.h index e9a7983eac..6c3b56732e 100644 --- a/source/ps/Pyrogenesis.h +++ b/source/ps/Pyrogenesis.h @@ -42,11 +42,7 @@ extern const wchar_t* psTranslate(const wchar_t* text); extern void psTranslateFree(const wchar_t* text); extern void psBundleLogs(FILE* f); -// (this is used by AppHooks during crash reporting, where it's useful -// not to allocate any memory.) -extern const char* psGetLogDir(); - -// same as psGetLogDir, but more convenient (yet doesn't cache the results). -extern fs::path psLogPath(); +extern void psSetLogDir(const char* path); // set during InitVfs +extern const char* psLogDir(); // used by AppHooks and engine code when reporting errors #endif diff --git a/source/ps/Util.cpp b/source/ps/Util.cpp index ea9869a539..5e7820c4c0 100644 --- a/source/ps/Util.cpp +++ b/source/ps/Util.cpp @@ -71,7 +71,7 @@ void WriteSystemInfo() struct utsname un; uname(&un); - fs::path pathname(psLogPath()/"system_info.txt"); + fs::path pathname(fs::path(psLogDir())/"system_info.txt"); FILE* f = fopen(pathname.external_file_string().c_str(), "w"); if(!f) return;