Merge branch 'development' into TIF/Runtime
This commit is contained in:
@@ -28,6 +28,10 @@ ly_add_target(
|
||||
AZ::AzToolsFramework
|
||||
)
|
||||
|
||||
if(LY_DEFAULT_PROJECT_PATH)
|
||||
set_property(TARGET AssetBuilder APPEND PROPERTY VS_DEBUGGER_COMMAND_ARGUMENTS "--project-path=\"${LY_DEFAULT_PROJECT_PATH}\"")
|
||||
endif()
|
||||
|
||||
# Aggregates all combined AssetBuilders into a single LY_ASSET_BUILDERS #define
|
||||
get_property(asset_builders GLOBAL PROPERTY LY_ASSET_BUILDERS)
|
||||
string (REPLACE ";" "," asset_builders "${asset_builders}")
|
||||
|
||||
@@ -259,8 +259,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
|
||||
|
||||
ly_add_googletest(
|
||||
NAME AZ::AssetProcessor.Tests
|
||||
TEST_COMMAND $<TARGET_FILE:AZ::AssetProcessor.Tests> --unittest
|
||||
TEST_COMMAND $<TARGET_FILE:AZ::AssetProcessor.Tests> --unittest --gtest_filter=-*.SUITE_sandbox*
|
||||
)
|
||||
|
||||
|
||||
endif()
|
||||
|
||||
@@ -11,9 +11,127 @@
|
||||
*/
|
||||
#include <native/FileWatcher/FileWatcher.h>
|
||||
|
||||
#include <QDirIterator>
|
||||
#include <QHash>
|
||||
#include <QMutex>
|
||||
|
||||
#include <unistd.h>
|
||||
#include <sys/inotify.h>
|
||||
|
||||
|
||||
static constexpr int s_handleToFolderMapLockTimeout = 1000; // 1 sec timeout for obtaining the handle to folder map lock
|
||||
static constexpr size_t s_iNotifyMaxEntries = 1024 * 16; // Control the maximum number of entries (from inotify) that can be read at one time
|
||||
static constexpr size_t s_iNotifyEventSize = sizeof(struct inotify_event);
|
||||
static constexpr size_t s_iNotifyReadBufferSize = s_iNotifyMaxEntries * s_iNotifyEventSize;
|
||||
|
||||
struct FolderRootWatch::PlatformImplementation
|
||||
{
|
||||
PlatformImplementation() { }
|
||||
PlatformImplementation() = default;
|
||||
|
||||
int m_iNotifyHandle = -1;
|
||||
QMutex m_handleToFolderMapLock;
|
||||
QHash<int, QString> m_handleToFolderMap;
|
||||
|
||||
bool Initialize()
|
||||
{
|
||||
if (m_iNotifyHandle < 0)
|
||||
{
|
||||
m_iNotifyHandle = inotify_init();
|
||||
}
|
||||
return (m_iNotifyHandle >= 0);
|
||||
}
|
||||
|
||||
void Finalize()
|
||||
{
|
||||
if (m_iNotifyHandle >= 0)
|
||||
{
|
||||
if (!m_handleToFolderMapLock.tryLock(s_handleToFolderMapLockTimeout))
|
||||
{
|
||||
AZ_Error("FileWatcher", false, "Unable to obtain inotify handle lock on thread");
|
||||
return;
|
||||
}
|
||||
|
||||
QHashIterator<int, QString> iter(m_handleToFolderMap);
|
||||
while (iter.hasNext())
|
||||
{
|
||||
iter.next();
|
||||
int watchHandle = iter.key();
|
||||
inotify_rm_watch(m_iNotifyHandle, watchHandle);
|
||||
}
|
||||
m_handleToFolderMap.clear();
|
||||
m_handleToFolderMapLock.unlock();
|
||||
|
||||
::close(m_iNotifyHandle);
|
||||
m_iNotifyHandle = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void AddWatchFolder(QString folder)
|
||||
{
|
||||
if (m_iNotifyHandle >= 0)
|
||||
{
|
||||
// Clean up the path before accepting it as a watch folder
|
||||
QString cleanPath = QDir::cleanPath(folder);
|
||||
|
||||
// Add the folder to watch and track it
|
||||
int watchHandle = inotify_add_watch(m_iNotifyHandle,
|
||||
cleanPath.toUtf8().constData(),
|
||||
IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY);
|
||||
|
||||
if (!m_handleToFolderMapLock.tryLock(s_handleToFolderMapLockTimeout))
|
||||
{
|
||||
AZ_Error("FileWatcher", false, "Unable to obtain inotify handle lock on thread");
|
||||
return;
|
||||
}
|
||||
m_handleToFolderMap[watchHandle] = cleanPath;
|
||||
m_handleToFolderMapLock.unlock();
|
||||
|
||||
// Add all the subfolders to watch and track them
|
||||
QDirIterator dirIter(folder, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
|
||||
|
||||
while (dirIter.hasNext())
|
||||
{
|
||||
QString dirName = dirIter.next();
|
||||
if (dirName.endsWith("/.") || dirName.endsWith("/.."))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int watchHandle = inotify_add_watch(m_iNotifyHandle,
|
||||
dirName.toUtf8().constData(),
|
||||
IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY);
|
||||
|
||||
if (!m_handleToFolderMapLock.tryLock(s_handleToFolderMapLockTimeout))
|
||||
{
|
||||
AZ_Error("FileWatcher", false, "Unable to obtain inotify handle lock on thread");
|
||||
return;
|
||||
}
|
||||
m_handleToFolderMap[watchHandle] = dirName;
|
||||
m_handleToFolderMapLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RemoveWatchFolder(int watchHandle)
|
||||
{
|
||||
if (m_iNotifyHandle >= 0)
|
||||
{
|
||||
if (!m_handleToFolderMapLock.tryLock(s_handleToFolderMapLockTimeout))
|
||||
{
|
||||
AZ_Error("FileWatcher", false, "Unable to obtain inotify handle lock on thread");
|
||||
return;
|
||||
}
|
||||
|
||||
QHash<int, QString>::iterator handleToRemove = m_handleToFolderMap.find(watchHandle);
|
||||
if (handleToRemove != m_handleToFolderMap.end())
|
||||
{
|
||||
inotify_rm_watch(m_iNotifyHandle, watchHandle);
|
||||
m_handleToFolderMap.erase(handleToRemove);
|
||||
}
|
||||
|
||||
m_handleToFolderMapLock.unlock();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
@@ -36,17 +154,86 @@ FolderRootWatch::~FolderRootWatch()
|
||||
|
||||
bool FolderRootWatch::Start()
|
||||
{
|
||||
// TODO: Implement for Linux
|
||||
return false;
|
||||
// inotify will be used by linux to monitor file changes within directories under the root folder
|
||||
if (!m_platformImpl->Initialize())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_platformImpl->AddWatchFolder(m_root);
|
||||
|
||||
m_shutdownThreadSignal = false;
|
||||
m_thread = std::thread([this]() { WatchFolderLoop(); });
|
||||
return true;
|
||||
}
|
||||
|
||||
void FolderRootWatch::Stop()
|
||||
{
|
||||
// TODO: Implement for Linux
|
||||
m_shutdownThreadSignal = true;
|
||||
|
||||
m_platformImpl->Finalize();
|
||||
|
||||
if (m_thread.joinable())
|
||||
{
|
||||
m_thread.join(); // wait for the thread to finish
|
||||
m_thread = std::thread(); //destroy
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void FolderRootWatch::WatchFolderLoop()
|
||||
{
|
||||
// TODO: Implement for Linux
|
||||
char eventBuffer[s_iNotifyReadBufferSize];
|
||||
while (!m_shutdownThreadSignal)
|
||||
{
|
||||
ssize_t bytesRead = ::read(m_platformImpl->m_iNotifyHandle, eventBuffer, s_iNotifyReadBufferSize);
|
||||
if (bytesRead < 0)
|
||||
{
|
||||
// Break out of the loop when the notify handle was closed (outside of this thread)
|
||||
break;
|
||||
}
|
||||
else if (bytesRead > 0)
|
||||
{
|
||||
for (size_t index=0; index<bytesRead;)
|
||||
{
|
||||
struct inotify_event *event = ( struct inotify_event * ) &eventBuffer[ index ];
|
||||
const char* eventName = event->name;
|
||||
|
||||
if (event->mask & (IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVE ))
|
||||
{
|
||||
QString pathStr = QString("%1%2%3").arg(m_platformImpl->m_handleToFolderMap[event->wd], QDir::separator(), event->name);
|
||||
|
||||
if (event->mask & (IN_CREATE | IN_MOVED_TO))
|
||||
{
|
||||
if ( event->mask & IN_ISDIR )
|
||||
{
|
||||
// New Directory, add it to the watch
|
||||
m_platformImpl->AddWatchFolder(pathStr);
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessNewFileEvent(pathStr);
|
||||
}
|
||||
}
|
||||
else if (event->mask & (IN_DELETE | IN_MOVED_FROM))
|
||||
{
|
||||
if (event->mask & IN_ISDIR)
|
||||
{
|
||||
// Directory Deleted, remove it from the watch
|
||||
m_platformImpl->RemoveWatchFolder(event->wd);
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessDeleteFileEvent(pathStr);
|
||||
}
|
||||
}
|
||||
else if ((event->mask & IN_MODIFY) && ((event->mask & IN_ISDIR) != IN_ISDIR))
|
||||
{
|
||||
ProcessModifyFileEvent(pathStr);
|
||||
}
|
||||
}
|
||||
index += s_iNotifyEventSize + event->len;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
/*
|
||||
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
* its licensors.
|
||||
*
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this
|
||||
* distribution (the "License"). All use of this software is governed by the License,
|
||||
* or, if provided, by the license below or the license accompanying this file. Do not
|
||||
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <native/FileWatcher/FileWatcher.h>
|
||||
|
||||
#include <AzCore/PlatformIncl.h>
|
||||
|
||||
struct FolderRootWatch::PlatformImplementation
|
||||
{
|
||||
PlatformImplementation() : m_directoryHandle(nullptr), m_ioHandle(nullptr) { }
|
||||
HANDLE m_directoryHandle;
|
||||
HANDLE m_ioHandle;
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/// FolderWatchRoot
|
||||
FolderRootWatch::FolderRootWatch(const QString rootFolder)
|
||||
: m_root(rootFolder)
|
||||
, m_shutdownThreadSignal(false)
|
||||
, m_fileWatcher(nullptr)
|
||||
, m_platformImpl(new PlatformImplementation())
|
||||
{
|
||||
}
|
||||
|
||||
FolderRootWatch::~FolderRootWatch()
|
||||
{
|
||||
// Destructor is required in here since this file contains the definition of struct PlatformImplementation
|
||||
Stop();
|
||||
|
||||
delete m_platformImpl;
|
||||
}
|
||||
|
||||
bool FolderRootWatch::Start()
|
||||
{
|
||||
m_platformImpl->m_directoryHandle = ::CreateFileW(m_root.toStdWString().data(), FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, nullptr);
|
||||
|
||||
if (m_platformImpl->m_directoryHandle != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
m_platformImpl->m_ioHandle = ::CreateIoCompletionPort(m_platformImpl->m_directoryHandle, nullptr, 1, 0);
|
||||
if (m_platformImpl->m_ioHandle != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
m_shutdownThreadSignal = false;
|
||||
m_thread = std::thread(std::bind(&FolderRootWatch::WatchFolderLoop, this));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void FolderRootWatch::Stop()
|
||||
{
|
||||
m_shutdownThreadSignal = true;
|
||||
CloseHandle(m_platformImpl->m_ioHandle);
|
||||
m_platformImpl->m_ioHandle = nullptr;
|
||||
|
||||
if (m_thread.joinable())
|
||||
{
|
||||
m_thread.join(); // wait for the thread to finish
|
||||
m_thread = std::thread(); //destroy
|
||||
}
|
||||
CloseHandle(m_platformImpl->m_directoryHandle);
|
||||
m_platformImpl->m_directoryHandle = nullptr;
|
||||
}
|
||||
|
||||
void FolderRootWatch::WatchFolderLoop()
|
||||
{
|
||||
FILE_NOTIFY_INFORMATION aFileNotifyInformationList[50000];
|
||||
QString path;
|
||||
OVERLAPPED aOverlapped;
|
||||
LPOVERLAPPED pOverlapped;
|
||||
DWORD dwByteCount;
|
||||
ULONG_PTR ulKey;
|
||||
|
||||
while (!m_shutdownThreadSignal)
|
||||
{
|
||||
::memset(aFileNotifyInformationList, 0, sizeof(aFileNotifyInformationList));
|
||||
::memset(&aOverlapped, 0, sizeof(aOverlapped));
|
||||
|
||||
if (::ReadDirectoryChangesW(m_platformImpl->m_directoryHandle, aFileNotifyInformationList, sizeof(aFileNotifyInformationList), true, FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_FILE_NAME, nullptr, &aOverlapped, nullptr))
|
||||
{
|
||||
//wait for up to a second for I/O to signal
|
||||
dwByteCount = 0;
|
||||
if (::GetQueuedCompletionStatus(m_platformImpl->m_ioHandle, &dwByteCount, &ulKey, &pOverlapped, INFINITE))
|
||||
{
|
||||
//if we are signaled to shutdown bypass
|
||||
if (!m_shutdownThreadSignal && ulKey)
|
||||
{
|
||||
if (dwByteCount)
|
||||
{
|
||||
int offset = 0;
|
||||
FILE_NOTIFY_INFORMATION* pFileNotifyInformation = aFileNotifyInformationList;
|
||||
do
|
||||
{
|
||||
pFileNotifyInformation = (FILE_NOTIFY_INFORMATION*)((char*)pFileNotifyInformation + offset);
|
||||
|
||||
path.clear();
|
||||
path.append(m_root);
|
||||
path.append(QString::fromWCharArray(pFileNotifyInformation->FileName, pFileNotifyInformation->FileNameLength / 2));
|
||||
|
||||
QString file = QDir::toNativeSeparators(QDir::cleanPath(path));
|
||||
|
||||
switch (pFileNotifyInformation->Action)
|
||||
{
|
||||
case FILE_ACTION_ADDED:
|
||||
case FILE_ACTION_RENAMED_NEW_NAME:
|
||||
ProcessNewFileEvent(file);
|
||||
break;
|
||||
case FILE_ACTION_REMOVED:
|
||||
case FILE_ACTION_RENAMED_OLD_NAME:
|
||||
ProcessDeleteFileEvent(file);
|
||||
break;
|
||||
case FILE_ACTION_MODIFIED:
|
||||
ProcessModifyFileEvent(file);
|
||||
break;
|
||||
}
|
||||
|
||||
offset = pFileNotifyInformation->NextEntryOffset;
|
||||
} while (offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ set(FILES
|
||||
native/AssetManager/AssetRequestHandler.cpp
|
||||
native/AssetManager/AssetRequestHandler.h
|
||||
native/AssetManager/assetScanFolderInfo.h
|
||||
native/AssetManager/assetScanFolderInfo.cpp
|
||||
native/AssetManager/assetScanner.cpp
|
||||
native/AssetManager/assetScanner.h
|
||||
native/AssetManager/assetScannerWorker.cpp
|
||||
|
||||
@@ -445,10 +445,10 @@ namespace AssetProcessor
|
||||
bool isExcludedDependency = dependencyPathSearch.starts_with(ExcludedDependenciesSymbol);
|
||||
dependencyPathSearch = isExcludedDependency ? dependencyPathSearch.substr(1) : dependencyPathSearch;
|
||||
bool isExactDependency = !AzFramework::StringFunc::Replace(dependencyPathSearch, '*', '%');
|
||||
SanitizeForDatabase(dependencyPathSearch);
|
||||
|
||||
if (cleanedupDependency.m_dependencyType == AssetBuilderSDK::ProductPathDependencyType::ProductFile)
|
||||
{
|
||||
SanitizeForDatabase(dependencyPathSearch);
|
||||
AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer productInfoContainer;
|
||||
QString productNameWithPlatform = QString("%1%2%3").arg(platform.c_str(), AZ_CORRECT_DATABASE_SEPARATOR_STRING, dependencyPathSearch.c_str());
|
||||
|
||||
@@ -508,6 +508,10 @@ namespace AssetProcessor
|
||||
}
|
||||
else
|
||||
{
|
||||
// For source assets, the casing of the input path must be maintained. Just fix up the path separators.
|
||||
AZStd::replace(dependencyPathSearch.begin(), dependencyPathSearch.end(), AZ_WRONG_DATABASE_SEPARATOR, AZ_CORRECT_DATABASE_SEPARATOR);
|
||||
AzFramework::StringFunc::Replace(dependencyPathSearch, AZ_DOUBLE_CORRECT_DATABASE_SEPARATOR, AZ_CORRECT_DATABASE_SEPARATOR_STRING);
|
||||
|
||||
// See if path matches any source files
|
||||
AzToolsFramework::AssetDatabase::SourceDatabaseEntryContainer sourceInfoContainer;
|
||||
|
||||
|
||||
@@ -190,44 +190,55 @@ Please note that only those seed files will get updated that are active for your
|
||||
void SourceFileRelocator::HandleMetaDataFiles(QStringList pathMatches, QHash<QString, int>& sourceIndexMap, const ScanFolderInfo* scanFolderInfo, SourceFileRelocationContainer& metadataFiles, bool excludeMetaDataFiles) const
|
||||
{
|
||||
QSet<QString> metaDataFileEntries;
|
||||
for (QString file : pathMatches)
|
||||
|
||||
// Remove all the metadata files
|
||||
if (excludeMetaDataFiles)
|
||||
{
|
||||
pathMatches.erase(AZStd::remove_if(pathMatches.begin(), pathMatches.end(), [this](const QString& file)
|
||||
{
|
||||
for (int idx = 0; idx < m_platformConfig->MetaDataFileTypesCount(); idx++)
|
||||
{
|
||||
const auto& [metadataType, extension] = m_platformConfig->GetMetaDataFileTypeAt(idx);
|
||||
if (file.endsWith("." + metadataType, Qt::CaseInsensitive))
|
||||
{
|
||||
AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Metadata file %s will be ignored because --excludeMetadataFiles was specified in the command line.\n",
|
||||
file.toUtf8().constData());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
pathMatches.end());
|
||||
}
|
||||
|
||||
for (const QString& file : pathMatches)
|
||||
{
|
||||
for (int idx = 0; idx < m_platformConfig->MetaDataFileTypesCount(); idx++)
|
||||
{
|
||||
QPair<QString, QString> metaInfo = m_platformConfig->GetMetaDataFileTypeAt(idx);
|
||||
if (file.endsWith("." + metaInfo.first, Qt::CaseInsensitive))
|
||||
const auto& [metadataType, extension] = m_platformConfig->GetMetaDataFileTypeAt(idx);
|
||||
if (file.endsWith("." + metadataType, Qt::CaseInsensitive))
|
||||
{
|
||||
//it is a metadata file
|
||||
if (excludeMetaDataFiles)
|
||||
const QString normalizedFilePath = AssetUtilities::NormalizeFilePath(file);
|
||||
if (!metaDataFileEntries.contains(normalizedFilePath))
|
||||
{
|
||||
AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Metadata file %s will be ignored because --excludeMetadataFiles was specified in the command line.\n",
|
||||
file.toUtf8().constData());
|
||||
break; // don't check it against other metafile entries, we've already ascertained its a metafile.
|
||||
}
|
||||
else
|
||||
{
|
||||
QString normalizedFilePath = AssetUtilities::NormalizeFilePath(file);
|
||||
if (metaDataFileEntries.find(normalizedFilePath) == metaDataFileEntries.end())
|
||||
{
|
||||
SourceFileRelocationInfo metaDataFile(file.toUtf8().data(), scanFolderInfo);
|
||||
metaDataFile.m_isMetaDataFile = true;
|
||||
metadataFiles.emplace_back(metaDataFile);
|
||||
metaDataFileEntries.insert(normalizedFilePath);
|
||||
}
|
||||
SourceFileRelocationInfo metaDataFile(file.toUtf8().data(), scanFolderInfo);
|
||||
metaDataFile.m_isMetaDataFile = true;
|
||||
metadataFiles.emplace_back(metaDataFile);
|
||||
metaDataFileEntries.insert(normalizedFilePath);
|
||||
}
|
||||
}
|
||||
else if (!excludeMetaDataFiles && (file.endsWith("." + metaInfo.second, Qt::CaseInsensitive) || metaInfo.second.isEmpty()))
|
||||
else if (!excludeMetaDataFiles && (file.endsWith("." + extension, Qt::CaseInsensitive) || extension.isEmpty()))
|
||||
{
|
||||
// if we are here it implies that a metadata file might exists for this source file,
|
||||
// add metadata file only if it exists and is not added already
|
||||
AZStd::string metadataFilePath(file.toUtf8().data());
|
||||
if (metaInfo.second.isEmpty())
|
||||
if (extension.isEmpty())
|
||||
{
|
||||
metadataFilePath.append(AZStd::string::format(".%s", metaInfo.first.toUtf8().data()));
|
||||
metadataFilePath.append(AZStd::string::format(".%s", metadataType.toUtf8().data()));
|
||||
}
|
||||
else
|
||||
{
|
||||
AZ::StringFunc::Path::ReplaceExtension(metadataFilePath, metaInfo.first.toUtf8().data());
|
||||
AZ::StringFunc::Path::ReplaceExtension(metadataFilePath, metadataType.toUtf8().data());
|
||||
};
|
||||
|
||||
// The metadata file can have a different case than the source file,
|
||||
|
||||
@@ -1697,7 +1697,10 @@ namespace AssetProcessor
|
||||
|
||||
if(productFileInfo.absoluteDir().entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot).empty())
|
||||
{
|
||||
productFileInfo.absoluteDir().rmdir(".");
|
||||
const QDir productDir = productFileInfo.absoluteDir();
|
||||
QDir parentDir = productDir;
|
||||
parentDir.cdUp();
|
||||
successfullyRemoved &= parentDir.rmdir(productDir.dirName());
|
||||
}
|
||||
|
||||
if (successfullyRemoved)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
* its licensors.
|
||||
*
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this
|
||||
* distribution (the "License"). All use of this software is governed by the License,
|
||||
* or, if provided, by the license below or the license accompanying this file. Do not
|
||||
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <native/AssetManager/assetScanFolderInfo.h>
|
||||
#include <native/utilities/assetUtils.h>
|
||||
|
||||
namespace AssetProcessor
|
||||
{
|
||||
ScanFolderInfo::ScanFolderInfo(
|
||||
QString path,
|
||||
QString displayName,
|
||||
QString portableKey,
|
||||
bool isRoot,
|
||||
bool recurseSubFolders,
|
||||
AZStd::vector<AssetBuilderSDK::PlatformInfo> platforms,
|
||||
int order,
|
||||
AZ::s64 scanFolderID,
|
||||
bool canSaveNewAssets)
|
||||
: m_scanPath(path)
|
||||
, m_displayName(displayName)
|
||||
, m_portableKey (portableKey)
|
||||
, m_isRoot(isRoot)
|
||||
, m_recurseSubFolders(recurseSubFolders)
|
||||
, m_order(order)
|
||||
, m_scanFolderID(scanFolderID)
|
||||
, m_platforms(platforms)
|
||||
, m_canSaveNewAssets(canSaveNewAssets)
|
||||
{
|
||||
m_scanPath = AssetUtilities::NormalizeFilePath(m_scanPath);
|
||||
// note that m_scanFolderID is 0 unless its filled in from the DB.
|
||||
}
|
||||
|
||||
} // end namespace AssetProcessor
|
||||
@@ -33,19 +33,7 @@ namespace AssetProcessor
|
||||
AZStd::vector<AssetBuilderSDK::PlatformInfo> platforms = AZStd::vector<AssetBuilderSDK::PlatformInfo>{},
|
||||
int order = 0,
|
||||
AZ::s64 scanFolderID = 0,
|
||||
bool canSaveNewAssets = false)
|
||||
: m_scanPath(path)
|
||||
, m_displayName(displayName)
|
||||
, m_portableKey (portableKey)
|
||||
, m_isRoot(isRoot)
|
||||
, m_recurseSubFolders(recurseSubFolders)
|
||||
, m_order(order)
|
||||
, m_scanFolderID(scanFolderID)
|
||||
, m_platforms(platforms)
|
||||
, m_canSaveNewAssets(canSaveNewAssets)
|
||||
{
|
||||
// note that m_scanFolderID is 0 unless its filled in from the DB.
|
||||
}
|
||||
bool canSaveNewAssets = false);
|
||||
|
||||
ScanFolderInfo() = default;
|
||||
ScanFolderInfo(const ScanFolderInfo& other) = default;
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace AssetProcessorMessagesTests
|
||||
using namespace AssetProcessor;
|
||||
using namespace AssetBuilderSDK;
|
||||
|
||||
static constexpr unsigned short AssetProcessorPort = static_cast<unsigned short>(888u);
|
||||
static constexpr unsigned short AssetProcessorPort{65535u};
|
||||
|
||||
class AssetProcessorMessages;
|
||||
|
||||
@@ -85,7 +85,6 @@ namespace AssetProcessorMessagesTests
|
||||
public:
|
||||
void SetUp() override
|
||||
{
|
||||
#if !AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
AssetUtilities::ResetGameName();
|
||||
|
||||
m_temporarySourceDir = QDir(m_temporaryDir.path());
|
||||
@@ -166,12 +165,10 @@ namespace AssetProcessorMessagesTests
|
||||
});
|
||||
|
||||
|
||||
#endif // !AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
#if !AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
QEventLoop eventLoop;
|
||||
|
||||
QObject::connect(m_batchApplicationManager->m_connectionManager, &ConnectionManager::ReadyToQuit, &eventLoop, &QEventLoop::quit);
|
||||
@@ -182,7 +179,6 @@ namespace AssetProcessorMessagesTests
|
||||
|
||||
m_assetSystemComponent->Deactivate();
|
||||
m_batchApplicationManager->Destroy();
|
||||
#endif // !AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
}
|
||||
|
||||
void RunNetworkRequest(AZStd::function<void()> func) const
|
||||
@@ -207,6 +203,7 @@ namespace AssetProcessorMessagesTests
|
||||
|
||||
thread.join();
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
MockAssetRequestHandler* m_assetRequestHandler{}; // Not owned, AP will delete this pointer
|
||||
@@ -226,11 +223,7 @@ namespace AssetProcessorMessagesTests
|
||||
AZStd::unique_ptr<AzFramework::AssetSystem::BaseAssetProcessorMessage> m_response;
|
||||
};
|
||||
|
||||
#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
TEST_F(AssetProcessorMessages, DISABLED_All)
|
||||
#else
|
||||
TEST_F(AssetProcessorMessages, All)
|
||||
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
{
|
||||
// Test that we can successfully send network messages and have them arrive for processing
|
||||
// For messages that have a response, it also verifies the response comes back
|
||||
@@ -311,11 +304,7 @@ namespace AssetProcessorMessagesTests
|
||||
});
|
||||
}
|
||||
|
||||
#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
TEST_F(AssetProcessorMessages, DISABLED_GetUnresolvedProductReferences_Succeeds)
|
||||
#else
|
||||
TEST_F(AssetProcessorMessages, GetUnresolvedProductReferences_Succeeds)
|
||||
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
{
|
||||
using namespace AzToolsFramework::AssetDatabase;
|
||||
|
||||
|
||||
@@ -112,7 +112,6 @@ namespace UnitTests
|
||||
SourceDatabaseEntry sourceFile8 = { m_data->m_scanFolder1.m_scanFolderID, "test.txt", AZ::Uuid::CreateRandom(), "AnalysisFingerprint" };
|
||||
SourceDatabaseEntry sourceFile9 = { m_data->m_scanFolder1.m_scanFolderID, "duplicate/folder/file1.tif", AZ::Uuid::CreateRandom(), "AnalysisFingerprint" };
|
||||
SourceDatabaseEntry sourceFile10 = { m_data->m_scanFolder1.m_scanFolderID, "folder/file.foo", AZ::Uuid::CreateRandom(), "AnalysisFingerprint" };
|
||||
SourceDatabaseEntry sourceFile11 = { m_data->m_scanFolder1.m_scanFolderID, "testfolder/file.foo", AZ::Uuid::CreateRandom(), "AnalysisFingerprint" };
|
||||
ASSERT_TRUE(m_data->m_connection->SetSource(sourceFile1));
|
||||
ASSERT_TRUE(m_data->m_connection->SetSource(sourceFile2));
|
||||
ASSERT_TRUE(m_data->m_connection->SetSource(sourceFile3));
|
||||
@@ -123,7 +122,6 @@ namespace UnitTests
|
||||
ASSERT_TRUE(m_data->m_connection->SetSource(sourceFile8));
|
||||
ASSERT_TRUE(m_data->m_connection->SetSource(sourceFile9));
|
||||
ASSERT_TRUE(m_data->m_connection->SetSource(sourceFile10));
|
||||
ASSERT_TRUE(m_data->m_connection->SetSource(sourceFile11));
|
||||
|
||||
SourceFileDependencyEntry dependency1 = { AZ::Uuid::CreateRandom(), "subfolder1/somefile.tif", "subfolder1/otherfile.tif", SourceFileDependencyEntry::TypeOfDependency::DEP_SourceToSource, false };
|
||||
SourceFileDependencyEntry dependency2 = { AZ::Uuid::CreateRandom(), "subfolder1/otherfile.tif", "otherfile.tif", SourceFileDependencyEntry::TypeOfDependency::DEP_JobToJob, false };
|
||||
@@ -176,8 +174,6 @@ namespace UnitTests
|
||||
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("dev/dummy/foo.metadataextension")));
|
||||
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("dev/folder/file.foo")));
|
||||
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("dev/folder/file.bar")));
|
||||
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("dev/testfolder/file.foo")));
|
||||
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("dev/testfolder/File.bar")));
|
||||
|
||||
if (AZ::IO::FileIOBase::GetInstance() == nullptr)
|
||||
{
|
||||
@@ -487,20 +483,12 @@ namespace UnitTests
|
||||
TestGetSourcesByPath("dev/", { }, false);
|
||||
}
|
||||
|
||||
#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
TEST_F(SourceFileRelocatorTest, DISABLED_GetSources_MultipleScanFolders_Fails)
|
||||
#else
|
||||
TEST_F(SourceFileRelocatorTest, GetSources_MultipleScanFolders_Fails)
|
||||
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
{
|
||||
TestGetSourcesByPath("*", { }, false);
|
||||
}
|
||||
|
||||
#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
TEST_F(SourceFileRelocatorTest, DISABLED_GetSources_PartialPath_FailsWithNoResults)
|
||||
#else
|
||||
TEST_F(SourceFileRelocatorTest, GetSources_PartialPath_FailsWithNoResults)
|
||||
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
{
|
||||
TestGetSourcesByPath("older/*", { }, false);
|
||||
}
|
||||
@@ -547,18 +535,6 @@ namespace UnitTests
|
||||
TestGetSourcesByPath(filePath.toUtf8().constData(), { "folder/file.foo" }, true, true);
|
||||
}
|
||||
|
||||
#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
TEST_F(SourceFileRelocatorTest, DISABLED_GetSources_HaveMetadataDifferentFileCase_AbsolutePath_Succeeds)
|
||||
#else
|
||||
TEST_F(SourceFileRelocatorTest, GetSources_HaveMetadataDifferentFileCase_AbsolutePath_Succeeds)
|
||||
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
{
|
||||
QDir tempPath(m_tempDir.path());
|
||||
|
||||
auto filePath = QDir(tempPath.absoluteFilePath(m_data->m_scanFolder1.m_scanFolder.c_str())).absoluteFilePath("testfolder/file.foo");
|
||||
TestGetSourcesByPath(filePath.toUtf8().constData(), { "testfolder/file.foo", "testfolder/File.bar" }, true, false);
|
||||
}
|
||||
|
||||
TEST_F(SourceFileRelocatorTest, GetMetaDataFile_SingleFileWildcard_Succeeds)
|
||||
{
|
||||
QDir tempPath(m_tempDir.path());
|
||||
@@ -921,32 +897,46 @@ namespace UnitTests
|
||||
ASSERT_FALSE(AZ::IO::FileIOBase::GetInstance()->Exists(filePath.toUtf8().constData()));
|
||||
}
|
||||
|
||||
#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
TEST_F(SourceFileRelocatorTest, DISABLED_Delete_Real_Readonly_Fails)
|
||||
#else
|
||||
TEST_F(SourceFileRelocatorTest, Delete_Real_Readonly_Fails)
|
||||
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
{
|
||||
struct AutoResetDirectoryReadOnlyState
|
||||
{
|
||||
AutoResetDirectoryReadOnlyState(QString dirName)
|
||||
: m_dirName(AZStd::move(dirName))
|
||||
{
|
||||
AZ::IO::SystemFile::SetWritable(m_dirName.toUtf8().constData(), false);
|
||||
}
|
||||
~AutoResetDirectoryReadOnlyState()
|
||||
{
|
||||
AZ::IO::SystemFile::SetWritable(m_dirName.toUtf8().constData(), true);
|
||||
}
|
||||
AZ_DISABLE_COPY_MOVE(AutoResetDirectoryReadOnlyState)
|
||||
private:
|
||||
QString m_dirName;
|
||||
};
|
||||
|
||||
QDir tempPath(m_tempDir.path());
|
||||
|
||||
auto filePath = QDir(tempPath.absoluteFilePath(m_data->m_scanFolder1.m_scanFolder.c_str())).absoluteFilePath("duplicate/file1.tif");
|
||||
|
||||
ASSERT_TRUE(AZ::IO::FileIOBase::GetInstance()->Exists(filePath.toUtf8().constData()));
|
||||
|
||||
AutoResetDirectoryReadOnlyState readOnlyResetter(QFileInfo(filePath).absoluteDir().absolutePath());
|
||||
|
||||
AZ::IO::SystemFile::SetWritable(filePath.toUtf8().constData(), false);
|
||||
|
||||
auto result = m_data->m_reporter->Delete(filePath.toUtf8().constData(), false);
|
||||
|
||||
ASSERT_TRUE(result.IsSuccess());
|
||||
EXPECT_TRUE(result.IsSuccess());
|
||||
|
||||
RelocationSuccess successResult = result.TakeValue();
|
||||
|
||||
ASSERT_EQ(successResult.m_moveSuccessCount, 0);
|
||||
ASSERT_EQ(successResult.m_moveFailureCount, 1);
|
||||
ASSERT_EQ(successResult.m_moveTotalCount, 1);
|
||||
ASSERT_EQ(successResult.m_updateTotalCount, 0);
|
||||
EXPECT_EQ(successResult.m_moveSuccessCount, 0);
|
||||
EXPECT_EQ(successResult.m_moveFailureCount, 1);
|
||||
EXPECT_EQ(successResult.m_moveTotalCount, 1);
|
||||
EXPECT_EQ(successResult.m_updateTotalCount, 0);
|
||||
|
||||
ASSERT_TRUE(AZ::IO::FileIOBase::GetInstance()->Exists(filePath.toUtf8().constData()));
|
||||
EXPECT_TRUE(AZ::IO::FileIOBase::GetInstance()->Exists(filePath.toUtf8().constData()));
|
||||
}
|
||||
|
||||
TEST_F(SourceFileRelocatorTest, Delete_Real_WithDependencies_Fails)
|
||||
|
||||
@@ -30,11 +30,7 @@ public:
|
||||
friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDependencies);
|
||||
friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDependencies_DeferredResolution);
|
||||
|
||||
#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, DISABLED_AssetProcessed_Impl_MultiplatformDependencies_SourcePath);
|
||||
#else
|
||||
friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDependencies_SourcePath);
|
||||
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
|
||||
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, DeleteFolder_SignalsDeleteOfContainedFiles);
|
||||
|
||||
@@ -70,19 +66,11 @@ public:
|
||||
friend class GTEST_TEST_CLASS_NAME_(AbsolutePathProductDependencyTest, UnresolvedProductPathDependency_AssetProcessedTwice_ValidatePathDependenciesMap);
|
||||
friend class GTEST_TEST_CLASS_NAME_(AbsolutePathProductDependencyTest, UnresolvedSourceFileTypeProductPathDependency_DependencyHasNoProductOutput_ValidatePathDependenciesMap);
|
||||
|
||||
#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, DISABLED_ModtimeSkipping_FileUnchanged_WithoutModtimeSkipping);
|
||||
#else
|
||||
friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_FileUnchanged_WithoutModtimeSkipping);
|
||||
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
|
||||
friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_FileUnchanged);
|
||||
|
||||
#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, DISABLED_ModtimeSkipping_EnablePlatform_ShouldProcessFilesForPlatform);
|
||||
#else
|
||||
friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_EnablePlatform_ShouldProcessFilesForPlatform);
|
||||
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
|
||||
friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_ModifyFile);
|
||||
friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_ModifyFile_AndThenRevert_ProcessesAgain);
|
||||
@@ -2362,11 +2350,7 @@ TEST_F(PathDependencyTest, ChangeDependencies_Existing_ResolveCorrectly)
|
||||
);
|
||||
}
|
||||
|
||||
#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
TEST_F(PathDependencyTest, DISABLED_MixedPathDependencies_Existing_ResolveCorrectly)
|
||||
#else
|
||||
TEST_F(PathDependencyTest, MixedPathDependencies_Existing_ResolveCorrectly)
|
||||
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
{
|
||||
using namespace AssetProcessor;
|
||||
using namespace AssetBuilderSDK;
|
||||
@@ -2661,11 +2645,7 @@ TEST_F(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDepende
|
||||
ASSERT_NE(SearchDependencies(dependencyContainer, asset1.m_products[0]), SearchDependencies(dependencyContainer, asset1.m_products[1]));
|
||||
}
|
||||
|
||||
#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
TEST_F(MultiplatformPathDependencyTest, DISABLED_AssetProcessed_Impl_MultiplatformDependencies_SourcePath)
|
||||
#else
|
||||
TEST_F(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDependencies_SourcePath)
|
||||
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
{
|
||||
// One product will be pc, one will be console (order is non-deterministic)
|
||||
TestAsset asset1("testAsset1");
|
||||
@@ -3894,7 +3874,7 @@ void ModtimeScanningTest::ProcessAssetJobs()
|
||||
|
||||
for (const auto& processResult : m_data->m_processResults)
|
||||
{
|
||||
auto file = QDir(processResult.m_destinationPath).absoluteFilePath(processResult.m_jobEntry.m_databaseSourceName + ".arc1");
|
||||
auto file = QDir(processResult.m_destinationPath).absoluteFilePath(processResult.m_jobEntry.m_databaseSourceName.toLower() + ".arc1");
|
||||
m_data->m_productPaths.emplace(
|
||||
QDir(processResult.m_jobEntry.m_watchFolderPath)
|
||||
.absoluteFilePath(processResult.m_jobEntry.m_databaseSourceName)
|
||||
@@ -3943,11 +3923,11 @@ void ModtimeScanningTest::ExpectWork(int createJobs, int processJobs)
|
||||
{
|
||||
ASSERT_TRUE(BlockUntilIdle(5000));
|
||||
|
||||
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, createJobs);
|
||||
ASSERT_EQ(m_data->m_processResults.size(), processJobs);
|
||||
ASSERT_FALSE(m_data->m_processResults[0].m_autoFail);
|
||||
ASSERT_FALSE(m_data->m_processResults[1].m_autoFail);
|
||||
ASSERT_EQ(m_data->m_deletedSources.size(), 0);
|
||||
EXPECT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, createJobs);
|
||||
EXPECT_EQ(m_data->m_processResults.size(), processJobs);
|
||||
EXPECT_FALSE(m_data->m_processResults[0].m_autoFail);
|
||||
EXPECT_FALSE(m_data->m_processResults[1].m_autoFail);
|
||||
EXPECT_EQ(m_data->m_deletedSources.size(), 0);
|
||||
|
||||
m_isIdling = false;
|
||||
}
|
||||
@@ -3975,11 +3955,7 @@ void ModtimeScanningTest::SetFileContents(QString filePath, QString contents)
|
||||
file.close();
|
||||
}
|
||||
|
||||
#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
TEST_F(ModtimeScanningTest, DISABLED_ModtimeSkipping_FileUnchanged_WithoutModtimeSkipping)
|
||||
#else
|
||||
TEST_F(ModtimeScanningTest, ModtimeSkipping_FileUnchanged_WithoutModtimeSkipping)
|
||||
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
{
|
||||
using namespace AzToolsFramework::AssetSystem;
|
||||
|
||||
@@ -4008,11 +3984,7 @@ TEST_F(ModtimeScanningTest, ModtimeSkipping_FileUnchanged)
|
||||
ExpectNoWork();
|
||||
}
|
||||
|
||||
#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
TEST_F(ModtimeScanningTest, DISABLED_ModtimeSkipping_EnablePlatform_ShouldProcessFilesForPlatform)
|
||||
#else
|
||||
TEST_F(ModtimeScanningTest, ModtimeSkipping_EnablePlatform_ShouldProcessFilesForPlatform)
|
||||
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
{
|
||||
using namespace AzToolsFramework::AssetSystem;
|
||||
|
||||
@@ -4633,11 +4605,7 @@ TEST_F(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_WildcardM
|
||||
dependList.clear();
|
||||
}
|
||||
|
||||
#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
TEST_F(AssetProcessorManagerTest, DISABLED_RemoveSource_RemoveCacheFolderIfEmpty_Ok)
|
||||
#else
|
||||
TEST_F(AssetProcessorManagerTest, RemoveSource_RemoveCacheFolderIfEmpty_Ok)
|
||||
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
{
|
||||
using namespace AssetProcessor;
|
||||
using namespace AssetBuilderSDK;
|
||||
|
||||
+40
-54
@@ -14,6 +14,7 @@
|
||||
#include "native/tests/platformconfiguration/platformconfigurationtests.h"
|
||||
|
||||
#include <AzTest/AzTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
const char TestAppRoot[] = "@exefolder@/testdata";
|
||||
const char EmptyDummyProjectName[] = "EmptyDummyProject";
|
||||
@@ -238,33 +239,6 @@ TEST_F(PlatformConfigurationUnitTests_OnePCHostFixture, GetScanFolderForFile_Sub
|
||||
EXPECT_STREQ(info->GetDisplayName().toUtf8().constData(), "Editor ScanFolder");
|
||||
}
|
||||
|
||||
// note that in the case of GetOverridingFile, this SHOULD return the correct case if an override is found
|
||||
// because its possible to override a file with another file with different case in a different scan folder
|
||||
// such a situation is supposed to be very rare, so the cost of correcting the case is mitigated.
|
||||
TEST_F(PlatformConfigurationUnitTests_OnePCHostFixture, GetOverridingFile_Exists_ReturnsCorrectCase)
|
||||
{
|
||||
using namespace AzToolsFramework::AssetSystem;
|
||||
using namespace AssetProcessor;
|
||||
|
||||
// create two scan folders, since its order dependent, the ScanFolder1 is the "winner" in tie breakers (when they both contain same file relpath)
|
||||
QString scanfolder1Path = m_tempPath.filePath("scanfolder1");
|
||||
QString scanfolder2Path = m_tempPath.filePath("scanfolder2");
|
||||
QString caseSensitiveDummyFileName = m_tempPath.absoluteFilePath("scanfolder1/TestCase.tXt");
|
||||
QString differentCaseDummyFileName = m_tempPath.absoluteFilePath("scanfolder2/testcase.txt");
|
||||
UnitTestUtils::CreateDummyFile(caseSensitiveDummyFileName, QString("testcase1\n"));
|
||||
UnitTestUtils::CreateDummyFile(differentCaseDummyFileName, QString("testcase2\n"));
|
||||
m_config->AddScanFolder(ScanFolderInfo(scanfolder1Path, "ScanFolder1", "sf1", false, true, m_platforms), true);
|
||||
m_config->AddScanFolder(ScanFolderInfo(scanfolder2Path, "ScanFolder2", "sf2", false, true, m_platforms), true);
|
||||
|
||||
// Perform the test by asking it whether anyone overrides "testcase" (lowercase) in scanfolder 2.
|
||||
QString overrider = m_config->GetOverridingFile("testcase.txt", scanfolder2Path);
|
||||
|
||||
ASSERT_FALSE(overrider.isEmpty());
|
||||
// the result should be the real actual case of the file in scanfolder 1:
|
||||
EXPECT_STREQ(overrider.toUtf8().constData(), caseSensitiveDummyFileName.toUtf8().constData());
|
||||
}
|
||||
|
||||
|
||||
TEST_F(PlatformConfigurationUnitTests_OnePCHostFixture, GetOverridingFile_ExistsButNotOverridden_ReturnsEmpty)
|
||||
{
|
||||
using namespace AzToolsFramework::AssetSystem;
|
||||
@@ -360,7 +334,7 @@ TEST_F(PlatformConfigurationUnitTests, TestFailReadConfigFile_RegularScanfolder)
|
||||
ASSERT_EQ(m_absorber.m_numErrorsAbsorbed, 0);
|
||||
|
||||
ASSERT_EQ(config.GetScanFolderCount(), 3); // the two, and then the one that has the same data as prior but different identifier.
|
||||
QString scanName = AssetUtilities::ComputeProjectPath() + " Scan Folder";
|
||||
QString scanName = AssetUtilities::ComputeProjectPath(true) + " Scan Folder";
|
||||
ASSERT_EQ(config.GetScanFolderAt(0).GetDisplayName(), scanName);
|
||||
ASSERT_EQ(config.GetScanFolderAt(0).RecurseSubFolders(), true);
|
||||
ASSERT_EQ(config.GetScanFolderAt(0).GetOrder(), 0);
|
||||
@@ -445,15 +419,11 @@ TEST_F(PlatformConfigurationUnitTests, TestFailReadConfigFile_RegularExcludes)
|
||||
ASSERT_FALSE(config.IsFileExcluded("blahblah/Levels/blahblahhold/whatever.test"));
|
||||
}
|
||||
|
||||
#if AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
TEST_F(PlatformConfigurationUnitTests, DISABLED_TestFailReadConfigFile_Recognizers)
|
||||
#else
|
||||
TEST_F(PlatformConfigurationUnitTests, TestFailReadConfigFile_Recognizers)
|
||||
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_PROCESSOR_TESTS
|
||||
{
|
||||
using namespace AzToolsFramework::AssetSystem;
|
||||
using namespace AssetProcessor;
|
||||
#if defined(AZ_PLATFORM_WINDOWS)
|
||||
#if defined(AZ_PLATFORM_WINDOWS) || defined(AZ_PLATFORM_LINUX)
|
||||
const char* platformWhichIsNotCurrentPlatform = "mac";
|
||||
#else
|
||||
const char* platformWhichIsNotCurrentPlatform = "pc";
|
||||
@@ -502,31 +472,47 @@ TEST_F(PlatformConfigurationUnitTests, TestFailReadConfigFile_Recognizers)
|
||||
// the "rend" test makes sure that even if you dont specify 'params' its still there by default for all enabled platforms.
|
||||
// (but platforms can override it)
|
||||
ASSERT_TRUE(recogs.contains("rend"));
|
||||
ASSERT_TRUE(recogs["rend"].m_platformSpecs.contains(AzToolsFramework::AssetSystem::GetHostAssetPlatform()));
|
||||
ASSERT_TRUE(recogs["rend"].m_platformSpecs.contains("android"));
|
||||
ASSERT_TRUE(recogs["rend"].m_platformSpecs.contains("server"));
|
||||
ASSERT_FALSE(recogs["rend"].m_platformSpecs.contains(platformWhichIsNotCurrentPlatform)); // this is not an enabled platform and should not be there.
|
||||
ASSERT_EQ(recogs["rend"].m_platformSpecs.size(), 3);
|
||||
ASSERT_EQ(recogs["rend"].m_platformSpecs[AzToolsFramework::AssetSystem::GetHostAssetPlatform()].m_extraRCParams, "rendererparams");
|
||||
ASSERT_EQ(recogs["rend"].m_platformSpecs["android"].m_extraRCParams, "rendererparams");
|
||||
ASSERT_EQ(recogs["rend"].m_platformSpecs["server"].m_extraRCParams, ""); // default if not specified is empty string
|
||||
EXPECT_THAT(
|
||||
recogs["rend"].m_platformSpecs.keys(),
|
||||
testing::AllOf(
|
||||
testing::UnorderedElementsAre(
|
||||
QString(AzToolsFramework::AssetSystem::GetHostAssetPlatform()),
|
||||
QString("android"),
|
||||
QString("server")
|
||||
),
|
||||
testing::Not(testing::Contains(platformWhichIsNotCurrentPlatform)) // this is not an enabled platform and should not be there.
|
||||
)
|
||||
);
|
||||
EXPECT_EQ(recogs["rend"].m_platformSpecs[AzToolsFramework::AssetSystem::GetHostAssetPlatform()].m_extraRCParams, "rendererparams");
|
||||
EXPECT_EQ(recogs["rend"].m_platformSpecs["android"].m_extraRCParams, "rendererparams");
|
||||
EXPECT_EQ(recogs["rend"].m_platformSpecs["server"].m_extraRCParams, ""); // default if not specified is empty string
|
||||
|
||||
ASSERT_TRUE(recogs.contains("alldefault"));
|
||||
ASSERT_TRUE(recogs["alldefault"].m_platformSpecs.contains(AzToolsFramework::AssetSystem::GetHostAssetPlatform()));
|
||||
ASSERT_TRUE(recogs["alldefault"].m_platformSpecs.contains("android"));
|
||||
ASSERT_TRUE(recogs["alldefault"].m_platformSpecs.contains("server"));
|
||||
ASSERT_FALSE(recogs["alldefault"].m_platformSpecs.contains(platformWhichIsNotCurrentPlatform)); // this is not an enabled platform and should not be there.
|
||||
ASSERT_EQ(recogs["alldefault"].m_platformSpecs.size(), 3);
|
||||
ASSERT_EQ(recogs["alldefault"].m_platformSpecs[AzToolsFramework::AssetSystem::GetHostAssetPlatform()].m_extraRCParams, "");
|
||||
ASSERT_EQ(recogs["alldefault"].m_platformSpecs["android"].m_extraRCParams, "");
|
||||
ASSERT_EQ(recogs["alldefault"].m_platformSpecs["server"].m_extraRCParams, "");
|
||||
EXPECT_THAT(
|
||||
recogs["alldefault"].m_platformSpecs.keys(),
|
||||
testing::AllOf(
|
||||
testing::UnorderedElementsAre(
|
||||
QString(AzToolsFramework::AssetSystem::GetHostAssetPlatform()),
|
||||
QString("android"),
|
||||
QString("server")
|
||||
),
|
||||
testing::Not(testing::Contains(platformWhichIsNotCurrentPlatform)) // this is not an enabled platform and should not be there.
|
||||
)
|
||||
);
|
||||
EXPECT_EQ(recogs["alldefault"].m_platformSpecs[AzToolsFramework::AssetSystem::GetHostAssetPlatform()].m_extraRCParams, "");
|
||||
EXPECT_EQ(recogs["alldefault"].m_platformSpecs["android"].m_extraRCParams, "");
|
||||
EXPECT_EQ(recogs["alldefault"].m_platformSpecs["server"].m_extraRCParams, "");
|
||||
|
||||
ASSERT_TRUE(recogs.contains("skipallbutone"));
|
||||
ASSERT_FALSE(recogs["skipallbutone"].m_platformSpecs.contains(AzToolsFramework::AssetSystem::GetHostAssetPlatform()));
|
||||
ASSERT_FALSE(recogs["skipallbutone"].m_platformSpecs.contains("android"));
|
||||
ASSERT_TRUE(recogs["skipallbutone"].m_platformSpecs.contains("server")); // server is only one enabled (set to copy)
|
||||
ASSERT_EQ(recogs["skipallbutone"].m_platformSpecs.size(), 1);
|
||||
ASSERT_EQ(recogs["skipallbutone"].m_platformSpecs["server"].m_extraRCParams, "copy");
|
||||
EXPECT_THAT(
|
||||
recogs["skipallbutone"].m_platformSpecs.keys(),
|
||||
testing::UnorderedElementsAre(
|
||||
QString("server") // server is only one enabled (set to copy)
|
||||
)
|
||||
);
|
||||
EXPECT_FALSE(recogs["skipallbutone"].m_platformSpecs.contains(AzToolsFramework::AssetSystem::GetHostAssetPlatform()));
|
||||
EXPECT_FALSE(recogs["skipallbutone"].m_platformSpecs.contains("android"));
|
||||
EXPECT_EQ(recogs["skipallbutone"].m_platformSpecs["server"].m_extraRCParams, "copy");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -137,10 +137,13 @@ TEST_F(AssetUtilitiesTest, UpdateToCorrectCase_ExistingFile_ReturnsTrue_Corrects
|
||||
thingsToTry << "SomeFile.TxT";
|
||||
thingsToTry << "otherfile.txt";
|
||||
thingsToTry << "subfolder1/otherfile.txt";
|
||||
|
||||
#if defined(AZ_PLATFORM_WINDOWS)
|
||||
thingsToTry << "subfolder2\\otherfile.txt";
|
||||
thingsToTry << "subFolder3\\somefile.txt";
|
||||
thingsToTry << "subFolder4\\subfolder6\\somefile.txt";
|
||||
thingsToTry << "subFolder5\\subfolder7/someFile.txt";
|
||||
#endif // AZ_PLATFORM_WINDOWS
|
||||
thingsToTry << "specialFileName[.txt";
|
||||
thingsToTry << "specialFileName].txt";
|
||||
thingsToTry << "specialFileName!.txt";
|
||||
|
||||
@@ -175,6 +175,16 @@ namespace AssetProcessor
|
||||
UNIT_TEST_EXPECT_FALSE(gameName.isEmpty());
|
||||
// should create cache folder in the root, and read everything from there.
|
||||
|
||||
// There is a sub-case of handling mixed cases, but is only supported on case-insensitive filesystems.
|
||||
#if defined(AZ_PLATFORM_LINUX)
|
||||
// Linux is case-sensitive, so 'basefile.txt' will stay the same case as the other subfolder versions
|
||||
constexpr const char* subfolder3BaseFilePath = "subfolder3/basefile.txt";
|
||||
constexpr int expectedLegacyAssetIdCount = 1;
|
||||
#else
|
||||
constexpr const char* subfolder3BaseFilePath = "subfolder3/BaseFile.txt";
|
||||
constexpr int expectedLegacyAssetIdCount = 2;
|
||||
#endif
|
||||
|
||||
QSet<QString> expectedFiles;
|
||||
// set up some interesting files:
|
||||
expectedFiles << tempPath.absoluteFilePath("rootfile2.txt");
|
||||
@@ -185,7 +195,9 @@ namespace AssetProcessor
|
||||
expectedFiles << tempPath.absoluteFilePath("subfolder2/aaa/bbb/basefile.txt");
|
||||
expectedFiles << tempPath.absoluteFilePath("subfolder2/aaa/bbb/ccc/basefile.txt");
|
||||
expectedFiles << tempPath.absoluteFilePath("subfolder2/aaa/bbb/ccc/ddd/basefile.txt");
|
||||
expectedFiles << tempPath.absoluteFilePath("subfolder3/BaseFile.txt"); // note the case upper here
|
||||
|
||||
expectedFiles << tempPath.absoluteFilePath(subfolder3BaseFilePath);
|
||||
|
||||
expectedFiles << tempPath.absoluteFilePath("subfolder8/a/b/c/test.txt");
|
||||
|
||||
// subfolder3 is not recursive so none of these should show up in any scan or override check
|
||||
@@ -1521,7 +1533,8 @@ namespace AssetProcessor
|
||||
|
||||
// -------------- override test -----------------
|
||||
// set up by letting it compile basefile.txt from 3:
|
||||
absolutePath = AssetUtilities::NormalizeFilePath(tempPath.absoluteFilePath("subfolder3/BaseFile.txt"));
|
||||
|
||||
absolutePath = AssetUtilities::NormalizeFilePath(tempPath.absoluteFilePath(subfolder3BaseFilePath));
|
||||
QMetaObject::invokeMethod(&apm, "AssessModifiedFile", Qt::QueuedConnection, Q_ARG(QString, absolutePath));
|
||||
UNIT_TEST_EXPECT_TRUE(BlockUntil(idling, 5000));
|
||||
|
||||
@@ -1583,8 +1596,7 @@ namespace AssetProcessor
|
||||
UNIT_TEST_EXPECT_TRUE(assetMessages.size() == 4);
|
||||
for (auto element : assetMessages)
|
||||
{
|
||||
// because the source asset had UPPER CASE in it, we should have multiple legacy IDs
|
||||
UNIT_TEST_EXPECT_TRUE(element.m_legacyAssetIds.size() == 2);
|
||||
UNIT_TEST_EXPECT_TRUE(element.m_legacyAssetIds.size() == expectedLegacyAssetIdCount);
|
||||
}
|
||||
|
||||
// ------------- setup complete, now do the test...
|
||||
@@ -1607,7 +1619,7 @@ namespace AssetProcessor
|
||||
|
||||
// delete the highest priority override file and ensure that it generates tasks
|
||||
// for the next highest priority! Basically, deleting this file should "reveal" the file underneath it in the other subfolder
|
||||
QString deletedFile = tempPath.absoluteFilePath("subfolder3/BaseFile.txt");
|
||||
QString deletedFile = tempPath.absoluteFilePath(subfolder3BaseFilePath);
|
||||
QString expectedReplacementInputFile = AssetUtilities::NormalizeFilePath(tempPath.absoluteFilePath("subfolder2/basefile.txt"));
|
||||
|
||||
UNIT_TEST_EXPECT_TRUE(QFile::remove(deletedFile));
|
||||
@@ -1621,6 +1633,11 @@ namespace AssetProcessor
|
||||
|
||||
sortAssetToProcessResultList(processResults);
|
||||
|
||||
#if defined(AZ_PLATFORM_LINUX)
|
||||
// On Linux, because of we cannot change the case of the source file, the job fingerprint is not updated due the case-switch so
|
||||
// there will be actually nothing to process
|
||||
UNIT_TEST_EXPECT_TRUE(processResults.size() == 0);
|
||||
#else
|
||||
// --------- same result as above ----------
|
||||
UNIT_TEST_EXPECT_TRUE(processResults.size() == 4); // 2 each for pc and android,since we have two recognizer for .txt file
|
||||
UNIT_TEST_EXPECT_TRUE(processResults[0].m_jobEntry.m_platformInfo.m_identifier == processResults[1].m_jobEntry.m_platformInfo.m_identifier);
|
||||
@@ -1641,7 +1658,7 @@ namespace AssetProcessor
|
||||
UNIT_TEST_EXPECT_TRUE(processFile1.startsWith(platformFolder));
|
||||
UNIT_TEST_EXPECT_TRUE(processResults[checkIdx].m_jobEntry.m_computedFingerprint != 0);
|
||||
}
|
||||
|
||||
#endif // defined(AZ_PLATFORM_LINUX)
|
||||
relativePathFromWatchFolder = "somefile.xxx";
|
||||
watchFolderPath = tempPath.absoluteFilePath("subfolder3");
|
||||
absolutePath = watchFolderPath + "/" + relativePathFromWatchFolder;
|
||||
@@ -2721,7 +2738,12 @@ namespace AssetProcessor
|
||||
{
|
||||
AssetBuilderSDK::JobDescriptor secondDescriptor = descriptor;
|
||||
secondDescriptor.m_jobKey = "yyy";
|
||||
#if defined(AZ_PLATFORM_WINDOWS)
|
||||
sourceFileDependency.m_sourceFileDependencyPath = "some\\random/Folders/FILEa.TxT";
|
||||
#else
|
||||
sourceFileDependency.m_sourceFileDependencyPath = "some/random/folders/FileA.txt";
|
||||
#endif // defined(AZ_PLATFORM_WINDOWS)
|
||||
|
||||
// ... declare a job dependency on job A ('FileA.txt', 'xxx', platform)
|
||||
AssetBuilderSDK::JobDependency jobDependency("xxx", platformInfo.m_identifier.c_str(), AssetBuilderSDK::JobDependencyType::Fingerprint, sourceFileDependency);
|
||||
secondDescriptor.m_jobDependencyList.push_back(jobDependency);
|
||||
@@ -2805,11 +2827,11 @@ namespace AssetProcessor
|
||||
QDir cacheRoot;
|
||||
UNIT_TEST_EXPECT_TRUE(AssetUtilities::ComputeProjectCacheRoot(cacheRoot));
|
||||
|
||||
QString productFileAPath = cacheRoot.filePath(QString("pc/FileAProduct.txt"));
|
||||
QString productFileBPath = cacheRoot.filePath(QString("pc/FileBProduct1.txt"));
|
||||
QString product2FileBPath = cacheRoot.filePath(QString("pc/FileBProduct2.txt"));
|
||||
QString productFileCPath = cacheRoot.filePath(QString("pc/FileCProduct.txt"));
|
||||
QString product2FileCPath = cacheRoot.filePath(QString("pc/FileCProduct2.txt"));
|
||||
QString productFileAPath = cacheRoot.filePath(QString("pc/fileaproduct.txt"));
|
||||
QString productFileBPath = cacheRoot.filePath(QString("pc/filebproduct1.txt"));
|
||||
QString product2FileBPath = cacheRoot.filePath(QString("pc/filebproduct2.txt"));
|
||||
QString productFileCPath = cacheRoot.filePath(QString("pc/filecproduct.txt"));
|
||||
QString product2FileCPath = cacheRoot.filePath(QString("pc/filecproduct2.txt"));
|
||||
|
||||
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(sourceFileAPath, ""));
|
||||
UNIT_TEST_EXPECT_TRUE(CreateDummyFile(sourceFileBPath, ""));
|
||||
|
||||
@@ -106,7 +106,7 @@ void FileWatcherUnitTestRunner::StartTest()
|
||||
AZ_TracePrintf(AssetProcessor::DebugChannel, "Waiting for remaining notifications: %d \n", outstandingFiles.count());
|
||||
}
|
||||
|
||||
if (outstandingFiles.count() > 0)
|
||||
if (outstandingFiles.count() > 0)
|
||||
{
|
||||
#if defined(AZ_ENABLE_TRACING)
|
||||
AZ_TracePrintf(AssetProcessor::DebugChannel, "Timed out waiting for file changes: %d / %d missed\n", outstandingFiles.count(), maxFiles);
|
||||
@@ -226,7 +226,9 @@ void FileWatcherUnitTestRunner::StartTest()
|
||||
UNIT_TEST_EXPECT_TRUE(fileAddCalled);
|
||||
UNIT_TEST_EXPECT_TRUE(fileRemoveCalled);
|
||||
|
||||
UNIT_TEST_EXPECT_TRUE(fileModifiedCalled); // modified should be called on the folder that the file lives in
|
||||
#if defined(AZ_PLATFORM_WINDOWS)
|
||||
UNIT_TEST_EXPECT_TRUE(fileModifiedCalled); // modified should be called on the folder that the file lives in (Only on Windows)
|
||||
#endif // AZ_PLATFORM_WINDOWS
|
||||
|
||||
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileRemoveName).toLower() == QDir::toNativeSeparators(originalName).toLower());
|
||||
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileAddName).toLower() == QDir::toNativeSeparators(newName1).toLower());
|
||||
@@ -249,7 +251,10 @@ void FileWatcherUnitTestRunner::StartTest()
|
||||
// the new1 was "removed" and the new2 was "added"
|
||||
UNIT_TEST_EXPECT_TRUE(fileAddCalled);
|
||||
UNIT_TEST_EXPECT_TRUE(fileRemoveCalled);
|
||||
UNIT_TEST_EXPECT_TRUE(fileModifiedCalled); // modified should be called on the folder that the file lives in
|
||||
#if defined(AZ_PLATFORM_WINDOWS)
|
||||
UNIT_TEST_EXPECT_TRUE(fileModifiedCalled); // modified should be called on the folder that the file lives in (Only on Windows)
|
||||
#endif // AZ_PLATFORM_WINDOWS
|
||||
|
||||
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileRemoveName).toLower() == QDir::toNativeSeparators(newName1).toLower());
|
||||
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileAddName).toLower() == QDir::toNativeSeparators(newName2).toLower());
|
||||
|
||||
@@ -270,11 +275,15 @@ void FileWatcherUnitTestRunner::StartTest()
|
||||
// the new1 was "removed" and the new2 was "added"
|
||||
UNIT_TEST_EXPECT_TRUE(fileAddCalled);
|
||||
UNIT_TEST_EXPECT_TRUE(fileRemoveCalled);
|
||||
UNIT_TEST_EXPECT_TRUE(fileModifiedCalled); // modified should be called on the folder that the file lives in
|
||||
#if defined(AZ_PLATFORM_WINDOWS)
|
||||
UNIT_TEST_EXPECT_TRUE(fileModifiedCalled); // modified should be called on the folder that the file lives in (Only on Windows)
|
||||
#endif // AZ_PLATFORM_WINDOWS
|
||||
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileRemoveName).toLower() == QDir::toNativeSeparators(newName2).toLower());
|
||||
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileAddName).toLower() == QDir::toNativeSeparators(newName3).toLower());
|
||||
|
||||
// final test... make sure that renaming a DIRECTORY works too
|
||||
#if !defined(AZ_PLATFORM_LINUX)
|
||||
// final test... make sure that renaming a DIRECTORY works too.
|
||||
// Note that linux does not get any callbacks if just the directory is renamed (from inotify)
|
||||
QDir renamer;
|
||||
fileAddCalled = false;
|
||||
fileRemoveCalled = false;
|
||||
@@ -297,7 +306,7 @@ void FileWatcherUnitTestRunner::StartTest()
|
||||
|
||||
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileRemoveName).toLower() == QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir3")).toLower());
|
||||
UNIT_TEST_EXPECT_TRUE(QDir::toNativeSeparators(fileAddName).toLower() == QDir::toNativeSeparators(tempDirPath.absoluteFilePath("dir4")).toLower());
|
||||
|
||||
#endif // AZ_PLATFORM_LINUX
|
||||
|
||||
QObject::disconnect(connectionRemove);
|
||||
QObject::disconnect(connectionAdd);
|
||||
|
||||
@@ -545,10 +545,13 @@ void RCcontrollerUnitTests::RunRCControllerTests()
|
||||
rcJob.SetCheckExclusiveLock(true);
|
||||
rcJob.Start();
|
||||
|
||||
|
||||
#if defined(AZ_PLATFORM_WINDOWS)
|
||||
// on windows, opening a file for reading locks it
|
||||
// but on other platforms, this is not the case.
|
||||
// we only expect work to begin when we can gain an exclusive lock on this file.
|
||||
UNIT_TEST_EXPECT_FALSE(UnitTestUtils::BlockUntil(beginWork, 5000));
|
||||
|
||||
#if defined(AZ_PLATFORM_WINDOWS)
|
||||
// Once we release the file, it should process normally
|
||||
lockFileTest.close();
|
||||
#else
|
||||
|
||||
@@ -227,8 +227,9 @@ void UtilitiesUnitTests::StartTest()
|
||||
#else
|
||||
int handle = open(lockTestFileName.toUtf8().constData(), O_RDONLY | O_EXLOCK | O_NONBLOCK);
|
||||
#endif // AZ_PLATFORM_WINDOWS
|
||||
UNIT_TEST_EXPECT_FALSE(AssetUtilities::CheckCanLock(lockTestFileName));
|
||||
|
||||
#if defined(AZ_PLATFORM_WINDOWS)
|
||||
UNIT_TEST_EXPECT_FALSE(AssetUtilities::CheckCanLock(lockTestFileName));
|
||||
lockTestFile.close();
|
||||
#else
|
||||
if (handle != -1)
|
||||
|
||||
@@ -507,8 +507,13 @@ namespace AssetUtilities
|
||||
return QString::fromUtf8(s_projectName.c_str(), aznumeric_cast<int>(s_projectName.size()));
|
||||
}
|
||||
|
||||
QString ComputeProjectPath()
|
||||
QString ComputeProjectPath(bool resetCachedProjectPath/*=false*/)
|
||||
{
|
||||
if (resetCachedProjectPath)
|
||||
{
|
||||
// Clear any cached value if reset was requested
|
||||
s_projectPath.clear();
|
||||
}
|
||||
if (s_projectPath.empty())
|
||||
{
|
||||
// Check command-line args first
|
||||
|
||||
@@ -104,7 +104,8 @@ namespace AssetUtilities
|
||||
QString ComputeProjectName(QString projectNameOverride = QString(), bool force = false);
|
||||
|
||||
//! Determine the absolute path of the current project
|
||||
QString ComputeProjectPath();
|
||||
//! The path computed path will be cached on subsequent calls unless resetCachedProjectPath=true
|
||||
QString ComputeProjectPath(bool resetCachedProjectPath = false);
|
||||
|
||||
//! Reads the allowed list directly from the bootstrap file
|
||||
QString ReadAllowedlistFromSettingsRegistry(QString initialFolder = QString());
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
|
||||
if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
|
||||
ly_get_list_relative_pal_filename(pal_dir ${CMAKE_CURRENT_LIST_DIR}/Platform/${PAL_PLATFORM_NAME})
|
||||
|
||||
ly_get_list_relative_pal_filename(pal_dir ${CMAKE_CURRENT_LIST_DIR}/Platform/${PAL_PLATFORM_NAME})
|
||||
|
||||
include(${pal_dir}/platform_traits_${PAL_PLATFORM_NAME_LOWERCASE}.cmake)
|
||||
include(${pal_dir}/platform_traits_${PAL_PLATFORM_NAME_LOWERCASE}.cmake)
|
||||
|
||||
if(PAL_TRAIT_AZTESTRUNNER_SUPPORTED AND NOT LY_MONOLITHIC_GAME)
|
||||
|
||||
ly_add_target(
|
||||
NAME AzTestRunner ${PAL_TRAIT_AZTESTRUNNER_LAUNCHER_TYPE}
|
||||
NAMESPACE AZ
|
||||
@@ -32,19 +32,23 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
|
||||
AZ::AzTest
|
||||
AZ::AzFramework
|
||||
)
|
||||
|
||||
if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
|
||||
|
||||
ly_add_target(
|
||||
NAME AzTestRunner.Tests ${PAL_TRAIT_TEST_TARGET_TYPE}
|
||||
NAMESPACE AZ
|
||||
FILES_CMAKE
|
||||
aztestrunner_test_files.cmake
|
||||
BUILD_DEPENDENCIES
|
||||
PRIVATE
|
||||
AZ::AzTest
|
||||
)
|
||||
|
||||
ly_add_target(
|
||||
NAME AzTestRunner.Tests ${PAL_TRAIT_TEST_TARGET_TYPE}
|
||||
NAMESPACE AZ
|
||||
FILES_CMAKE
|
||||
aztestrunner_test_files.cmake
|
||||
BUILD_DEPENDENCIES
|
||||
PRIVATE
|
||||
AZ::AzTest
|
||||
)
|
||||
ly_add_googletest(
|
||||
NAME AZ::AzTestRunner.Tests
|
||||
)
|
||||
|
||||
ly_add_googletest(
|
||||
NAME AZ::AzTestRunner.Tests
|
||||
)
|
||||
endif()
|
||||
|
||||
endif()
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
|
||||
set(PAL_TRAIT_AZTESTRUNNER_SUPPORTED TRUE)
|
||||
set(PAL_TRAIT_AZTESTRUNNER_LAUNCHER_TYPE MODULE)
|
||||
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
|
||||
set(PAL_TRAIT_AZTESTRUNNER_SUPPORTED TRUE)
|
||||
set(PAL_TRAIT_AZTESTRUNNER_LAUNCHER_TYPE EXECUTABLE)
|
||||
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
|
||||
set(PAL_TRAIT_AZTESTRUNNER_SUPPORTED TRUE)
|
||||
set(PAL_TRAIT_AZTESTRUNNER_LAUNCHER_TYPE EXECUTABLE)
|
||||
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
|
||||
set(PAL_TRAIT_AZTESTRUNNER_SUPPORTED TRUE)
|
||||
set(PAL_TRAIT_AZTESTRUNNER_LAUNCHER_TYPE EXECUTABLE)
|
||||
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
|
||||
set(PAL_TRAIT_AZTESTRUNNER_SUPPORTED TRUE)
|
||||
set(PAL_TRAIT_AZTESTRUNNER_LAUNCHER_TYPE EXECUTABLE)
|
||||
|
||||
|
||||
@@ -20,12 +20,11 @@ if (NOT python_package_name)
|
||||
message(WARNING "Python was not found in the package assocation list. Did someone call ly_associate_package(xxxxxxx Python) ?")
|
||||
endif()
|
||||
|
||||
|
||||
ly_add_target(
|
||||
NAME ProjectManager APPLICATION
|
||||
OUTPUT_NAME o3de
|
||||
NAME ProjectManager.Static STATIC
|
||||
NAMESPACE AZ
|
||||
AUTOMOC
|
||||
AUTORCC
|
||||
FILES_CMAKE
|
||||
project_manager_files.cmake
|
||||
Platform/${PAL_PLATFORM_NAME}/PAL_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake
|
||||
@@ -47,6 +46,60 @@ ly_add_target(
|
||||
3rdParty::pybind11
|
||||
AZ::AzCore
|
||||
AZ::AzFramework
|
||||
AZ::AzToolsFramework
|
||||
AZ::AzQtComponents
|
||||
)
|
||||
)
|
||||
|
||||
ly_add_target(
|
||||
NAME ProjectManager APPLICATION
|
||||
OUTPUT_NAME o3de
|
||||
NAMESPACE AZ
|
||||
AUTORCC
|
||||
FILES_CMAKE
|
||||
project_manager_app_files.cmake
|
||||
INCLUDE_DIRECTORIES
|
||||
PRIVATE
|
||||
Source
|
||||
BUILD_DEPENDENCIES
|
||||
PRIVATE
|
||||
3rdParty::Qt::Core
|
||||
3rdParty::Qt::Concurrent
|
||||
3rdParty::Qt::Widgets
|
||||
3rdParty::Python
|
||||
3rdParty::pybind11
|
||||
AZ::AzCore
|
||||
AZ::AzFramework
|
||||
AZ::AzQtComponents
|
||||
AZ::ProjectManager.Static
|
||||
)
|
||||
|
||||
if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
|
||||
ly_add_target(
|
||||
NAME ProjectManager.Tests EXECUTABLE
|
||||
NAMESPACE AZ
|
||||
AUTORCC
|
||||
FILES_CMAKE
|
||||
project_manager_tests_files.cmake
|
||||
Platform/${PAL_PLATFORM_NAME}/PAL_${PAL_PLATFORM_NAME_LOWERCASE}_tests_files.cmake
|
||||
INCLUDE_DIRECTORIES
|
||||
PRIVATE
|
||||
Source
|
||||
Platform/${PAL_PLATFORM_NAME}
|
||||
BUILD_DEPENDENCIES
|
||||
PRIVATE
|
||||
3rdParty::Qt::Core
|
||||
3rdParty::Qt::Concurrent
|
||||
3rdParty::Qt::Widgets
|
||||
3rdParty::Python
|
||||
3rdParty::pybind11
|
||||
AZ::AzTest
|
||||
AZ::AzFramework
|
||||
AZ::AzFrameworkTestShared
|
||||
AZ::ProjectManager.Static
|
||||
)
|
||||
|
||||
ly_add_googletest(
|
||||
NAME AZ::ProjectManager.Tests
|
||||
TEST_COMMAND $<TARGET_FILE:AZ::ProjectManager.Tests> --unittest
|
||||
)
|
||||
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
|
||||
set(FILES
|
||||
ProjectManager_Test_Traits_Platform.h
|
||||
ProjectManager_Test_Traits_Linux.h
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
* its licensors.
|
||||
*
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this
|
||||
* distribution (the "License"). All use of this software is governed by the License,
|
||||
* or, if provided, by the license below or the license accompanying this file. Do not
|
||||
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#define AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS true
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
* its licensors.
|
||||
*
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this
|
||||
* distribution (the "License"). All use of this software is governed by the License,
|
||||
* or, if provided, by the license below or the license accompanying this file. Do not
|
||||
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <ProjectManager_Test_Traits_Linux.h>
|
||||
@@ -0,0 +1,15 @@
|
||||
#
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
|
||||
set(FILES
|
||||
ProjectManager_Test_Traits_Platform.h
|
||||
ProjectManager_Test_Traits_Mac.h
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
* its licensors.
|
||||
*
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this
|
||||
* distribution (the "License"). All use of this software is governed by the License,
|
||||
* or, if provided, by the license below or the license accompanying this file. Do not
|
||||
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#define AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS false
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
* its licensors.
|
||||
*
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this
|
||||
* distribution (the "License"). All use of this software is governed by the License,
|
||||
* or, if provided, by the license below or the license accompanying this file. Do not
|
||||
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <ProjectManager_Test_Traits_Mac.h>
|
||||
@@ -0,0 +1,15 @@
|
||||
#
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
|
||||
set(FILES
|
||||
ProjectManager_Test_Traits_Platform.h
|
||||
ProjectManager_Test_Traits_Windows.h
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
* its licensors.
|
||||
*
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this
|
||||
* distribution (the "License"). All use of this software is governed by the License,
|
||||
* or, if provided, by the license below or the license accompanying this file. Do not
|
||||
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <ProjectManager_Test_Traits_Windows.h>
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
* its licensors.
|
||||
*
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this
|
||||
* distribution (the "License"). All use of this software is governed by the License,
|
||||
* or, if provided, by the license below or the license accompanying this file. Do not
|
||||
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#define AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS false
|
||||
@@ -7,6 +7,11 @@ QMainWindow {
|
||||
margin:0;
|
||||
}
|
||||
|
||||
#ScreensCtrl {
|
||||
min-width:1200px;
|
||||
min-height:800px;
|
||||
}
|
||||
|
||||
QPushButton:focus {
|
||||
outline: none;
|
||||
border:1px solid #1e70eb;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
IDI_ICON1 ICON DISCARDABLE "o3de_editor.ico"
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
* its licensors.
|
||||
*
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this
|
||||
* distribution (the "License"). All use of this software is governed by the License,
|
||||
* or, if provided, by the license below or the license accompanying this file. Do not
|
||||
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <Application.h>
|
||||
#include <ProjectUtils.h>
|
||||
|
||||
#include <AzCore/IO/FileIO.h>
|
||||
#include <AzCore/Utils/Utils.h>
|
||||
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
|
||||
#include <AzFramework/Logging/LoggingComponent.h>
|
||||
#include <AzQtComponents/Utilities/HandleDpiAwareness.h>
|
||||
#include <AzQtComponents/Components/StyleManager.h>
|
||||
#include <AzQtComponents/Components/WindowDecorationWrapper.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDir>
|
||||
#include <QMessageBox>
|
||||
|
||||
namespace O3DE::ProjectManager
|
||||
{
|
||||
Application::~Application()
|
||||
{
|
||||
TearDown();
|
||||
}
|
||||
|
||||
bool Application::Init(bool interactive)
|
||||
{
|
||||
constexpr const char* applicationName { "O3DE" };
|
||||
|
||||
QApplication::setOrganizationName(applicationName);
|
||||
QApplication::setOrganizationDomain("o3de.org");
|
||||
|
||||
QCoreApplication::setApplicationName(applicationName);
|
||||
QCoreApplication::setApplicationVersion("1.0");
|
||||
|
||||
// Use the LogComponent for non-dev logging log
|
||||
RegisterComponentDescriptor(AzFramework::LogComponent::CreateDescriptor());
|
||||
|
||||
// set the log alias to .o3de/Logs instead of the default user/logs
|
||||
AZ::IO::FixedMaxPath path = AZ::Utils::GetO3deLogsDirectory();
|
||||
|
||||
// DevWriteStorage is where the event log is written during development
|
||||
m_settingsRegistry->Set(AZ::SettingsRegistryMergeUtils::FilePathKey_DevWriteStorage, path.LexicallyNormal().Native());
|
||||
|
||||
// Save event logs to .o3de/Logs/eventlogger/EventLogO3DE.azsl
|
||||
m_settingsRegistry->Set(AZ::SettingsRegistryMergeUtils::BuildTargetNameKey, applicationName);
|
||||
|
||||
Start(AzFramework::Application::Descriptor());
|
||||
|
||||
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||
QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
|
||||
QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings);
|
||||
|
||||
QLocale::setDefault(QLocale(QLocale::English, QLocale::UnitedStates));
|
||||
|
||||
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
|
||||
AzQtComponents::Utilities::HandleDpiAwareness(AzQtComponents::Utilities::SystemDpiAware);
|
||||
|
||||
// Create the actual Qt Application - this needs to happen before using QMessageBox
|
||||
m_app.reset(new QApplication(*GetArgC(), *GetArgV()));
|
||||
|
||||
if(!InitLog(applicationName))
|
||||
{
|
||||
AZ_Warning("ProjectManager", false, "Failed to init logging");
|
||||
}
|
||||
|
||||
m_pythonBindings = AZStd::make_unique<PythonBindings>(GetEngineRoot());
|
||||
if (!m_pythonBindings || !m_pythonBindings->PythonStarted())
|
||||
{
|
||||
if (interactive)
|
||||
{
|
||||
QMessageBox::critical(nullptr, QObject::tr("Failed to start Python"),
|
||||
QObject::tr("This tool requires an O3DE engine with a Python runtime, "
|
||||
"but either Python is missing or mis-configured. Please rename "
|
||||
"your python/runtime folder to python/runtime_bak, then run "
|
||||
"python/get_python.bat to restore the Python runtime folder."));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const AZ::CommandLine* commandLine = GetCommandLine();
|
||||
AZ_Assert(commandLine, "Failed to get command line");
|
||||
|
||||
ProjectManagerScreen startScreen = ProjectManagerScreen::Projects;
|
||||
if (size_t screenSwitchCount = commandLine->GetNumSwitchValues("screen"); screenSwitchCount > 0)
|
||||
{
|
||||
QString screenOption = commandLine->GetSwitchValue("screen", screenSwitchCount - 1).c_str();
|
||||
ProjectManagerScreen screen = ProjectUtils::GetProjectManagerScreen(screenOption);
|
||||
if (screen != ProjectManagerScreen::Invalid)
|
||||
{
|
||||
startScreen = screen;
|
||||
}
|
||||
}
|
||||
|
||||
AZ::IO::FixedMaxPath projectPath;
|
||||
if (size_t projectSwitchCount = commandLine->GetNumSwitchValues("project-path"); projectSwitchCount > 0)
|
||||
{
|
||||
projectPath = commandLine->GetSwitchValue("project-path", projectSwitchCount - 1).c_str();
|
||||
}
|
||||
|
||||
m_mainWindow.reset(new ProjectManagerWindow(nullptr, projectPath, startScreen));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::InitLog(const char* logName)
|
||||
{
|
||||
if (!m_entity)
|
||||
{
|
||||
// override the log alias to the O3de Logs directory instead of the default project user/Logs folder
|
||||
AZ::IO::FixedMaxPath path = AZ::Utils::GetO3deLogsDirectory();
|
||||
AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance();
|
||||
AZ_Assert(fileIO, "Failed to get FileIOBase instance");
|
||||
|
||||
fileIO->SetAlias("@log@", path.LexicallyNormal().Native().c_str());
|
||||
|
||||
// this entity exists because we need a home for LogComponent
|
||||
// and cannot use the system entity because we need to be able to call SetLogFileBaseName
|
||||
// so the log will be named O3DE.log
|
||||
m_entity = aznew AZ::Entity("Application Entity");
|
||||
if (m_entity)
|
||||
{
|
||||
AzFramework::LogComponent* logger = aznew AzFramework::LogComponent();
|
||||
AZ_Assert(logger, "Failed to create LogComponent");
|
||||
logger->SetLogFileBaseName(logName);
|
||||
m_entity->AddComponent(logger);
|
||||
m_entity->Init();
|
||||
m_entity->Activate();
|
||||
}
|
||||
}
|
||||
|
||||
return m_entity != nullptr;
|
||||
}
|
||||
|
||||
void Application::TearDown()
|
||||
{
|
||||
if (m_entity)
|
||||
{
|
||||
m_entity->Deactivate();
|
||||
delete m_entity;
|
||||
m_entity = nullptr;
|
||||
}
|
||||
|
||||
m_pythonBindings.reset();
|
||||
m_mainWindow.reset();
|
||||
m_app.reset();
|
||||
}
|
||||
|
||||
bool Application::Run()
|
||||
{
|
||||
// Set up the Style Manager
|
||||
AzQtComponents::StyleManager styleManager(qApp);
|
||||
styleManager.initialize(qApp, GetEngineRoot());
|
||||
|
||||
// setup stylesheets and hot reloading
|
||||
AZ::IO::FixedMaxPath engineRoot(GetEngineRoot());
|
||||
QDir rootDir(engineRoot.c_str());
|
||||
const auto pathOnDisk = rootDir.absoluteFilePath("Code/Tools/ProjectManager/Resources");
|
||||
const auto qrcPath = QStringLiteral(":/ProjectManager/style");
|
||||
AzQtComponents::StyleManager::addSearchPaths("style", pathOnDisk, qrcPath, engineRoot);
|
||||
|
||||
// set stylesheet after creating the main window or their styles won't get updated
|
||||
AzQtComponents::StyleManager::setStyleSheet(m_mainWindow.data(), QStringLiteral("style:ProjectManager.qss"));
|
||||
|
||||
// the decoration wrapper is intended to remember window positioning and sizing
|
||||
auto wrapper = new AzQtComponents::WindowDecorationWrapper();
|
||||
wrapper->setGuest(m_mainWindow.data());
|
||||
wrapper->show();
|
||||
m_mainWindow->show();
|
||||
|
||||
qApp->setQuitOnLastWindowClosed(true);
|
||||
|
||||
// Run the application
|
||||
return qApp->exec();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
* its licensors.
|
||||
*
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this
|
||||
* distribution (the "License"). All use of this software is governed by the License,
|
||||
* or, if provided, by the license below or the license accompanying this file. Do not
|
||||
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#if !defined(Q_MOC_RUN)
|
||||
#include <AzFramework/Application/Application.h>
|
||||
#include <QCoreApplication>
|
||||
#include <PythonBindings.h>
|
||||
#include <ProjectManagerWindow.h>
|
||||
#endif
|
||||
|
||||
namespace AZ
|
||||
{
|
||||
class Entity;
|
||||
}
|
||||
|
||||
namespace O3DE::ProjectManager
|
||||
{
|
||||
class Application
|
||||
: public AzFramework::Application
|
||||
{
|
||||
public:
|
||||
using AzFramework::Application::Application;
|
||||
virtual ~Application();
|
||||
|
||||
bool Init(bool interactive = true);
|
||||
bool Run();
|
||||
void TearDown();
|
||||
|
||||
private:
|
||||
bool InitLog(const char* logName);
|
||||
|
||||
AZStd::unique_ptr<PythonBindings> m_pythonBindings;
|
||||
QSharedPointer<QCoreApplication> m_app;
|
||||
QSharedPointer<ProjectManagerWindow> m_mainWindow;
|
||||
|
||||
AZ::Entity* m_entity = nullptr;
|
||||
};
|
||||
}
|
||||
@@ -25,7 +25,7 @@ namespace O3DE::ProjectManager
|
||||
EngineSettingsScreen::EngineSettingsScreen(QWidget* parent)
|
||||
: ScreenWidget(parent)
|
||||
{
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
auto* layout = new QVBoxLayout();
|
||||
layout->setAlignment(Qt::AlignTop);
|
||||
|
||||
setObjectName("engineSettingsScreen");
|
||||
|
||||
@@ -82,6 +82,8 @@ namespace O3DE::ProjectManager
|
||||
|
||||
m_headerWidget->ReinitForProject();
|
||||
|
||||
connect(m_gemModel, &GemModel::dataChanged, m_filterWidget, &GemFilterWidget::ResetGemStatusFilter);
|
||||
|
||||
// Select the first entry after everything got correctly sized
|
||||
QTimer::singleShot(200, [=]{
|
||||
QModelIndex firstModelIndex = m_gemListView->model()->index(0,0);
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace O3DE::ProjectManager
|
||||
const QVector<QString>& elementNames,
|
||||
const QVector<int>& elementCounts,
|
||||
bool showAllLessButton,
|
||||
bool collapsed,
|
||||
int defaultShowCount,
|
||||
QWidget* parent)
|
||||
: QWidget(parent)
|
||||
@@ -40,6 +41,7 @@ namespace O3DE::ProjectManager
|
||||
QHBoxLayout* collapseLayout = new QHBoxLayout();
|
||||
m_collapseButton = new QPushButton();
|
||||
m_collapseButton->setCheckable(true);
|
||||
m_collapseButton->setChecked(collapsed);
|
||||
m_collapseButton->setFlat(true);
|
||||
m_collapseButton->setFocusPolicy(Qt::NoFocus);
|
||||
m_collapseButton->setFixedWidth(s_collapseButtonSize);
|
||||
@@ -178,6 +180,11 @@ namespace O3DE::ProjectManager
|
||||
return m_buttonGroup;
|
||||
}
|
||||
|
||||
bool FilterCategoryWidget::IsCollapsed()
|
||||
{
|
||||
return m_collapseButton->isChecked();
|
||||
}
|
||||
|
||||
GemFilterWidget::GemFilterWidget(GemSortFilterProxyModel* filterProxyModel, QWidget* parent)
|
||||
: QScrollArea(parent)
|
||||
, m_filterProxyModel(filterProxyModel)
|
||||
@@ -193,20 +200,106 @@ namespace O3DE::ProjectManager
|
||||
QWidget* mainWidget = new QWidget();
|
||||
setWidget(mainWidget);
|
||||
|
||||
m_mainLayout = new QVBoxLayout();
|
||||
m_mainLayout->setAlignment(Qt::AlignTop);
|
||||
mainWidget->setLayout(m_mainLayout);
|
||||
QVBoxLayout* mainLayout = new QVBoxLayout();
|
||||
mainLayout->setAlignment(Qt::AlignTop);
|
||||
mainWidget->setLayout(mainLayout);
|
||||
|
||||
QLabel* filterByLabel = new QLabel("Filter by");
|
||||
filterByLabel->setStyleSheet("font-size: 16px;");
|
||||
m_mainLayout->addWidget(filterByLabel);
|
||||
mainLayout->addWidget(filterByLabel);
|
||||
|
||||
QWidget* filterSection = new QWidget(this);
|
||||
mainLayout->addWidget(filterSection);
|
||||
|
||||
m_filterLayout = new QVBoxLayout();
|
||||
m_filterLayout->setAlignment(Qt::AlignTop);
|
||||
m_filterLayout->setContentsMargins(0, 0, 0, 0);
|
||||
filterSection->setLayout(m_filterLayout);
|
||||
|
||||
ResetGemStatusFilter();
|
||||
AddGemOriginFilter();
|
||||
AddTypeFilter();
|
||||
AddPlatformFilter();
|
||||
AddFeatureFilter();
|
||||
}
|
||||
|
||||
void GemFilterWidget::ResetGemStatusFilter()
|
||||
{
|
||||
QVector<QString> elementNames;
|
||||
QVector<int> elementCounts;
|
||||
const int totalGems = m_gemModel->rowCount();
|
||||
const int selectedGemTotal = m_gemModel->TotalAddedGems();
|
||||
|
||||
elementNames.push_back(GemSortFilterProxyModel::GetGemStatusString(GemSortFilterProxyModel::GemStatus::Unselected));
|
||||
elementCounts.push_back(totalGems - selectedGemTotal);
|
||||
|
||||
elementNames.push_back(GemSortFilterProxyModel::GetGemStatusString(GemSortFilterProxyModel::GemStatus::Selected));
|
||||
elementCounts.push_back(selectedGemTotal);
|
||||
|
||||
bool wasCollapsed = false;
|
||||
if (m_statusFilter)
|
||||
{
|
||||
wasCollapsed = m_statusFilter->IsCollapsed();
|
||||
}
|
||||
|
||||
FilterCategoryWidget* filterWidget =
|
||||
new FilterCategoryWidget("Status", elementNames, elementCounts, /*showAllLessButton=*/false, /*collapsed*/wasCollapsed);
|
||||
if (m_statusFilter)
|
||||
{
|
||||
m_filterLayout->replaceWidget(m_statusFilter, filterWidget);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_filterLayout->addWidget(filterWidget);
|
||||
}
|
||||
|
||||
m_statusFilter->deleteLater();
|
||||
m_statusFilter = filterWidget;
|
||||
|
||||
const GemSortFilterProxyModel::GemStatus currentFilterState = m_filterProxyModel->GetGemStatus();
|
||||
const QList<QAbstractButton*> buttons = m_statusFilter->GetButtonGroup()->buttons();
|
||||
for (int statusFilterIndex = 0; statusFilterIndex < buttons.size(); ++statusFilterIndex)
|
||||
{
|
||||
const GemSortFilterProxyModel::GemStatus gemStatus = static_cast<GemSortFilterProxyModel::GemStatus>(statusFilterIndex);
|
||||
QAbstractButton* button = buttons[statusFilterIndex];
|
||||
|
||||
if (static_cast<GemSortFilterProxyModel::GemStatus>(statusFilterIndex) == currentFilterState)
|
||||
{
|
||||
button->setChecked(true);
|
||||
}
|
||||
|
||||
connect(
|
||||
button, &QAbstractButton::toggled, this,
|
||||
[=](bool checked)
|
||||
{
|
||||
GemSortFilterProxyModel::GemStatus filterStatus = m_filterProxyModel->GetGemStatus();
|
||||
if (checked)
|
||||
{
|
||||
if (filterStatus == GemSortFilterProxyModel::GemStatus::NoFilter)
|
||||
{
|
||||
filterStatus = gemStatus;
|
||||
}
|
||||
else
|
||||
{
|
||||
filterStatus = GemSortFilterProxyModel::GemStatus::NoFilter;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (filterStatus != gemStatus)
|
||||
{
|
||||
filterStatus = static_cast<GemSortFilterProxyModel::GemStatus>(!gemStatus);
|
||||
}
|
||||
else
|
||||
{
|
||||
filterStatus = GemSortFilterProxyModel::GemStatus::NoFilter;
|
||||
}
|
||||
}
|
||||
m_filterProxyModel->SetGemStatus(filterStatus);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void GemFilterWidget::AddGemOriginFilter()
|
||||
{
|
||||
QVector<QString> elementNames;
|
||||
@@ -233,7 +326,7 @@ namespace O3DE::ProjectManager
|
||||
}
|
||||
|
||||
FilterCategoryWidget* filterWidget = new FilterCategoryWidget("Provider", elementNames, elementCounts, /*showAllLessButton=*/false);
|
||||
m_mainLayout->addWidget(filterWidget);
|
||||
m_filterLayout->addWidget(filterWidget);
|
||||
|
||||
const QList<QAbstractButton*> buttons = filterWidget->GetButtonGroup()->buttons();
|
||||
for (int i = 0; i < buttons.size(); ++i)
|
||||
@@ -283,7 +376,7 @@ namespace O3DE::ProjectManager
|
||||
}
|
||||
|
||||
FilterCategoryWidget* filterWidget = new FilterCategoryWidget("Type", elementNames, elementCounts, /*showAllLessButton=*/false);
|
||||
m_mainLayout->addWidget(filterWidget);
|
||||
m_filterLayout->addWidget(filterWidget);
|
||||
|
||||
const QList<QAbstractButton*> buttons = filterWidget->GetButtonGroup()->buttons();
|
||||
for (int i = 0; i < buttons.size(); ++i)
|
||||
@@ -333,7 +426,7 @@ namespace O3DE::ProjectManager
|
||||
}
|
||||
|
||||
FilterCategoryWidget* filterWidget = new FilterCategoryWidget("Supported Platforms", elementNames, elementCounts, /*showAllLessButton=*/false);
|
||||
m_mainLayout->addWidget(filterWidget);
|
||||
m_filterLayout->addWidget(filterWidget);
|
||||
|
||||
const QList<QAbstractButton*> buttons = filterWidget->GetButtonGroup()->buttons();
|
||||
for (int i = 0; i < buttons.size(); ++i)
|
||||
@@ -388,8 +481,8 @@ namespace O3DE::ProjectManager
|
||||
}
|
||||
|
||||
FilterCategoryWidget* filterWidget = new FilterCategoryWidget("Features", elementNames, elementCounts,
|
||||
/*showAllLessButton=*/true, /*defaultShowCount=*/5);
|
||||
m_mainLayout->addWidget(filterWidget);
|
||||
/*showAllLessButton=*/true, false, /*defaultShowCount=*/5);
|
||||
m_filterLayout->addWidget(filterWidget);
|
||||
|
||||
const QList<QAbstractButton*> buttons = filterWidget->GetButtonGroup()->buttons();
|
||||
for (int i = 0; i < buttons.size(); ++i)
|
||||
|
||||
@@ -37,11 +37,14 @@ namespace O3DE::ProjectManager
|
||||
const QVector<QString>& elementNames,
|
||||
const QVector<int>& elementCounts,
|
||||
bool showAllLessButton = true,
|
||||
bool collapsed = false,
|
||||
int defaultShowCount = 4,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
QButtonGroup* GetButtonGroup();
|
||||
|
||||
bool IsCollapsed();
|
||||
|
||||
private:
|
||||
void UpdateCollapseState();
|
||||
void UpdateSeeMoreLess();
|
||||
@@ -66,14 +69,18 @@ namespace O3DE::ProjectManager
|
||||
explicit GemFilterWidget(GemSortFilterProxyModel* filterProxyModel, QWidget* parent = nullptr);
|
||||
~GemFilterWidget() = default;
|
||||
|
||||
public slots:
|
||||
void ResetGemStatusFilter();
|
||||
|
||||
private:
|
||||
void AddGemOriginFilter();
|
||||
void AddTypeFilter();
|
||||
void AddPlatformFilter();
|
||||
void AddFeatureFilter();
|
||||
|
||||
QVBoxLayout* m_mainLayout = nullptr;
|
||||
QVBoxLayout* m_filterLayout = nullptr;
|
||||
GemModel* m_gemModel = nullptr;
|
||||
GemSortFilterProxyModel* m_filterProxyModel = nullptr;
|
||||
FilterCategoryWidget* m_statusFilter = nullptr;
|
||||
};
|
||||
} // namespace O3DE::ProjectManager
|
||||
|
||||
@@ -204,7 +204,6 @@ namespace O3DE::ProjectManager
|
||||
painter->save();
|
||||
const QRect buttonRect = CalcButtonRect(contentRect);
|
||||
QPoint circleCenter;
|
||||
QString buttonText;
|
||||
|
||||
const bool isAdded = GemModel::IsAdded(modelIndex);
|
||||
if (isAdded)
|
||||
@@ -213,34 +212,15 @@ namespace O3DE::ProjectManager
|
||||
painter->setPen(m_buttonEnabledColor);
|
||||
|
||||
circleCenter = buttonRect.center() + QPoint(buttonRect.width() / 2 - s_buttonBorderRadius + 1, 1);
|
||||
buttonText = "Added";
|
||||
}
|
||||
else
|
||||
{
|
||||
circleCenter = buttonRect.center() + QPoint(-buttonRect.width() / 2 + s_buttonBorderRadius, 1);
|
||||
buttonText = "Get";
|
||||
}
|
||||
|
||||
// Rounded rect
|
||||
painter->drawRoundedRect(buttonRect, s_buttonBorderRadius, s_buttonBorderRadius);
|
||||
|
||||
// Text
|
||||
QFont font;
|
||||
QRect textRect = GetTextRect(font, buttonText, s_buttonFontSize);
|
||||
if (isAdded)
|
||||
{
|
||||
textRect = QRect(buttonRect.left(), buttonRect.top(), buttonRect.width() - s_buttonCircleRadius * 2.0, buttonRect.height());
|
||||
}
|
||||
else
|
||||
{
|
||||
textRect = QRect(buttonRect.left() + s_buttonCircleRadius * 2.0, buttonRect.top(), buttonRect.width() - s_buttonCircleRadius * 2.0, buttonRect.height());
|
||||
}
|
||||
|
||||
font.setPixelSize(s_buttonFontSize);
|
||||
painter->setFont(font);
|
||||
painter->setPen(m_textColor);
|
||||
painter->drawText(textRect, Qt::AlignCenter, buttonText);
|
||||
|
||||
// Circle
|
||||
painter->setBrush(m_textColor);
|
||||
painter->drawEllipse(circleCenter, s_buttonCircleRadius, s_buttonCircleRadius);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <QStandardItemModel>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
#include <QSpacerItem>
|
||||
|
||||
namespace O3DE::ProjectManager
|
||||
{
|
||||
@@ -74,6 +75,15 @@ namespace O3DE::ProjectManager
|
||||
gemSummaryLabel->setStyleSheet("font-size: 12px;");
|
||||
columnHeaderLayout->addWidget(gemSummaryLabel);
|
||||
|
||||
QSpacerItem* horizontalSpacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum);
|
||||
columnHeaderLayout->addSpacerItem(horizontalSpacer);
|
||||
|
||||
QLabel* gemSelectedLabel = new QLabel(tr("Selected"));
|
||||
gemSelectedLabel->setStyleSheet("font-size: 12px;");
|
||||
columnHeaderLayout->addWidget(gemSelectedLabel);
|
||||
|
||||
columnHeaderLayout->addSpacing(60);
|
||||
|
||||
vLayout->addLayout(columnHeaderLayout);
|
||||
}
|
||||
} // namespace O3DE::ProjectManager
|
||||
|
||||
@@ -235,4 +235,19 @@ namespace O3DE::ProjectManager
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int GemModel::TotalAddedGems() const
|
||||
{
|
||||
int result = 0;
|
||||
for (int row = 0; row < rowCount(); ++row)
|
||||
{
|
||||
const QModelIndex modelIndex = index(row, 0);
|
||||
if (IsAdded(modelIndex))
|
||||
{
|
||||
++result;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace O3DE::ProjectManager
|
||||
|
||||
@@ -63,6 +63,8 @@ namespace O3DE::ProjectManager
|
||||
QVector<QModelIndex> GatherGemsToBeAdded() const;
|
||||
QVector<QModelIndex> GatherGemsToBeRemoved() const;
|
||||
|
||||
int TotalAddedGems() const;
|
||||
|
||||
private:
|
||||
enum UserRole
|
||||
{
|
||||
|
||||
@@ -37,6 +37,16 @@ namespace O3DE::ProjectManager
|
||||
return false;
|
||||
}
|
||||
|
||||
// Gem status
|
||||
if (m_gemStatusFilter != GemStatus::NoFilter)
|
||||
{
|
||||
const GemStatus sourceGemStatus = static_cast<GemStatus>(GemModel::IsAdded(sourceIndex));
|
||||
if (m_gemStatusFilter != sourceGemStatus)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Gem origins
|
||||
if (m_gemOriginFilter)
|
||||
{
|
||||
@@ -125,6 +135,19 @@ namespace O3DE::ProjectManager
|
||||
return true;
|
||||
}
|
||||
|
||||
QString GemSortFilterProxyModel::GetGemStatusString(GemStatus status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case Unselected:
|
||||
return "Unselected";
|
||||
case Selected:
|
||||
return "Selected";
|
||||
default:
|
||||
return "<Unknown Gem Status>";
|
||||
}
|
||||
}
|
||||
|
||||
void GemSortFilterProxyModel::InvalidateFilter()
|
||||
{
|
||||
invalidate();
|
||||
|
||||
@@ -29,8 +29,17 @@ namespace O3DE::ProjectManager
|
||||
Q_OBJECT // AUTOMOC
|
||||
|
||||
public:
|
||||
enum GemStatus
|
||||
{
|
||||
NoFilter = -1,
|
||||
Unselected,
|
||||
Selected
|
||||
};
|
||||
|
||||
GemSortFilterProxyModel(GemModel* sourceModel, QObject* parent = nullptr);
|
||||
|
||||
static QString GetGemStatusString(GemStatus status);
|
||||
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override;
|
||||
|
||||
GemModel* GetSourceModel() const { return m_sourceModel; }
|
||||
@@ -38,6 +47,9 @@ namespace O3DE::ProjectManager
|
||||
|
||||
void SetSearchString(const QString& searchString) { m_searchString = searchString; InvalidateFilter(); }
|
||||
|
||||
GemStatus GetGemStatus() const { return m_gemStatusFilter; }
|
||||
void SetGemStatus(GemStatus gemStatus) { m_gemStatusFilter = gemStatus; InvalidateFilter(); }
|
||||
|
||||
GemInfo::GemOrigins GetGemOrigins() const { return m_gemOriginFilter; }
|
||||
void SetGemOrigins(const GemInfo::GemOrigins& gemOrigins) { m_gemOriginFilter = gemOrigins; InvalidateFilter(); }
|
||||
|
||||
@@ -61,6 +73,7 @@ namespace O3DE::ProjectManager
|
||||
AzQtComponents::SelectionProxyModel* m_selectionProxyModel = nullptr;
|
||||
|
||||
QString m_searchString;
|
||||
GemStatus m_gemStatusFilter = GemStatus::NoFilter;
|
||||
GemInfo::GemOrigins m_gemOriginFilter = {};
|
||||
GemInfo::Platforms m_platformFilter = {};
|
||||
GemInfo::Types m_typeFilter = {};
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace O3DE::ProjectManager
|
||||
{
|
||||
setObjectName("labelButton");
|
||||
|
||||
QVBoxLayout* vLayout = new QVBoxLayout(this);
|
||||
QVBoxLayout* vLayout = new QVBoxLayout();
|
||||
vLayout->setContentsMargins(0, 0, 0, 0);
|
||||
vLayout->setSpacing(5);
|
||||
|
||||
@@ -151,7 +151,6 @@ namespace O3DE::ProjectManager
|
||||
|
||||
void ProjectButton::ReadySetup()
|
||||
{
|
||||
connect(m_projectImageLabel, &LabelButton::triggered, [this]() { emit OpenProject(m_projectInfo.m_path); });
|
||||
connect(m_projectImageLabel->GetBuildButton(), &QPushButton::clicked, [this](){ emit BuildProject(m_projectInfo); });
|
||||
|
||||
QMenu* menu = new QMenu(this);
|
||||
|
||||
@@ -13,21 +13,11 @@
|
||||
#include <ProjectManagerWindow.h>
|
||||
#include <ScreensCtrl.h>
|
||||
|
||||
#include <AzQtComponents/Components/StyleManager.h>
|
||||
#include <AzCore/IO/FileIO.h>
|
||||
#include <AzCore/IO/Path/Path.h>
|
||||
#include <AzFramework/CommandLine/CommandLine.h>
|
||||
#include <AzFramework/Application/Application.h>
|
||||
|
||||
#include <QDir>
|
||||
|
||||
namespace O3DE::ProjectManager
|
||||
{
|
||||
ProjectManagerWindow::ProjectManagerWindow(QWidget* parent, const AZ::IO::PathView& engineRootPath, const AZ::IO::PathView& projectPath, ProjectManagerScreen startScreen)
|
||||
ProjectManagerWindow::ProjectManagerWindow(QWidget* parent, const AZ::IO::PathView& projectPath, ProjectManagerScreen startScreen)
|
||||
: QMainWindow(parent)
|
||||
{
|
||||
m_pythonBindings = AZStd::make_unique<PythonBindings>(engineRootPath);
|
||||
|
||||
setWindowTitle(tr("O3DE Project Manager"));
|
||||
|
||||
ScreensCtrl* screensCtrl = new ScreensCtrl();
|
||||
@@ -44,15 +34,6 @@ namespace O3DE::ProjectManager
|
||||
|
||||
setCentralWidget(screensCtrl);
|
||||
|
||||
// setup stylesheets and hot reloading
|
||||
QDir rootDir = QString::fromUtf8(engineRootPath.Native().data(), aznumeric_cast<int>(engineRootPath.Native().size()));
|
||||
const auto pathOnDisk = rootDir.absoluteFilePath("Code/Tools/ProjectManager/Resources");
|
||||
const auto qrcPath = QStringLiteral(":/ProjectManager/style");
|
||||
AzQtComponents::StyleManager::addSearchPaths("style", pathOnDisk, qrcPath, engineRootPath);
|
||||
|
||||
// set stylesheet after creating the screens or their styles won't get updated
|
||||
AzQtComponents::StyleManager::setStyleSheet(this, QStringLiteral("style:ProjectManager.qss"));
|
||||
|
||||
// always push the projects screen first so we have something to come back to
|
||||
if (startScreen != ProjectManagerScreen::Projects)
|
||||
{
|
||||
@@ -66,10 +47,4 @@ namespace O3DE::ProjectManager
|
||||
emit screensCtrl->NotifyCurrentProject(path);
|
||||
}
|
||||
}
|
||||
|
||||
ProjectManagerWindow::~ProjectManagerWindow()
|
||||
{
|
||||
m_pythonBindings.reset();
|
||||
}
|
||||
|
||||
} // namespace O3DE::ProjectManager
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
#if !defined(Q_MOC_RUN)
|
||||
#include <QMainWindow>
|
||||
#include <PythonBindings.h>
|
||||
#include <AzCore/IO/Path/Path.h>
|
||||
#include <ScreenDefs.h>
|
||||
#endif
|
||||
|
||||
@@ -25,12 +25,8 @@ namespace O3DE::ProjectManager
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ProjectManagerWindow(QWidget* parent, const AZ::IO::PathView& engineRootPath, const AZ::IO::PathView& projectPath,
|
||||
explicit ProjectManagerWindow(QWidget* parent, const AZ::IO::PathView& projectPath,
|
||||
ProjectManagerScreen startScreen = ProjectManagerScreen::Projects);
|
||||
~ProjectManagerWindow();
|
||||
|
||||
private:
|
||||
AZStd::unique_ptr<PythonBindings> m_pythonBindings;
|
||||
};
|
||||
|
||||
} // namespace O3DE::ProjectManager
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace O3DE::ProjectManager
|
||||
// if we don't set this in a frame (just use a sub-layout) all the content will align incorrectly horizontally
|
||||
QFrame* projectSettingsFrame = new QFrame(this);
|
||||
projectSettingsFrame->setObjectName("projectSettings");
|
||||
m_verticalLayout = new QVBoxLayout(this);
|
||||
m_verticalLayout = new QVBoxLayout();
|
||||
|
||||
// you cannot remove content margins in qss
|
||||
m_verticalLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
@@ -85,7 +85,7 @@ namespace O3DE::ProjectManager
|
||||
QFrame* frame = new QFrame(this);
|
||||
frame->setObjectName("firstTimeContent");
|
||||
{
|
||||
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||
QVBoxLayout* layout = new QVBoxLayout();
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setAlignment(Qt::AlignTop);
|
||||
frame->setLayout(layout);
|
||||
@@ -100,7 +100,7 @@ namespace O3DE::ProjectManager
|
||||
"available by downloading our sample project."));
|
||||
layout->addWidget(introLabel);
|
||||
|
||||
QHBoxLayout* buttonLayout = new QHBoxLayout(this);
|
||||
QHBoxLayout* buttonLayout = new QHBoxLayout();
|
||||
buttonLayout->setAlignment(Qt::AlignLeft);
|
||||
buttonLayout->setSpacing(s_spacerSize);
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ namespace O3DE::ProjectManager
|
||||
PythonBindings::PythonBindings(const AZ::IO::PathView& enginePath)
|
||||
: m_enginePath(enginePath)
|
||||
{
|
||||
StartPython();
|
||||
m_pythonStarted = StartPython();
|
||||
}
|
||||
|
||||
PythonBindings::~PythonBindings()
|
||||
@@ -234,6 +234,11 @@ namespace O3DE::ProjectManager
|
||||
StopPython();
|
||||
}
|
||||
|
||||
bool PythonBindings::PythonStarted()
|
||||
{
|
||||
return m_pythonStarted && Py_IsInitialized();
|
||||
}
|
||||
|
||||
bool PythonBindings::StartPython()
|
||||
{
|
||||
if (Py_IsInitialized())
|
||||
@@ -246,7 +251,7 @@ namespace O3DE::ProjectManager
|
||||
AZStd::string pyBasePath = Platform::GetPythonHomePath(PY_PACKAGE, m_enginePath.c_str());
|
||||
if (!AZ::IO::SystemFile::Exists(pyBasePath.c_str()))
|
||||
{
|
||||
AZ_Warning("python", false, "Python home path must exist. path:%s", pyBasePath.c_str());
|
||||
AZ_Error("python", false, "Python home path does not exist: %s", pyBasePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -277,11 +282,12 @@ namespace O3DE::ProjectManager
|
||||
AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
|
||||
pybind11::gil_scoped_acquire acquire;
|
||||
|
||||
// Setup sys.path
|
||||
int result = PyRun_SimpleString("import sys");
|
||||
AZ_Warning("ProjectManagerWindow", result != -1, "Import sys failed");
|
||||
result = PyRun_SimpleString(AZStd::string::format("sys.path.append('%s')", m_enginePath.c_str()).c_str());
|
||||
AZ_Warning("ProjectManagerWindow", result != -1, "Append to sys path failed");
|
||||
// sanity import check
|
||||
if (PyRun_SimpleString("import sys") != 0)
|
||||
{
|
||||
AZ_Assert(false, "Import sys failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// import required modules
|
||||
m_cmake = pybind11::module::import("o3de.cmake");
|
||||
@@ -295,11 +301,11 @@ namespace O3DE::ProjectManager
|
||||
// make sure the engine is registered
|
||||
RegisterThisEngine();
|
||||
|
||||
return result == 0 && !PyErr_Occurred();
|
||||
return !PyErr_Occurred();
|
||||
}
|
||||
catch ([[maybe_unused]] const std::exception& e)
|
||||
{
|
||||
AZ_Warning("ProjectManagerWindow", false, "Py_Initialize() failed with %s", e.what());
|
||||
AZ_Assert(false, "Py_Initialize() failed with %s", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -350,6 +356,11 @@ namespace O3DE::ProjectManager
|
||||
|
||||
AZ::Outcome<void, AZStd::string> PythonBindings::ExecuteWithLockErrorHandling(AZStd::function<void()> executionCallback)
|
||||
{
|
||||
if (!Py_IsInitialized())
|
||||
{
|
||||
return AZ::Failure<AZStd::string>("Python is not initialized");
|
||||
}
|
||||
|
||||
AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
|
||||
pybind11::gil_scoped_release release;
|
||||
pybind11::gil_scoped_acquire acquire;
|
||||
@@ -452,9 +463,9 @@ namespace O3DE::ProjectManager
|
||||
return result;
|
||||
}
|
||||
|
||||
AZ::Outcome<GemInfo> PythonBindings::GetGemInfo(const QString& path)
|
||||
AZ::Outcome<GemInfo> PythonBindings::GetGemInfo(const QString& path, const QString& projectPath)
|
||||
{
|
||||
GemInfo gemInfo = GemInfoFromPath(pybind11::str(path.toStdString()));
|
||||
GemInfo gemInfo = GemInfoFromPath(pybind11::str(path.toStdString()), pybind11::str(projectPath.toStdString()));
|
||||
if (gemInfo.IsValid())
|
||||
{
|
||||
return AZ::Success(AZStd::move(gemInfo));
|
||||
@@ -473,7 +484,7 @@ namespace O3DE::ProjectManager
|
||||
{
|
||||
for (auto path : m_manifest.attr("get_engine_gems")())
|
||||
{
|
||||
gems.push_back(GemInfoFromPath(path));
|
||||
gems.push_back(GemInfoFromPath(path, pybind11::none()));
|
||||
}
|
||||
});
|
||||
if (!result.IsSuccess())
|
||||
@@ -494,7 +505,7 @@ namespace O3DE::ProjectManager
|
||||
pybind11::str pyProjectPath = projectPath.toStdString();
|
||||
for (auto path : m_manifest.attr("get_all_gems")(pyProjectPath))
|
||||
{
|
||||
gems.push_back(GemInfoFromPath(path));
|
||||
gems.push_back(GemInfoFromPath(path, pyProjectPath));
|
||||
}
|
||||
});
|
||||
if (!result.IsSuccess())
|
||||
@@ -632,12 +643,12 @@ namespace O3DE::ProjectManager
|
||||
}
|
||||
}
|
||||
|
||||
GemInfo PythonBindings::GemInfoFromPath(pybind11::handle path)
|
||||
GemInfo PythonBindings::GemInfoFromPath(pybind11::handle path, pybind11::handle pyProjectPath)
|
||||
{
|
||||
GemInfo gemInfo;
|
||||
gemInfo.m_path = Py_To_String(path);
|
||||
|
||||
auto data = m_manifest.attr("get_gem_json_data")(pybind11::none(), path);
|
||||
auto data = m_manifest.attr("get_gem_json_data")(pybind11::none(), path, pyProjectPath);
|
||||
if (pybind11::isinstance<pybind11::dict>(data))
|
||||
{
|
||||
try
|
||||
@@ -782,12 +793,12 @@ namespace O3DE::ProjectManager
|
||||
});
|
||||
}
|
||||
|
||||
ProjectTemplateInfo PythonBindings::ProjectTemplateInfoFromPath(pybind11::handle path)
|
||||
ProjectTemplateInfo PythonBindings::ProjectTemplateInfoFromPath(pybind11::handle path, pybind11::handle pyProjectPath)
|
||||
{
|
||||
ProjectTemplateInfo templateInfo;
|
||||
templateInfo.m_path = Py_To_String(pybind11::str(path));
|
||||
|
||||
auto data = m_manifest.attr("get_template_json_data")(pybind11::none(), path);
|
||||
auto data = m_manifest.attr("get_template_json_data")(pybind11::none(), path, pyProjectPath);
|
||||
if (pybind11::isinstance<pybind11::dict>(data))
|
||||
{
|
||||
try
|
||||
@@ -829,14 +840,15 @@ namespace O3DE::ProjectManager
|
||||
return templateInfo;
|
||||
}
|
||||
|
||||
AZ::Outcome<QVector<ProjectTemplateInfo>> PythonBindings::GetProjectTemplates()
|
||||
AZ::Outcome<QVector<ProjectTemplateInfo>> PythonBindings::GetProjectTemplates(const QString& projectPath)
|
||||
{
|
||||
QVector<ProjectTemplateInfo> templates;
|
||||
|
||||
bool result = ExecuteWithLock([&] {
|
||||
pybind11::str pyProjectPath = projectPath.toStdString();
|
||||
for (auto path : m_manifest.attr("get_templates_for_project_creation")())
|
||||
{
|
||||
templates.push_back(ProjectTemplateInfoFromPath(path));
|
||||
templates.push_back(ProjectTemplateInfoFromPath(path, pyProjectPath));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -34,12 +34,14 @@ namespace O3DE::ProjectManager
|
||||
~PythonBindings() override;
|
||||
|
||||
// PythonBindings overrides
|
||||
bool PythonStarted() override;
|
||||
|
||||
// Engine
|
||||
AZ::Outcome<EngineInfo> GetEngineInfo() override;
|
||||
bool SetEngineInfo(const EngineInfo& engineInfo) override;
|
||||
|
||||
// Gem
|
||||
AZ::Outcome<GemInfo> GetGemInfo(const QString& path) override;
|
||||
AZ::Outcome<GemInfo> GetGemInfo(const QString& path, const QString& projectPath = {}) override;
|
||||
AZ::Outcome<QVector<GemInfo>, AZStd::string> GetEngineGemInfos() override;
|
||||
AZ::Outcome<QVector<GemInfo>, AZStd::string> GetAllGemInfos(const QString& projectPath) override;
|
||||
AZ::Outcome<QVector<AZStd::string>, AZStd::string> GetEnabledGemNames(const QString& projectPath) override;
|
||||
@@ -55,21 +57,23 @@ namespace O3DE::ProjectManager
|
||||
AZ::Outcome<void, AZStd::string> RemoveGemFromProject(const QString& gemPath, const QString& projectPath) override;
|
||||
|
||||
// ProjectTemplate
|
||||
AZ::Outcome<QVector<ProjectTemplateInfo>> GetProjectTemplates() override;
|
||||
AZ::Outcome<QVector<ProjectTemplateInfo>> GetProjectTemplates(const QString& projectPath = {}) override;
|
||||
|
||||
private:
|
||||
AZ_DISABLE_COPY_MOVE(PythonBindings);
|
||||
|
||||
AZ::Outcome<void, AZStd::string> ExecuteWithLockErrorHandling(AZStd::function<void()> executionCallback);
|
||||
bool ExecuteWithLock(AZStd::function<void()> executionCallback);
|
||||
GemInfo GemInfoFromPath(pybind11::handle path);
|
||||
GemInfo GemInfoFromPath(pybind11::handle path, pybind11::handle pyProjectPath);
|
||||
ProjectInfo ProjectInfoFromPath(pybind11::handle path);
|
||||
ProjectTemplateInfo ProjectTemplateInfoFromPath(pybind11::handle path);
|
||||
ProjectTemplateInfo ProjectTemplateInfoFromPath(pybind11::handle path, pybind11::handle pyProjectPath);
|
||||
bool RegisterThisEngine();
|
||||
bool StartPython();
|
||||
bool StopPython();
|
||||
|
||||
|
||||
bool m_pythonStarted = false;
|
||||
|
||||
AZ::IO::FixedMaxPath m_enginePath;
|
||||
pybind11::handle m_engineTemplate;
|
||||
AZStd::recursive_mutex m_lock;
|
||||
|
||||
@@ -34,6 +34,12 @@ namespace O3DE::ProjectManager
|
||||
IPythonBindings() = default;
|
||||
virtual ~IPythonBindings() = default;
|
||||
|
||||
/**
|
||||
* Get whether Python was started or not. All Python functionality will fail if Python
|
||||
* failed to start.
|
||||
* @return true if Python was started successfully, false on failure
|
||||
*/
|
||||
virtual bool PythonStarted() = 0;
|
||||
|
||||
// Engine
|
||||
|
||||
@@ -57,7 +63,7 @@ namespace O3DE::ProjectManager
|
||||
* @param path the absolute path to the Gem
|
||||
* @return an outcome with GemInfo on success
|
||||
*/
|
||||
virtual AZ::Outcome<GemInfo> GetGemInfo(const QString& path) = 0;
|
||||
virtual AZ::Outcome<GemInfo> GetGemInfo(const QString& path, const QString& projectPath = {}) = 0;
|
||||
|
||||
/**
|
||||
* Get all available gem infos. This concatenates gems registered by the engine and the project.
|
||||
@@ -147,7 +153,7 @@ namespace O3DE::ProjectManager
|
||||
* Get info about all known project templates
|
||||
* @return an outcome with ProjectTemplateInfos on success
|
||||
*/
|
||||
virtual AZ::Outcome<QVector<ProjectTemplateInfo>> GetProjectTemplates() = 0;
|
||||
virtual AZ::Outcome<QVector<ProjectTemplateInfo>> GetProjectTemplates(const QString& projectPath = {}) = 0;
|
||||
};
|
||||
|
||||
using PythonBindingsInterface = AZ::Interface<IPythonBindings>;
|
||||
|
||||
@@ -189,11 +189,13 @@ namespace O3DE::ProjectManager
|
||||
{
|
||||
if (m_stack->currentIndex() == ScreenOrder::Gems)
|
||||
{
|
||||
m_header->setSubTitle(QString(tr("Configure Gems for \"%1\"")).arg(m_projectInfo.m_projectName));
|
||||
m_nextButton->setText(tr("Confirm"));
|
||||
m_header->setTitle(QString(tr("Edit Project Settings: \"%1\"")).arg(m_projectInfo.m_projectName));
|
||||
m_header->setSubTitle(QString(tr("Configure Gems")));
|
||||
m_nextButton->setText(tr("Finalize"));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_header->setTitle("");
|
||||
m_header->setSubTitle(QString(tr("Edit Project Settings: \"%1\"")).arg(m_projectInfo.m_projectName));
|
||||
m_nextButton->setText(tr("Save"));
|
||||
}
|
||||
|
||||
@@ -10,85 +10,26 @@
|
||||
*
|
||||
*/
|
||||
|
||||
#include <AzQtComponents/Utilities/HandleDpiAwareness.h>
|
||||
#include <AzQtComponents/Components/StyleManager.h>
|
||||
#include <AzCore/Component/ComponentApplication.h>
|
||||
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
|
||||
#include <AzCore/IO/Path/Path.h>
|
||||
#include <AzFramework/CommandLine/CommandLine.h>
|
||||
|
||||
#include <ProjectManagerWindow.h>
|
||||
#include <ProjectUtils.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCoreApplication>
|
||||
#include <QGuiApplication>
|
||||
|
||||
using namespace O3DE::ProjectManager;
|
||||
#include <AzQtComponents/Utilities/QtPluginPaths.h>
|
||||
#include <Application.h>
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
QApplication::setOrganizationName("O3DE");
|
||||
QApplication::setOrganizationDomain("o3de.org");
|
||||
QCoreApplication::setApplicationName("ProjectManager");
|
||||
QCoreApplication::setApplicationVersion("1.0");
|
||||
|
||||
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||
QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
|
||||
QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings);
|
||||
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
|
||||
AzQtComponents::Utilities::HandleDpiAwareness(AzQtComponents::Utilities::SystemDpiAware);
|
||||
|
||||
AZ::AllocatorInstance<AZ::SystemAllocator>::Create();
|
||||
int runSuccess = 0;
|
||||
|
||||
// Call before using any Qt, or the app may not be able to locate Qt libs
|
||||
AzQtComponents::PrepareQtPaths();
|
||||
|
||||
O3DE::ProjectManager::Application application(&argc, &argv);
|
||||
if (!application.Init())
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
|
||||
// Need to use settings registry to get EngineRootFolder
|
||||
AZ::IO::FixedMaxPath engineRootPath;
|
||||
{
|
||||
AZ::ComponentApplication componentApplication;
|
||||
auto settingsRegistry = AZ::SettingsRegistry::Get();
|
||||
settingsRegistry->Get(engineRootPath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder);
|
||||
}
|
||||
|
||||
AzQtComponents::StyleManager styleManager(&app);
|
||||
styleManager.initialize(&app, engineRootPath);
|
||||
|
||||
// Get the initial start screen if one is provided via command line
|
||||
constexpr char optionPrefix[] = "--";
|
||||
AZ::CommandLine commandLine(optionPrefix);
|
||||
commandLine.Parse(argc, argv);
|
||||
|
||||
ProjectManagerScreen startScreen = ProjectManagerScreen::Projects;
|
||||
if(commandLine.HasSwitch("screen"))
|
||||
{
|
||||
QString screenOption = commandLine.GetSwitchValue("screen", 0).c_str();
|
||||
ProjectManagerScreen screen = ProjectUtils::GetProjectManagerScreen(screenOption);
|
||||
if (screen != ProjectManagerScreen::Invalid)
|
||||
{
|
||||
startScreen = screen;
|
||||
}
|
||||
}
|
||||
|
||||
AZ::IO::FixedMaxPath projectPath;
|
||||
if (commandLine.HasSwitch("project-path"))
|
||||
{
|
||||
projectPath = commandLine.GetSwitchValue("project-path", 0).c_str();
|
||||
}
|
||||
|
||||
ProjectManagerWindow window(nullptr, engineRootPath, projectPath, startScreen);
|
||||
window.show();
|
||||
|
||||
// somethings is preventing us from moving the window to the center of the
|
||||
// primary screen - likely an Az style or component helper
|
||||
constexpr int width = 1200;
|
||||
constexpr int height = 800;
|
||||
window.resize(width, height);
|
||||
|
||||
runSuccess = app.exec();
|
||||
AZ_Error("ProjectManager", false, "Failed to initialize");
|
||||
runSuccess = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
runSuccess = application.Run() ? 0 : 1;
|
||||
}
|
||||
AZ::AllocatorInstance<AZ::SystemAllocator>::Destroy();
|
||||
|
||||
return runSuccess;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
|
||||
set(FILES
|
||||
Resources/ProjectManager.rc
|
||||
Resources/ProjectManager.qrc
|
||||
Resources/ProjectManager.qss
|
||||
Source/main.cpp
|
||||
)
|
||||
@@ -1,4 +1,5 @@
|
||||
#
|
||||
#
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
@@ -10,9 +11,8 @@
|
||||
#
|
||||
|
||||
set(FILES
|
||||
Resources/ProjectManager.qrc
|
||||
Resources/ProjectManager.qss
|
||||
Source/main.cpp
|
||||
Source/Application.h
|
||||
Source/Application.cpp
|
||||
Source/ScreenDefs.h
|
||||
Source/ScreenFactory.h
|
||||
Source/ScreenFactory.cpp
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#
|
||||
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
# its licensors.
|
||||
#
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this
|
||||
# distribution (the "License"). All use of this software is governed by the License,
|
||||
# or, if provided, by the license below or the license accompanying this file. Do not
|
||||
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
|
||||
set(FILES
|
||||
Resources/ProjectManager.qrc
|
||||
Resources/ProjectManager.qss
|
||||
tests/ApplicationTests.cpp
|
||||
tests/main.cpp
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
* its licensors.
|
||||
*
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this
|
||||
* distribution (the "License"). All use of this software is governed by the License,
|
||||
* or, if provided, by the license below or the license accompanying this file. Do not
|
||||
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <AzCore/UnitTest/TestTypes.h>
|
||||
#include <AzCore/std/smart_ptr/make_shared.h>
|
||||
#include <Application.h>
|
||||
#include <ProjectManager_Test_Traits_Platform.h>
|
||||
|
||||
namespace O3DE::ProjectManager
|
||||
{
|
||||
class ProjectManagerApplicationTests
|
||||
: public ::UnitTest::ScopedAllocatorSetupFixture
|
||||
{
|
||||
public:
|
||||
|
||||
ProjectManagerApplicationTests()
|
||||
{
|
||||
m_application = AZStd::make_unique<ProjectManager::Application>();
|
||||
}
|
||||
|
||||
~ProjectManagerApplicationTests()
|
||||
{
|
||||
m_application.reset();
|
||||
}
|
||||
|
||||
AZStd::unique_ptr<ProjectManager::Application> m_application;
|
||||
};
|
||||
|
||||
#if AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS
|
||||
TEST_F(ProjectManagerApplicationTests, DISABLED_Application_Init_Succeeds)
|
||||
#else
|
||||
TEST_F(ProjectManagerApplicationTests, Application_Init_Succeeds)
|
||||
#endif // !AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS
|
||||
{
|
||||
// we don't want to interact with actual GUI or display it
|
||||
EXPECT_TRUE(m_application->Init(/*interactive=*/false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
* its licensors.
|
||||
*
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this
|
||||
* distribution (the "License"). All use of this software is governed by the License,
|
||||
* or, if provided, by the license below or the license accompanying this file. Do not
|
||||
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <AzTest/AzTest.h>
|
||||
|
||||
DECLARE_AZ_UNIT_TEST_MAIN();
|
||||
|
||||
int runDefaultRunner(int argc, char* argv[])
|
||||
{
|
||||
INVOKE_AZ_UNIT_TEST_MAIN(nullptr)
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
if (argc == 1)
|
||||
{
|
||||
// if no parameters are provided, add the --unittests parameter
|
||||
constexpr int defaultArgc = 2;
|
||||
char unittest_arg[] = "--unittests"; // Conversion from string literal to char* is not allowed per ISO C++11
|
||||
char* defaultArgv[defaultArgc] = { argv[0], unittest_arg };
|
||||
return runDefaultRunner(defaultArgc, defaultArgv);
|
||||
}
|
||||
INVOKE_AZ_UNIT_TEST_MAIN(nullptr);
|
||||
return 0;
|
||||
}
|
||||
@@ -46,22 +46,69 @@ namespace AZ
|
||||
serializeContext->Class<AssImpTransformImporter, SceneCore::LoadingComponent>()->Version(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void GetAllBones(const aiScene* scene, AZStd::unordered_map<AZStd::string, const aiBone*>& boneLookup)
|
||||
{
|
||||
for (unsigned meshIndex = 0; meshIndex < scene->mNumMeshes; ++meshIndex)
|
||||
{
|
||||
const aiMesh* mesh = scene->mMeshes[meshIndex];
|
||||
|
||||
for (unsigned boneIndex = 0; boneIndex < mesh->mNumBones; ++boneIndex)
|
||||
{
|
||||
const aiBone* bone = mesh->mBones[boneIndex];
|
||||
|
||||
boneLookup[bone->mName.C_Str()] = bone;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Events::ProcessingResult AssImpTransformImporter::ImportTransform(AssImpSceneNodeAppendedContext& context)
|
||||
{
|
||||
AZ_TraceContext("Importer", "transform");
|
||||
const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
|
||||
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
|
||||
|
||||
|
||||
if (currentNode == scene->mRootNode || IsPivotNode(currentNode->mName))
|
||||
{
|
||||
return Events::ProcessingResult::Ignored;
|
||||
}
|
||||
|
||||
aiMatrix4x4 combinedTransform = GetConcatenatedLocalTransform(currentNode);
|
||||
AZStd::unordered_map<AZStd::string, const aiBone*> boneLookup;
|
||||
GetAllBones(scene, boneLookup);
|
||||
|
||||
auto boneIterator = boneLookup.find(currentNode->mName.C_Str());
|
||||
const bool isBone = boneIterator != boneLookup.end();
|
||||
|
||||
aiMatrix4x4 combinedTransform;
|
||||
|
||||
if (isBone)
|
||||
{
|
||||
auto parentNode = currentNode->mParent;
|
||||
|
||||
aiMatrix4x4 offsetMatrix = boneIterator->second->mOffsetMatrix;
|
||||
aiMatrix4x4 parentOffset {};
|
||||
|
||||
auto parentBoneIterator = boneLookup.find(parentNode->mName.C_Str());
|
||||
|
||||
if (parentNode && parentBoneIterator != boneLookup.end())
|
||||
{
|
||||
const auto& parentBone = parentBoneIterator->second;
|
||||
|
||||
parentOffset = parentBone->mOffsetMatrix;
|
||||
}
|
||||
|
||||
auto inverseOffset = offsetMatrix;
|
||||
inverseOffset.Inverse();
|
||||
|
||||
combinedTransform = parentOffset * inverseOffset;
|
||||
}
|
||||
else
|
||||
{
|
||||
combinedTransform = GetConcatenatedLocalTransform(currentNode);
|
||||
}
|
||||
|
||||
DataTypes::MatrixType localTransform = AssImpSDKWrapper::AssImpTypeConverter::ToTransform(combinedTransform);
|
||||
|
||||
|
||||
context.m_sourceSceneSystem.SwapTransformForUpAxis(localTransform);
|
||||
context.m_sourceSceneSystem.ConvertUnit(localTransform);
|
||||
|
||||
@@ -105,9 +152,7 @@ namespace AZ
|
||||
}
|
||||
else
|
||||
{
|
||||
bool addedData = context.m_scene.GetGraph().SetContent(
|
||||
context.m_currentGraphPosition,
|
||||
transformData);
|
||||
bool addedData = context.m_scene.GetGraph().SetContent(context.m_currentGraphPosition, transformData);
|
||||
|
||||
AZ_Error(SceneAPI::Utilities::ErrorWindow, addedData, "Failed to add node data");
|
||||
return addedData ? Events::ProcessingResult::Success : Events::ProcessingResult::Failure;
|
||||
|
||||
@@ -69,13 +69,14 @@ namespace AZ
|
||||
// aiProcess_JoinIdenticalVertices is not enabled because O3DE has a mesh optimizer that also does this,
|
||||
// this flag is disabled to keep AssImp output similar to FBX SDK to reduce downstream bugs for the initial AssImp release.
|
||||
// There's currently a minimum of properties and flags set to maximize compatibility with the existing node graph.
|
||||
|
||||
// aiProcess_LimitBoneWeights is not enabled because it will remove bones which are not associated with a mesh.
|
||||
// This results in the loss of the offset matrix data for nodes without a mesh which is required for the Transform Importer.
|
||||
m_importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false);
|
||||
m_importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, false);
|
||||
m_sceneFileName = fileName;
|
||||
m_assImpScene = m_importer.ReadFile(fileName,
|
||||
aiProcess_Triangulate //Triangulates all faces of all meshes
|
||||
| aiProcess_LimitBoneWeights //Limits the number of bones that can affect a vertex to a maximum value
|
||||
//dropping the least important and re-normalizing
|
||||
| aiProcess_GenNormals); //Generate normals for meshes
|
||||
|
||||
#if AZ_TRAIT_COMPILER_SUPPORT_CSIGNAL
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace AZ
|
||||
Application::Application(int argc, char** argv)
|
||||
: AzToolsFramework::ToolsApplication(&argc, &argv)
|
||||
{
|
||||
// We need a specialized variant of EditorEntityContextCompnent for the SliceConverter, so we register the descriptor here.
|
||||
// We need a specialized variant of EditorEntityContextComponent for the SliceConverter, so we register the descriptor here.
|
||||
RegisterComponentDescriptor(AzToolsFramework::SliceConverterEditorEntityContextComponent::CreateDescriptor());
|
||||
|
||||
AZ::IO::FixedMaxPath projectPath = AZ::Utils::GetProjectPath();
|
||||
|
||||
@@ -128,8 +128,7 @@ namespace AZ
|
||||
return result;
|
||||
}
|
||||
|
||||
bool SliceConverter::ConvertSliceFile(
|
||||
AZ::SerializeContext* serializeContext, const AZStd::string& slicePath, bool isDryRun)
|
||||
bool SliceConverter::ConvertSliceFile(AZ::SerializeContext* serializeContext, const AZStd::string& slicePath, bool isDryRun)
|
||||
{
|
||||
/* To convert a slice file, we read the input file in via ObjectStream, then use the "class ready" callback to convert
|
||||
* the data in memory to a Prefab.
|
||||
@@ -177,11 +176,23 @@ namespace AZ
|
||||
}
|
||||
|
||||
AZ::Entity* rootEntity = reinterpret_cast<AZ::Entity*>(classPtr);
|
||||
return ConvertSliceToPrefab(context, outputPath, isDryRun, rootEntity);
|
||||
bool convertResult = ConvertSliceToPrefab(context, outputPath, isDryRun, rootEntity);
|
||||
|
||||
// Delete the root entity pointer. Otherwise, it will leak itself along with all of the slice asset references held
|
||||
// within it.
|
||||
delete rootEntity;
|
||||
return convertResult;
|
||||
};
|
||||
|
||||
// Read in the slice file and call the callback on completion to convert the read-in slice to a prefab.
|
||||
if (!Utilities::InspectSerializedFile(inputPath.c_str(), serializeContext, callback))
|
||||
// This will also load dependent slice assets, but no other dependent asset types.
|
||||
// Since we're not actually initializing any of the entities, we don't need any of the non-slice assets to be loaded.
|
||||
if (!Utilities::InspectSerializedFile(
|
||||
inputPath.c_str(), serializeContext, callback,
|
||||
[](const AZ::Data::AssetFilterInfo& filterInfo)
|
||||
{
|
||||
return (filterInfo.m_assetType == azrtti_typeid<AZ::SliceAsset>());
|
||||
}))
|
||||
{
|
||||
AZ_Warning("Convert-Slice", false, "Failed to load '%s'. File may not contain an object stream.", inputPath.c_str());
|
||||
result = false;
|
||||
@@ -219,8 +230,12 @@ namespace AZ
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get all of the entities from the slice.
|
||||
// Get all of the entities from the slice. We're taking ownership of them, so we also remove them from the slice component
|
||||
// without deleting them.
|
||||
constexpr bool deleteEntities = false;
|
||||
constexpr bool removeEmptyInstances = true;
|
||||
SliceComponent::EntityList sliceEntities = sliceComponent->GetNewEntities();
|
||||
sliceComponent->RemoveAllEntities(deleteEntities, removeEmptyInstances);
|
||||
AZ_Printf("Convert-Slice", " Slice contains %zu entities.\n", sliceEntities.size());
|
||||
|
||||
// Create the Prefab with the entities from the slice.
|
||||
@@ -256,13 +271,19 @@ namespace AZ
|
||||
for (auto& alias : entityAliases)
|
||||
{
|
||||
auto id = sourceInstance->GetEntityId(alias);
|
||||
auto result = m_aliasIdMapper.emplace(TemplateEntityIdPair(templateId, id), alias);
|
||||
auto result = m_aliasIdMapper.emplace(id, SliceEntityMappingInfo(templateId, alias));
|
||||
if (!result.second)
|
||||
{
|
||||
AZ_Printf("Convert-Slice", " Duplicate entity alias -> entity id entries found, conversion may not be successful.\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Save off a mapping of the slice's metadata entity ID as well, even though we never converted the entity itself.
|
||||
// This will help us better detect entity ID mapping errors for nested slice instances.
|
||||
AZ::Entity* metadataEntity = sliceComponent->GetMetadataEntity();
|
||||
constexpr bool isMetadataEntity = true;
|
||||
m_aliasIdMapper.emplace(metadataEntity->GetId(), SliceEntityMappingInfo(templateId, "MetadataEntity", isMetadataEntity));
|
||||
|
||||
// Update the prefab template with the fixed-up data in our prefab instance.
|
||||
AzToolsFramework::Prefab::PrefabDom prefabDom;
|
||||
bool storeResult = AzToolsFramework::Prefab::PrefabDomUtils::StoreInstanceInPrefabDom(*sourceInstance, prefabDom);
|
||||
@@ -342,10 +363,9 @@ namespace AZ
|
||||
// For each nested slice, convert it.
|
||||
for (auto& slice : sliceList)
|
||||
{
|
||||
// Get the nested slice asset
|
||||
// Get the nested slice asset. These should already be preloaded due to loading the root asset.
|
||||
auto sliceAsset = slice.GetSliceAsset();
|
||||
sliceAsset.QueueLoad();
|
||||
sliceAsset.BlockUntilLoadComplete();
|
||||
AZ_Assert(sliceAsset.IsReady(), "slice asset hasn't been loaded yet!");
|
||||
|
||||
// The slice list gives us asset IDs, and we need to get to the source path. So first we get the asset path from the ID,
|
||||
// then we get the source path from the asset path.
|
||||
@@ -393,6 +413,21 @@ namespace AZ
|
||||
"Convert-Slice", " Attaching %zu instances of nested slice '%s'.\n", instances.size(),
|
||||
nestedPrefabPath.Native().c_str());
|
||||
|
||||
// Before processing any further, save off all the known entity IDs from all the instances and how they map back to
|
||||
// the base nested prefab that they've come from (i.e. this one). As we proceed up the chain of nesting, this will
|
||||
// build out a hierarchical list of owning instances for each entity that we can trace upwards to know where to add
|
||||
// the entity into our nested prefab instance.
|
||||
// This step needs to occur *before* converting the instances themselves, because while converting instances, they
|
||||
// might have entity ID references that point to other instances. By having the full instance entity ID map in place
|
||||
// before conversion, we'll be able to fix them up appropriately.
|
||||
|
||||
for (auto& instance : instances)
|
||||
{
|
||||
AZStd::string instanceAlias = GetInstanceAlias(instance);
|
||||
UpdateSliceEntityInstanceMappings(instance.GetEntityIdToBaseMap(), instanceAlias);
|
||||
}
|
||||
|
||||
// Now that we have all the entity ID mappings, convert all the instances.
|
||||
for (auto& instance : instances)
|
||||
{
|
||||
bool instanceConvertResult = ConvertSliceInstance(instance, sliceAsset, nestedTemplate, sourceInstance);
|
||||
@@ -406,6 +441,28 @@ namespace AZ
|
||||
return true;
|
||||
}
|
||||
|
||||
AZStd::string SliceConverter::GetInstanceAlias(const AZ::SliceComponent::SliceInstance& instance)
|
||||
{
|
||||
// When creating the new instance, we would like to have deterministic instance aliases. Prefabs that depend on this one
|
||||
// will have patches that reference the alias, so if we reconvert this slice a second time, we would like it to produce
|
||||
// the same results. To get a deterministic and unique alias, we rely on the slice instance. The slice instance contains
|
||||
// a map of slice entity IDs to unique instance entity IDs. We'll just consistently use the first entry in the map as the
|
||||
// unique instance ID.
|
||||
AZStd::string instanceAlias;
|
||||
auto entityIdMap = instance.GetEntityIdMap();
|
||||
if (!entityIdMap.empty())
|
||||
{
|
||||
instanceAlias = AZStd::string::format("Instance_%s", entityIdMap.begin()->second.ToString().c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
AZ_Error("Convert-Slice", false, " Couldn't create deterministic instance alias.");
|
||||
instanceAlias = AZStd::string::format("Instance_%s", AZ::Entity::MakeId().ToString().c_str());
|
||||
}
|
||||
return instanceAlias;
|
||||
}
|
||||
|
||||
|
||||
bool SliceConverter::ConvertSliceInstance(
|
||||
AZ::SliceComponent::SliceInstance& instance,
|
||||
AZ::Data::Asset<AZ::SliceAsset>& sliceAsset,
|
||||
@@ -429,6 +486,8 @@ namespace AZ
|
||||
auto instanceToTemplateInterface = AZ::Interface<AzToolsFramework::Prefab::InstanceToTemplateInterface>::Get();
|
||||
auto prefabSystemComponentInterface = AZ::Interface<AzToolsFramework::Prefab::PrefabSystemComponentInterface>::Get();
|
||||
|
||||
AZStd::string instanceAlias = GetInstanceAlias(instance);
|
||||
|
||||
// Create a new unmodified prefab Instance for the nested slice instance.
|
||||
auto nestedInstance = AZStd::make_unique<AzToolsFramework::Prefab::Instance>();
|
||||
AzToolsFramework::Prefab::Instance::EntityList newEntities;
|
||||
@@ -465,15 +524,62 @@ namespace AZ
|
||||
auto instantiated =
|
||||
dataPatch.Apply(&sourceObjects, dependentSlice->GetSerializeContext(), filterDesc, sourceDataFlags, targetDataFlags);
|
||||
|
||||
// Run through all the instantiated entities and fix up their parent hierarchy:
|
||||
// - Invalid parents need to get set to the container.
|
||||
// - Valid parents into the top-level instance mean that the nested slice instance is also child-nested under an entity.
|
||||
// Prefabs handle this type of nesting differently - we need to set the parent to the container, and the container's
|
||||
// parent to that other instance.
|
||||
auto containerEntity = nestedInstance->GetContainerEntity();
|
||||
auto containerEntityId = containerEntity->get().GetId();
|
||||
for (auto entity : instantiated->m_entities)
|
||||
// Replace all the entities in the instance with the new patched ones. To do this, we'll remove all existing entities
|
||||
// throughout the entire nested hierarchy, then add the new patched entities back in at the appropriate place in the hierarchy.
|
||||
// (This is easier than trying to figure out what the patched data changes are - we can let the JSON patch handle it for us)
|
||||
|
||||
nestedInstance->RemoveNestedEntities(
|
||||
[](const AZStd::unique_ptr<AZ::Entity>&)
|
||||
{
|
||||
return true;
|
||||
});
|
||||
|
||||
AZStd::vector<AZStd::pair<AZ::Entity*, AzToolsFramework::Prefab::Instance*>> addedEntityList;
|
||||
|
||||
for (auto& entity : instantiated->m_entities)
|
||||
{
|
||||
auto entityEntry = m_aliasIdMapper.find(entity->GetId());
|
||||
if (entityEntry != m_aliasIdMapper.end())
|
||||
{
|
||||
auto& mappingStruct = entityEntry->second;
|
||||
|
||||
// Starting with the current nested instance, walk downwards through the nesting hierarchy until we're at the
|
||||
// correct level for this instanced entity ID, then add it. Because we're adding it with the non-instanced alias,
|
||||
// it doesn't matter what the slice's instanced entity ID is, and the JSON patch will correctly pick up the changes
|
||||
// we've made for this instance.
|
||||
AzToolsFramework::Prefab::Instance* addingInstance = nestedInstance.get();
|
||||
for (auto it = mappingStruct.m_nestedInstanceAliases.rbegin(); it != mappingStruct.m_nestedInstanceAliases.rend(); it++)
|
||||
{
|
||||
auto foundInstance = addingInstance->FindNestedInstance(*it);
|
||||
if (foundInstance.has_value())
|
||||
{
|
||||
addingInstance = &(foundInstance->get());
|
||||
}
|
||||
else
|
||||
{
|
||||
AZ_Assert(false, "Couldn't find nested instance %s", it->c_str());
|
||||
}
|
||||
}
|
||||
addingInstance->AddEntity(*entity, mappingStruct.m_entityAlias);
|
||||
addedEntityList.emplace_back(entity, addingInstance);
|
||||
}
|
||||
else
|
||||
{
|
||||
AZ_Assert(false, "Failed to find entity alias.");
|
||||
nestedInstance->AddEntity(*entity);
|
||||
addedEntityList.emplace_back(entity, nestedInstance.get());
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& [entity, addingInstance] : addedEntityList)
|
||||
{
|
||||
// Fix up the parent hierarchy:
|
||||
// - Invalid parents need to get set to the container.
|
||||
// - Valid parents into the top-level instance mean that the nested slice instance is also child-nested under an entity.
|
||||
// Prefabs handle this type of nesting differently - we need to set the parent to the container, and the container's
|
||||
// parent to that other instance.
|
||||
auto containerEntity = addingInstance->GetContainerEntity();
|
||||
auto containerEntityId = containerEntity->get().GetId();
|
||||
AzToolsFramework::Components::TransformComponent* transformComponent =
|
||||
entity->FindComponent<AzToolsFramework::Components::TransformComponent>();
|
||||
if (transformComponent)
|
||||
@@ -482,70 +588,76 @@ namespace AZ
|
||||
auto parentId = transformComponent->GetParentId();
|
||||
if (parentId.IsValid())
|
||||
{
|
||||
auto parentAlias = m_aliasIdMapper.find(TemplateEntityIdPair(topLevelInstance->GetTemplateId(), parentId));
|
||||
if (parentAlias != m_aliasIdMapper.end())
|
||||
// Look to see if the parent ID exists in the same instance (i.e. an entity in the nested slice is a
|
||||
// child of an entity in the containing slice). If this case exists, we need to adjust the parents so that
|
||||
// the child entity connects to the prefab container, and the *container* is the child of the entity in the
|
||||
// containing slice. (i.e. go from A->B to A->container->B)
|
||||
auto parentEntry = m_aliasIdMapper.find(parentId);
|
||||
if (parentEntry != m_aliasIdMapper.end())
|
||||
{
|
||||
// Set the container's parent to this entity's parent, and set this entity's parent to the container
|
||||
// (i.e. go from A->B to A->container->B)
|
||||
auto newParentId = topLevelInstance->GetEntityId(parentAlias->second);
|
||||
SetParentEntity(containerEntity->get(), newParentId, false);
|
||||
onlySetIfInvalid = false;
|
||||
auto& parentMappingInfo = parentEntry->second;
|
||||
if (parentMappingInfo.m_templateId != addingInstance->GetTemplateId())
|
||||
{
|
||||
if (topLevelInstance->GetTemplateId() == parentMappingInfo.m_templateId)
|
||||
{
|
||||
parentId = topLevelInstance->GetEntityId(parentMappingInfo.m_entityAlias);
|
||||
}
|
||||
else
|
||||
{
|
||||
AzToolsFramework::Prefab::Instance* parentInstance = addingInstance;
|
||||
|
||||
while ((parentInstance->GetParentInstance().has_value()) &&
|
||||
(parentInstance->GetTemplateId() != parentMappingInfo.m_templateId))
|
||||
{
|
||||
parentInstance = &(parentInstance->GetParentInstance()->get());
|
||||
}
|
||||
|
||||
if (parentInstance->GetTemplateId() == parentMappingInfo.m_templateId)
|
||||
{
|
||||
parentId = parentInstance->GetEntityId(parentMappingInfo.m_entityAlias);
|
||||
}
|
||||
else
|
||||
{
|
||||
AZ_Assert(false, "Could not find parent instance");
|
||||
}
|
||||
}
|
||||
|
||||
// Set the container's parent to this entity's parent, and set this entity's parent to the container
|
||||
// auto newParentId = topLevelInstance->GetEntityId(parentMappingInfo.m_entityAlias);
|
||||
SetParentEntity(containerEntity->get(), parentId, false);
|
||||
onlySetIfInvalid = false;
|
||||
}
|
||||
}
|
||||
|
||||
// If the parent ID is valid, but NOT in the top-level instance, then it's just a nested hierarchy inside
|
||||
// the slice and we don't need to adjust anything. "onlySetIfInvalid" will still be true, which means we
|
||||
// won't change the parent ID below.
|
||||
}
|
||||
|
||||
SetParentEntity(*entity, containerEntityId, onlySetIfInvalid);
|
||||
SetParentEntity(*entity, containerEntityId, onlySetIfInvalid);
|
||||
}
|
||||
}
|
||||
|
||||
// Replace all the entities in the instance with the new patched ones.
|
||||
// (This is easier than trying to figure out what the patched data changes are - we can let the JSON patch handle it for us)
|
||||
nestedInstance->RemoveNestedEntities(
|
||||
[](const AZStd::unique_ptr<AZ::Entity>&)
|
||||
{
|
||||
return true;
|
||||
});
|
||||
for (auto& entity : instantiated->m_entities)
|
||||
{
|
||||
auto entityAlias = m_aliasIdMapper.find(TemplateEntityIdPair(nestedInstance->GetTemplateId(), entity->GetId()));
|
||||
if (entityAlias != m_aliasIdMapper.end())
|
||||
{
|
||||
nestedInstance->AddEntity(*entity, entityAlias->second);
|
||||
}
|
||||
else
|
||||
{
|
||||
AZ_Assert(false, "Failed to find entity alias.");
|
||||
nestedInstance->AddEntity(*entity);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the container entity of the nested prefab to have the top-level prefab as the parent if it hasn't already gotten
|
||||
// another entity as its parent.
|
||||
{
|
||||
auto containerEntity = nestedInstance->GetContainerEntity();
|
||||
constexpr bool onlySetIfInvalid = true;
|
||||
SetParentEntity(containerEntity->get(), topLevelInstance->GetContainerEntityId(), onlySetIfInvalid);
|
||||
}
|
||||
|
||||
// After doing all of the above, run through entity references in any of the patched entities, and fix up the entity IDs to
|
||||
// match the new ones in our prefabs.
|
||||
RemapIdReferences(m_aliasIdMapper, topLevelInstance, nestedInstance.get(), instantiated, dependentSlice->GetSerializeContext());
|
||||
|
||||
// Add the nested instance itself to the top-level prefab. To do this, we need to add it to our top-level instance,
|
||||
// create a patch out of it, and patch the top-level prefab template.
|
||||
|
||||
AzToolsFramework::Prefab::PrefabDom topLevelInstanceDomBefore;
|
||||
instanceToTemplateInterface->GenerateDomForInstance(topLevelInstanceDomBefore, *topLevelInstance);
|
||||
|
||||
// When creating the new instance, we would like to have deterministic instance aliases. Prefabs that depend on this one
|
||||
// will have patches that reference the alias, so if we reconvert this slice a second time, we would like it to produce
|
||||
// the same results. To get a deterministic and unique alias, we rely on the slice instance. The slice instance contains
|
||||
// a map of slice entity IDs to unique instance entity IDs. We'll just consistently use the first entry in the map as the
|
||||
// unique instance ID.
|
||||
AZStd::string instanceAlias;
|
||||
auto entityIdMap = instance.GetEntityIdMap();
|
||||
if (!entityIdMap.empty())
|
||||
{
|
||||
instanceAlias = AZStd::string::format("Instance_%s", entityIdMap.begin()->second.ToString().c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
instanceAlias = AZStd::string::format("Instance_%s", AZ::Entity::MakeId().ToString().c_str());
|
||||
}
|
||||
// Use the deterministic instance alias for this new instance
|
||||
AzToolsFramework::Prefab::Instance& addedInstance = topLevelInstance->AddInstance(AZStd::move(nestedInstance), instanceAlias);
|
||||
|
||||
AzToolsFramework::Prefab::PrefabDom topLevelInstanceDomAfter;
|
||||
@@ -670,5 +782,136 @@ namespace AZ
|
||||
AZ_Error("Convert-Slice", disconnected, "Asset Processor failed to disconnect successfully.");
|
||||
}
|
||||
|
||||
void SliceConverter::UpdateSliceEntityInstanceMappings(
|
||||
const AZ::SliceComponent::EntityIdToEntityIdMap& sliceEntityIdMap, const AZStd::string& currentInstanceAlias)
|
||||
{
|
||||
// For each instanced entity, map its ID all the way back to the original prefab template and entity ID that it came from.
|
||||
// This counts on being run recursively from the leaf nodes upwards, so we first get B->A,
|
||||
// then C->B which becomes a C->A entry, then D->C which becomes D->A, etc.
|
||||
for (auto& [newId, oldId] : sliceEntityIdMap)
|
||||
{
|
||||
// Try to find the conversion chain from the old ID. if it's there, copy it and use it for the new ID, plus add this
|
||||
// instance's name to the end of the chain. If it's not there, skip it, since it's probably the slice metadata entity,
|
||||
// which we didn't convert.
|
||||
auto parentEntry = m_aliasIdMapper.find(oldId);
|
||||
if (parentEntry != m_aliasIdMapper.end())
|
||||
{
|
||||
// Only add this instance's name if we don't already have an entry for the new ID.
|
||||
if (m_aliasIdMapper.find(newId) == m_aliasIdMapper.end())
|
||||
{
|
||||
auto newMappingEntry = m_aliasIdMapper.emplace(newId, parentEntry->second).first;
|
||||
newMappingEntry->second.m_nestedInstanceAliases.emplace_back(currentInstanceAlias);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we already had an entry for the new ID, it might be because the old and new ID are the same. This happens
|
||||
// when nesting multiple prefabs directly underneath each other without a nesting entity in-between.
|
||||
// If the IDs are different, it's an unexpected error condition.
|
||||
AZ_Assert(oldId == newId, "The same entity instance ID has unexpectedly appeared twice in the same nested prefab.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AZ_Warning("Convert-Slice", false, " Couldn't find an entity ID conversion for %s.", oldId.ToString().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SliceConverter::RemapIdReferences(
|
||||
const AZStd::unordered_map<AZ::EntityId, SliceEntityMappingInfo>& idMapper,
|
||||
AzToolsFramework::Prefab::Instance* topLevelInstance,
|
||||
AzToolsFramework::Prefab::Instance* nestedInstance,
|
||||
SliceComponent::InstantiatedContainer* instantiatedEntities,
|
||||
SerializeContext* context)
|
||||
{
|
||||
// Given a set of instantiated entities, run through all of them, look for entity references, and replace the entity IDs with
|
||||
// new ones that match up with our prefabs.
|
||||
|
||||
IdUtils::Remapper<EntityId>::ReplaceIdsAndIdRefs(
|
||||
instantiatedEntities,
|
||||
[idMapper, &topLevelInstance, &nestedInstance](
|
||||
const EntityId& sourceId, bool isEntityId, [[maybe_unused]] const AZStd::function<EntityId()>& idGenerator) -> EntityId
|
||||
{
|
||||
EntityId newId = sourceId;
|
||||
|
||||
// Only convert valid entity references. Actual entity IDs have already been taken care of elsewhere, so ignore them.
|
||||
if (!isEntityId && sourceId.IsValid())
|
||||
{
|
||||
auto entityEntry = idMapper.find(sourceId);
|
||||
|
||||
// Since we've already remapped transform hierarchies to include container entities, it's possible that our entity
|
||||
// reference is pointing to a container, which means it won't be in our slice mapping table. In that case, just
|
||||
// return it as-is.
|
||||
if (entityEntry == idMapper.end())
|
||||
{
|
||||
return sourceId;
|
||||
}
|
||||
|
||||
// We've got a slice->prefab mapping entry, so now we need to use it.
|
||||
auto& mappingStruct = entityEntry->second;
|
||||
|
||||
if (mappingStruct.m_nestedInstanceAliases.empty())
|
||||
{
|
||||
// If we don't have a chain of nested instance aliases, then this entity reference is either within the
|
||||
// current nested instance or it's pointing to an entity in the top-level instance. We'll try them both
|
||||
// to look for a match.
|
||||
|
||||
EntityId prefabId = nestedInstance->GetEntityId(mappingStruct.m_entityAlias);
|
||||
if (!prefabId.IsValid())
|
||||
{
|
||||
prefabId = topLevelInstance->GetEntityId(mappingStruct.m_entityAlias);
|
||||
}
|
||||
|
||||
if (prefabId.IsValid())
|
||||
{
|
||||
newId = prefabId;
|
||||
}
|
||||
else
|
||||
{
|
||||
AZ_Error("Convert-Slice", false, " Couldn't find source ID %s", sourceId.ToString().c_str());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// We *do* have a chain of nested instance aliases. This chain could either be relative to the nested instance
|
||||
// or the top-level instance. We can tell which one it is by which one can find the first nested instance
|
||||
// alias.
|
||||
|
||||
AzToolsFramework::Prefab::Instance* entityInstance = nestedInstance;
|
||||
auto it = mappingStruct.m_nestedInstanceAliases.rbegin();
|
||||
if (!entityInstance->FindNestedInstance(*it).has_value())
|
||||
{
|
||||
entityInstance = topLevelInstance;
|
||||
}
|
||||
|
||||
// Now that we've got a starting point, iterate through the chain of nested instance aliases to find the
|
||||
// correct instance to get the entity ID for. We have to go from slice IDs -> entity aliases -> entity IDs
|
||||
// because prefab instance creation can change some of our entity IDs along the way.
|
||||
for (; it != mappingStruct.m_nestedInstanceAliases.rend(); it++)
|
||||
{
|
||||
auto foundInstance = entityInstance->FindNestedInstance(*it);
|
||||
if (foundInstance.has_value())
|
||||
{
|
||||
entityInstance = &(foundInstance->get());
|
||||
}
|
||||
else
|
||||
{
|
||||
AZ_Assert(false, "Couldn't find nested instance %s", it->c_str());
|
||||
}
|
||||
}
|
||||
|
||||
EntityId prefabId = entityInstance->GetEntityId(mappingStruct.m_entityAlias);
|
||||
if (prefabId.IsValid())
|
||||
{
|
||||
newId = prefabId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newId;
|
||||
},
|
||||
context);
|
||||
}
|
||||
|
||||
} // namespace SerializeContextTools
|
||||
} // namespace AZ
|
||||
|
||||
@@ -42,7 +42,27 @@ namespace AZ
|
||||
bool ConvertSliceFiles(Application& application);
|
||||
|
||||
private:
|
||||
using TemplateEntityIdPair = AZStd::pair<AzToolsFramework::Prefab::TemplateId, AZ::EntityId>;
|
||||
// When converting slice entities, especially for nested slices, we need to keep track of the original
|
||||
// entity ID, the entity alias it uses in the prefab, and which template and nested instance path it maps to.
|
||||
// As we encounter each instanced entity ID, we can look it up in this structure and use this to determine how to properly
|
||||
// add it to the correct place in the hierarchy.
|
||||
struct SliceEntityMappingInfo
|
||||
{
|
||||
SliceEntityMappingInfo(
|
||||
AzToolsFramework::Prefab::TemplateId templateId,
|
||||
AzToolsFramework::Prefab::EntityAlias entityAlias,
|
||||
bool isMetadataEntity = false)
|
||||
: m_templateId(templateId)
|
||||
, m_entityAlias(entityAlias)
|
||||
, m_isMetadataEntity(isMetadataEntity)
|
||||
{
|
||||
}
|
||||
|
||||
AzToolsFramework::Prefab::TemplateId m_templateId;
|
||||
AzToolsFramework::Prefab::EntityAlias m_entityAlias;
|
||||
AZStd::vector<AzToolsFramework::Prefab::InstanceAlias> m_nestedInstanceAliases;
|
||||
bool m_isMetadataEntity{ false };
|
||||
};
|
||||
|
||||
bool ConnectToAssetProcessor();
|
||||
void DisconnectFromAssetProcessor();
|
||||
@@ -60,10 +80,22 @@ namespace AZ
|
||||
void SetParentEntity(const AZ::Entity& entity, const AZ::EntityId& parentId, bool onlySetIfInvalid);
|
||||
void PrintPrefab(AzToolsFramework::Prefab::TemplateId templateId);
|
||||
bool SavePrefab(AZ::IO::PathView outputPath, AzToolsFramework::Prefab::TemplateId templateId);
|
||||
void UpdateSliceEntityInstanceMappings(
|
||||
const AZ::SliceComponent::EntityIdToEntityIdMap& sliceEntityIdMap,
|
||||
const AZStd::string& currentInstanceAlias);
|
||||
AZStd::string GetInstanceAlias(const AZ::SliceComponent::SliceInstance& instance);
|
||||
|
||||
// Track all of the entity IDs created and the prefab entity aliases that map to them. This mapping is used
|
||||
// with nested slice conversion to remap parent entity IDs to the correct prefab entity IDs.
|
||||
AZStd::unordered_map<TemplateEntityIdPair, AzToolsFramework::Prefab::EntityAlias> m_aliasIdMapper;
|
||||
void RemapIdReferences(
|
||||
const AZStd::unordered_map<AZ::EntityId, SliceEntityMappingInfo>& idMapper,
|
||||
AzToolsFramework::Prefab::Instance* topLevelInstance,
|
||||
AzToolsFramework::Prefab::Instance* nestedInstance,
|
||||
SliceComponent::InstantiatedContainer* instantiatedEntities,
|
||||
SerializeContext* context);
|
||||
|
||||
// Track all of the entity IDs created and associate them with enough conversion information to know how to place the
|
||||
// entities in the correct place in the prefab hierarchy and fix up parent entity ID mappings to work with the nested
|
||||
// prefab schema.
|
||||
AZStd::unordered_map<AZ::EntityId, SliceEntityMappingInfo> m_aliasIdMapper;
|
||||
|
||||
// Track all of the created prefab template IDs on a slice conversion so that they can get removed at the end of the
|
||||
// conversion for that file.
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace AzToolsFramework
|
||||
SliceConverterEditorEntityContextComponent() : EditorEntityContextComponent() {}
|
||||
|
||||
// Simple API to selectively disable this logic *only* when performing slice to prefab conversion.
|
||||
static void DisableOnContextEntityLogic()
|
||||
static inline void DisableOnContextEntityLogic()
|
||||
{
|
||||
m_enableOnContextEntityLogic = false;
|
||||
}
|
||||
|
||||
@@ -209,7 +209,11 @@ namespace AZ::SerializeContextTools
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Utilities::InspectSerializedFile(const char* filePath, SerializeContext* sc, const ObjectStream::ClassReadyCB& classCallback)
|
||||
bool Utilities::InspectSerializedFile(
|
||||
const char* filePath,
|
||||
SerializeContext* sc,
|
||||
const ObjectStream::ClassReadyCB& classCallback,
|
||||
Data::AssetFilterCB assetFilterCallback)
|
||||
{
|
||||
if (!AZ::IO::FileIOBase::GetInstance()->Exists(filePath))
|
||||
{
|
||||
@@ -248,9 +252,9 @@ namespace AZ::SerializeContextTools
|
||||
AZ::IO::MemoryStream stream(data.data(), fileLength);
|
||||
|
||||
ObjectStream::FilterDescriptor filter;
|
||||
// Never load dependencies. That's another file that would need to be processed
|
||||
// By default, never load dependencies. That's another file that would need to be processed
|
||||
// separately from this one.
|
||||
filter.m_assetCB = AZ::Data::AssetFilterNoAssetLoading;
|
||||
filter.m_assetCB = assetFilterCallback;
|
||||
if (!ObjectStream::LoadBlocking(&stream, *sc, classCallback, filter))
|
||||
{
|
||||
AZ_Printf("Verify", "Failed to deserialize '%s'\n", filePath);
|
||||
|
||||
@@ -39,7 +39,11 @@ namespace AZ
|
||||
|
||||
static AZStd::vector<AZ::Uuid> GetSystemComponents(const Application& application);
|
||||
|
||||
static bool InspectSerializedFile(const char* filePath, SerializeContext* sc, const ObjectStream::ClassReadyCB& classCallback);
|
||||
static bool InspectSerializedFile(
|
||||
const char* filePath,
|
||||
SerializeContext* sc,
|
||||
const ObjectStream::ClassReadyCB& classCallback,
|
||||
Data::AssetFilterCB assetFilterCallback = AZ::Data::AssetFilterNoAssetLoading);
|
||||
|
||||
private:
|
||||
Utilities() = delete;
|
||||
|
||||
Reference in New Issue
Block a user