reworked userdir path selection, now has 4 "tiers":
authorhavoc <havoc@d7cf8633-e32d-0410-b094-e92efae38249>
Sun, 1 May 2011 16:33:10 +0000 (16:33 +0000)
committerhavoc <havoc@d7cf8633-e32d-0410-b094-e92efae38249>
Sun, 1 May 2011 16:33:10 +0000 (16:33 +0000)
NOHOME - uses basedir for configs
HOME - uses basedir on Windows, ~/.gamename/ on OSX and Linux
MYGAMES - uses My Games on Windows, ~/.gamename/ on Linux and OSX
SAVEDGAMES - uses Saved Games/gamename/ on Vista+, ~/.gamename/ on Linux
and ~/Library/Application Support/gamename/ on OSX
The tiers are tried in reverse order and any existing one will be used,
new ones will always be the last one.
If gamedirname1 is "id1" then Windows will use NOHOME rather than create
a new userdir when none are found. (but you can still create one or use
-mygames or -savedgames commandline options to make it do so)

git-svn-id: svn://svn.icculus.org/twilight/trunk/darkplaces@11085 d7cf8633-e32d-0410-b094-e92efae38249

common.h
fs.c

index ecd8b85..e207c1a 100644 (file)
--- a/common.h
+++ b/common.h
@@ -251,6 +251,16 @@ extern int dpvsnprintf (char *buffer, size_t buffersize, const char *format, va_
 extern struct cvar_s   registered;
 extern struct cvar_s   cmdline;
 
+typedef enum userdirmode_e
+{
+       USERDIRMODE_NOHOME, // basedir only
+       USERDIRMODE_HOME, // Windows basedir, general POSIX (~/.)
+       USERDIRMODE_MYGAMES, // pre-Vista (My Documents/My Games/), general POSIX (~/.)
+       USERDIRMODE_SAVEDGAMES, // Vista (%USERPROFILE%/Saved Games/), OSX (~/Library/Application Support/), Linux (~/.config)
+       USERDIRMODE_COUNT
+}
+userdirmode_t;
+
 typedef enum gamemode_e
 {
        GAME_NORMAL,
diff --git a/fs.c b/fs.c
index b26dd4f..d2fbb52 100644 (file)
--- a/fs.c
+++ b/fs.c
@@ -60,6 +60,7 @@
 
 // largefile support for Win32
 #ifdef WIN32
+#undef lseek
 # define lseek _lseeki64
 #endif
 
@@ -411,7 +412,46 @@ static dllfunction_t shfolderfuncs[] =
        {"SHGetFolderPathA", (void **) &qSHGetFolderPath},
        {NULL, NULL}
 };
+static const char* shfolderdllnames [] =
+{
+       "shfolder.dll",  // IE 4, or Win NT and higher
+       NULL
+};
 static dllhandle_t shfolder_dll = NULL;
+
+const GUID qFOLDERID_SavedGames = {0x4C5C32FF, 0xBB9D, 0x43b0, {0xB5, 0xB4, 0x2D, 0x72, 0xE5, 0x4E, 0xAA, 0xA4}}; 
+#define qREFKNOWNFOLDERID GUID
+#define qKF_FLAG_CREATE 0x8000
+#define qKF_FLAG_NO_ALIAS 0x1000
+static HRESULT (WINAPI *qSHGetKnownFolderPath) (qREFKNOWNFOLDERID rfid, DWORD dwFlags, HANDLE hToken, PWSTR *ppszPath);
+static dllfunction_t shell32funcs[] =
+{
+       {"SHGetKnownFolderPathA", (void **) &qSHGetKnownFolderPath},
+       {NULL, NULL}
+};
+static const char* shell32dllnames [] =
+{
+       "shell32.dll",  // Vista and higher
+       NULL
+};
+static dllhandle_t shell32_dll = NULL;
+
+static HRESULT (WINAPI *qCoInitializeEx)(LPVOID pvReserved, DWORD dwCoInit);
+static void (WINAPI *qCoUninitialize)(void);
+static void (WINAPI *qCoTaskMemFree)(LPVOID pv);
+static dllfunction_t ole32funcs[] =
+{
+       {"CoInitializeEx", (void **) &qCoInitializeEx},
+       {"CoUninitialize", (void **) &qCoUninitialize},
+       {"CoTaskMemFree", (void **) &qCoTaskMemFree},
+       {NULL, NULL}
+};
+static const char* ole32dllnames [] =
+{
+       "ole32.dll", // 2000 and higher
+       NULL
+};
+static dllhandle_t ole32_dll = NULL;
 #endif
 
 /*
@@ -1692,138 +1732,160 @@ void FS_Init_SelfPack (void)
        }
 }
 
-/*
-================
-FS_Init
-================
-*/
-void FS_Init (void)
+int FS_ChooseUserDir(userdirmode_t userdirmode, char *userdir, size_t userdirsize)
 {
-       const char *p;
-       int i;
-#ifdef WIN32
-       TCHAR mydocsdir[MAX_PATH + 1];
+#if defined(__IPHONEOS__)
+       if (userdirmode == USERDIRMODE_HOME)
+       {
+               // fs_basedir is "" by default, to utilize this you can simply add your gamedir to the Resources in xcode
+               // fs_userdir stores configurations to the Documents folder of the app
+               strlcpy(userdir, maxlength, "../Documents/");
+               return 1;
+       }
+       return -1;
+
+#elif defined(WIN32)
+       char *homedir;
 #if _MSC_VER >= 1400
        size_t homedirlen;
 #endif
-#endif
-#ifndef __IPHONEOS__
-       char *homedir;
-#endif
-
-#ifdef WIN32
-       const char* dllnames [] =
-       {
-               "shfolder.dll",  // IE 4, or Win NT and higher
-               NULL
-       };
-       Sys_LoadLibrary(dllnames, &shfolder_dll, shfolderfuncs);
-       // don't care for the result; if it fails, %USERPROFILE% will be used instead
-#endif
-
-       *fs_basedir = 0;
-       *fs_userdir = 0;
-       *fs_gamedir = 0;
-
-#ifdef __IPHONEOS__
-       // fs_basedir is "" by default, to utilize this you can simply add your gamedir to the Resources in xcode
-       // fs_userdir stores configurations to the Documents folder of the app
-       strlcpy(fs_userdir, "../Documents/", sizeof(fs_userdir));
-#else
-       // Add the personal game directory
-       if((i = COM_CheckParm("-userdir")) && i < com_argc - 1)
-       {
-               dpsnprintf(fs_userdir, sizeof(fs_userdir), "%s/", com_argv[i+1]);
-       }
-       else if(COM_CheckParm("-nohome"))
-       {
-               *fs_userdir = 0;
-       }
-       else
+       TCHAR mydocsdir[MAX_PATH + 1];
+       wchar_t *savedgamesdirw;
+       char savedgamesdir[MAX_OSPATH];
+       int fd;
+       
+       userdir[0] = 0;
+       switch(userdirmode)
        {
-#ifdef WIN32
-               if(qSHGetFolderPath && (qSHGetFolderPath(NULL, CSIDL_PERSONAL, NULL, 0, mydocsdir) == S_OK))
+       default:
+               return -1;
+       case USERDIRMODE_NOHOME:
+               strlcpy(userdir, fs_basedir, userdirsize);
+               break;
+       case USERDIRMODE_MYGAMES:
+               if (!shfolder_dll)
+                       Sys_LoadLibrary(shfolderdllnames, &shfolder_dll, shfolderfuncs);
+               mydocsdir[0] = 0;
+               if (qSHGetFolderPath && qSHGetFolderPath(NULL, CSIDL_PERSONAL, NULL, 0, mydocsdir) == S_OK)
                {
-                       dpsnprintf(fs_userdir, sizeof(fs_userdir), "%s/My Games/%s/", mydocsdir, gameuserdirname);
-                       Con_DPrintf("Obtained personal directory %s from SHGetFolderPath\n", fs_userdir);
+                       dpsnprintf(userdir, userdirsize, "%s/My Games/%s/", mydocsdir, gameuserdirname);
+                       break;
                }
-               else
-               {
-                       // use the environment
 #if _MSC_VER >= 1400
-                       _dupenv_s (&homedir, &homedirlen, "USERPROFILE");
+               _dupenv_s(&homedir, &homedirlen, "USERPROFILE");
+               if(homedir)
+               {
+                       dpsnprintf(userdir, userdirsize, "%s/.%s/", homedir, gameuserdirname);
+                       free(homedir);
+                       break;
+               }
 #else
-                       homedir = getenv("USERPROFILE");
+               homedir = getenv("USERPROFILE");
+               if(homedir)
+               {
+                       dpsnprintf(userdir, userdirsize, "%s/.%s/", homedir, gameuserdirname);
+                       break;
+               }
 #endif
-
-                       if(homedir)
+               return -1;
+       case USERDIRMODE_SAVEDGAMES:
+               if (!shell32_dll)
+                       Sys_LoadLibrary(shell32dllnames, &shell32_dll, shell32funcs);
+               if (!ole32_dll)
+                       Sys_LoadLibrary(ole32dllnames, &ole32_dll, ole32funcs);
+               if (qSHGetKnownFolderPath && qCoInitializeEx && qCoTaskMemFree && qCoUninitialize)
+               {
+                       savedgamesdir[0] = 0;
+                       qCoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
+                       if (qSHGetKnownFolderPath(qFOLDERID_SavedGames, qKF_FLAG_CREATE | qKF_FLAG_NO_ALIAS, NULL, &savedgamesdirw) == S_OK)
                        {
-                               dpsnprintf(fs_userdir, sizeof(fs_userdir), "%s/My Documents/My Games/%s/", homedir, gameuserdirname);
+                               memset(savedgamesdir, 0, sizeof(savedgamesdir));
 #if _MSC_VER >= 1400
-                               free(homedir);
+                               wcstombs_s(NULL, savedgamesdir, sizeof(savedgamesdir), savedgamesdirw, sizeof(savedgamesdir)-1);
+#else
+                               wcstombs(savedgamesdir, savedgamesdirw, sizeof(savedgamesdir)-1);
 #endif
-                               Con_DPrintf("Obtained personal directory %s from environment\n", fs_userdir);
+                               qCoTaskMemFree(savedgamesdirw);
+                       }
+                       qCoUninitialize();
+                       if (savedgamesdir[0])
+                       {
+                               dpsnprintf(userdir, userdirsize, "%s/%s/", savedgamesdir, gameuserdirname);
+                               break;
                        }
                }
-
-               if(!*fs_userdir)
-                       Con_DPrintf("Could not obtain home directory; not supporting -mygames\n");
+               return -1;
+       }
 #else
-               homedir = getenv ("HOME");
+       int fd;
+       char *homedir;
+       userdir[0] = 0;
+       switch(userdirmode)
+       {
+       default:
+               return -1;
+       case USERDIRMODE_NOHOME:
+               strlcpy(userdir, fs_basedir, userdirsize);
+               break;
+       case USERDIRMODE_HOME:
+               homedir = getenv("HOME");
                if(homedir)
                {
-#ifdef __APPLE__
-                       dpsnprintf(fs_userdir, sizeof(fs_userdir), "%s/Library/Application Support/%s/", homedir, gameuserdirname);
+                       dpsnprintf(userdir, userdirsize, "%s/.%s/", homedir, gameuserdirname);
+                       break;
+               }
+               return -1;
+       case USERDIRMODE_SAVEDGAMES:
+               homedir = getenv("HOME");
+               if(homedir)
+               {
+#ifdef MACOSX
+                       dpsnprintf(userdir, userdirsize, "%s/Library/Application Support/%s/", homedir, gameuserdirname);
 #else
-                       dpsnprintf(fs_userdir, sizeof(fs_userdir), "%s/.%s/", homedir, gameuserdirname);
+                       // the XDG say some files would need to go in:
+                       // XDG_CONFIG_HOME (or ~/.config/%s/)
+                       // XDG_DATA_HOME (or ~/.local/share/%s/)
+                       // XDG_CACHE_HOME (or ~/.cache/%s/)
+                       // and also search the following global locations if defined:
+                       // XDG_CONFIG_DIRS (normally /etc/xdg/%s/)
+                       // XDG_DATA_DIRS (normally /usr/share/%s/)
+                       // this would be too complicated...
+                       return -1;
 #endif
+                       break;
                }
-
-               if(!*fs_userdir)
-                       Con_DPrintf("Could not obtain home directory; assuming -nohome\n");
+               return -1;
+       }
 #endif
 
-#ifdef WIN32
-               if(!COM_CheckParm("-mygames"))
-               {
+       // see if we can write to this path (note: won't create path)
 #if _MSC_VER >= 1400
-                       int fd;
-                       _sopen_s(&fd, va("%s%s/config.cfg", fs_basedir, gamedirname1), O_WRONLY | O_CREAT, _SH_DENYNO, _S_IREAD | _S_IWRITE); // note: no O_TRUNC here!
+       _sopen_s(&fd, va("%s%s/config.cfg", userdir, gamedirname1), O_WRONLY | O_CREAT, _SH_DENYNO, _S_IREAD | _S_IWRITE); // note: no O_TRUNC here!
 #else
-                       int fd = open (va("%s%s/config.cfg", fs_basedir, gamedirname1), O_WRONLY | O_CREAT, 0666); // note: no O_TRUNC here!
-#endif
-                       if(fd >= 0)
-                       {
-                               close(fd);
-                               *fs_userdir = 0; // we have write access to the game dir, so let's use it
-                       }
-               }
+       fd = open (va("%s%s/config.cfg", userdir, gamedirname1), O_WRONLY | O_CREAT, 0666); // note: no O_TRUNC here!
 #endif
+       if(fd >= 0)
+       {
+               close(fd);
+               return 1; // good choice - the path exists and is writable
        }
+       else
+               return 0; // probably good - failed to write but maybe we need to create path
+}
 
-       strlcpy(fs_gamedir, "", sizeof(fs_gamedir));
+/*
+================
+FS_Init
+================
+*/
+void FS_Init (void)
+{
+       const char *p;
+       int i;
 
-// If the base directory is explicitly defined by the compilation process
-#ifdef DP_FS_BASEDIR
-       strlcpy(fs_basedir, DP_FS_BASEDIR, sizeof(fs_basedir));
-#else
        *fs_basedir = 0;
-
-#ifdef MACOSX
-       // FIXME: is there a better way to find the directory outside the .app?
-       if (strstr(com_argv[0], ".app/"))
-       {
-               char *split;
-
-               split = strstr(com_argv[0], ".app/");
-               while (split > com_argv[0] && *split != '/')
-                       split--;
-               strlcpy(fs_basedir, com_argv[0], sizeof(fs_basedir));
-               fs_basedir[split - com_argv[0]] = 0;
-       }
-#endif
-#endif
-#endif
+       *fs_userdir = 0;
+       *fs_gamedir = 0;
 
        // -basedir <path>
        // Overrides the system supplied base directory (under GAMENAME)
@@ -1836,11 +1898,88 @@ void FS_Init (void)
                if (i > 0 && (fs_basedir[i-1] == '\\' || fs_basedir[i-1] == '/'))
                        fs_basedir[i-1] = 0;
        }
+       else
+       {
+// If the base directory is explicitly defined by the compilation process
+#ifdef DP_FS_BASEDIR
+               strlcpy(fs_basedir, DP_FS_BASEDIR, sizeof(fs_basedir));
+#elif defined(MACOSX)
+               // FIXME: is there a better way to find the directory outside the .app?
+               // FIXME: check if game data is inside .app bundle
+               if (strstr(com_argv[0], ".app/"))
+               {
+                       char *split;
+                       strlcpy(fs_basedir, com_argv[0], sizeof(fs_basedir));
+                       split = strstr(fs_basedir, ".app/");
+                       if (split)
+                       {
+                               while (split > fs_basedir && *split != '/')
+                                       split--;
+                               *split = 0;
+                       }
+               }
+#endif
+       }
 
+       // make sure the appending of a path separator won't create an unterminated string
+       memset(fs_basedir + sizeof(fs_basedir) - 2, 0, 2);
        // add a path separator to the end of the basedir if it lacks one
        if (fs_basedir[0] && fs_basedir[strlen(fs_basedir) - 1] != '/' && fs_basedir[strlen(fs_basedir) - 1] != '\\')
                strlcat(fs_basedir, "/", sizeof(fs_basedir));
 
+       // Add the personal game directory
+       if((i = COM_CheckParm("-userdir")) && i < com_argc - 1)
+               dpsnprintf(fs_userdir, sizeof(fs_userdir), "%s/", com_argv[i+1]);
+       else if (COM_CheckParm("-nohome"))
+               *fs_userdir = 0; // user wants roaming installation, no userdir
+       else
+       {
+               int dirmode;
+               int highestuserdirmode = USERDIRMODE_COUNT - 1;
+               int preferreduserdirmode = USERDIRMODE_COUNT - 1;
+               int userdirstatus[USERDIRMODE_COUNT];
+#ifdef WIN32
+               // historical behavior...
+               if (!strcmp(gamedirname1, "id1"))
+                       preferreduserdirmode = USERDIRMODE_NOHOME;
+#endif
+               // check what limitations the user wants to impose
+               if (COM_CheckParm("-home")) preferreduserdirmode = USERDIRMODE_HOME;
+               if (COM_CheckParm("-mygames")) preferreduserdirmode = USERDIRMODE_MYGAMES;
+               if (COM_CheckParm("-savedgames")) preferreduserdirmode = USERDIRMODE_SAVEDGAMES;
+               // gather the status of the possible userdirs
+               for (dirmode = 0;dirmode < USERDIRMODE_COUNT;dirmode++)
+               {
+                       userdirstatus[dirmode] = FS_ChooseUserDir((userdirmode_t)dirmode, fs_userdir, sizeof(fs_userdir));
+                       if (userdirstatus[dirmode] == 1)
+                               Con_DPrintf("userdir %i = %s (writable)\n", dirmode, fs_userdir);
+                       else if (userdirstatus[dirmode] == 0)
+                               Con_DPrintf("userdir %i = %s (not writable or does not exist)\n", dirmode, fs_userdir);
+                       else
+                               Con_DPrintf("userdir %i (not applicable)\n", dirmode);
+               }
+               // some games may prefer writing to basedir, but if write fails we
+               // have to search for a real userdir...
+               if (preferreduserdirmode == 0 && userdirstatus[0] < 1)
+                       preferreduserdirmode = highestuserdirmode;
+               // check for an existing userdir and continue using it if possible...
+               for (dirmode = USERDIRMODE_COUNT - 1;dirmode > 0;dirmode--)
+                       if (userdirstatus[dirmode] == 1)
+                               break;
+               // if no existing userdir found, make a new one...
+               if (dirmode == 0 && preferreduserdirmode > 0)
+                       for (dirmode = preferreduserdirmode;dirmode > 0;dirmode--)
+                               if (userdirstatus[dirmode] >= 0)
+                                       break;
+               // and finally, we picked one...
+               FS_ChooseUserDir((userdirmode_t)dirmode, fs_userdir, sizeof(fs_userdir));
+               Con_DPrintf("userdir %i is the winner\n", dirmode);
+       }
+
+       // if userdir equal to basedir, clear it to avoid confusion later
+       if (!strcmp(fs_basedir, fs_userdir))
+               fs_userdir[0] = 0;
+
        FS_ListGameDirs();
 
        p = FS_CheckGameDir(gamedirname1);
@@ -1908,6 +2047,8 @@ void FS_Shutdown (void)
 
 #ifdef WIN32
        Sys_UnloadLibrary (&shfolder_dll);
+       Sys_UnloadLibrary (&shell32_dll);
+       Sys_UnloadLibrary (&ole32_dll);
 #endif
 }