raze/source/common/searchpaths.cpp
Christoph Oelckers ac87665972 - use std::filesystem for directory traversal.
So far implemented for scanning search paths
2019-10-29 19:53:46 +01:00

546 lines
16 KiB
C++

//-------------------------------------------------------------------------
/*
Copyright (C) 2010-2019 EDuke32 developers and contributors
Copyright (C) 2019 Nuke.YKT
Copyright (C) 2019 Christoph Oelckers
This is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 2
as published by the Free Software Foundation.
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 <filesystem>
#include "i_specialpaths.h"
#include "compat.h"
#include "gameconfigfile.h"
#include "cmdlib.h"
#include "utf8.h"
//
// Search path management
//
namespace fs = std::filesystem;
#ifndef _WIN32
//-------------------------------------------------------------------------
//
//
//
//-------------------------------------------------------------------------
static void G_AddSteamPaths(TArray<FString> &searchpaths, const char *basepath)
{
FString path;
// Duke Nukem 3D: Megaton Edition (Steam)
path.Format("%s/steamapps/common/Duke Nukem 3D/gameroot", basepath);
searchpaths.Push(path);
path.Format("%s/steamapps/common/Duke Nukem 3D/gameroot/addons/dc", basepath);
searchpaths.Push(path);
path.Format("%s/steamapps/common/Duke Nukem 3D/gameroot/addons/nw", basepath);
searchpaths.Push(path);
path.Format("%s/steamapps/common/Duke Nukem 3D/gameroot/addons/vacation", basepath);
searchpaths.Push(path);
// Duke Nukem 3D (3D Realms Anthology (Steam) / Kill-A-Ton Collection 2015)
#ifdef __APPLE__
path.Format("%s/steamapps/common/Duke Nukem 3D/Duke Nukem 3D.app/drive_c/Program Files/Duke Nukem 3D", basepath);
searchpaths.Push(path);
#endif
// NAM (Steam)
#ifdef __APPLE__
path.Format("%s/steamapps/common/Nam/Nam.app/Contents/Resources/Nam.boxer/C.harddisk/NAM", basepath);
#else
path.Format("%s/steamapps/common/Nam/NAM", basepath);
#endif
searchpaths.Push(path);
// WWII GI (Steam)
path.Format("%s/steamapps/common/World War II GI/WW2GI", basepath);
searchpaths.Push(path);
}
//-------------------------------------------------------------------------
//
// A bare-bones "parser" for Valve's KeyValues VDF format.
// There is no guarantee this will function properly with ill-formed files.
//
//-------------------------------------------------------------------------
static void KeyValues_SkipWhitespace(char **vdfbuf, char * const vdfbufend)
{
while (((*vdfbuf)[0] == ' ' || (*vdfbuf)[0] == '\n' || (*vdfbuf)[0] == '\r' || (*vdfbuf)[0] == '\t' || (*vdfbuf)[0] == '\0') && *vdfbuf < vdfbufend)
(*vdfbuf)++;
// comments
if ((*vdfbuf) + 2 < vdfbufend && (*vdfbuf)[0] == '/' && (*vdfbuf)[1] == '/')
{
while ((*vdfbuf)[0] != '\n' && (*vdfbuf)[0] != '\r' && *vdfbuf < vdfbufend)
(*vdfbuf)++;
KeyValues_SkipWhitespace(vdfbuf, vdfbufend);
}
}
static void KeyValues_SkipToEndOfQuotedToken(char **vdfbuf, char * const vdfbufend)
{
(*vdfbuf)++;
while ((*vdfbuf)[0] != '\"' && (*vdfbuf)[-1] != '\\' && *vdfbuf < vdfbufend)
(*vdfbuf)++;
}
static void KeyValues_SkipToEndOfUnquotedToken(char **vdfbuf, char * const vdfbufend)
{
while ((*vdfbuf)[0] != ' ' && (*vdfbuf)[0] != '\n' && (*vdfbuf)[0] != '\r' && (*vdfbuf)[0] != '\t' && (*vdfbuf)[0] != '\0' && *vdfbuf < vdfbufend)
(*vdfbuf)++;
}
static void KeyValues_SkipNextWhatever(char **vdfbuf, char * const vdfbufend)
{
KeyValues_SkipWhitespace(vdfbuf, vdfbufend);
if (*vdfbuf == vdfbufend)
return;
if ((*vdfbuf)[0] == '{')
{
(*vdfbuf)++;
do
{
KeyValues_SkipNextWhatever(vdfbuf, vdfbufend);
}
while ((*vdfbuf)[0] != '}');
(*vdfbuf)++;
}
else if ((*vdfbuf)[0] == '\"')
KeyValues_SkipToEndOfQuotedToken(vdfbuf, vdfbufend);
else if ((*vdfbuf)[0] != '}')
KeyValues_SkipToEndOfUnquotedToken(vdfbuf, vdfbufend);
KeyValues_SkipWhitespace(vdfbuf, vdfbufend);
}
static char* KeyValues_NormalizeToken(char **vdfbuf, char * const vdfbufend)
{
char *token = *vdfbuf;
if ((*vdfbuf)[0] == '\"' && *vdfbuf < vdfbufend)
{
token++;
KeyValues_SkipToEndOfQuotedToken(vdfbuf, vdfbufend);
(*vdfbuf)[0] = '\0';
// account for escape sequences
char *writeseeker = token, *readseeker = token;
while (readseeker <= *vdfbuf)
{
if (readseeker[0] == '\\')
readseeker++;
writeseeker[0] = readseeker[0];
writeseeker++;
readseeker++;
}
return token;
}
KeyValues_SkipToEndOfUnquotedToken(vdfbuf, vdfbufend);
(*vdfbuf)[0] = '\0';
return token;
}
static void KeyValues_FindKey(char **vdfbuf, char * const vdfbufend, const char *token)
{
char *ParentKey = KeyValues_NormalizeToken(vdfbuf, vdfbufend);
if (token != NULL) // pass in NULL to find the next key instead of a specific one
while (Bstrcmp(ParentKey, token) != 0 && *vdfbuf < vdfbufend)
{
KeyValues_SkipNextWhatever(vdfbuf, vdfbufend);
ParentKey = KeyValues_NormalizeToken(vdfbuf, vdfbufend);
}
KeyValues_SkipWhitespace(vdfbuf, vdfbufend);
}
static int32_t KeyValues_FindParentKey(char **vdfbuf, char * const vdfbufend, const char *token)
{
KeyValues_SkipWhitespace(vdfbuf, vdfbufend);
// end of scope
if ((*vdfbuf)[0] == '}')
return 0;
KeyValues_FindKey(vdfbuf, vdfbufend, token);
// ignore the wrong type
while ((*vdfbuf)[0] != '{' && *vdfbuf < vdfbufend)
{
KeyValues_SkipNextWhatever(vdfbuf, vdfbufend);
KeyValues_FindKey(vdfbuf, vdfbufend, token);
}
if (*vdfbuf == vdfbufend)
return 0;
return 1;
}
static char* KeyValues_FindKeyValue(char **vdfbuf, char * const vdfbufend, const char *token)
{
KeyValues_SkipWhitespace(vdfbuf, vdfbufend);
// end of scope
if ((*vdfbuf)[0] == '}')
return NULL;
KeyValues_FindKey(vdfbuf, vdfbufend, token);
// ignore the wrong type
while ((*vdfbuf)[0] == '{' && *vdfbuf < vdfbufend)
{
KeyValues_SkipNextWhatever(vdfbuf, vdfbufend);
KeyValues_FindKey(vdfbuf, vdfbufend, token);
}
KeyValues_SkipWhitespace(vdfbuf, vdfbufend);
if (*vdfbuf == vdfbufend)
return NULL;
return KeyValues_NormalizeToken(vdfbuf, vdfbufend);
}
//-------------------------------------------------------------------------
//
//
//
//-------------------------------------------------------------------------
static void G_ParseSteamKeyValuesForPaths(TArray<FString> &searchpaths, const char *vdf)
{
FileReader fr;
if (fr.Open(vdf))
{
auto data = fr.Read();
if (data.Size() == 0) return;
}
auto vdfvuf = (char*)data.Data();
auto vdfbufend = vdfbuf + data.Size();
if (KeyValues_FindParentKey(&vdfbuf, vdfbufend, "LibraryFolders"))
{
char *result;
vdfbuf++;
while ((result = KeyValues_FindKeyValue(&vdfbuf, vdfbufend, NULL)) != NULL)
G_AddSteamPaths(searchpaths, result);
}
}
#endif
#if defined (__FreeBSD__) || defined(__OpenBSD__) || defined (__linux__)
//-------------------------------------------------------------------------
//
//
//
//-------------------------------------------------------------------------
void G_AddExternalSearchPaths(TArray<FString> &searchpaths)
{
FString path;
char *homepath = Bgethomedir();
path.Format("%s/.steam/steam", homepath);
G_AddSteamPaths(searchpaths, buf);
path.Format("%s/.steam/steam/steamapps/libraryfolders.vdf", homepath);
G_ParseSteamKeyValuesForPaths(searchpaths, buf);
}
#elif defined __APPLE__
//-------------------------------------------------------------------------
//
//
//
//-------------------------------------------------------------------------
void G_AddExternalSearchPaths(TArray<FString> &searchpaths)
{
char *applications[] = { osx_getapplicationsdir(0), osx_getapplicationsdir(1) };
char *support[] = { osx_getsupportdir(0), osx_getsupportdir(1) };
FString path;
char buf[BMAX_PATH];
int32_t i;
for (i = 0; i < 2; i++)
{
path.Format("%s/Steam", support[i]);
G_AddSteamPaths(searchpaths, buf);
path.Format("%s/Steam/steamapps/libraryfolders.vdf", support[i]);
G_ParseSteamKeyValuesForPaths(searchpaths, buf);
// Duke Nukem 3D: Atomic Edition (GOG.com)
path.Format("%s/Duke Nukem 3D.app/Contents/Resources/Duke Nukem 3D.boxer/C.harddisk", applications[i]);
searchpaths.Push(path);
}
for (i = 0; i < 2; i++)
{
Xfree(applications[i]);
Xfree(support[i]);
}
}
#elif defined (_WIN32)
//-------------------------------------------------------------------------
//
//
//
//-------------------------------------------------------------------------
void G_AddExternalSearchPaths(TArray<FString> &searchpaths)
{
char buf[BMAX_PATH] = {0};
DWORD bufsize;
// Duke Nukem 3D: 20th Anniversary World Tour (Steam)
bufsize = sizeof(buf);
if (ReadRegistryValue(R"(SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 434050)", "InstallLocation", buf, &bufsize))
{
searchpaths.Push(buf);
}
// Duke Nukem 3D: Megaton Edition (Steam)
bufsize = sizeof(buf);
if (ReadRegistryValue(R"(SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 225140)", "InstallLocation", buf, &bufsize))
{
char * const suffix = buf + bufsize - 1;
size_t const remaining = sizeof(buf) - bufsize;
strncpy(suffix, "/gameroot", remaining);
searchpaths.Push(buf);
strncpy(suffix, "/gameroot/addons/dc", remaining);
searchpaths.Push(buf);
strncpy(suffix, "/gameroot/addons/nw", remaining);
searchpaths.Push(buf);
strncpy(suffix, "/gameroot/addons/vacation", remaining);
searchpaths.Push(buf);
}
// Duke Nukem 3D (3D Realms Anthology (Steam) / Kill-A-Ton Collection 2015)
bufsize = sizeof(buf);
if (ReadRegistryValue(R"(SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 359850)", "InstallLocation", buf, &bufsize))
{
char * const suffix = buf + bufsize - 1;
size_t const remaining = sizeof(buf) - bufsize;
strncpy(suffix, "/Duke Nukem 3D", remaining);
searchpaths.Push(buf);
}
// Duke Nukem 3D: Atomic Edition (GOG.com)
bufsize = sizeof(buf);
if (ReadRegistryValue("SOFTWARE\\GOG.com\\GOGDUKE3D", "PATH", buf, &bufsize))
{
searchpaths.Push(buf);
}
// Duke Nukem 3D (3D Realms Anthology)
bufsize = sizeof(buf);
if (ReadRegistryValue("SOFTWARE\\3DRealms\\Duke Nukem 3D", NULL, buf, &bufsize))
{
char * const suffix = buf + bufsize - 1;
size_t const remaining = sizeof(buf) - bufsize;
strncpy(suffix, "/Duke Nukem 3D", remaining);
searchpaths.Push(buf);
}
// 3D Realms Anthology
bufsize = sizeof(buf);
if (ReadRegistryValue("SOFTWARE\\3DRealms\\Anthology", NULL, buf, &bufsize))
{
char * const suffix = buf + bufsize - 1;
size_t const remaining = sizeof(buf) - bufsize;
Bstrncpy(suffix, "/Duke Nukem 3D", remaining);
searchpaths.Push(buf);
}
// NAM (Steam)
bufsize = sizeof(buf);
if (ReadRegistryValue(R"(SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 329650)", "InstallLocation", buf, &bufsize))
{
char * const suffix = buf + bufsize - 1;
size_t const remaining = sizeof(buf) - bufsize;
Bstrncpy(suffix, "/NAM", remaining);
searchpaths.Push(buf);
}
// WWII GI (Steam)
bufsize = sizeof(buf);
if (ReadRegistryValue(R"(SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 376750)", "InstallLocation", buf, &bufsize))
{
char * const suffix = buf + bufsize - 1;
size_t const remaining = sizeof(buf) - bufsize;
Bstrncpy(suffix, "/WW2GI", remaining);
searchpaths.Push(buf);
}
// Redneck Rampage (GOG.com)
bufsize = sizeof(buf);
if (ReadRegistryValue("SOFTWARE\\GOG.com\\GOGREDNECKRAMPAGE", "PATH", buf, &bufsize))
{
searchpaths.Push(buf);
}
// Redneck Rampage Rides Again (GOG.com)
bufsize = sizeof(buf);
if (ReadRegistryValue("SOFTWARE\\GOG.com\\GOGCREDNECKRIDESAGAIN", "PATH", buf, &bufsize))
{
searchpaths.Push(buf);
}
// Blood: One Unit Whole Blood (Steam)
bufsize = sizeof(buf);
if (ReadRegistryValue(R"(SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 299030)", "InstallLocation", buf, &bufsize))
{
searchpaths.Push(buf);
}
// Blood: One Unit Whole Blood (GOG.com)
bufsize = sizeof(buf);
if (ReadRegistryValue("SOFTWARE\\GOG.com\\GOGONEUNITONEBLOOD", "PATH", buf, &bufsize))
{
searchpaths.Push(buf);
}
// Blood: Fresh Supply (Steam)
bufsize = sizeof(buf);
if (ReadRegistryValue(R"(SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 1010750)", "InstallLocation", buf, &bufsize))
{
searchpaths.Push(buf);
strncat(buf, R"(\addons\Cryptic Passage)", 23);
searchpaths.Push(buf);
}
// Blood: Fresh Supply (GOG.com)
bufsize = sizeof(buf);
if (ReadRegistryValue(R"(SOFTWARE\Wow6432Node\GOG.com\Games\1374469660)", "path", buf, &bufsize))
{
searchpaths.Push(buf);
strncat(buf, R"(\addons\Cryptic Passage)", 23);
searchpaths.Push(buf);
}
}
#endif
//==========================================================================
//
// Windows version
//
//==========================================================================
void CollectSubdirectories(TArray<FString> &searchpath, const char *dirmatch)
{
try
{
FString dirpath = MakeUTF8(dirmatch); // convert into clean UTF-8
dirpath.Truncate(dirpath.Len() - 1); // remove the '*'
fs::path path = fs::u8path(dirpath.GetChars());
if (fs::exists(path) && fs::is_directory(path))
{
for (const auto& entry : fs::directory_iterator(path))
{
if (fs::is_directory(entry.status()))
{
auto filename = entry.path().filename().u8string();
FString newdir = dirpath + filename.c_str();
searchpath.Push(newdir);
}
}
}
}
catch (fs::filesystem_error &)
{
// Just ignore this path if it caused an error.
}
}
//==========================================================================
//
// CollectSearchPaths
//
// collect all paths in a local array for easier management
//
//==========================================================================
TArray<FString> CollectSearchPaths()
{
TArray<FString> searchpaths;
if (GameConfig->SetSection("GameSearch.Directories"))
{
const char *key;
const char *value;
while (GameConfig->NextInSection(key, value))
{
if (stricmp(key, "Path") == 0)
{
FString nice = NicePath(value);
if (nice.Len() > 0)
{
#ifdef _WIN32
if (isalpha(nice[0] && nice[1] == ':' && nice[2] != '/')) continue; // ignore drive relative paths because they are meaningless.
#endif
// A path ending with "/*" means to add all subdirectories.
if (nice[nice.Len()-2] == '/' && nice[nice.Len()-1] == '*')
{
CollectSubdirectories(searchpaths, nice);
}
// Checking Steam via a list entry allows easy removal if not wanted.
else if (nice.CompareNoCase("$STEAM") == 0)
{
G_AddExternalSearchPaths(searchpaths);
}
else
{
searchpaths.Push(nice);
}
}
}
}
}
// Unify and remove trailing slashes
for (auto &str : searchpaths)
{
str.Substitute("\\", "/");
str.Substitute("//", "/"); // Double slashes can happen when constructing paths so just get rid of them here.
if (str.Back() == '/') str.Truncate(str.Len() - 1);
}
return searchpaths;
}