/* * Copyright (c) Contributors to the Open 3D Engine Project. * For complete copyright and license terms please see the LICENSE at the root of this distribution. * * SPDX-License-Identifier: Apache-2.0 OR MIT * */ #include "EditorDefs.h" #include "PathUtil.h" #include #include #include // for ebus events #include #include #include namespace Path { ////////////////////////////////////////////////////////////////////////// void SplitPath(const QString& rstrFullPathFilename, QString& rstrDriveLetter, QString& rstrDirectory, QString& rstrFilename, QString& rstrExtension) { AZStd::string strFullPathString(rstrFullPathFilename.toUtf8().data()); AZStd::string strDriveLetter; AZStd::string strDirectory; AZStd::string strFilename; AZStd::string strExtension; char* szPath((char*)strFullPathString.c_str()); char* pchLastPosition(szPath); char* pchCurrentPosition(szPath); char* pchAuxPosition(szPath); // Directory named filenames containing ":" are invalid, so we can assume if there is a : // it will be the drive name. pchCurrentPosition = strchr(pchLastPosition, ':'); if (pchCurrentPosition == nullptr) { rstrDriveLetter = ""; } else { strDriveLetter.assign(pchLastPosition, pchCurrentPosition + 1); pchLastPosition = pchCurrentPosition + 1; } pchCurrentPosition = strrchr(pchLastPosition, '\\'); pchAuxPosition = strrchr(pchLastPosition, '/'); if ((pchCurrentPosition == nullptr) && (pchAuxPosition == nullptr)) { rstrDirectory = ""; } else { // Since NULL is < valid pointer, so this will work. if (pchAuxPosition > pchCurrentPosition) { pchCurrentPosition = pchAuxPosition; } strDirectory.assign(pchLastPosition, pchCurrentPosition + 1); pchLastPosition = pchCurrentPosition + 1; } pchCurrentPosition = strrchr(pchLastPosition, '.'); if (pchCurrentPosition == nullptr) { rstrExtension = ""; strFilename.assign(pchLastPosition); } else { strExtension.assign(pchCurrentPosition); strFilename.assign(pchLastPosition, pchCurrentPosition); } rstrDriveLetter = strDriveLetter.c_str(); rstrDirectory = strDirectory.c_str(); rstrFilename = strFilename.c_str(); rstrExtension = strExtension.c_str(); } ////////////////////////////////////////////////////////////////////////// void GetDirectoryQueue(const QString& rstrSourceDirectory, QStringList& rcstrDirectoryTree) { AZStd::string strCurrentDirectoryName; AZStd::string strSourceDirectory(rstrSourceDirectory.toUtf8().data()); const char* szSourceDirectory(strSourceDirectory.c_str()); const char* pchCurrentPosition(szSourceDirectory); const char* pchLastPosition(szSourceDirectory); rcstrDirectoryTree.clear(); if (strSourceDirectory.empty()) { return; } // It removes as many slashes the path has in its start... // MAYBE and just maybe we should consider paths starting with // more than 2 slashes invalid paths... while ((*pchLastPosition == '\\') || (*pchLastPosition == '/')) { ++pchLastPosition; ++pchCurrentPosition; } do { pchCurrentPosition = strpbrk(pchLastPosition, "\\/"); if (pchCurrentPosition == nullptr) { break; } strCurrentDirectoryName.assign(pchLastPosition, pchCurrentPosition); pchLastPosition = pchCurrentPosition + 1; // Again, here we are skipping as many consecutive slashes. while ((*pchLastPosition == '\\') || (*pchLastPosition == '/')) { ++pchLastPosition; } rcstrDirectoryTree.push_back(strCurrentDirectoryName.c_str()); } while (true); } ////////////////////////////////////////////////////////////////////////// void ConvertSlashToBackSlash(QString& rstrStringToConvert) { rstrStringToConvert.replace('/', '\\'); rstrStringToConvert = CaselessPaths(rstrStringToConvert); } ////////////////////////////////////////////////////////////////////////// void ConvertBackSlashToSlash(QString& rstrStringToConvert) { rstrStringToConvert.replace('\\', '/'); rstrStringToConvert = CaselessPaths(rstrStringToConvert); } ////////////////////////////////////////////////////////////////////////// void SurroundWithQuotes(QString& rstrSurroundString) { QString strSurroundString(rstrSurroundString); if (!strSurroundString.isEmpty()) { if (strSurroundString[0] != '\"') { strSurroundString.insert(0, "\""); } if (strSurroundString[strSurroundString.size() - 1] != '\"') { strSurroundString.insert(strSurroundString.size(), "\""); } } else { strSurroundString.insert(0, "\""); strSurroundString.insert(strSurroundString.size(), "\""); } rstrSurroundString = strSurroundString; } ////////////////////////////////////////////////////////////////////////// QString GetExecutableFullPath() { return QDir::toNativeSeparators(QCoreApplication::applicationFilePath()); } ////////////////////////////////////////////////////////////////////////// QString GetWindowsTempDirectory() { return QDir::tempPath(); } ////////////////////////////////////////////////////////////////////////// QString GetEngineRootPath() { const AZ::IO::FixedMaxPathString engineRoot = AZ::Utils::GetEnginePath(); return QString::fromUtf8(engineRoot.c_str(), static_cast(engineRoot.size())); } ////////////////////////////////////////////////////////////////////////// QString& ReplaceFilename(const QString& strFilepath, const QString& strFilename, QString& strOutputFilename, bool bCallCaselessPath) { QString strDriveLetter; QString strDirectory; QString strOriginalFilename; QString strExtension; SplitPath(strFilepath, strDriveLetter, strDirectory, strOriginalFilename, strExtension); strOutputFilename = strDriveLetter; strOutputFilename += strDirectory; strOutputFilename += strFilename; strOutputFilename += strExtension; if (bCallCaselessPath) { strOutputFilename = CaselessPaths(strOutputFilename); } return strOutputFilename; } bool IsFolder(const char* pPath) { return AZ::IO::FileIOBase::GetInstance()->IsDirectory(pPath); } ////////////////////////////////////////////////////////////////////////// QString GetUserSandboxFolder() { return QString::fromUtf8("@user@/Sandbox/"); } ////////////////////////////////////////////////////////////////////////// QString GetResolvedUserSandboxFolder() { AZ::IO::FixedMaxPath userSandboxFolderPath; gEnv->pFileIO->ResolvePath(userSandboxFolderPath, GetUserSandboxFolder().toUtf8().constData()); return QString::fromUtf8(userSandboxFolderPath.c_str(), static_cast(userSandboxFolderPath.Native().size())); } // internal function, you should use GetEditingGameDataFolder instead. AZStd::string GetGameAssetsFolder() { AZ::IO::Path projectPath; if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr) { settingsRegistry->Get(projectPath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_ProjectPath); } return projectPath.Native(); } /// Get the data folder AZStd::string GetEditingGameDataFolder() { // Define here the mod name static AZ::IO::FixedMaxPathString s_currentModName; if (s_currentModName.empty()) { return GetGameAssetsFolder(); } AZStd::string str(GetGameAssetsFolder()); str += "Mods\\"; str += s_currentModName.c_str(); return str; } AZStd::string MakeModPathFromGamePath(const char* relGamePath) { return GetEditingGameDataFolder() + "\\" + relGamePath; } QString FullPathToLevelPath(const QString& path) { if (path.isEmpty()) { return ""; } QString relGamePath; if (!QFileInfo(path).isRelative()) { relGamePath = GetRelativePath(path); } else { relGamePath = path; } QString levelpath = GetIEditor()->GetLevelFolder(); QString str = levelpath; str.replace('/', '\\'); levelpath = CaselessPaths(str); // Create relative path QString relLevelPath = QDir(levelpath).relativeFilePath(relGamePath); if (relLevelPath.isEmpty()) { assert(0); return path; } relLevelPath.remove(QRegularExpression(QStringLiteral(R"(^[\\/.]*)"))); return relLevelPath; } QString Make(const QString& path, const QString& file) { if (gEnv->pCryPak->IsAbsPath(file.toUtf8().data())) { return file; } return CaselessPaths(AddPathSlash(path) + file); } QString GetRelativePath(const QString& fullPath, bool bRelativeToGameFolder /*= false*/) { if (fullPath.isEmpty()) { return ""; } bool relPathFound = false; AZStd::string relativePath; AZStd::string fullAssetPath(fullPath.toUtf8().data()); EBUS_EVENT_RESULT(relPathFound, AzToolsFramework::AssetSystemRequestBus, GetRelativeProductPathFromFullSourceOrProductPath, fullAssetPath, relativePath); if (relPathFound) { // do not normalize this path, it will already be an appropriate asset ID. return CaselessPaths(relativePath.c_str()); } AZ::IO::FixedMaxPath rootPath = bRelativeToGameFolder ? AZ::Utils::GetProjectPath() : AZ::Utils::GetEnginePath(); AZ::IO::FixedMaxPath resolvedFullPath; AZ::IO::FileIOBase::GetDirectInstance()->ResolvePath(resolvedFullPath, fullPath.toUtf8().constData()); // Create relative path return CaselessPaths(resolvedFullPath.LexicallyProximate(rootPath).MakePreferred().c_str()); } QString GamePathToFullPath(const QString& path) { using namespace AzToolsFramework; AZ_Warning("GamePathToFullPath", path.size() <= AZ::IO::MaxPathLength, "Path exceeds maximum path length of %zu", AZ::IO::MaxPathLength); if (path.size() <= AZ::IO::MaxPathLength) { // first, adjust the file name for mods: bool fullPathFound = false; AZ::IO::Path assetFullPath; AZ::IO::Path adjustedFilePath = path.toUtf8().constData(); AssetSystemRequestBus::BroadcastResult(fullPathFound, &AssetSystemRequestBus::Events::GetFullSourcePathFromRelativeProductPath, adjustedFilePath.Native(), assetFullPath.Native()); if (fullPathFound) { //if the bus message succeeds than normalize return assetFullPath.LexicallyNormal().c_str(); } // if the bus message didn't succeed, check if he path exist as a resolved path else { // Not all systems have been converted to use local paths. Some editor files save XML files directly, and a full or correctly aliased path is already passed in. // If the path passed in exists already, then return the resolved filepath if (AZ::IO::FileIOBase::GetDirectInstance()->Exists(adjustedFilePath.c_str())) { AZ::IO::FixedMaxPath resolvedPath; AZ::IO::FileIOBase::GetDirectInstance()->ResolvePath(resolvedPath, adjustedFilePath); return QString::fromUtf8(resolvedPath.c_str(), static_cast(resolvedPath.Native().size())); } return path; } } else { return QString{}; } } QString ToUnixPath(const QString& strPath, bool bCallCaselessPath) { QString str = strPath; str.replace('\\', '/'); return bCallCaselessPath ? CaselessPaths(str) : str; } QString RemoveBackslash(QString path) { if (path.isEmpty()) { return path; } int iLenMinus1 = path.length() - 1; QChar cLastChar = path[iLenMinus1]; if (cLastChar == '\\' || cLastChar == '/') { return CaselessPaths(path.mid(0, iLenMinus1)); } return CaselessPaths(path); } QString SubDirectoryCaseInsensitive(const QString& path, const QStringList& parts) { if (parts.isEmpty()) { return path; } QStringList modifiedParts = parts; auto currentPart = modifiedParts.takeFirst(); // case insensitive iterator QDirIterator it(path); while (it.hasNext()) { it.next(); // the current part already exists, use it, case doesn't matter auto actualName = it.fileName(); if (QString::compare(actualName, currentPart, Qt::CaseInsensitive) == 0) { return SubDirectoryCaseInsensitive(QDir(path).absoluteFilePath(actualName), modifiedParts); } } // the current path doesn't exist yet, so just create the complete path in one rush return QDir(path).absoluteFilePath(parts.join('/')); } }