llrender and lldir merge. Removed duplicate assets from skins. cleaned up skin textures.xml files to only include changes from default.
This commit is contained in:
@@ -71,8 +71,15 @@ LLDir_Linux gDirUtil;
|
||||
|
||||
LLDir *gDirUtilp = (LLDir *)&gDirUtil;
|
||||
|
||||
/// Values for findSkinnedFilenames(subdir) parameter
|
||||
const char
|
||||
*LLDir::XUI = "xui",
|
||||
*LLDir::TEXTURES = "textures",
|
||||
*LLDir::SKINBASE = "";
|
||||
|
||||
static const char* const empty = "";
|
||||
std::string LLDir::sDumpDir = "";
|
||||
|
||||
LLDir::LLDir()
|
||||
: mAppName(""),
|
||||
mExecutablePathAndName(""),
|
||||
@@ -85,7 +92,9 @@ LLDir::LLDir()
|
||||
mOSCacheDir(""),
|
||||
mCAFile(""),
|
||||
mTempDir(""),
|
||||
mDirDelimiter("/") // fallback to forward slash if not overridden
|
||||
mDirDelimiter("/"), // fallback to forward slash if not overridden
|
||||
mLanguage("en"),
|
||||
mUserName("undefined")
|
||||
{
|
||||
}
|
||||
|
||||
@@ -97,7 +106,11 @@ std::vector<std::string> LLDir::getFilesInDir(const std::string &dirname)
|
||||
{
|
||||
//Returns a vector of fullpath filenames.
|
||||
|
||||
#if LL_WINDOWS
|
||||
boost::filesystem::path p (utf8str_to_utf16str(dirname).c_str());
|
||||
#else
|
||||
boost::filesystem::path p (dirname);
|
||||
#endif
|
||||
std::vector<std::string> v;
|
||||
|
||||
if (exists(p))
|
||||
@@ -188,19 +201,33 @@ S32 LLDir::deleteFilesInDir(const std::string &dirname, const std::string &mask)
|
||||
U32 LLDir::deleteDirAndContents(const std::string& dir_name)
|
||||
{
|
||||
//Removes the directory and its contents. Returns number of files removed.
|
||||
// Singu Note: boost::filesystem throws exceptions
|
||||
S32 res = 0;
|
||||
|
||||
U32 num_deleted = 0;
|
||||
|
||||
try
|
||||
try
|
||||
{
|
||||
res = boost::filesystem::remove_all(dir_name);
|
||||
#if LL_WINDOWS
|
||||
boost::filesystem::path dir_path(utf8str_to_utf16str(dir_name).c_str());
|
||||
#else
|
||||
boost::filesystem::path dir_path(dir_name);
|
||||
#endif
|
||||
if (boost::filesystem::exists (dir_path))
|
||||
{
|
||||
if (!boost::filesystem::is_empty (dir_path))
|
||||
{ // Directory has content
|
||||
num_deleted = boost::filesystem::remove_all (dir_path);
|
||||
}
|
||||
else
|
||||
{ // Directory is empty
|
||||
boost::filesystem::remove (dir_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(const boost::filesystem::filesystem_error& e)
|
||||
{
|
||||
LL_WARNS() << "boost::filesystem::remove_all(\"" + dir_name + "\") failed: '" + e.code().message() + "'" << LL_ENDL;
|
||||
}
|
||||
|
||||
return res;
|
||||
catch (boost::filesystem::filesystem_error &er)
|
||||
{
|
||||
LL_WARNS() << "Failed to delete " << dir_name << " with error " << er.code().message() << LL_ENDL;
|
||||
}
|
||||
return num_deleted;
|
||||
}
|
||||
|
||||
const std::string LLDir::findFile(const std::string &filename,
|
||||
@@ -346,6 +373,12 @@ const std::string LLDir::getCacheDir(bool get_default) const
|
||||
}
|
||||
}
|
||||
|
||||
#if (defined(_WIN64) || defined(__amd64__) || defined(__x86_64__))
|
||||
#define OS_CACHE_DIR "SingularityViewer64"
|
||||
#else
|
||||
#define OS_CACHE_DIR "Obsidian"
|
||||
#endif
|
||||
|
||||
// Return the default cache directory
|
||||
std::string LLDir::buildSLOSCacheDir() const
|
||||
{
|
||||
@@ -363,11 +396,7 @@ std::string LLDir::buildSLOSCacheDir() const
|
||||
}
|
||||
else
|
||||
{
|
||||
#if defined(_WIN64)
|
||||
res = add(getOSCacheDir(), "SingularityViewer64");
|
||||
#else
|
||||
res = add(getOSCacheDir(), "SingularityViewer");
|
||||
#endif
|
||||
res = add(getOSCacheDir(), OS_CACHE_DIR);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -390,20 +419,26 @@ const std::string &LLDir::getDirDelimiter() const
|
||||
return mDirDelimiter;
|
||||
}
|
||||
|
||||
const std::string& LLDir::getDefaultSkinDir() const
|
||||
{
|
||||
return mDefaultSkinDir;
|
||||
}
|
||||
|
||||
const std::string &LLDir::getSkinDir() const
|
||||
{
|
||||
return mSkinDir;
|
||||
}
|
||||
|
||||
const std::string& LLDir::getUserDefaultSkinDir() const
|
||||
{
|
||||
return mUserDefaultSkinDir;
|
||||
}
|
||||
|
||||
const std::string &LLDir::getUserSkinDir() const
|
||||
{
|
||||
return mUserSkinDir;
|
||||
}
|
||||
|
||||
const std::string& LLDir::getDefaultSkinDir() const
|
||||
{
|
||||
return mDefaultSkinDir;
|
||||
}
|
||||
|
||||
const std::string LLDir::getSkinBaseDir() const
|
||||
{
|
||||
@@ -415,6 +450,11 @@ const std::string &LLDir::getLLPluginDir() const
|
||||
return mLLPluginDir;
|
||||
}
|
||||
|
||||
const std::string &LLDir::getUserName() const
|
||||
{
|
||||
return mUserName;
|
||||
}
|
||||
|
||||
static std::string ELLPathToString(ELLPath location)
|
||||
{
|
||||
typedef std::map<ELLPath, const char*> ELLPathMap;
|
||||
@@ -602,7 +642,7 @@ std::string LLDir::getBaseFileName(const std::string& filepath, bool strip_exten
|
||||
std::string LLDir::getDirName(const std::string& filepath) const
|
||||
{
|
||||
std::size_t offset = filepath.find_last_of(getDirDelimiter());
|
||||
S32 len = (offset == std::string::npos) ? 0 : offset;
|
||||
size_t len = (offset == std::string::npos) ? 0 : offset;
|
||||
std::string dirname = filepath.substr(0, len);
|
||||
return dirname;
|
||||
}
|
||||
@@ -618,31 +658,207 @@ std::string LLDir::getExtension(const std::string& filepath) const
|
||||
return exten;
|
||||
}
|
||||
|
||||
std::string LLDir::findSkinnedFilename(const std::string &filename) const
|
||||
std::string LLDir::findSkinnedFilenameBaseLang(const std::string &subdir,
|
||||
const std::string &filename,
|
||||
ESkinConstraint constraint) const
|
||||
{
|
||||
return findSkinnedFilename("", "", filename);
|
||||
// This implementation is basically just as described in the declaration comments.
|
||||
std::vector<std::string> found(findSkinnedFilenames(subdir, filename, constraint));
|
||||
if (found.empty())
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return found.front();
|
||||
}
|
||||
|
||||
std::string LLDir::findSkinnedFilename(const std::string &subdir, const std::string &filename) const
|
||||
std::string LLDir::findSkinnedFilename(const std::string &subdir,
|
||||
const std::string &filename,
|
||||
ESkinConstraint constraint) const
|
||||
{
|
||||
return findSkinnedFilename("", subdir, filename);
|
||||
// This implementation is basically just as described in the declaration comments.
|
||||
std::vector<std::string> found(findSkinnedFilenames(subdir, filename, constraint));
|
||||
if (found.empty())
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return found.back();
|
||||
}
|
||||
|
||||
std::string LLDir::findSkinnedFilename(const std::string &subdir1, const std::string &subdir2, const std::string &filename) const
|
||||
// This method exists because the two code paths for
|
||||
// findSkinnedFilenames(ALL_SKINS) and findSkinnedFilenames(CURRENT_SKIN) must
|
||||
// generate the list of candidate pathnames in identical ways. The only
|
||||
// difference is in the body of the inner loop.
|
||||
template <typename FUNCTION>
|
||||
void LLDir::walkSearchSkinDirs(const std::string& subdir,
|
||||
const std::vector<std::string>& subsubdirs,
|
||||
const std::string& filename,
|
||||
const FUNCTION& function) const
|
||||
{
|
||||
// generate subdirectory path fragment, e.g. "/foo/bar", "/foo", ""
|
||||
std::string subdirs = ((subdir1.empty() ? "" : mDirDelimiter) + subdir1)
|
||||
+ ((subdir2.empty() ? "" : mDirDelimiter) + subdir2);
|
||||
BOOST_FOREACH(std::string skindir, mSearchSkinDirs)
|
||||
{
|
||||
std::string subdir_path(add(skindir, subdir));
|
||||
BOOST_FOREACH(std::string subsubdir, subsubdirs)
|
||||
{
|
||||
std::string full_path(add(add(subdir_path, subsubdir), filename));
|
||||
if (fileExists(full_path))
|
||||
{
|
||||
function(subsubdir, full_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> search_paths;
|
||||
|
||||
search_paths.push_back(getUserSkinDir() + subdirs); // first look in user skin override
|
||||
search_paths.push_back(getSkinDir() + subdirs); // then in current skin
|
||||
search_paths.push_back(getDefaultSkinDir() + subdirs); // then default skin
|
||||
search_paths.push_back(getCacheDir() + subdirs); // and last in preload directory
|
||||
// ridiculous little helper function that should go away when we can use lambda
|
||||
inline void push_back(std::vector<std::string>& vector, const std::string& value)
|
||||
{
|
||||
vector.push_back(value);
|
||||
}
|
||||
|
||||
std::string found_file = findFile(filename, search_paths);
|
||||
return found_file;
|
||||
typedef std::map<std::string, std::string> StringMap;
|
||||
// ridiculous little helper function that should go away when we can use lambda
|
||||
inline void store_in_map(StringMap& map, const std::string& key, const std::string& value)
|
||||
{
|
||||
map[key] = value;
|
||||
}
|
||||
|
||||
std::vector<std::string> LLDir::findSkinnedFilenames(const std::string& subdir,
|
||||
const std::string& filename,
|
||||
ESkinConstraint constraint) const
|
||||
{
|
||||
// Recognize subdirs that have no localization.
|
||||
static const std::set<std::string> sUnlocalized = list_of
|
||||
("") // top-level directory not localized
|
||||
("textures") // textures not localized
|
||||
;
|
||||
|
||||
LL_DEBUGS("LLDir") << "subdir '" << subdir << "', filename '" << filename
|
||||
<< "', constraint "
|
||||
<< ((constraint == CURRENT_SKIN)? "CURRENT_SKIN" : "ALL_SKINS")
|
||||
<< LL_ENDL;
|
||||
|
||||
// Cache the default language directory for each subdir we've encountered.
|
||||
// A cache entry whose value is the empty string means "not localized,
|
||||
// don't bother checking again."
|
||||
static StringMap sLocalized;
|
||||
|
||||
// Check whether we've already discovered if this subdir is localized.
|
||||
StringMap::const_iterator found = sLocalized.find(subdir);
|
||||
if (found == sLocalized.end())
|
||||
{
|
||||
// We have not yet determined that. Is it one of the subdirs "known"
|
||||
// to be unlocalized?
|
||||
if (sUnlocalized.find(subdir) != sUnlocalized.end())
|
||||
{
|
||||
// This subdir is known to be unlocalized. Remember that.
|
||||
found = sLocalized.insert(StringMap::value_type(subdir, "")).first;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We do not recognize this subdir. Investigate.
|
||||
std::string subdir_path(add(getDefaultSkinDir(), subdir));
|
||||
if (fileExists(add(subdir_path, "en")))
|
||||
{
|
||||
// defaultSkinDir/subdir contains subdir "en". That's our
|
||||
// default language; this subdir is localized.
|
||||
found = sLocalized.insert(StringMap::value_type(subdir, "en")).first;
|
||||
}
|
||||
else if (fileExists(add(subdir_path, "en-us")))
|
||||
{
|
||||
// defaultSkinDir/subdir contains subdir "en-us" but not "en".
|
||||
// Set as default language; this subdir is localized.
|
||||
found = sLocalized.insert(StringMap::value_type(subdir, "en-us")).first;
|
||||
}
|
||||
else
|
||||
{
|
||||
// defaultSkinDir/subdir contains neither "en" nor "en-us".
|
||||
// Assume it's not localized. Remember that assumption.
|
||||
found = sLocalized.insert(StringMap::value_type(subdir, "")).first;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Every code path above should have resulted in 'found' becoming a valid
|
||||
// iterator to an entry in sLocalized.
|
||||
llassert(found != sLocalized.end());
|
||||
|
||||
// Now -- is this subdir localized, or not? The answer determines what
|
||||
// subdirectories we check (under subdir) for the requested filename.
|
||||
std::vector<std::string> subsubdirs;
|
||||
if (found->second.empty())
|
||||
{
|
||||
// subdir is not localized. filename should be located directly within it.
|
||||
subsubdirs.push_back("");
|
||||
}
|
||||
else
|
||||
{
|
||||
// subdir is localized, and found->second is the default language
|
||||
// directory within it. Check both the default language and the
|
||||
// current language -- if it differs from the default, of course.
|
||||
subsubdirs.push_back(found->second);
|
||||
if (mLanguage != found->second)
|
||||
{
|
||||
subsubdirs.push_back(mLanguage);
|
||||
}
|
||||
}
|
||||
|
||||
// Build results vector.
|
||||
std::vector<std::string> results;
|
||||
// The process we use depends on 'constraint'.
|
||||
if (constraint != CURRENT_SKIN) // meaning ALL_SKINS
|
||||
{
|
||||
// ALL_SKINS is simpler: just return every pathname generated by
|
||||
// walkSearchSkinDirs(). Tricky bit: walkSearchSkinDirs() passes its
|
||||
// FUNCTION the subsubdir as well as the full pathname. We just want
|
||||
// the full pathname.
|
||||
walkSearchSkinDirs(subdir, subsubdirs, filename,
|
||||
boost::bind(push_back, boost::ref(results), _2));
|
||||
}
|
||||
else // CURRENT_SKIN
|
||||
{
|
||||
// CURRENT_SKIN turns out to be a bit of a misnomer because we might
|
||||
// still return files from two different skins. In any case, this
|
||||
// value of 'constraint' means we will return at most two paths: one
|
||||
// for the default language, one for the current language (supposing
|
||||
// those differ).
|
||||
// It is important to allow a user to override only the localization
|
||||
// for a particular file, for all viewer installs, without also
|
||||
// overriding the default-language file.
|
||||
// It is important to allow a user to override only the default-
|
||||
// language file, for all viewer installs, without also overriding the
|
||||
// applicable localization of that file.
|
||||
// Therefore, treat the default language and the current language as
|
||||
// two separate cases. For each, capture the most-specialized file
|
||||
// that exists.
|
||||
// Use a map keyed by subsubdir (i.e. language code). This allows us
|
||||
// to handle the case of a single subsubdirs entry with the same logic
|
||||
// that handles two. For every real file path generated by
|
||||
// walkSearchSkinDirs(), update the map entry for its subsubdir.
|
||||
StringMap path_for;
|
||||
walkSearchSkinDirs(subdir, subsubdirs, filename,
|
||||
boost::bind(store_in_map, boost::ref(path_for), _1, _2));
|
||||
// Now that we have a path for each of the default language and the
|
||||
// current language, copy them -- in proper order -- into results.
|
||||
// Don't drive this by walking the map itself: it matters that we
|
||||
// generate results in the same order as subsubdirs.
|
||||
BOOST_FOREACH(std::string subsubdir, subsubdirs)
|
||||
{
|
||||
StringMap::const_iterator found(path_for.find(subsubdir));
|
||||
if (found != path_for.end())
|
||||
{
|
||||
results.push_back(found->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LL_DEBUGS("LLDir") << empty;
|
||||
const char* comma = "";
|
||||
BOOST_FOREACH(std::string path, results)
|
||||
{
|
||||
LL_CONT << comma << "'" << path << "'";
|
||||
comma = ", ";
|
||||
}
|
||||
LL_CONT << LL_ENDL;
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
std::string LLDir::getTempFilename() const
|
||||
@@ -657,7 +873,7 @@ std::string LLDir::getTempFilename() const
|
||||
}
|
||||
|
||||
// static
|
||||
std::string LLDir::getScrubbedFileName(const std::string uncleanFileName)
|
||||
std::string LLDir::getScrubbedFileName(const std::string& uncleanFileName)
|
||||
{
|
||||
std::string name(uncleanFileName);
|
||||
std::string illegalChars(getForbiddenFileChars());
|
||||
@@ -668,8 +884,8 @@ std::string LLDir::getScrubbedFileName(const std::string uncleanFileName)
|
||||
// replace any illegal file chars with and underscore '_'
|
||||
for( unsigned int i = 0; i < illegalChars.length(); i++ )
|
||||
{
|
||||
int j = -1;
|
||||
while((j = name.find(illegalChars[i])) > -1)
|
||||
size_t j = std::string::npos;
|
||||
while ((j = name.find(illegalChars[i])) != std::string::npos)
|
||||
{
|
||||
name[j] = '_';
|
||||
}
|
||||
@@ -683,6 +899,19 @@ std::string LLDir::getForbiddenFileChars()
|
||||
return "\\/:*?\"<>|";
|
||||
}
|
||||
|
||||
// static
|
||||
std::string LLDir::getGridSpecificDir( const std::string& in, const std::string& grid )
|
||||
{
|
||||
std::string ret;
|
||||
if (!grid.empty())
|
||||
{
|
||||
std::string gridlower(grid);
|
||||
LLStringUtil::toLower(gridlower);
|
||||
ret = in + "@" + gridlower;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void LLDir::setLindenUserDir(const std::string &grid, const std::string &first, const std::string &last)
|
||||
{
|
||||
// if both first and last aren't set, assume we're grabbing the cached dir
|
||||
@@ -692,15 +921,7 @@ void LLDir::setLindenUserDir(const std::string &grid, const std::string &first,
|
||||
// utterly consistent with our firstname/lastname case.
|
||||
std::string userlower(first+"_"+last);
|
||||
LLStringUtil::toLower(userlower);
|
||||
mLindenUserDir = add(getOSUserAppDir(), userlower);
|
||||
|
||||
if (!grid.empty())
|
||||
{
|
||||
std::string gridlower(grid);
|
||||
LLStringUtil::toLower(gridlower);
|
||||
mLindenUserDir += "@";
|
||||
mLindenUserDir += gridlower;
|
||||
}
|
||||
mLindenUserDir = getGridSpecificDir(grid, add(getOSUserAppDir(), userlower));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -737,7 +958,7 @@ void LLDir::makePortable()
|
||||
initAppDirs(mAppName); // This is kinda lazy, but it's probably the quickest, most uniform way.
|
||||
}
|
||||
|
||||
void LLDir::setChatLogsDir(const std::string &path)
|
||||
void LLDir::setChatLogsDir( const std::string& path)
|
||||
{
|
||||
if (!path.empty() )
|
||||
{
|
||||
@@ -749,6 +970,11 @@ void LLDir::setChatLogsDir(const std::string &path)
|
||||
}
|
||||
}
|
||||
|
||||
void LLDir::updatePerAccountChatLogsDir(const std::string &grid)
|
||||
{
|
||||
mPerAccountChatLogsDir = getGridSpecificDir(grid, add(getChatLogsDir(), mUserName));
|
||||
}
|
||||
|
||||
void LLDir::setPerAccountChatLogsDir(const std::string &grid, const std::string &first, const std::string &last)
|
||||
{
|
||||
// if both first and last aren't set, assume we're grabbing the cached dir
|
||||
@@ -758,14 +984,9 @@ void LLDir::setPerAccountChatLogsDir(const std::string &grid, const std::string
|
||||
// utterly consistent with our firstname/lastname case.
|
||||
std::string userlower(first+"_"+last);
|
||||
LLStringUtil::toLower(userlower);
|
||||
mPerAccountChatLogsDir = add(getChatLogsDir(), userlower);
|
||||
if (!grid.empty())
|
||||
{
|
||||
std::string gridlower(grid);
|
||||
LLStringUtil::toLower(gridlower);
|
||||
mPerAccountChatLogsDir += "@";
|
||||
mPerAccountChatLogsDir += gridlower;
|
||||
}
|
||||
|
||||
mUserName = userlower;
|
||||
updatePerAccountChatLogsDir(grid);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -773,21 +994,59 @@ void LLDir::setPerAccountChatLogsDir(const std::string &grid, const std::string
|
||||
}
|
||||
}
|
||||
|
||||
void LLDir::setSkinFolder(const std::string &skin_folder)
|
||||
void LLDir::setSkinFolder(const std::string &skin_folder, const std::string& language)
|
||||
{
|
||||
mSkinDir = getSkinBaseDir();
|
||||
append(mSkinDir, skin_folder);
|
||||
LL_DEBUGS("LLDir") << "Setting skin '" << skin_folder << "', language '" << language << "'"
|
||||
<< LL_ENDL;
|
||||
mSkinName = skin_folder;
|
||||
mLanguage = language;
|
||||
|
||||
// user modifications to current skin
|
||||
// e.g. c:\documents and settings\users\username\application data\second life\skins\dazzle
|
||||
mUserSkinDir = getOSUserAppDir();
|
||||
append(mUserSkinDir, "skins_sg1");
|
||||
append(mUserSkinDir, skin_folder);
|
||||
// This method is called multiple times during viewer initialization. Each
|
||||
// time it's called, reset mSearchSkinDirs.
|
||||
mSearchSkinDirs.clear();
|
||||
|
||||
// base skin which is used as fallback for all skinned files
|
||||
// e.g. c:\program files\secondlife\skins\default
|
||||
mDefaultSkinDir = getSkinBaseDir();
|
||||
append(mDefaultSkinDir, "default");
|
||||
// This is always the most general of the search skin directories.
|
||||
addSearchSkinDir(mDefaultSkinDir);
|
||||
|
||||
mSkinDir = getSkinBaseDir();
|
||||
append(mSkinDir, skin_folder);
|
||||
// Next level of generality is a skin installed with the viewer.
|
||||
addSearchSkinDir(mSkinDir);
|
||||
|
||||
// user modifications to skins, current and default
|
||||
// e.g. c:\documents and settings\users\username\application data\second life\skins\dazzle
|
||||
mUserSkinDir = getOSUserAppDir();
|
||||
append(mUserSkinDir, "skins_sg1");
|
||||
mUserDefaultSkinDir = mUserSkinDir;
|
||||
append(mUserDefaultSkinDir, "default");
|
||||
append(mUserSkinDir, skin_folder);
|
||||
// Next level of generality is user modifications to default skin...
|
||||
addSearchSkinDir(mUserDefaultSkinDir);
|
||||
// then user-defined skins.
|
||||
addSearchSkinDir(mUserSkinDir);
|
||||
}
|
||||
|
||||
void LLDir::addSearchSkinDir(const std::string& skindir)
|
||||
{
|
||||
if (std::find(mSearchSkinDirs.begin(), mSearchSkinDirs.end(), skindir) == mSearchSkinDirs.end())
|
||||
{
|
||||
LL_DEBUGS("LLDir") << "search skin: '" << skindir << "'" << LL_ENDL;
|
||||
mSearchSkinDirs.push_back(skindir);
|
||||
}
|
||||
}
|
||||
|
||||
std::string LLDir::getSkinFolder() const
|
||||
{
|
||||
return mSkinName;
|
||||
}
|
||||
|
||||
std::string LLDir::getLanguage() const
|
||||
{
|
||||
return mLanguage;
|
||||
}
|
||||
|
||||
bool LLDir::setCacheDir(const std::string &path)
|
||||
|
||||
Reference in New Issue
Block a user