diff --git a/Code/Editor/CMakeLists.txt b/Code/Editor/CMakeLists.txt index fe694f165a..fca16a2093 100644 --- a/Code/Editor/CMakeLists.txt +++ b/Code/Editor/CMakeLists.txt @@ -163,6 +163,8 @@ ly_add_target( editor_files.cmake PLATFORM_INCLUDE_FILES Platform/${PAL_PLATFORM_NAME}/editor_${PAL_PLATFORM_NAME_LOWERCASE}.cmake + TARGET_PROPERTIES + LY_INSTALL_GENERATE_RUN_TARGET TRUE BUILD_DEPENDENCIES PRIVATE 3rdParty::Qt::Core diff --git a/Code/Framework/AzCore/AzCore/Settings/SettingsRegistryMergeUtils.cpp b/Code/Framework/AzCore/AzCore/Settings/SettingsRegistryMergeUtils.cpp index 74038c8aed..6b89b8c044 100644 --- a/Code/Framework/AzCore/AzCore/Settings/SettingsRegistryMergeUtils.cpp +++ b/Code/Framework/AzCore/AzCore/Settings/SettingsRegistryMergeUtils.cpp @@ -57,6 +57,7 @@ namespace AZ::Internal // and avoid all this logic. using namespace AZ::SettingsRegistryMergeUtils; + using FixedValueString = AZ::SettingsRegistryInterface::FixedValueString; AZ::IO::FixedMaxPath engineRoot; if (auto engineManifestPath = AZ::Utils::GetEngineManifestPath(); !engineManifestPath.empty()) @@ -72,45 +73,16 @@ namespace AZ::Internal struct EngineInfo { AZ::IO::FixedMaxPath m_path; - AZ::SettingsRegistryInterface::FixedValueString m_moniker; + FixedValueString m_moniker; }; struct EnginePathsVisitor : public AZ::SettingsRegistryInterface::Visitor { void Visit( - [[maybe_unused]] AZStd::string_view path, [[maybe_unused]] AZStd::string_view valueName, + [[maybe_unused]] AZStd::string_view path, AZStd::string_view valueName, [[maybe_unused]] AZ::SettingsRegistryInterface::Type type, AZStd::string_view value) override { - m_enginePaths.emplace_back(EngineInfo{AZ::IO::FixedMaxPath{value}.LexicallyNormal(), {}}); - } - - AZ::SettingsRegistryInterface::VisitResponse Traverse( - [[maybe_unused]] AZStd::string_view path, AZStd::string_view valueName, - AZ::SettingsRegistryInterface::VisitAction action, AZ::SettingsRegistryInterface::Type type) override - { - auto response = AZ::SettingsRegistryInterface::VisitResponse::Continue; - if (action == AZ::SettingsRegistryInterface::VisitAction::Begin) - { - if (type == AZ::SettingsRegistryInterface::Type::Array) - { - if (valueName.compare("engines") != 0) - { - response = AZ::SettingsRegistryInterface::VisitResponse::Skip; - } - } - } - else if (action == AZ::SettingsRegistryInterface::VisitAction::Value) - { - if (type == AZ::SettingsRegistryInterface::Type::String) - { - if (valueName.compare("path") != 0) - { - response = AZ::SettingsRegistryInterface::VisitResponse::Skip; - } - } - } - - return response; + m_enginePaths.emplace_back(EngineInfo{ AZ::IO::FixedMaxPath{value}.LexicallyNormal(), FixedValueString{valueName} }); } AZStd::vector m_enginePaths{}; @@ -119,11 +91,11 @@ namespace AZ::Internal EnginePathsVisitor pathVisitor; if (manifestLoaded) { - auto enginePathsKey = AZ::SettingsRegistryInterface::FixedValueString::format("%s/engines", EngineManifestRootKey); + auto enginePathsKey = FixedValueString::format("%s/engines_path", EngineManifestRootKey); settingsRegistry.Visit(pathVisitor, enginePathsKey); } - const auto engineMonikerKey = AZ::SettingsRegistryInterface::FixedValueString::format("%s/engine_name", EngineSettingsRootKey); + const auto engineMonikerKey = FixedValueString::format("%s/engine_name", EngineSettingsRootKey); AZStd::set projectPathsNotFound; @@ -135,7 +107,15 @@ namespace AZ::Internal if (settingsRegistry.MergeSettingsFile( engineSettingsPath.Native(), AZ::SettingsRegistryInterface::Format::JsonMergePatch, EngineSettingsRootKey)) { - settingsRegistry.Get(engineInfo.m_moniker, engineMonikerKey); + FixedValueString engineName; + settingsRegistry.Get(engineName, engineMonikerKey); + AZ_Warning("SettingsRegistryMergeUtils",engineInfo.m_moniker == engineName, + R"(The engine name key "%s" mapped to engine path "%s" within the global manifest of "%s")" + R"( does not match the "engine_name" field "%s" in the engine.json)" "\n" + "This engine should be re-registered.", + engineInfo.m_moniker.c_str(), engineInfo.m_path.c_str(), engineManifestPath.c_str(), + engineName.c_str()) + engineInfo.m_moniker = engineName; } } diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Application/AtomToolsApplication.h b/Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Application/AtomToolsApplication.h new file mode 100644 index 0000000000..bceea24aad --- /dev/null +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Application/AtomToolsApplication.h @@ -0,0 +1,112 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace AtomToolsFramework +{ + //! Base class for Atom tools to inherit from + class AtomToolsApplication + : public AzFramework::Application + , public AzQtComponents::AzQtApplication + , protected AzToolsFramework::AssetDatabase::AssetDatabaseRequestsBus::Handler + , protected AzFramework::AssetSystemStatusBus::Handler + , protected AzToolsFramework::EditorPythonConsoleNotificationBus::Handler + , protected AZ::UserSettingsOwnerRequestBus::Handler + { + public: + AZ_TYPE_INFO(AtomTools::AtomToolsApplication, "{A0DF25BA-6F74-4F11-9F85-0F99278D5986}"); + + using Base = AzFramework::Application; + + AtomToolsApplication(int* argc, char*** argv); + + ////////////////////////////////////////////////////////////////////////// + // AzFramework::Application + void CreateReflectionManager() override; + void Reflect(AZ::ReflectContext* context) override; + void RegisterCoreComponents() override; + AZ::ComponentTypeList GetRequiredSystemComponents() const override; + void CreateStaticModules(AZStd::vector& outModules) override; + const char* GetCurrentConfigurationName() const override; + void StartCommon(AZ::Entity* systemEntity) override; + void Tick(float deltaOverride = -1.f) override; + void Stop() override; + + protected: + ////////////////////////////////////////////////////////////////////////// + // AssetDatabaseRequestsBus::Handler overrides... + bool GetAssetDatabaseLocation(AZStd::string& result) override; + ////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////// + // AzFramework::Application overrides... + void Destroy() override; + ////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////// + // AzFramework::AssetSystemStatusBus::Handler overrides... + void AssetSystemAvailable() override; + ////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////// + // AZ::ComponentApplication overrides... + void QueryApplicationType(AZ::ApplicationTypeQuery& appType) const override; + ////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////// + // AZ::UserSettingsOwnerRequestBus::Handler overrides... + void SaveSettings() override; + ////////////////////////////////////////////////////////////////////////// + + //////////////////////////////////////////////////////////////////////// + // EditorPythonConsoleNotificationBus::Handler overrides... + void OnTraceMessage(AZStd::string_view message) override; + void OnErrorMessage(AZStd::string_view message) override; + void OnExceptionMessage(AZStd::string_view message) override; + //////////////////////////////////////////////////////////////////////// + + virtual AZStd::string GetBuildTargetName() const; + virtual AZStd::vector GetCriticalAssetFilters() const; + + virtual void LoadSettings(); + virtual void UnloadSettings(); + virtual void CompileCriticalAssets(); + virtual void ProcessCommandLine(const AZ::CommandLine& commandLine); + virtual bool LaunchDiscoveryService(); + virtual void StartInternal(); + + static void PyIdleWaitFrames(uint32_t frames); + + AzToolsFramework::TraceLogger m_traceLogger; + + //! Local user settings are used to store material browser tree expansion state + AZ::UserSettingsProvider m_localUserSettings; + + //! Are local settings loaded + bool m_activatedLocalUserSettings = false; + + QTimer m_timer; + + AtomToolsFramework::LocalSocket m_socket; + AtomToolsFramework::LocalServer m_server; + }; +} // namespace AtomToolsFramework diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Application/AtomToolsApplication.cpp b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Application/AtomToolsApplication.cpp new file mode 100644 index 0000000000..3a542db1a4 --- /dev/null +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Application/AtomToolsApplication.cpp @@ -0,0 +1,512 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT +#include +#include +AZ_POP_DISABLE_WARNING + +namespace AtomToolsFramework +{ + AZStd::string AtomToolsApplication::GetBuildTargetName() const + { + return AZStd::string("AtomTools"); + } + + const char* AtomToolsApplication::GetCurrentConfigurationName() const + { +#if defined(_RELEASE) + return "ReleaseAtomTools"; +#elif defined(_DEBUG) + return "DebugAtomTools"; +#else + return "ProfileAtomTools"; +#endif + } + + AtomToolsApplication::AtomToolsApplication(int* argc, char*** argv) + : Application(argc, argv) + , AzQtApplication(*argc, *argv) + { + connect(&m_timer, &QTimer::timeout, this, [&]() + { + this->PumpSystemEventLoopUntilEmpty(); + this->Tick(); + }); + } + + void AtomToolsApplication::CreateReflectionManager() + { + Base::CreateReflectionManager(); + GetSerializeContext()->CreateEditContext(); + } + + void AtomToolsApplication::Reflect(AZ::ReflectContext* context) + { + Base::Reflect(context); + + AzToolsFramework::AssetBrowser::AssetBrowserEntry::Reflect(context); + AzToolsFramework::AssetBrowser::RootAssetBrowserEntry::Reflect(context); + AzToolsFramework::AssetBrowser::FolderAssetBrowserEntry::Reflect(context); + AzToolsFramework::AssetBrowser::SourceAssetBrowserEntry::Reflect(context); + AzToolsFramework::AssetBrowser::ProductAssetBrowserEntry::Reflect(context); + + AzToolsFramework::QTreeViewWithStateSaving::Reflect(context); + AzToolsFramework::QWidgetSavedState::Reflect(context); + + if (auto behaviorContext = azrtti_cast(context)) + { + auto targetName = GetBuildTargetName(); + + // this will put these methods into the 'azlmbr.AtomTools.general' module + auto addGeneral = [targetName](AZ::BehaviorContext::GlobalMethodBuilder methodBuilder) + { + methodBuilder->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation) + ->Attribute(AZ::Script::Attributes::Category, "Editor") + ->Attribute(AZ::Script::Attributes::Module, targetName); + }; + // The reflection here is based on patterns in CryEditPythonHandler::Reflect + addGeneral(behaviorContext->Method( + "idle_wait_frames", &AtomToolsApplication::PyIdleWaitFrames, nullptr, + "Waits idling for a frames. Primarily used for auto-testing.")); + } + } + + void AtomToolsApplication::RegisterCoreComponents() + { + Base::RegisterCoreComponents(); + RegisterComponentDescriptor(AzToolsFramework::AssetBrowser::AssetBrowserComponent::CreateDescriptor()); + RegisterComponentDescriptor(AzToolsFramework::Thumbnailer::ThumbnailerComponent::CreateDescriptor()); + RegisterComponentDescriptor(AzToolsFramework::Components::PropertyManagerComponent::CreateDescriptor()); + RegisterComponentDescriptor(AzToolsFramework::AssetSystem::AssetSystemComponent::CreateDescriptor()); + RegisterComponentDescriptor(AzToolsFramework::PerforceComponent::CreateDescriptor()); + } + + AZ::ComponentTypeList AtomToolsApplication::GetRequiredSystemComponents() const + { + AZ::ComponentTypeList components = Base::GetRequiredSystemComponents(); + + components.insert( + components.end(), + { + azrtti_typeid(), + azrtti_typeid(), + azrtti_typeid(), + azrtti_typeid(), + }); + + return components; + } + + void AtomToolsApplication::CreateStaticModules(AZStd::vector& outModules) + { + Base::CreateStaticModules(outModules); + outModules.push_back(aznew AzToolsFramework::AzToolsFrameworkModule); + } + + void AtomToolsApplication::StartCommon(AZ::Entity* systemEntity) + { + AzFramework::AssetSystemStatusBus::Handler::BusConnect(); + AzToolsFramework::EditorPythonConsoleNotificationBus::Handler::BusConnect(); + + Base::StartCommon(systemEntity); + + StartInternal(); + + m_timer.start(); + } + + void AtomToolsApplication::Destroy() + { + AzToolsFramework::EditorPythonConsoleNotificationBus::Handler::BusDisconnect(); + AzToolsFramework::AssetDatabase::AssetDatabaseRequestsBus::Handler::BusDisconnect(); + + AzFramework::AssetSystemRequestBus::Broadcast(&AzFramework::AssetSystem::AssetSystemRequests::StartDisconnectingAssetProcessor); + Base::Destroy(); + } + + AZStd::vector AtomToolsApplication::GetCriticalAssetFilters() const + { + return AZStd::vector({}); + } + + void AtomToolsApplication::AssetSystemAvailable() + { + bool connectedToAssetProcessor = false; + + // When the AssetProcessor is already launched it should take less than a second to perform a connection + // but when the AssetProcessor needs to be launch it could take up to 15 seconds to have the AssetProcessor initialize + // and able to negotiate a connection when running a debug build + // and to negotiate a connection + + auto targetName = GetBuildTargetName(); + + AzFramework::AssetSystem::ConnectionSettings connectionSettings; + AzFramework::AssetSystem::ReadConnectionSettingsFromSettingsRegistry(connectionSettings); + connectionSettings.m_connectionDirection = + AzFramework::AssetSystem::ConnectionSettings::ConnectionDirection::ConnectToAssetProcessor; + connectionSettings.m_connectionIdentifier = GetBuildTargetName(); + connectionSettings.m_loggingCallback = [targetName]([[maybe_unused]] AZStd::string_view logData) + { + AZ_TracePrintf(targetName.c_str(), "%.*s", aznumeric_cast(logData.size()), logData.data()); + }; + AzFramework::AssetSystemRequestBus::BroadcastResult( + connectedToAssetProcessor, &AzFramework::AssetSystemRequestBus::Events::EstablishAssetProcessorConnection, connectionSettings); + + if (connectedToAssetProcessor) + { + CompileCriticalAssets(); + } + + AzFramework::AssetSystemStatusBus::Handler::BusDisconnect(); + } + + void AtomToolsApplication::CompileCriticalAssets() + { + AZ_TracePrintf(GetBuildTargetName().c_str(), "Compiling critical assets.\n"); + + QStringList failedAssets; + + // Forced asset processor to synchronously process all critical assets + // Note: with AssetManager's current implementation, a compiled asset won't be added in asset registry until next system tick. + // So the asset id won't be found right after CompileAssetSync call. + for (const AZStd::string& assetFilters : GetCriticalAssetFilters()) + { + AZ_TracePrintf(GetBuildTargetName().c_str(), "Compiling critical asset matching: %s.\n", assetFilters.c_str()); + + // Wait for the asset be compiled + AzFramework::AssetSystem::AssetStatus status = AzFramework::AssetSystem::AssetStatus_Unknown; + AzFramework::AssetSystemRequestBus::BroadcastResult( + status, &AzFramework::AssetSystemRequestBus::Events::CompileAssetSync, assetFilters); + if (status != AzFramework::AssetSystem::AssetStatus_Compiled) + { + failedAssets.append(assetFilters.c_str()); + } + } + + if (!failedAssets.empty()) + { + QMessageBox::critical( + activeWindow(), QString("Failed to compile critical assets"), + QString("Failed to compile the following critical assets:\n%1\n%2") + .arg(failedAssets.join(",\n")) + .arg("Make sure this is an Atom project.")); + ExitMainLoop(); + } + } + + void AtomToolsApplication::SaveSettings() + { + if (m_activatedLocalUserSettings) + { + AZ::SerializeContext* context = nullptr; + AZ::ComponentApplicationBus::BroadcastResult(context, &AZ::ComponentApplicationRequests::GetSerializeContext); + AZ_Assert(context, "No serialize context"); + + char resolvedPath[AZ_MAX_PATH_LEN] = ""; + AZStd::string fileName = "@user@/" + GetBuildTargetName() + "UserSettings.xml"; + + AZ::IO::FileIOBase::GetInstance()->ResolvePath( + fileName.c_str(), resolvedPath, AZ_ARRAY_SIZE(resolvedPath)); + m_localUserSettings.Save(resolvedPath, context); + } + } + + void AtomToolsApplication::LoadSettings() + { + AZ::SerializeContext* context = nullptr; + AZ::ComponentApplicationBus::BroadcastResult(context, &AZ::ComponentApplicationRequests::GetSerializeContext); + AZ_Assert(context, "No serialize context"); + + char resolvedPath[AZ_MAX_PATH_LEN] = ""; + AZStd::string fileName = "@user@/" + GetBuildTargetName() + "UserSettings.xml"; + + AZ::IO::FileIOBase::GetInstance()->ResolvePath(fileName.c_str(), resolvedPath, AZ_MAX_PATH_LEN); + + m_localUserSettings.Load(resolvedPath, context); + m_localUserSettings.Activate(AZ::UserSettings::CT_LOCAL); + AZ::UserSettingsOwnerRequestBus::Handler::BusConnect(AZ::UserSettings::CT_LOCAL); + m_activatedLocalUserSettings = true; + } + + void AtomToolsApplication::UnloadSettings() + { + if (m_activatedLocalUserSettings) + { + SaveSettings(); + m_localUserSettings.Deactivate(); + AZ::UserSettingsOwnerRequestBus::Handler::BusDisconnect(); + m_activatedLocalUserSettings = false; + } + } + + void AtomToolsApplication::ProcessCommandLine(const AZ::CommandLine& commandLine) + { + const AZStd::string timeoputSwitchName = "timeout"; + if (commandLine.HasSwitch(timeoputSwitchName)) + { + const AZStd::string& timeoutValue = commandLine.GetSwitchValue(timeoputSwitchName, 0); + const uint32_t timeoutInMs = atoi(timeoutValue.c_str()); + AZ_Printf(GetBuildTargetName().c_str(), "Timeout scheduled, shutting down in %u ms", timeoutInMs); + QTimer::singleShot( + timeoutInMs, + [this] + { + AZ_Printf(GetBuildTargetName().c_str(), "Timeout reached, shutting down"); + ExitMainLoop(); + }); + } + + // Process command line options for running one or more python scripts on startup + const AZStd::string runPythonScriptSwitchName = "runpython"; + size_t runPythonScriptCount = commandLine.GetNumSwitchValues(runPythonScriptSwitchName); + for (size_t runPythonScriptIndex = 0; runPythonScriptIndex < runPythonScriptCount; ++runPythonScriptIndex) + { + const AZStd::string runPythonScriptPath = commandLine.GetSwitchValue(runPythonScriptSwitchName, runPythonScriptIndex); + AZStd::vector runPythonArgs; + + AZ_Printf(GetBuildTargetName().c_str(), "Launching script: %s", runPythonScriptPath.c_str()); + AzToolsFramework::EditorPythonRunnerRequestBus::Broadcast( + &AzToolsFramework::EditorPythonRunnerRequestBus::Events::ExecuteByFilenameWithArgs, runPythonScriptPath, runPythonArgs); + } + + const AZStd::string exitAfterCommandsSwitchName = "exitaftercommands"; + if (commandLine.HasSwitch(exitAfterCommandsSwitchName)) + { + ExitMainLoop(); + } + } + + bool AtomToolsApplication::LaunchDiscoveryService() + { + // Determine if this is the first launch of the tool by attempting to connect to a running server + if (m_socket.Connect(QApplication::applicationName())) + { + // If the server was located, the application is already running. + // Forward commandline options to other application instance. + QByteArray buffer; + buffer.append("ProcessCommandLine:"); + + // Add the command line options from this process to the message, skipping the executable path + for (int argi = 1; argi < m_argC; ++argi) + { + buffer.append(QString(m_argV[argi]).append("\n").toUtf8()); + } + + // Inject command line option to always bring the main window to the foreground + buffer.append("--activatewindow\n"); + + m_socket.Send(buffer); + m_socket.Disconnect(); + return false; + } + + // Setup server to handle basic commands + m_server.SetReadHandler( + [this](const QByteArray& buffer) + { + // Handle commmand line params from connected socket + if (buffer.startsWith("ProcessCommandLine:")) + { + // Remove header and parse commands + AZStd::string params(buffer.data(), buffer.size()); + params = params.substr(strlen("ProcessCommandLine:")); + + AZStd::vector tokens; + AZ::StringFunc::Tokenize(params, tokens, "\n"); + + if (!tokens.empty()) + { + AZ::CommandLine commandLine; + commandLine.Parse(tokens); + ProcessCommandLine(commandLine); + } + } + }); + + // Launch local server + if (!m_server.Connect(QApplication::applicationName())) + { + return false; + } + + return true; + } + + void AtomToolsApplication::StartInternal() + { + if (WasExitMainLoopRequested()) + { + return; + } + + AZStd::string fileName = GetBuildTargetName() + ".log"; + + m_traceLogger.WriteStartupLog(fileName.c_str()); + + if (!LaunchDiscoveryService()) + { + ExitMainLoop(); + return; + } + + AzToolsFramework::AssetDatabase::AssetDatabaseRequestsBus::Handler::BusConnect(); + AzToolsFramework::AssetBrowser::AssetDatabaseLocationNotificationBus::Broadcast( + &AzToolsFramework::AssetBrowser::AssetDatabaseLocationNotifications::OnDatabaseInitialized); + + AZ::Data::AssetCatalogRequestBus::Broadcast(&AZ::Data::AssetCatalogRequestBus::Events::LoadCatalog, "@assets@/assetcatalog.xml"); + + AZ::RPI::RPISystemInterface::Get()->InitializeSystemAssets(); + + LoadSettings(); + + auto editorPythonEventsInterface = AZ::Interface::Get(); + if (editorPythonEventsInterface) + { + // The PythonSystemComponent does not call StartPython to allow for lazy python initialization, so start it here + // The PythonSystemComponent will call StopPython when it deactivates, so we do not need our own corresponding call to + // StopPython + editorPythonEventsInterface->StartPython(); + } + + // Delay execution of commands and scripts post initialization + QTimer::singleShot( + 0, + [this]() + { + ProcessCommandLine(m_commandLine); + }); + } + + bool AtomToolsApplication::GetAssetDatabaseLocation(AZStd::string& result) + { + AZ::SettingsRegistryInterface* settingsRegistry = AZ::SettingsRegistry::Get(); + AZ::IO::FixedMaxPath assetDatabaseSqlitePath; + if (settingsRegistry && + settingsRegistry->Get(assetDatabaseSqlitePath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_CacheProjectRootFolder)) + { + assetDatabaseSqlitePath /= "assetdb.sqlite"; + result = AZStd::string_view(assetDatabaseSqlitePath.Native()); + return true; + } + + return false; + } + + void AtomToolsApplication::Tick(float deltaOverride) + { + TickSystem(); + Base::Tick(deltaOverride); + + if (WasExitMainLoopRequested()) + { + m_timer.disconnect(); + quit(); + } + } + + void AtomToolsApplication::Stop() + { + UnloadSettings(); + Base::Stop(); + } + + void AtomToolsApplication::QueryApplicationType(AZ::ApplicationTypeQuery& appType) const + { + appType.m_maskValue = AZ::ApplicationTypeQuery::Masks::Game; + } + + void AtomToolsApplication::OnTraceMessage([[maybe_unused]] AZStd::string_view message) + { +#if defined(AZ_ENABLE_TRACING) + AZStd::vector lines; + AzFramework::StringFunc::Tokenize( + message, lines, "\n", + false, // Keep empty strings + false // Keep space strings + ); + + for (auto& line : lines) + { + AZ_TracePrintf(GetBuildTargetName().c_str(), "Python: %s\n", line.c_str()); + } +#endif + } + + void AtomToolsApplication::OnErrorMessage(AZStd::string_view message) + { + // Use AZ_TracePrintf instead of AZ_Error or AZ_Warning to avoid all the metadata noise + OnTraceMessage(message); + } + + void AtomToolsApplication::OnExceptionMessage([[maybe_unused]] AZStd::string_view message) + { + AZ_Error(GetBuildTargetName().c_str(), false, "Python: " AZ_STRING_FORMAT, AZ_STRING_ARG(message)); + } + + // Copied from PyIdleWaitFrames in CryEdit.cpp + void AtomToolsApplication::PyIdleWaitFrames(uint32_t frames) + { + struct Ticker : public AZ::TickBus::Handler + { + Ticker(QEventLoop* loop, uint32_t targetFrames) + : m_loop(loop) + , m_targetFrames(targetFrames) + { + AZ::TickBus::Handler::BusConnect(); + } + ~Ticker() + { + AZ::TickBus::Handler::BusDisconnect(); + } + + void OnTick(float deltaTime, AZ::ScriptTimePoint time) override + { + AZ_UNUSED(deltaTime); + AZ_UNUSED(time); + if (++m_elapsedFrames == m_targetFrames) + { + m_loop->quit(); + } + } + QEventLoop* m_loop = nullptr; + uint32_t m_elapsedFrames = 0; + uint32_t m_targetFrames = 0; + }; + + QEventLoop loop; + Ticker ticker(&loop, frames); + loop.exec(); + } +} // namespace AtomToolsFramework diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/atomtoolsframework_files.cmake b/Gems/Atom/Tools/AtomToolsFramework/Code/atomtoolsframework_files.cmake index dfe222bfc5..86fc3f5f5e 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/atomtoolsframework_files.cmake +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/atomtoolsframework_files.cmake @@ -7,6 +7,7 @@ # set(FILES + Include/AtomToolsFramework/Application/AtomToolsApplication.h Include/AtomToolsFramework/Communication/LocalServer.h Include/AtomToolsFramework/Communication/LocalSocket.h Include/AtomToolsFramework/Debug/TraceRecorder.h @@ -23,6 +24,7 @@ set(FILES Include/AtomToolsFramework/Viewport/RenderViewportWidget.h Include/AtomToolsFramework/Viewport/ModularViewportCameraController.h Include/AtomToolsFramework/Viewport/ModularViewportCameraControllerRequestBus.h + Source/Application/AtomToolsApplication.cpp Source/Communication/LocalServer.cpp Source/Communication/LocalSocket.cpp Source/Debug/TraceRecorder.cpp diff --git a/Gems/Atom/Tools/MaterialEditor/Code/Source/MaterialEditorApplication.cpp b/Gems/Atom/Tools/MaterialEditor/Code/Source/MaterialEditorApplication.cpp index 10ad1df5f3..7b9372e72b 100644 --- a/Gems/Atom/Tools/MaterialEditor/Code/Source/MaterialEditorApplication.cpp +++ b/Gems/Atom/Tools/MaterialEditor/Code/Source/MaterialEditorApplication.cpp @@ -6,7 +6,22 @@ * */ +#include + +#include +#include + +#include +#include + +#include + +#include +#include +#include + #include +#include #include #include #include @@ -26,24 +41,9 @@ #include #include -#include - -#include -#include - #include #include -#include -#include - -#include - -#include -#include -#include -#include - AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT #include #include @@ -52,12 +52,12 @@ AZ_POP_DISABLE_WARNING namespace MaterialEditor { //! This function returns the build system target name of "MaterialEditor - AZStd::string_view GetBuildTargetName() + AZStd::string MaterialEditorApplication::GetBuildTargetName() const { #if !defined (LY_CMAKE_TARGET) #error "LY_CMAKE_TARGET must be defined in order to add this source file to a CMake executable target" #endif - return AZStd::string_view{ LY_CMAKE_TARGET }; + return AZStd::string{ LY_CMAKE_TARGET }; } const char* MaterialEditorApplication::GetCurrentConfigurationName() const @@ -72,19 +72,13 @@ namespace MaterialEditor } MaterialEditorApplication::MaterialEditorApplication(int* argc, char*** argv) - : Application(argc, argv) - , AzQtApplication(*argc, *argv) + : AtomToolsApplication(argc, argv) + { QApplication::setApplicationName("O3DE Material Editor"); AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddBuildSystemTargetSpecialization( *AZ::SettingsRegistry::Get(), GetBuildTargetName()); - - connect(&m_timer, &QTimer::timeout, this, [&]() - { - this->PumpSystemEventLoopUntilEmpty(); - this->Tick(); - }); } MaterialEditorApplication::~MaterialEditorApplication() @@ -94,88 +88,14 @@ namespace MaterialEditor AzToolsFramework::EditorPythonConsoleNotificationBus::Handler::BusDisconnect(); } - void MaterialEditorApplication::CreateReflectionManager() - { - Application::CreateReflectionManager(); - GetSerializeContext()->CreateEditContext(); - } - - void MaterialEditorApplication::Reflect(AZ::ReflectContext* context) - { - Application::Reflect(context); - - AzToolsFramework::AssetBrowser::AssetBrowserEntry::Reflect(context); - AzToolsFramework::AssetBrowser::RootAssetBrowserEntry::Reflect(context); - AzToolsFramework::AssetBrowser::FolderAssetBrowserEntry::Reflect(context); - AzToolsFramework::AssetBrowser::SourceAssetBrowserEntry::Reflect(context); - AzToolsFramework::AssetBrowser::ProductAssetBrowserEntry::Reflect(context); - - AzToolsFramework::QTreeViewWithStateSaving::Reflect(context); - AzToolsFramework::QWidgetSavedState::Reflect(context); - - if (auto behaviorContext = azrtti_cast(context)) - { - // this will put these methods into the 'azlmbr.materialeditor.general' module - auto addGeneral = [](AZ::BehaviorContext::GlobalMethodBuilder methodBuilder) - { - methodBuilder->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation) - ->Attribute(AZ::Script::Attributes::Category, "Editor") - ->Attribute(AZ::Script::Attributes::Module, "materialeditor.general"); - }; - // The reflection here is based on patterns in CryEditPythonHandler::Reflect - addGeneral(behaviorContext->Method("idle_wait_frames", &MaterialEditorApplication::PyIdleWaitFrames, nullptr, "Waits idling for a frames. Primarily used for auto-testing.")); - } - } - - void MaterialEditorApplication::RegisterCoreComponents() - { - Application::RegisterCoreComponents(); - RegisterComponentDescriptor(AzToolsFramework::AssetBrowser::AssetBrowserComponent::CreateDescriptor()); - RegisterComponentDescriptor(AzToolsFramework::Thumbnailer::ThumbnailerComponent::CreateDescriptor()); - RegisterComponentDescriptor(AzToolsFramework::Components::PropertyManagerComponent::CreateDescriptor()); - RegisterComponentDescriptor(AzToolsFramework::AssetSystem::AssetSystemComponent::CreateDescriptor()); - RegisterComponentDescriptor(AzToolsFramework::PerforceComponent::CreateDescriptor()); - } - - AZ::ComponentTypeList MaterialEditorApplication::GetRequiredSystemComponents() const - { - AZ::ComponentTypeList components = Application::GetRequiredSystemComponents(); - - components.insert(components.end(), { - azrtti_typeid(), - azrtti_typeid(), - azrtti_typeid(), - azrtti_typeid(), - }); - - return components; - } - void MaterialEditorApplication::CreateStaticModules(AZStd::vector& outModules) { - Application::CreateStaticModules(outModules); - outModules.push_back(aznew AzToolsFramework::AzToolsFrameworkModule); + Base::CreateStaticModules(outModules); outModules.push_back(aznew MaterialDocumentModule); outModules.push_back(aznew MaterialViewportModule); outModules.push_back(aznew MaterialEditorWindowModule); } - void MaterialEditorApplication::StartCommon(AZ::Entity* systemEntity) - { - { - //[GFX TODO][ATOM-408] This needs to be updated in some way to support the MaterialViewport render widget - } - - AzFramework::AssetSystemStatusBus::Handler::BusConnect(); - AzToolsFramework::EditorPythonConsoleNotificationBus::Handler::BusConnect(); - - AzFramework::Application::StartCommon(systemEntity); - - StartInternal(); - - m_timer.start(); - } - void MaterialEditorApplication::OnMaterialEditorWindowClosing() { ExitMainLoop(); @@ -187,120 +107,14 @@ namespace MaterialEditor MaterialEditor::MaterialEditorWindowFactoryRequestBus::Broadcast( &MaterialEditor::MaterialEditorWindowFactoryRequestBus::Handler::DestroyMaterialEditorWindow); - AzToolsFramework::EditorPythonConsoleNotificationBus::Handler::BusDisconnect(); - AzToolsFramework::AssetDatabase::AssetDatabaseRequestsBus::Handler::BusDisconnect(); MaterialEditorWindowNotificationBus::Handler::BusDisconnect(); - AZ::Debug::TraceMessageBus::Handler::BusDisconnect(); - m_logFile = {}; - m_startupLogSink = {}; - - AzFramework::AssetSystemRequestBus::Broadcast(&AzFramework::AssetSystem::AssetSystemRequests::StartDisconnectingAssetProcessor); - Application::Destroy(); + Base::Destroy(); } - void MaterialEditorApplication::AssetSystemAvailable() + AZStd::vector MaterialEditorApplication::GetCriticalAssetFilters() const { - bool connectedToAssetProcessor = false; - - // When the AssetProcessor is already launched it should take less than a second to perform a connection - // but when the AssetProcessor needs to be launch it could take up to 15 seconds to have the AssetProcessor initialize - // and able to negotiate a connection when running a debug build - // and to negotiate a connection - - AzFramework::AssetSystem::ConnectionSettings connectionSettings; - AzFramework::AssetSystem::ReadConnectionSettingsFromSettingsRegistry(connectionSettings); - connectionSettings.m_connectionDirection = AzFramework::AssetSystem::ConnectionSettings::ConnectionDirection::ConnectToAssetProcessor; - connectionSettings.m_connectionIdentifier = "MaterialEditor"; - connectionSettings.m_loggingCallback = []([[maybe_unused]] AZStd::string_view logData) - { - AZ_TracePrintf("Material Editor", "%.*s", aznumeric_cast(logData.size()), logData.data()); - }; - AzFramework::AssetSystemRequestBus::BroadcastResult(connectedToAssetProcessor, - &AzFramework::AssetSystemRequestBus::Events::EstablishAssetProcessorConnection, connectionSettings); - if (connectedToAssetProcessor) - { - CompileCriticalAssets(); - } - - AzFramework::AssetSystemStatusBus::Handler::BusDisconnect(); - } - - - void MaterialEditorApplication::CompileCriticalAssets() - { - AZ_TracePrintf("MaterialEditor", "Compiling critical assets.\n"); - - // List of common asset filters for things that need to be compiled to run the material editor - // Some of these things will not be necessary once we have proper support for queued asset loading and reloading - const AZStd::string assetFiltersArray[] = - { - "passes/", - "config/", - "MaterialEditor/", - }; - - QStringList failedAssets; - - // Forced asset processor to synchronously process all critical assets - // Note: with AssetManager's current implementation, a compiled asset won't be added in asset registry until next system tick. - // So the asset id won't be found right after CompileAssetSync call. - for (const AZStd::string& assetFilters : assetFiltersArray) - { - AZ_TracePrintf("MaterialEditor", "Compiling critical asset matching: %s.\n", assetFilters.c_str()); - - // Wait for the asset be compiled - AzFramework::AssetSystem::AssetStatus status = AzFramework::AssetSystem::AssetStatus_Unknown; - AzFramework::AssetSystemRequestBus::BroadcastResult( - status, &AzFramework::AssetSystemRequestBus::Events::CompileAssetSync, assetFilters); - if (status != AzFramework::AssetSystem::AssetStatus_Compiled) - { - failedAssets.append(assetFilters.c_str()); - } - } - - if (!failedAssets.empty()) - { - QMessageBox::critical(activeWindow(), - QString("Failed to compile critical assets"), - QString("Failed to compile the following critical assets:\n%1\n%2") - .arg(failedAssets.join(",\n")) - .arg("Make sure this is an Atom project.")); - ExitMainLoop(); - } - } - - void MaterialEditorApplication::SaveSettings() - { - if (m_activatedLocalUserSettings) - { - AZ::SerializeContext* context = nullptr; - AZ::ComponentApplicationBus::BroadcastResult(context, &AZ::ComponentApplicationRequests::GetSerializeContext); - AZ_Assert(context, "No serialize context"); - - char resolvedPath[AZ_MAX_PATH_LEN] = ""; - AZ::IO::FileIOBase::GetInstance()->ResolvePath("@user@/MaterialEditorUserSettings.xml", resolvedPath, AZ_ARRAY_SIZE(resolvedPath)); - m_localUserSettings.Save(resolvedPath, context); - } - } - - bool MaterialEditorApplication::OnOutput(const char* window, const char* message) - { - // Suppress spam from the Source Control system - if (0 == strncmp(window, AzToolsFramework::SCC_WINDOW, AZ_ARRAY_SIZE(AzToolsFramework::SCC_WINDOW))) - { - return true; - } - - if (m_logFile) - { - m_logFile->AppendLog(AzFramework::LogFile::SEV_NORMAL, window, message); - } - else - { - m_startupLogSink.push_back({ window, message }); - } - return false; + return AZStd::vector({ "passes/", "config/", "MaterialEditor" }); } void MaterialEditorApplication::ProcessCommandLine(const AZ::CommandLine& commandLine) @@ -312,195 +126,27 @@ namespace MaterialEditor &MaterialEditor::MaterialEditorWindowRequestBus::Handler::ActivateWindow); } - const AZStd::string timeoputSwitchName = "timeout"; - if (commandLine.HasSwitch(timeoputSwitchName)) - { - const AZStd::string& timeoutValue = commandLine.GetSwitchValue(timeoputSwitchName, 0); - const uint32_t timeoutInMs = atoi(timeoutValue.c_str()); - AZ_Printf("MaterialEditor", "Timeout scheduled, shutting down in %u ms", timeoutInMs); - QTimer::singleShot(timeoutInMs, [this] { - AZ_Printf("MaterialEditor", "Timeout reached, shutting down"); - ExitMainLoop(); - }); - } - - // Process command line options for running one or more python scripts on startup - const AZStd::string runPythonScriptSwitchName = "runpython"; - size_t runPythonScriptCount = commandLine.GetNumSwitchValues(runPythonScriptSwitchName); - for (size_t runPythonScriptIndex = 0; runPythonScriptIndex < runPythonScriptCount; ++runPythonScriptIndex) - { - const AZStd::string runPythonScriptPath = commandLine.GetSwitchValue(runPythonScriptSwitchName, runPythonScriptIndex); - AZStd::vector runPythonArgs; - - AZ_Printf("MaterialEditor", "Launching script: %s", runPythonScriptPath.c_str()); - AzToolsFramework::EditorPythonRunnerRequestBus::Broadcast( - &AzToolsFramework::EditorPythonRunnerRequestBus::Events::ExecuteByFilenameWithArgs, - runPythonScriptPath, - runPythonArgs); - } - // Process command line options for opening one or more material documents on startup size_t openDocumentCount = commandLine.GetNumMiscValues(); for (size_t openDocumentIndex = 0; openDocumentIndex < openDocumentCount; ++openDocumentIndex) { const AZStd::string openDocumentPath = commandLine.GetMiscValue(openDocumentIndex); - AZ_Printf("MaterialEditor", "Opening document: %s", openDocumentPath.c_str()); + AZ_Printf(GetBuildTargetName().c_str(), "Opening document: %s", openDocumentPath.c_str()); MaterialDocumentSystemRequestBus::Broadcast(&MaterialDocumentSystemRequestBus::Events::OpenDocument, openDocumentPath); } - const AZStd::string exitAfterCommandsSwitchName = "exitaftercommands"; - if (commandLine.HasSwitch(exitAfterCommandsSwitchName)) - { - ExitMainLoop(); - } - } - - void MaterialEditorApplication::LoadSettings() - { - AZ::SerializeContext* context = nullptr; - AZ::ComponentApplicationBus::BroadcastResult(context, &AZ::ComponentApplicationRequests::GetSerializeContext); - AZ_Assert(context, "No serialize context"); - - char resolvedPath[AZ_MAX_PATH_LEN] = ""; - AZ::IO::FileIOBase::GetInstance()->ResolvePath("@user@/EditorUserSettings.xml", resolvedPath, AZ_MAX_PATH_LEN); - - m_localUserSettings.Load(resolvedPath, context); - m_localUserSettings.Activate(AZ::UserSettings::CT_LOCAL); - AZ::UserSettingsOwnerRequestBus::Handler::BusConnect(AZ::UserSettings::CT_LOCAL); - m_activatedLocalUserSettings = true; - } - - void MaterialEditorApplication::UnloadSettings() - { - if (m_activatedLocalUserSettings) - { - SaveSettings(); - m_localUserSettings.Deactivate(); - AZ::UserSettingsOwnerRequestBus::Handler::BusDisconnect(); - m_activatedLocalUserSettings = false; - } - } - - bool MaterialEditorApplication::LaunchDiscoveryService() - { - // Determine if this is the first launch of the tool by attempting to connect to a running server - if (m_socket.Connect(QApplication::applicationName())) - { - // If the server was located, the application is already running. - // Forward commandline options to other application instance. - QByteArray buffer; - buffer.append("ProcessCommandLine:"); - - // Add the command line options from this process to the message, skipping the executable path - for (int argi = 1; argi < m_argC; ++argi) - { - buffer.append(QString(m_argV[argi]).append("\n").toUtf8()); - } - - // Inject command line option to always bring the main window to the foreground - buffer.append("--activatewindow\n"); - - m_socket.Send(buffer); - m_socket.Disconnect(); - return false; - } - - // Setup server to handle basic commands - m_server.SetReadHandler([this](const QByteArray& buffer) { - // Handle commmand line params from connected socket - if (buffer.startsWith("ProcessCommandLine:")) - { - // Remove header and parse commands - AZStd::string params(buffer.data(), buffer.size()); - params = params.substr(strlen("ProcessCommandLine:")); - - AZStd::vector tokens; - AZ::StringFunc::Tokenize(params, tokens, "\n"); - - if (!tokens.empty()) - { - AZ::CommandLine commandLine; - commandLine.Parse(tokens); - ProcessCommandLine(commandLine); - } - } - }); - - // Launch local server - if (!m_server.Connect(QApplication::applicationName())) - { - return false; - } - - return true; + Base::ProcessCommandLine(commandLine); } void MaterialEditorApplication::StartInternal() { - if (WasExitMainLoopRequested()) - { - return; - } - - m_traceLogger.WriteStartupLog("MaterialEditor.log"); - - if (!LaunchDiscoveryService()) - { - ExitMainLoop(); - return; - } - - AzToolsFramework::AssetDatabase::AssetDatabaseRequestsBus::Handler::BusConnect(); - AzToolsFramework::AssetBrowser::AssetDatabaseLocationNotificationBus::Broadcast(&AzToolsFramework::AssetBrowser::AssetDatabaseLocationNotifications::OnDatabaseInitialized); - - AZ::Data::AssetCatalogRequestBus::Broadcast(&AZ::Data::AssetCatalogRequestBus::Events::LoadCatalog, "@assets@/assetcatalog.xml"); - - AZ::RPI::RPISystemInterface::Get()->InitializeSystemAssets(); - - LoadSettings(); + Base::StartInternal(); MaterialEditorWindowNotificationBus::Handler::BusConnect(); MaterialEditor::MaterialEditorWindowFactoryRequestBus::Broadcast( &MaterialEditor::MaterialEditorWindowFactoryRequestBus::Handler::CreateMaterialEditorWindow); - - auto editorPythonEventsInterface = AZ::Interface::Get(); - if (editorPythonEventsInterface) - { - // The PythonSystemComponent does not call StartPython to allow for lazy python initialization, so start it here - // The PythonSystemComponent will call StopPython when it deactivates, so we do not need our own corresponding call to StopPython - editorPythonEventsInterface->StartPython(); - } - - // Delay execution of commands and scripts post initialization - QTimer::singleShot(0, [this]() { ProcessCommandLine(m_commandLine); }); - } - - bool MaterialEditorApplication::GetAssetDatabaseLocation(AZStd::string& result) - { - AZ::SettingsRegistryInterface* settingsRegistry = AZ::SettingsRegistry::Get(); - AZ::IO::FixedMaxPath assetDatabaseSqlitePath; - if (settingsRegistry && settingsRegistry->Get(assetDatabaseSqlitePath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_CacheProjectRootFolder)) - { - assetDatabaseSqlitePath /= "assetdb.sqlite"; - result = AZStd::string_view(assetDatabaseSqlitePath.Native()); - return true; - } - - return false; - } - - void MaterialEditorApplication::Tick(float deltaOverride) - { - TickSystem(); - Application::Tick(deltaOverride); - - if (WasExitMainLoopRequested()) - { - m_timer.disconnect(); - quit(); - } } void MaterialEditorApplication::Stop() @@ -508,76 +154,6 @@ namespace MaterialEditor MaterialEditor::MaterialEditorWindowFactoryRequestBus::Broadcast( &MaterialEditor::MaterialEditorWindowFactoryRequestBus::Handler::DestroyMaterialEditorWindow); - UnloadSettings(); - AzFramework::Application::Stop(); + Base::Stop(); } - - void MaterialEditorApplication::QueryApplicationType(AZ::ApplicationTypeQuery& appType) const - { - appType.m_maskValue = AZ::ApplicationTypeQuery::Masks::Game; - } - - void MaterialEditorApplication::OnTraceMessage([[maybe_unused]] AZStd::string_view message) - { -#if defined(AZ_ENABLE_TRACING) - AZStd::vector lines; - AzFramework::StringFunc::Tokenize( - message, - lines, - "\n", - false, // Keep empty strings - false // Keep space strings - ); - - for (auto& line : lines) - { - AZ_TracePrintf("MaterialEditor", "Python: %s\n", line.c_str()); - } -#endif - } - - void MaterialEditorApplication::OnErrorMessage(AZStd::string_view message) - { - // Use AZ_TracePrintf instead of AZ_Error or AZ_Warning to avoid all the metadata noise - OnTraceMessage(message); - } - - void MaterialEditorApplication::OnExceptionMessage([[maybe_unused]] AZStd::string_view message) - { - AZ_Error("MaterialEditor", false, "Python: " AZ_STRING_FORMAT, AZ_STRING_ARG(message)); - } - - // Copied from PyIdleWaitFrames in CryEdit.cpp - void MaterialEditorApplication::PyIdleWaitFrames(uint32_t frames) - { - struct Ticker : public AZ::TickBus::Handler - { - Ticker(QEventLoop* loop, uint32_t targetFrames) : m_loop(loop), m_targetFrames(targetFrames) - { - AZ::TickBus::Handler::BusConnect(); - } - ~Ticker() - { - AZ::TickBus::Handler::BusDisconnect(); - } - - void OnTick(float deltaTime, AZ::ScriptTimePoint time) override - { - AZ_UNUSED(deltaTime); - AZ_UNUSED(time); - if (++m_elapsedFrames == m_targetFrames) - { - m_loop->quit(); - } - } - QEventLoop* m_loop = nullptr; - uint32_t m_elapsedFrames = 0; - uint32_t m_targetFrames = 0; - }; - - QEventLoop loop; - Ticker ticker(&loop, frames); - loop.exec(); - } - } // namespace MaterialEditor diff --git a/Gems/Atom/Tools/MaterialEditor/Code/Source/MaterialEditorApplication.h b/Gems/Atom/Tools/MaterialEditor/Code/Source/MaterialEditorApplication.h index e6fe9401f6..b1c742d4dd 100644 --- a/Gems/Atom/Tools/MaterialEditor/Code/Source/MaterialEditorApplication.h +++ b/Gems/Atom/Tools/MaterialEditor/Code/Source/MaterialEditorApplication.h @@ -10,19 +10,7 @@ #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include #include @@ -31,41 +19,24 @@ namespace MaterialEditor class MaterialThumbnailRenderer; class MaterialEditorApplication - : public AzFramework::Application - , public AzQtComponents::AzQtApplication - , private AzToolsFramework::AssetDatabase::AssetDatabaseRequestsBus::Handler + : public AtomToolsFramework::AtomToolsApplication , private MaterialEditorWindowNotificationBus::Handler - , private AzFramework::AssetSystemStatusBus::Handler - , private AZ::UserSettingsOwnerRequestBus::Handler - , private AZ::Debug::TraceMessageBus::Handler - , private AzToolsFramework::EditorPythonConsoleNotificationBus::Handler { public: AZ_TYPE_INFO(MaterialEditor::MaterialEditorApplication, "{30F90CA5-1253-49B5-8143-19CEE37E22BB}"); - using Base = AzFramework::Application; + using Base = AtomToolsFramework::AtomToolsApplication; MaterialEditorApplication(int* argc, char*** argv); virtual ~MaterialEditorApplication(); ////////////////////////////////////////////////////////////////////////// // AzFramework::Application - void CreateReflectionManager() override; - void Reflect(AZ::ReflectContext* context) override; - void RegisterCoreComponents() override; - AZ::ComponentTypeList GetRequiredSystemComponents() const override; void CreateStaticModules(AZStd::vector& outModules) override; const char* GetCurrentConfigurationName() const override; - void StartCommon(AZ::Entity* systemEntity) override; - void Tick(float deltaOverride = -1.f) override; void Stop() override; private: - ////////////////////////////////////////////////////////////////////////// - // AssetDatabaseRequestsBus::Handler overrides... - bool GetAssetDatabaseLocation(AZStd::string& result) override; - ////////////////////////////////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////// // MaterialEditorWindowNotificationBus::Handler overrides... void OnMaterialEditorWindowClosing() override; @@ -76,66 +47,12 @@ namespace MaterialEditor void Destroy() override; ////////////////////////////////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////// - // AZ::ComponentApplication overrides... - void QueryApplicationType(AZ::ApplicationTypeQuery& appType) const override; - ////////////////////////////////////////////////////////////////////////// + void ProcessCommandLine(const AZ::CommandLine& commandLine) override; + void StartInternal() override; + AZStd::string GetBuildTargetName() const override; - //////////////////////////////////////////////////////////////////////// - // EditorPythonConsoleNotificationBus::Handler overrides... - void OnTraceMessage(AZStd::string_view message) override; - void OnErrorMessage(AZStd::string_view message) override; - void OnExceptionMessage(AZStd::string_view message) override; - //////////////////////////////////////////////////////////////////////// - - ////////////////////////////////////////////////////////////////////////// - // AzFramework::AssetSystemStatusBus::Handler overrides... - void AssetSystemAvailable() override; - ////////////////////////////////////////////////////////////////////////// - - ////////////////////////////////////////////////////////////////////////// - // AZ::UserSettingsOwnerRequestBus::Handler overrides... - void SaveSettings() override; - ////////////////////////////////////////////////////////////////////////// - - ////////////////////////////////////////////////////////////////////////// - // AZ::Debug::TraceMessageBus::Handler overrides... - bool OnOutput(const char* window, const char* message) override; - ////////////////////////////////////////////////////////////////////////// - - void CompileCriticalAssets(); - - void ProcessCommandLine(const AZ::CommandLine& commandLine); - - void LoadSettings(); - void UnloadSettings(); - - bool LaunchDiscoveryService(); - - void StartInternal(); - - static void PyIdleWaitFrames(uint32_t frames); - - struct LogMessage - { - AZStd::string window; - AZStd::string message; - }; - - AZStd::vector m_startupLogSink; - AZStd::unique_ptr m_logFile; - - AzToolsFramework::TraceLogger m_traceLogger; - - //! Local user settings are used to store material browser tree expansion state - AZ::UserSettingsProvider m_localUserSettings; - - //! Are local settings loaded - bool m_activatedLocalUserSettings = false; - - QTimer m_timer; - - AtomToolsFramework::LocalSocket m_socket; - AtomToolsFramework::LocalServer m_server; - }; + //! List of common asset filters for things that need to be compiled to run the material editor + //! Some of these things will not be necessary once we have proper support for queued asset loading and reloading + AZStd::vector GetCriticalAssetFilters() const override; + }; } // namespace MaterialEditor diff --git a/Gems/Atom/Tools/ShaderManagementConsole/Code/Source/ShaderManagementConsoleApplication.cpp b/Gems/Atom/Tools/ShaderManagementConsole/Code/Source/ShaderManagementConsoleApplication.cpp index a39480637b..7d2aa02e19 100644 --- a/Gems/Atom/Tools/ShaderManagementConsole/Code/Source/ShaderManagementConsoleApplication.cpp +++ b/Gems/Atom/Tools/ShaderManagementConsole/Code/Source/ShaderManagementConsoleApplication.cpp @@ -48,7 +48,7 @@ AZ_POP_DISABLE_WARNING namespace ShaderManagementConsole { - AZStd::string_view GetBuildTargetName() + AZStd::string ShaderManagementConsoleApplication::GetBuildTargetName() const { #if !defined (LY_CMAKE_TARGET) #error "LY_CMAKE_TARGET must be defined in order to add this source file to a CMake executable target" @@ -68,99 +68,22 @@ namespace ShaderManagementConsole } ShaderManagementConsoleApplication::ShaderManagementConsoleApplication(int* argc, char*** argv) - : Application(argc, argv) - , AzQtApplication(*argc, *argv) + : AtomToolsApplication(argc, argv) { QApplication::setApplicationName("O3DE Shader Management Console"); // The settings registry has been created at this point, so add the CMake target AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddBuildSystemTargetSpecialization( *AZ::SettingsRegistry::Get(), GetBuildTargetName()); - - connect(&m_timer, &QTimer::timeout, this, [&]() - { - this->PumpSystemEventLoopUntilEmpty(); - this->Tick(); - }); - } - - void ShaderManagementConsoleApplication::CreateReflectionManager() - { - Application::CreateReflectionManager(); - GetSerializeContext()->CreateEditContext(); - } - - void ShaderManagementConsoleApplication::Reflect(AZ::ReflectContext* context) - { - Application::Reflect(context); - - AzToolsFramework::AssetBrowser::AssetBrowserEntry::Reflect(context); - AzToolsFramework::AssetBrowser::RootAssetBrowserEntry::Reflect(context); - AzToolsFramework::AssetBrowser::FolderAssetBrowserEntry::Reflect(context); - AzToolsFramework::AssetBrowser::SourceAssetBrowserEntry::Reflect(context); - AzToolsFramework::AssetBrowser::ProductAssetBrowserEntry::Reflect(context); - - AzToolsFramework::QTreeViewWithStateSaving::Reflect(context); - AzToolsFramework::QWidgetSavedState::Reflect(context); - - if (auto behaviorContext = azrtti_cast(context)) - { - // this will put these methods into the 'azlmbr.shadermanagementconsole.general' module - auto addGeneral = [](AZ::BehaviorContext::GlobalMethodBuilder methodBuilder) - { - methodBuilder->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation) - ->Attribute(AZ::Script::Attributes::Category, "Editor") - ->Attribute(AZ::Script::Attributes::Module, "shadermanagementconsole.general"); - }; - // The reflection here is based on patterns in CryEditPythonHandler::Reflect - addGeneral(behaviorContext->Method("idle_wait_frames", &ShaderManagementConsoleApplication::PyIdleWaitFrames, nullptr, "Waits idling for a frames. Primarily used for auto-testing.")); - } - } - - void ShaderManagementConsoleApplication::RegisterCoreComponents() - { - Application::RegisterCoreComponents(); - RegisterComponentDescriptor(AzToolsFramework::AssetBrowser::AssetBrowserComponent::CreateDescriptor()); - RegisterComponentDescriptor(AzToolsFramework::Thumbnailer::ThumbnailerComponent::CreateDescriptor()); - RegisterComponentDescriptor(AzToolsFramework::Components::PropertyManagerComponent::CreateDescriptor()); - RegisterComponentDescriptor(AzToolsFramework::AssetSystem::AssetSystemComponent::CreateDescriptor()); - RegisterComponentDescriptor(AzToolsFramework::PerforceComponent::CreateDescriptor()); - } - - AZ::ComponentTypeList ShaderManagementConsoleApplication::GetRequiredSystemComponents() const - { - AZ::ComponentTypeList components = Application::GetRequiredSystemComponents(); - - components.insert(components.end(), { - azrtti_typeid(), - azrtti_typeid(), - azrtti_typeid(), - azrtti_typeid(), - }); - - return components; } void ShaderManagementConsoleApplication::CreateStaticModules(AZStd::vector& outModules) { - Application::CreateStaticModules(outModules); - outModules.push_back(aznew AzToolsFramework::AzToolsFrameworkModule); + Base::CreateStaticModules(outModules); outModules.push_back(aznew ShaderManagementConsoleDocumentModule); outModules.push_back(aznew ShaderManagementConsoleWindowModule); } - void ShaderManagementConsoleApplication::StartCommon(AZ::Entity* systemEntity) - { - AzFramework::AssetSystemStatusBus::Handler::BusConnect(); - AzToolsFramework::EditorPythonConsoleNotificationBus::Handler::BusConnect(); - - AzFramework::Application::StartCommon(systemEntity); - - StartInternal(); - - m_timer.start(); - } - void ShaderManagementConsoleApplication::OnShaderManagementConsoleWindowClosing() { ExitMainLoop(); @@ -174,110 +97,13 @@ namespace ShaderManagementConsole ShaderManagementConsole::ShaderManagementConsoleWindowRequestBus::Broadcast(&ShaderManagementConsole::ShaderManagementConsoleWindowRequestBus::Handler::DestroyShaderManagementConsoleWindow); ShaderManagementConsoleWindowNotificationBus::Handler::BusDisconnect(); - AzToolsFramework::AssetDatabase::AssetDatabaseRequestsBus::Handler::BusDisconnect(); - AzFramework::AssetSystemRequestBus::Broadcast(&AzFramework::AssetSystem::AssetSystemRequests::StartDisconnectingAssetProcessor); - - Application::Destroy(); + Base::Destroy(); } - void ShaderManagementConsoleApplication::AssetSystemAvailable() + AZStd::vector ShaderManagementConsoleApplication::GetCriticalAssetFilters() const { - // Try connect to AP first before try to launch it manually. - bool connected = false; - auto ConnectToAssetProcessorWithIdentifier = [&connected](AzFramework::AssetSystem::AssetSystemRequests* assetSystemRequests) - { - // When the AssetProcessor is already launched it should take less than a second to perform a connection - // but when the AssetProcessor needs to be launch it could take up to 15 seconds to have the AssetProcessor initialize - // and able to negotiate a connection when running a debug build - // and to negotiate a connection - - AzFramework::AssetSystem::ConnectionSettings connectionSettings; - AzFramework::AssetSystem::ReadConnectionSettingsFromSettingsRegistry(connectionSettings); - connectionSettings.m_connectionDirection = AzFramework::AssetSystem::ConnectionSettings::ConnectionDirection::ConnectToAssetProcessor; - connectionSettings.m_connectionIdentifier = "Shader Management Console"; - connectionSettings.m_loggingCallback = []([[maybe_unused]] AZStd::string_view logData) - { - AZ_TracePrintf("Shader Management Console", "%.*s", aznumeric_cast(logData.size()), logData.data()); - }; - - connected = assetSystemRequests->EstablishAssetProcessorConnection(connectionSettings); - }; - AzFramework::AssetSystemRequestBus::Broadcast(ConnectToAssetProcessorWithIdentifier); - - if (connected) - { - CompileCriticalAssets(); - } - - AzFramework::AssetSystemStatusBus::Handler::BusDisconnect(); - } - - void ShaderManagementConsoleApplication::CompileCriticalAssets() - { - AZ_TracePrintf("Shader Management Console", "Compiling critical assets.\n"); - - // List of common asset filters for things that need to be compiled to run - // Some of these things will not be necessary once we have proper support for queued asset loading and reloading - const AZStd::string assetFilterss[] = - { - "passes/", - "config/", - }; - - QStringList failedAssets; - - // Forced asset processor to synchronously process all critical assets - // Note: with AssetManager's current implementation, a compiled asset won't be added in asset registry until next system tick. - // So the asset id won't be found right after CompileAssetSync call. - for (const AZStd::string& assetFilters : assetFilterss) - { - AZ_TracePrintf("Shader Management Console", "Compiling critical asset matching: %s.\n", assetFilters.c_str()); - - // Wait for the asset be compiled - AzFramework::AssetSystem::AssetStatus status = AzFramework::AssetSystem::AssetStatus_Unknown; - AzFramework::AssetSystemRequestBus::BroadcastResult( - status, &AzFramework::AssetSystemRequestBus::Events::CompileAssetSync, assetFilters); - if (status != AzFramework::AssetSystem::AssetStatus_Compiled) - { - failedAssets.append(assetFilters.c_str()); - } - } - - if (!failedAssets.empty()) - { - QMessageBox::critical(activeWindow(), - QString("Failed to compile critical assets"), - QString("Failed to compile the following critical assets:\n%1\n%2") - .arg(failedAssets.join(",\n")) - .arg("Make sure this is an Atom project.")); - m_closing = true; - } - } - - void ShaderManagementConsoleApplication::SaveSettings() - { - if (m_activatedLocalUserSettings) - { - AZ::SerializeContext* context = nullptr; - AZ::ComponentApplicationBus::BroadcastResult(context, &AZ::ComponentApplicationRequests::GetSerializeContext); - AZ_Assert(context, "No serialize context"); - - char resolvedPath[AZ_MAX_PATH_LEN] = ""; - AZ::IO::FileIOBase::GetInstance()->ResolvePath("@user@/EditorUserSettings.xml", resolvedPath, AZ_ARRAY_SIZE(resolvedPath)); - m_localUserSettings.Save(resolvedPath, context); - } - } - - bool ShaderManagementConsoleApplication::OnPrintf(const char* window, const char* /*message*/) - { - // Suppress spam from the Source Control system - if (0 == strncmp(window, AzToolsFramework::SCC_WINDOW, AZ_ARRAY_SIZE(AzToolsFramework::SCC_WINDOW))) - { - return true; - } - - return false; + return AZStd::vector({ "passes/", "config/" }); } void ShaderManagementConsoleApplication::ProcessCommandLine() @@ -304,173 +130,12 @@ namespace ShaderManagementConsole } } - void ShaderManagementConsoleApplication::LoadSettings() - { - AZ::SerializeContext* context = nullptr; - AZ::ComponentApplicationBus::BroadcastResult(context, &AZ::ComponentApplicationRequests::GetSerializeContext); - AZ_Assert(context, "No serialize context"); - - char resolvedPath[AZ_MAX_PATH_LEN] = ""; - AZ::IO::FileIOBase::GetInstance()->ResolvePath("@user@/EditorUserSettings.xml", resolvedPath, AZ_MAX_PATH_LEN); - - m_localUserSettings.Load(resolvedPath, context); - m_localUserSettings.Activate(AZ::UserSettings::CT_LOCAL); - AZ::UserSettingsOwnerRequestBus::Handler::BusConnect(AZ::UserSettings::CT_LOCAL); - m_activatedLocalUserSettings = true; - } - - void ShaderManagementConsoleApplication::UnloadSettings() - { - if (m_activatedLocalUserSettings) - { - SaveSettings(); - m_localUserSettings.Deactivate(); - AZ::UserSettingsOwnerRequestBus::Handler::BusDisconnect(); - m_activatedLocalUserSettings = false; - } - } - - bool ShaderManagementConsoleApplication::LaunchDiscoveryService() - { - const QStringList arguments = { "-fail_silently" }; - - return AtomToolsFramework::LaunchTool("GridHub", AZ_TRAIT_SHADER_MANAGEMENT_CONSOLE_EXT, arguments); - } - void ShaderManagementConsoleApplication::StartInternal() { - if (m_closing) - { - return; - } - - m_traceLogger.WriteStartupLog("ShaderManagementConsole.log"); - - //[GFX TODO][ATOM-415] Try to factor out some of this stuff with AtomSampleViewerApplication - AzToolsFramework::AssetDatabase::AssetDatabaseRequestsBus::Handler::BusConnect(); - AzToolsFramework::AssetBrowser::AssetDatabaseLocationNotificationBus::Broadcast(&AzToolsFramework::AssetBrowser::AssetDatabaseLocationNotifications::OnDatabaseInitialized); - - AZ::Data::AssetCatalogRequestBus::Broadcast(&AZ::Data::AssetCatalogRequestBus::Events::LoadCatalog, "@assets@/assetcatalog.xml"); - - AZ::RPI::RPISystemInterface::Get()->InitializeSystemAssets(); - - LoadSettings(); - - LaunchDiscoveryService(); + Base::StartInternal(); ShaderManagementConsoleWindowNotificationBus::Handler::BusConnect(); ShaderManagementConsole::ShaderManagementConsoleWindowRequestBus::Broadcast(&ShaderManagementConsole::ShaderManagementConsoleWindowRequestBus::Handler::CreateShaderManagementConsoleWindow); - - auto editorPythonEventsInterface = AZ::Interface::Get(); - if (editorPythonEventsInterface) - { - // The PythonSystemComponent does not call StartPython to allow for lazy python initialization, so start it here - // The PythonSystemComponent will call StopPython when it deactivates, so we do not need our own corresponding call to StopPython - editorPythonEventsInterface->StartPython(); - } - - ProcessCommandLine(); } - - bool ShaderManagementConsoleApplication::GetAssetDatabaseLocation(AZStd::string& result) - { - AZ::SettingsRegistryInterface* settingsRegistry = AZ::SettingsRegistry::Get(); - AZ::IO::FixedMaxPath assetDatabaseSqlitePath; - if (settingsRegistry && settingsRegistry->Get(assetDatabaseSqlitePath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_CacheProjectRootFolder)) - { - assetDatabaseSqlitePath /= "assetdb.sqlite"; - result = AZStd::string_view(assetDatabaseSqlitePath.Native()); - return true; - } - - return false; - } - - void ShaderManagementConsoleApplication::Tick(float deltaOverride) - { - TickSystem(); - Application::Tick(deltaOverride); - - if (m_closing) - { - m_timer.disconnect(); - quit(); - } - } - - void ShaderManagementConsoleApplication::Stop() - { - UnloadSettings(); - AzFramework::Application::Stop(); - } - - void ShaderManagementConsoleApplication::QueryApplicationType(AZ::ApplicationTypeQuery& appType) const - { - appType.m_maskValue = AZ::ApplicationTypeQuery::Masks::Game; - } - - void ShaderManagementConsoleApplication::OnTraceMessage([[maybe_unused]] AZStd::string_view message) - { -#if defined(AZ_ENABLE_TRACING) - AZStd::vector lines; - AzFramework::StringFunc::Tokenize( - message, - lines, - "\n", - false, // Keep empty strings - false // Keep space strings - ); - - for (auto& line : lines) - { - AZ_TracePrintf("Shader Management Console", "Python: %s\n", line.c_str()); - } -#endif - } - - void ShaderManagementConsoleApplication::OnErrorMessage(AZStd::string_view message) - { - // Use AZ_TracePrintf instead of AZ_Error or AZ_Warning to avoid all the metadata noise - OnTraceMessage(message); - } - - void ShaderManagementConsoleApplication::OnExceptionMessage([[maybe_unused]] AZStd::string_view message) - { - AZ_Error("Shader Management Console", false, "Python: " AZ_STRING_FORMAT, AZ_STRING_ARG(message)); - } - - // Copied from PyIdleWaitFrames in CryEdit.cpp - void ShaderManagementConsoleApplication::PyIdleWaitFrames(uint32_t frames) - { - struct Ticker : public AZ::TickBus::Handler - { - Ticker(QEventLoop* loop, uint32_t targetFrames) : m_loop(loop), m_targetFrames(targetFrames) - { - AZ::TickBus::Handler::BusConnect(); - } - ~Ticker() - { - AZ::TickBus::Handler::BusDisconnect(); - } - - void OnTick(float deltaTime, AZ::ScriptTimePoint time) override - { - AZ_UNUSED(deltaTime); - AZ_UNUSED(time); - if (++m_elapsedFrames == m_targetFrames) - { - m_loop->quit(); - } - } - QEventLoop* m_loop = nullptr; - uint32_t m_elapsedFrames = 0; - uint32_t m_targetFrames = 0; - }; - - QEventLoop loop; - Ticker ticker(&loop, frames); - loop.exec(); - } - } // namespace ShaderManagementConsole diff --git a/Gems/Atom/Tools/ShaderManagementConsole/Code/Source/ShaderManagementConsoleApplication.h b/Gems/Atom/Tools/ShaderManagementConsole/Code/Source/ShaderManagementConsoleApplication.h index a137e3a646..5d3696fee3 100644 --- a/Gems/Atom/Tools/ShaderManagementConsole/Code/Source/ShaderManagementConsoleApplication.h +++ b/Gems/Atom/Tools/ShaderManagementConsole/Code/Source/ShaderManagementConsoleApplication.h @@ -8,63 +8,32 @@ #pragma once -#include -#include -#include -#include - -#include -#include - -#include -#include -#include - #include #include - -#include +#include #include namespace ShaderManagementConsole { class ShaderManagementConsoleApplication - : public AzFramework::Application - , public AzQtComponents::AzQtApplication - , private AzToolsFramework::AssetDatabase::AssetDatabaseRequestsBus::Handler + : public AtomToolsFramework::AtomToolsApplication , private ShaderManagementConsoleWindowNotificationBus::Handler - , private AzFramework::AssetSystemStatusBus::Handler - , private AZ::UserSettingsOwnerRequestBus::Handler - , private AZ::Debug::TraceMessageBus::Handler - , private AzToolsFramework::EditorPythonConsoleNotificationBus::Handler { public: - AZ_TYPE_INFO(ShaderManagementConsole::ShaderManagementConsoleApplication, "{30F90CA5-1253-49B5-8143-19CEE37E22BB}"); + AZ_TYPE_INFO(ShaderManagementConsole::ShaderManagementConsoleApplication, "{A31B1AEB-4DA3-49CD-884A-CC998FF7546F}"); - using Base = AzFramework::Application; + using Base = AtomToolsFramework::AtomToolsApplication; ShaderManagementConsoleApplication(int* argc, char*** argv); virtual ~ShaderManagementConsoleApplication() = default; ////////////////////////////////////////////////////////////////////////// // AzFramework::Application - void CreateReflectionManager() override; - void Reflect(AZ::ReflectContext* context) override; - void RegisterCoreComponents() override; - AZ::ComponentTypeList GetRequiredSystemComponents() const override; void CreateStaticModules(AZStd::vector& outModules) override; const char* GetCurrentConfigurationName() const override; - void StartCommon(AZ::Entity* systemEntity) override; - void Tick(float deltaOverride = -1.f) override; - void Stop() override; private: - ////////////////////////////////////////////////////////////////////////// - // AssetDatabaseRequestsBus::Handler overrides... - bool GetAssetDatabaseLocation(AZStd::string& result) override; - ////////////////////////////////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////// // ShaderManagementConsoleWindowNotificationBus::Handler overrides... void OnShaderManagementConsoleWindowClosing() override; @@ -75,57 +44,9 @@ namespace ShaderManagementConsole void Destroy() override; ////////////////////////////////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////// - // AzFramework::ApplicationRequests::Bus overrides... - void QueryApplicationType(AZ::ApplicationTypeQuery& appType) const override; - ////////////////////////////////////////////////////////////////////////// - - //////////////////////////////////////////////////////////////////////// - // EditorPythonConsoleNotificationBus::Handler overrides... - void OnTraceMessage(AZStd::string_view message) override; - void OnErrorMessage(AZStd::string_view message) override; - void OnExceptionMessage(AZStd::string_view message) override; - //////////////////////////////////////////////////////////////////////// - - ////////////////////////////////////////////////////////////////////////// - // AzFramework::AssetSystemStatusBus::Handler overrides... - void AssetSystemAvailable() override; - ////////////////////////////////////////////////////////////////////////// - - ////////////////////////////////////////////////////////////////////////// - // AZ::UserSettingsOwnerRequestBus::Handler overrides... - void SaveSettings() override; - ////////////////////////////////////////////////////////////////////////// - - ////////////////////////////////////////////////////////////////////////// - // AZ::Debug::TraceMessageBus::Handler overrides... - bool OnPrintf(const char* window, const char* message) override; - ////////////////////////////////////////////////////////////////////////// - - void CompileCriticalAssets(); - void ProcessCommandLine(); - - void LoadSettings(); - void UnloadSettings(); - - bool LaunchDiscoveryService(); - - void StartInternal(); - - static void PyIdleWaitFrames(uint32_t frames); - - AzToolsFramework::TraceLogger m_traceLogger; - - //! Local user settings are used to store asset browser tree expansion state - AZ::UserSettingsProvider m_localUserSettings; - - //! Are local settings loaded - bool m_activatedLocalUserSettings = false; - - QTimer m_timer; - - bool m_started = false; - bool m_closing = false; + void StartInternal() override; + AZStd::string GetBuildTargetName() const override; + AZStd::vector GetCriticalAssetFilters() const override; }; } // namespace ShaderManagementConsole diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index 9a46bb10de..21c7ce80d6 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -69,19 +69,24 @@ namespace Multiplayer { using namespace AzNetworking; - AZ_CVAR(uint16_t, cl_clientport, 0, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port to bind to for game traffic when connecting to a remote host, a value of 0 will select any available port"); - AZ_CVAR(AZ::CVarFixedString, cl_serveraddr, AZ::CVarFixedString(LocalHost), nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The address of the remote server or host to connect to"); - AZ_CVAR(AZ::CVarFixedString, cl_serverpassword, "", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Optional server password"); + AZ_CVAR(uint16_t, cl_clientport, 0, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, + "The port to bind to for game traffic when connecting to a remote host, a value of 0 will select any available port"); + AZ_CVAR(AZ::CVarFixedString, cl_serveraddr, AZ::CVarFixedString(LocalHost), nullptr, AZ::ConsoleFunctorFlags::DontReplicate, + "The address of the remote server or host to connect to"); AZ_CVAR(uint16_t, cl_serverport, DefaultServerPort, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port of the remote host to connect to for game traffic"); AZ_CVAR(uint16_t, sv_port, DefaultServerPort, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port that this multiplayer gem will bind to for game traffic"); AZ_CVAR(AZ::CVarFixedString, sv_map, "nolevel", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The map the server should load"); - AZ_CVAR(AZ::CVarFixedString, sv_gamerules, "norules", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "GameRules server works with"); AZ_CVAR(ProtocolType, sv_protocol, ProtocolType::Udp, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "This flag controls whether we use TCP or UDP for game networking"); AZ_CVAR(bool, sv_isDedicated, true, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Whether the host command creates an independent or client hosted server"); AZ_CVAR(bool, sv_isTransient, true, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Whether a dedicated server shuts down if all existing connections disconnect."); - AZ_CVAR(AZ::TimeMs, cl_defaultNetworkEntityActivationTimeSliceMs, AZ::TimeMs{ 0 }, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Max Ms to use to activate entities coming from the network, 0 means instantiate everything"); + AZ_CVAR(AZ::TimeMs, cl_defaultNetworkEntityActivationTimeSliceMs, AZ::TimeMs{ 0 }, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, + "Max Ms to use to activate entities coming from the network, 0 means instantiate everything"); AZ_CVAR(AZ::TimeMs, sv_serverSendRateMs, AZ::TimeMs{ 50 }, nullptr, AZ::ConsoleFunctorFlags::Null, "Minimum number of milliseconds between each network update"); - AZ_CVAR(AZ::CVarFixedString, sv_defaultPlayerSpawnAsset, "prefabs/player.network.spawnable", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The default spawnable to use when a new player connects"); + AZ_CVAR(AZ::CVarFixedString, sv_defaultPlayerSpawnAsset, "prefabs/player.network.spawnable", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, + "The default spawnable to use when a new player connects"); + AZ_CVAR(float, cl_renderTickBlendBase, 0.15f, nullptr, AZ::ConsoleFunctorFlags::Null, + "The base used for blending between network updates, 0.1 will be quite linear, 0.2 or 0.3 will " + "slow down quicker and may be better suited to connections with highly variable latency"); void MultiplayerSystemComponent::Reflect(AZ::ReflectContext* context) { @@ -546,7 +551,7 @@ namespace Multiplayer if ((GetAgentType() == MultiplayerAgentType::Client) && (packet.GetHostFrameId() > m_lastReplicatedHostFrameId)) { // Update client to latest server time - m_renderBlendFactor = 0.0f; + m_tickFactor = 0.0f; m_lastReplicatedHostTimeMs = packet.GetHostTimeMs(); m_lastReplicatedHostFrameId = packet.GetHostFrameId(); m_networkTime.AlterTime(m_lastReplicatedHostFrameId, m_lastReplicatedHostTimeMs, AzNetworking::InvalidConnectionId); @@ -849,10 +854,18 @@ namespace Multiplayer void MultiplayerSystemComponent::TickVisibleNetworkEntities(float deltaTime, float serverRateSeconds) { + m_tickFactor += deltaTime / serverRateSeconds; // Linear close to the origin, but asymptote at y = 1 - const float targetAdjustBlend = AZStd::clamp(deltaTime / serverRateSeconds, 0.0f, 1.0f); - m_renderBlendFactor = 1.0f - (std::pow(0.2f, m_renderBlendFactor + targetAdjustBlend)); - AZLOG(NET_Blending, "Computed blend factor of %0.2f using a frametime of %0.2f and a serverTickRate of %0.2f", m_renderBlendFactor, deltaTime, serverRateSeconds); + const float renderBlendFactor = AZStd::clamp(1.0f - (std::pow(cl_renderTickBlendBase, m_tickFactor)), 0.0f, 1.0f); + AZLOG + ( + NET_Blending, + "Computed blend factor of %0.3f using a tick factor of %0.3f, a frametime of %0.3f and a serverTickRate of %0.3f", + renderBlendFactor, + m_tickFactor, + deltaTime, + serverRateSeconds + ); if (Camera::ActiveCameraRequestBus::HasHandlers()) { @@ -895,7 +908,7 @@ namespace Multiplayer for (NetBindComponent* netBindComponent : gatheredEntities) { - netBindComponent->NotifyPreRender(deltaTime, m_renderBlendFactor); + netBindComponent->NotifyPreRender(deltaTime, renderBlendFactor); } } else @@ -907,7 +920,7 @@ namespace Multiplayer NetBindComponent* netBindComponent = entity->FindComponent(); if (netBindComponent != nullptr) { - netBindComponent->NotifyPreRender(deltaTime, m_renderBlendFactor); + netBindComponent->NotifyPreRender(deltaTime, renderBlendFactor); } } } diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h index 37e8138a71..ab37958962 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h @@ -155,7 +155,7 @@ namespace Multiplayer HostFrameId m_lastReplicatedHostFrameId = HostFrameId(0); double m_serverSendAccumulator = 0.0; - float m_renderBlendFactor = 0.0f; + float m_tickFactor = 0.0f; #if !defined(AZ_RELEASE_BUILD) MultiplayerEditorConnection m_editorConnectionListener; diff --git a/cmake/LYTestWrappers.cmake b/cmake/LYTestWrappers.cmake index 535f00f58d..efb19a20dd 100644 --- a/cmake/LYTestWrappers.cmake +++ b/cmake/LYTestWrappers.cmake @@ -219,8 +219,17 @@ function(ly_add_test) VS_DEBUGGER_COMMAND_ARGUMENTS "${test_arguments_line}" ) + # In the case where we are creating a custom target, we need to add dependency to the target + if(ly_add_test_PARENT_NAME AND NOT ${ly_add_test_NAME} STREQUAL ${ly_add_test_PARENT_NAME}) + ly_add_dependencies(${unaliased_test_name} ${ly_add_test_PARENT_NAME}) + endif() + endif() + # For test projects that are custom targets, pass a props file that sets the project as "Console" so + # it leaves the console open when it finishes + set_target_properties(${unaliased_test_name} PROPERTIES VS_USER_PROPS "${LY_ROOT_FOLDER}/cmake/Platform/Common/TestProject.props") + # Include additional dependencies if (ly_add_test_RUNTIME_DEPENDENCIES) ly_add_dependencies(${unaliased_test_name} ${ly_add_test_RUNTIME_DEPENDENCIES}) diff --git a/cmake/Platform/Common/Directory.Build.props b/cmake/Platform/Common/Directory.Build.props index 73aa984073..76e4b28922 100644 --- a/cmake/Platform/Common/Directory.Build.props +++ b/cmake/Platform/Common/Directory.Build.props @@ -17,7 +17,12 @@ SPDX-License-Identifier: Apache-2.0 OR MIT - TurnOffAllWarnings + + TurnOffAllWarnings + + \ No newline at end of file diff --git a/cmake/Platform/Common/Install_common.cmake b/cmake/Platform/Common/Install_common.cmake index 2157a632c7..612358f938 100644 --- a/cmake/Platform/Common/Install_common.cmake +++ b/cmake/Platform/Common/Install_common.cmake @@ -10,6 +10,15 @@ include(cmake/FileUtil.cmake) set(CMAKE_INSTALL_MESSAGE NEVER) # Simplify messages to reduce output noise +define_property(TARGET PROPERTY LY_INSTALL_GENERATE_RUN_TARGET + BRIEF_DOCS "Defines if a \"RUN\" targets should be created when installing this target Gem" + FULL_DOCS [[ + Property which is set on targets that should generate a "RUN" + target when installed. This \"RUN\" target helps to run the + binary from the installed location directly from the IDE. + ]] +) + ly_set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME Core) cmake_path(RELATIVE_PATH CMAKE_RUNTIME_OUTPUT_DIRECTORY BASE_DIRECTORY ${CMAKE_BINARY_DIR} OUTPUT_VARIABLE runtime_output_directory) @@ -117,15 +126,19 @@ function(ly_setup_target OUTPUT_CONFIGURED_TARGET ALIAS_TARGET_NAME absolute_tar set(NAMESPACE_PLACEHOLDER "") set(NAME_PLACEHOLDER ${TARGET_NAME}) endif() + get_target_property(should_create_helper ${TARGET_NAME} LY_INSTALL_GENERATE_RUN_TARGET) + if(should_create_helper) + set(NAME_PLACEHOLDER ${NAME_PLACEHOLDER}.Imported) + endif() set(TARGET_TYPE_PLACEHOLDER "") - get_target_property(target_type ${NAME_PLACEHOLDER} TYPE) + get_target_property(target_type ${TARGET_NAME} TYPE) # Remove the _LIBRARY since we dont need to pass that to ly_add_targets string(REPLACE "_LIBRARY" "" TARGET_TYPE_PLACEHOLDER ${target_type}) # For HEADER_ONLY libs we end up generating "INTERFACE" libraries, need to specify HEADERONLY instead string(REPLACE "INTERFACE" "HEADERONLY" TARGET_TYPE_PLACEHOLDER ${TARGET_TYPE_PLACEHOLDER}) if(TARGET_TYPE_PLACEHOLDER STREQUAL "MODULE") - get_target_property(gem_module ${NAME_PLACEHOLDER} GEM_MODULE) + get_target_property(gem_module ${TARGET_NAME} GEM_MODULE) if(gem_module) set(TARGET_TYPE_PLACEHOLDER "GEM_MODULE") endif() @@ -158,7 +171,6 @@ function(ly_setup_target OUTPUT_CONFIGURED_TARGET ALIAS_TARGET_NAME absolute_tar unset(RUNTIME_DEPENDENCIES_PLACEHOLDER) endif() - get_target_property(inteface_build_dependencies_props ${TARGET_NAME} INTERFACE_LINK_LIBRARIES) unset(INTERFACE_BUILD_DEPENDENCIES_PLACEHOLDER) if(inteface_build_dependencies_props) @@ -182,6 +194,23 @@ function(ly_setup_target OUTPUT_CONFIGURED_TARGET ALIAS_TARGET_NAME absolute_tar list(REMOVE_DUPLICATES INTERFACE_BUILD_DEPENDENCIES_PLACEHOLDER) string(REPLACE ";" "\n" INTERFACE_BUILD_DEPENDENCIES_PLACEHOLDER "${INTERFACE_BUILD_DEPENDENCIES_PLACEHOLDER}") + # If the target is an executable/application, add a custom target so we can debug the target in project-centric workflow + if(should_create_helper) + string(REPLACE ".Imported" "" RUN_TARGET_NAME ${NAME_PLACEHOLDER}) + set(target_types_with_debugging_helper EXECUTABLE APPLICATION) + if(NOT target_type IN_LIST target_types_with_debugging_helper) + message(FATAL_ERROR "Cannot generate a RUN target for ${TARGET_NAME}, type is ${target_type}") + endif() + set(TARGET_RUN_HELPER +"add_custom_target(${RUN_TARGET_NAME}) +set_target_properties(${RUN_TARGET_NAME} PROPERTIES + FOLDER \"CMakePredefinedTargets/SDK\" + VS_DEBUGGER_COMMAND \$> + VS_DEBUGGER_COMMAND_ARGUMENTS \"--project-path=\${LY_DEFAULT_PROJECT_PATH}\" +)" +) + endif() + # Config file set(target_file_contents "# Generated by O3DE install\n\n") if(NOT target_type STREQUAL INTERFACE_LIBRARY) @@ -194,13 +223,13 @@ function(ly_setup_target OUTPUT_CONFIGURED_TARGET ALIAS_TARGET_NAME absolute_tar set(target_location "\${LY_ROOT_FOLDER}/${library_output_directory}/${PAL_PLATFORM_NAME}/$/${target_library_output_subdirectory}/$") elseif(target_type STREQUAL SHARED_LIBRARY) string(APPEND target_file_contents -"set_property(TARGET ${TARGET_NAME} +"set_property(TARGET ${NAME_PLACEHOLDER} APPEND_STRING PROPERTY IMPORTED_IMPLIB $<$$:\"\${LY_ROOT_FOLDER}/${archive_output_directory}/${PAL_PLATFORM_NAME}/$/$\"$ ) ") string(APPEND target_file_contents -"set_property(TARGET ${TARGET_NAME} +"set_property(TARGET ${NAME_PLACEHOLDER} PROPERTY IMPORTED_IMPLIB_$> \"\${LY_ROOT_FOLDER}/${archive_output_directory}/${PAL_PLATFORM_NAME}/$/$\" ) @@ -212,11 +241,11 @@ function(ly_setup_target OUTPUT_CONFIGURED_TARGET ALIAS_TARGET_NAME absolute_tar if(target_location) string(APPEND target_file_contents -"set_property(TARGET ${TARGET_NAME} +"set_property(TARGET ${NAME_PLACEHOLDER} APPEND_STRING PROPERTY IMPORTED_LOCATION $<$$:${target_location}$ ) -set_property(TARGET ${TARGET_NAME} +set_property(TARGET ${NAME_PLACEHOLDER} PROPERTY IMPORTED_LOCATION_$> ${target_location} ) diff --git a/cmake/Platform/Common/TestProject.props b/cmake/Platform/Common/TestProject.props new file mode 100644 index 0000000000..dff509b7ff --- /dev/null +++ b/cmake/Platform/Common/TestProject.props @@ -0,0 +1,16 @@ + + + + + + + + Console + + + diff --git a/cmake/SettingsRegistry.cmake b/cmake/SettingsRegistry.cmake index b740fefd25..ebf2254dc6 100644 --- a/cmake/SettingsRegistry.cmake +++ b/cmake/SettingsRegistry.cmake @@ -159,10 +159,6 @@ function(ly_delayed_generate_settings_registry) message(FATAL_ERROR "Dependency ${gem_target} from ${target} does not exist") endif() - get_property(has_manually_added_dependencies TARGET ${gem_target} PROPERTY MANUALLY_ADDED_DEPENDENCIES SET) - get_target_property(target_type ${gem_target} TYPE) - - ly_get_gem_module_root(gem_module_root ${gem_target}) file(RELATIVE_PATH gem_module_root_relative_to_engine_root ${LY_ROOT_FOLDER} ${gem_module_root}) @@ -180,7 +176,8 @@ function(ly_delayed_generate_settings_registry) list(JOIN target_gem_dependencies_names ",\n" target_gem_dependencies_names) string(CONFIGURE ${gems_json_template} gem_json @ONLY) get_target_property(is_imported ${target} IMPORTED) - if(is_imported) + get_target_property(target_type ${target} TYPE) + if(is_imported OR target_type STREQUAL UTILITY) unset(target_dir) foreach(conf IN LISTS CMAKE_CONFIGURATION_TYPES) string(TOUPPER ${conf} UCONF) diff --git a/cmake/install/InstalledTarget.in b/cmake/install/InstalledTarget.in index 0503fd5f2b..a4f4fa4763 100644 --- a/cmake/install/InstalledTarget.in +++ b/cmake/install/InstalledTarget.in @@ -17,6 +17,8 @@ ly_add_target( @RUNTIME_DEPENDENCIES_PLACEHOLDER@ ) +@TARGET_RUN_HELPER@ + set(configs @CMAKE_CONFIGURATION_TYPES@) foreach(config ${configs}) include("@NAME_PLACEHOLDER@_${config}.cmake" OPTIONAL) diff --git a/scripts/o3de/o3de/manifest.py b/scripts/o3de/o3de/manifest.py index 5bc6b95358..7e504d29cd 100644 --- a/scripts/o3de/o3de/manifest.py +++ b/scripts/o3de/o3de/manifest.py @@ -236,15 +236,13 @@ def get_gems_from_subdirectories(external_subdirs: list) -> list: # Data query methods -def get_this_engine() -> dict: - json_data = load_o3de_manifest() - engine_data = find_engine_data(json_data) - return engine_data - - def get_engines() -> list: json_data = load_o3de_manifest() - return json_data['engines'] if 'engines' in json_data else [] + engine_list = json_data['engines'] if 'engines' in json_data else [] + # Convert each engine dict entry into a string entry + return list(map( + lambda engine_object: engine_object.get('path', '') if isinstance(engine_object, dict) else engine_object, + engine_list)) def get_projects() -> list: @@ -424,20 +422,6 @@ def get_templates_for_generic_creation(): # temporary until we have a better wa return list(filter(filter_project_and_gem_templates_out, get_all_templates())) -def find_engine_data(json_data: dict, - engine_path: str or pathlib.Path = None) -> dict or None: - if not engine_path: - engine_path = get_this_engine_path() - engine_path = pathlib.Path(engine_path).resolve() - - for engine_object in json_data['engines']: - engine_object_path = pathlib.Path(engine_object['path']).resolve() - if engine_path == engine_object_path: - return engine_object - - return None - - def get_engine_json_data(engine_name: str = None, engine_path: str or pathlib.Path = None) -> dict or None: if not engine_name and not engine_path: @@ -639,8 +623,13 @@ def get_registered(engine_name: str = None, # check global first then this engine if isinstance(engine_name, str): - for engine in json_data['engines']: - engine_path = pathlib.Path(engine['path']).resolve() + engines = get_engines() + for engine in engines: + if isinstance(engine, dict): + engine_path = pathlib.Path(engine['path']).resolve() + else: + engine_path = pathlib.Path(engine_object).resolve() + engine_json = engine_path / 'engine.json' with engine_json.open('r') as f: try: diff --git a/scripts/o3de/o3de/print_registration.py b/scripts/o3de/o3de/print_registration.py index 10316cc4a8..b164243b7c 100644 --- a/scripts/o3de/o3de/print_registration.py +++ b/scripts/o3de/o3de/print_registration.py @@ -40,61 +40,67 @@ def get_project_path(project_path: pathlib.Path, project_name: str) -> pathlib.P def print_this_engine(verbose: int) -> int: - engine_data = manifest.get_this_engine() - print(json.dumps(engine_data, indent=4)) - result = True + this_engine_path = manifest.get_this_engine_path() + print(f'This Engine:\n{json.dumps(str(this_engine_path), indent=4)}') if verbose > 0: - result = print_manifest_json_data(engine_data, 'engine.json', 'This Engine', + return print_manifest_json_data([this_engine_path], 'This Engine', manifest.get_engine_json_data, 'engine_path') - return 0 if result else 1 + return 0 def print_engines(verbose: int) -> None: engines_data = manifest.get_engines() - print(json.dumps(engines_data, indent=4)) + print(f'Engine Paths:\n{json.dumps(engines_data, indent=4)}') if verbose > 0: - return print_manifest_json_data(engines_data, 'engine.json', 'Engines', + return print_manifest_json_data(engines_data, 'Engine Jsons', manifest.get_engine_json_data, 'engine_path') return 0 def print_projects(verbose: int) -> int: projects_data = manifest.get_projects() - print(json.dumps(projects_data, indent=4)) + print(f'Project Paths:\n{json.dumps(projects_data, indent=4)}') if verbose > 0: - return print_manifest_json_data(projects_data, 'project.json', 'Projects', + return print_manifest_json_data(projects_data, 'Project Jsons', manifest.get_project_json_data, 'project_path') return 0 def print_gems(verbose: int) -> int: gems_data = manifest.get_gems() - print(json.dumps(gems_data, indent=4)) + print(f'Gem Paths:\n{json.dumps(gems_data, indent=4)}') if verbose > 0: - return print_manifest_json_data(gems_data, 'gem.json', 'Gems', + return print_manifest_json_data(gems_data, 'Gem Jsons', manifest.get_gem_json_data, 'gem_path') return 0 +def print_external_subdirectories(verbose: int) -> int: + external_subdirs_data = manifest.get_external_subdirectories() + print(f'External Subdirectories:\n{json.dumps(external_subdirs_data, indent=4)}') + return 0 + + + def print_templates(verbose: int) -> int: templates_data = manifest.get_templates() - print(json.dumps(templates_data, indent=4)) + print(f'Template Paths:\n{json.dumps(templates_data, indent=4)}') if verbose > 0: - return print_manifest_json_data(templates_data, 'template.json', 'Templates', + return print_manifest_json_data(templates_data, 'Template Jsons', manifest.get_template_json_data, 'template_path') return 0 def print_restricted(verbose: int) -> int: restricted_data = manifest.get_restricted() - print(json.dumps(restricted_data, indent=4)) + print(f'Restricted Paths:\n{json.dumps(restricted_data, indent=4)}') if verbose > 0: - return print_manifest_json_data(restricted_data, 'restricted.json', 'Restricted', + return print_manifest_json_data(restricted_data, 'Restricted Jsons', manifest.get_restricted_json_data, 'restricted_path') return 0 @@ -102,47 +108,47 @@ def print_restricted(verbose: int) -> int: # Engine output methods def print_engine_projects(verbose: int) -> int: engine_projects_data = manifest.get_engine_projects() - print(json.dumps(engine_projects_data, indent=4)) + print(f'Project Paths:\n{json.dumps(engine_projects_data, indent=4)}') if verbose > 0: - return print_manifest_json_data(engine_projects_data, 'project.json', 'Projects', + return print_manifest_json_data(engine_projects_data, 'Project Jsons', manifest.get_project_json_data, 'project_path') return 0 def print_engine_gems(verbose: int) -> int: engine_gems_data = manifest.get_engine_gems() - print(json.dumps(engine_gems_data, indent=4)) + print(f'Gem Paths:\n{json.dumps(engine_gems_data, indent=4)}') if verbose > 0: - return print_manifest_json_data(engine_gems_data, 'gem.json', 'Gems', + return print_manifest_json_data(engine_gems_data, 'Gem Jsons', manifest.get_gem_json_data, 'gem_path') return 0 def print_engine_templates(verbose: int) -> int: engine_templates_data = manifest.get_engine_templates() - print(json.dumps(engine_templates_data, indent=4)) + print(f'Template Paths:\n{json.dumps(engine_templates_data, indent=4)}') if verbose > 0: - return print_manifest_json_data(engine_templates_data, 'template.json', 'Templates', + return print_manifest_json_data(engine_templates_data, 'Template Jsons', manifest.get_template_json_data, 'template_path') return 0 def print_engine_restricted(verbose: int) -> int: engine_restricted_data = manifest.get_engine_restricted() - print(json.dumps(engine_restricted_data, indent=4)) + print(f'Restricted Paths:\n{json.dumps(engine_restricted_data, indent=4)}') if verbose > 0: - return print_manifest_json_data(engine_restricted_data, 'restricted.json', 'Restricted', + return print_manifest_json_data(engine_restricted_data, 'Restricted Jsons', manifest.get_restricted_json_data, 'restricted_path') return 0 -def print_engine_external_subdirectories() -> int: +def print_engine_external_subdirectories(verbose: int) -> int: external_subdirs_data = manifest.get_engine_external_subdirectories() - print(json.dumps(external_subdirs_data, indent=4)) + print(f'External Subdirectories:\n{json.dumps(external_subdirs_data, indent=4)}') return 0 @@ -153,21 +159,21 @@ def print_project_gems(verbose: int, project_path: pathlib.Path, project_name: s return 1 project_gems_data = manifest.get_project_gems(project_path) - print(json.dumps(project_gems_data, indent=4)) + print(f'Gem Paths:\n{json.dumps(project_gems_data, indent=4)}') if verbose > 0: - return print_manifest_json_data(project_gems_data, 'gem.json', 'Gems', + return print_manifest_json_data(project_gems_data, 'Gems Jsons', manifest.get_gem_json_data, 'gem_path') return 0 -def print_project_external_subdirectories(project_path: pathlib.Path, project_name: str) -> int: +def print_project_external_subdirectories(verbose: int, project_path: pathlib.Path, project_name: str) -> int: project_path = get_project_path(project_path, project_name) if not project_path: return 1 external_subdirs_data = manifest.get_project_external_subdirectories(project_path) - print(json.dumps(external_subdirs_data, indent=4)) + print(f'External Subdirectories:\n{json.dumps(external_subdirs_data, indent=4)}') return 0 @@ -177,9 +183,9 @@ def print_project_templates(verbose: int, project_path: pathlib.Path, project_na return 1 project_templates_data = manifest.get_project_templates(project_path) - print(json.dumps(project_templates_data, indent=4)) + print(f'Template Paths:\n{json.dumps(project_templates_data, indent=4)}') if verbose > 0: - return print_manifest_json_data(project_templates_data, 'template.json', 'Templates', + return print_manifest_json_data(project_templates_data, 'Template Jsons', manifest.get_template_json_data, 'template_path') return 0 @@ -190,73 +196,118 @@ def print_project_restricted(verbose: int, project_path: pathlib.Path, project_n return 1 project_restricted_data = manifest.get_project_restricted(project_path) - print(json.dumps(project_restricted_data, indent=4)) + print(f'Restricted Paths:\n{json.dumps(project_restricted_data, indent=4)}') if verbose > 0: - return print_manifest_json_data(project_restricted_data, 'restricted.json', 'Restricted', + return print_manifest_json_data(project_restricted_data, 'Restricted Jsons', manifest.get_restricted_json_data, 'restricted_path') return 0 def print_all_projects(verbose: int) -> int: all_projects_data = manifest.get_all_projects() - print(json.dumps(all_projects_data, indent=4)) + print(f'Project Paths:\n{json.dumps(all_projects_data, indent=4)}') if verbose > 0: - return print_manifest_json_data(all_projects_data, 'project.json', 'Projects', + return print_manifest_json_data(all_projects_data, 'Project Jsons', manifest.get_project_json_data, 'project_path') return 0 -def print_all_gems(verbose: int) -> int: - all_gems_data = manifest.get_all_gems() - print(json.dumps(all_gems_data, indent=4)) +def print_all_gems(verbose: int, project_path: pathlib.Path = None, project_name: str = None) -> int: + all_gems = manifest.get_gems() + all_gems.extend(manifest.get_engine_gems()) + + # If a project path or project name is supplied query the gems from that project, otherwise query the gems from + # all projects + project_path = get_project_path(project_path, project_name) if project_path or project_name else None + projects = [project_path] if project_path else manifest.get_all_projects() + for project in projects: + all_gems.extend(manifest.get_project_gems(project)) + + # Filter out duplicates + all_gems = list(dict.fromkeys(all_gems)) + print(f'Gem Paths:\n{json.dumps(all_gems, indent=4)}') if verbose > 0: - return print_manifest_json_data(all_gems_data, 'gem.json', 'Gems', + return print_manifest_json_data(all_gems, 'Gem Jsons', manifest.get_gem_json_data, 'gem_path') return 0 -def print_all_external_subdirectories() -> int: - all_external_subdirectories_data = manifest.get_all_external_subdirectories() - print(json.dumps(all_external_subdirectories_data, indent=4)) +def print_all_external_subdirectories(verbose: int, project_path: pathlib.Path = None, project_name: str = None) -> int: + all_external_subdirectories = manifest.get_external_subdirectories() + all_external_subdirectories.extend(manifest.get_engine_external_subdirectories()) + + # If a project path or project name is supplied query the external subdirectories from that project, + # otherwise query the external subdirectories from all projects + project_path = get_project_path(project_path, project_name) if project_path or project_name else None + projects = [project_path] if project_path else manifest.get_all_projects() + for project in projects: + all_external_subdirectories.extend(manifest.get_project_external_subdirectories(project)) + + # Filter out duplicates + all_external_subdirectories = list(dict.fromkeys(all_external_subdirectories)) + print(f'External Subdirectories:\n{json.dumps(all_external_subdirectories, indent=4)}') return 0 -def print_all_templates(verbose: int) -> int: - all_templates_data = manifest.get_all_templates() - print(json.dumps(all_templates_data, indent=4)) + +def print_all_templates(verbose: int, project_path: pathlib.Path = None, project_name: str = None) -> int: + all_templates = manifest.get_templates() + all_templates.extend(manifest.get_engine_templates()) + + # If a project path or project name is supplied query the templates from that project, + # otherwise query the templates from all projects + project_path = get_project_path(project_path, project_name) if project_path or project_name else None + projects = [project_path] if project_path else manifest.get_all_projects() + for project in projects: + all_templates.extend(manifest.get_project_templates(project)) + + # Filter out duplicates + all_templates = list(dict.fromkeys(all_templates)) + print(f'Template Paths:\n{json.dumps(all_templates, indent=4)}') if verbose > 0: - return print_manifest_json_data(all_templates_data, 'template.json', 'Templates', + return print_manifest_json_data(all_templates, 'Template Jsons', manifest.get_template_json_data, 'template_path') return 0 -def print_all_restricted(verbose: int) -> int: - all_restricted_data = manifest.get_all_restricted() - print(json.dumps(all_restricted_data, indent=4)) +def print_all_restricted(verbose: int, project_path: pathlib.Path = None, project_name: str = None) -> int: + all_restricted = manifest.get_restricted() + all_restricted.extend(manifest.get_engine_restricted()) + + # If a project path or project name is supplied query the restricted from that project, + # otherwise query the restricted from all projects + project_path = get_project_path(project_path, project_name) if project_path or project_name else None + projects = [project_path] if project_path else manifest.get_all_projects() + for project in projects: + all_restricted.extend(manifest.get_project_restricted(project)) + + # Filter out duplicates + all_restricted = list(dict.fromkeys(all_restricted)) + print(f'Restricted Paths:\n{json.dumps(all_restricted, indent=4)}') if verbose > 0: - return print_manifest_json_data(all_restricted_data, 'restricted.json', 'Restricted', + return print_manifest_json_data(all_restricted, 'Restricted Jsons', manifest.get_restricted_json_data, 'restricted_path') return 0 -def print_manifest_json_data(uri_json_data: dict, json_filename: str, +def print_manifest_json_data(uri_json_data: list, print_prefix: str, get_json_func: callable, get_json_data_kw: str) -> int: print('\n') print(f"{print_prefix}================================================") for manifest_uri in uri_json_data: # if it's not local it should be in the cache - parsed_uri = urllib.parse.urlparse(manifest_uri) + parsed_uri = urllib.parse.urlparse(pathlib.Path(manifest_uri).as_posix()) if parsed_uri.scheme in ['http', 'https', 'ftp', 'ftps']: repo_sha256 = hashlib.sha256(manifest_uri.encode()) cache_folder = manifest.get_o3de_cache_folder() manifest_json_path = cache_folder / str(repo_sha256.hexdigest() + '.json') else: - manifest_json_path = pathlib.Path(manifest_uri).resolve() / json_filename + manifest_json_path = pathlib.Path(manifest_uri).resolve() - json_data = get_json_func(**{get_json_data_kwargs: manifest_json_path}) + json_data = get_json_func(**{get_json_data_kw: manifest_json_path}) if json_data: print(manifest_json_path) print(json.dumps(json_data, indent=4) + '\n') @@ -284,29 +335,30 @@ def print_repos_data(repos_data: dict) -> int: return 0 -def register_show_repos(verbose: int) -> None: +def print_repos(verbose: int) -> int: repos_data = manifest.get_repos() print(json.dumps(repos_data, indent=4)) if verbose > 0: - return print_repos_data(repos_data) == 0 + return print_repos_data(repos_data) return 0 -def register_show(verbose: int) -> None: +def register_show(verbose: int, project_path: pathlib.Path = None, project_name: str = None) -> int: json_data = manifest.load_o3de_manifest() print(f"{manifest.get_o3de_manifest()}:") print(json.dumps(json_data, indent=4)) - result = True + result = 0 if verbose > 0: - result = print_manifest_json_data(manifest.get_engines()) == 0 and result - result = print_manifest_json_data(manifest.get_all_projects()) == 0 and result - result = print_manifest_json_data(manifest.get_gems()) == 0 and result - result = print_manifest_json_data(manifest.get_all_templates()) == 0 and result - result = print_manifest_json_data(manifest.get_all_restricted()) == 0 and result - result = print_repos_data(manifest.get_repos()) == 0 and result - return 0 if result else 1 + result = print_engines(verbose) or result + result = print_all_projects(verbose) or result + result = print_all_gems(verbose, project_path, project_name) or result + result = print_all_templates(verbose, project_path, project_name) or result + result = print_all_restricted(verbose, project_path, project_name) or result + result = print_repos(verbose) or result + + return result def _run_register_show(args: argparse) -> int: @@ -321,6 +373,8 @@ def _run_register_show(args: argparse) -> int: return print_projects(args.verbose) elif args.gems: return print_gems(args.verbose) + elif args.external_subdirectories: + return print_external_subdirectories(args.verbose) elif args.templates: return print_templates(args.verbose) elif args.repos: @@ -333,7 +387,7 @@ def _run_register_show(args: argparse) -> int: elif args.engine_gems: return print_engine_gems(args.verbose) elif args.engine_external_subdirectories: - return print_engine_external_subdirectories() + return print_engine_external_subdirectories(args.verbose) elif args.engine_templates: return print_engine_templates(args.verbose) elif args.engine_restricted: @@ -342,7 +396,7 @@ def _run_register_show(args: argparse) -> int: elif args.project_gems: return print_project_gems(args.verbose, args.project_path, args.project_name) elif args.project_external_subdirectories: - return print_project_external_subdirectories(args.project_path, args.project_name) + return print_project_external_subdirectories(args.verbose, args.project_path, args.project_name) elif args.project_templates: return print_project_templates(args.verbose, args.project_path, args.project_name) elif args.project_restricted: @@ -351,16 +405,16 @@ def _run_register_show(args: argparse) -> int: elif args.all_projects: return print_all_projects(args.verbose) elif args.all_gems: - return print_all_gems(args.verbose) + return print_all_gems(args.verbose, args.project_path, args.project_name) elif args.all_external_subdirectories: - return print_all_external_subdirectories() + return print_all_external_subdirectories(args.verbose, args.project_path, args.project_name) elif args.all_templates: - return print_all_templates(args.verbose) + return print_all_templates(args.verbose, args.project_path, args.project_name) elif args.all_restricted: - return print_all_restricted(args.verbose) + return print_all_restricted(args.verbose, args.project_path, args.project_name) else: - return register_show(args.verbose) + return register_show(args.verbose, args.project_path, args.project_name) def add_parser_args(parser): @@ -393,6 +447,9 @@ def add_parser_args(parser): group.add_argument('-rs', '--restricted', action='store_true', required=False, default=False, help='Output the restricted directories registered in the global ~/.o3de/o3de_manifest.json.') + group.add_argument('-es', '--external-subdirectories', action='store_true', required=False, + default=False, + help='Output the external subdirectories registered in the global ~/.o3de/o3de_manifest.json.') group.add_argument('-ep', '--engine-projects', action='store_true', required=False, default=False, @@ -428,16 +485,28 @@ def add_parser_args(parser): help='Output all projects registered in the ~/.o3de/o3de_manifest.json and the current engine.json. Ignores repos.') group.add_argument('-ag', '--all-gems', action='store_true', required=False, default=False, - help='Output all gems registered in the ~/.o3de/o3de_manifest.json and the current engine.json. Ignores repos') + help='Output all gems registered in the ~/.o3de/o3de_manifest.json and the current engine.json.' + ' If --project-path or --project-name option is supplied, outputs gems registered in' + ' that project\'s project.json otherwise outputs registered gems from all registered projects.' + ' Ignores repos') group.add_argument('-at', '--all-templates', action='store_true', required=False, default=False, - help='Output all templates registered in the ~/.o3de/o3de_manifest.json and the current engine.json. Ignores repos.') + help='Output all templates registered in the ~/.o3de/o3de_manifest.json and the current engine.json.' + ' If --project-path or --project-name option is supplied, outputs templates registered in' + ' that project\'s project.json otherwise outputs registered templates from all registered' + ' projects. Ignores repos') group.add_argument('-ares', '--all-restricted', action='store_true', required=False, default=False, - help='Output all restricted directory registered in the ~/.o3de/o3de_manifest.json and the current engine.json.') + help='Output all restricted directory registered in the ~/.o3de/o3de_manifest.json and the current engine.json.' + ' If --project-path or --project-name option is supplied, outputs restricted' + ' directories registered in that project\'s project.json otherwise outputs restricted' + ' directories registered from all registered projects. Ignores repos') group.add_argument('-aes', '--all-external-subdirectories', action='store_true', default=False, - help='Output all external subdirectories registered in the ~/.o3de/o3de_manifest.json and the current engine.json.') + help='Output all external subdirectories registered in the ~/.o3de/o3de_manifest.json and the current engine.json.' + ' If --project-path or --project-name options is supplied, outputs external' + ' subdirectories registered in that project\'s project.json otherwise outputs external' + ' subdirectories registered from all registered projects. Ignores repos') parser.add_argument('-v', '--verbose', action='count', required=False, default=0, diff --git a/scripts/o3de/o3de/register.py b/scripts/o3de/o3de/register.py index fb91cb5bea..8481c5fae0 100644 --- a/scripts/o3de/o3de/register.py +++ b/scripts/o3de/o3de/register.py @@ -261,43 +261,6 @@ def add_engine_name_to_path(json_data: dict, engine_path: pathlib.Path, force: b return 0 -def register_engine_path(json_data: dict, - engine_path: pathlib.Path, - remove: bool = False, - force: bool = False) -> int: - if not engine_path: - logger.error(f'Engine path cannot be empty.') - return 1 - engine_path = pathlib.Path(engine_path).resolve() - - for engine_object in json_data.get('engines', []): - if isinstance(engine_object, dict): - engine_object_path = pathlib.Path(engine_object['path']).resolve() - else: - engine_object_path = pathlib.Path(engine_object).resolve() - if engine_object_path == engine_path: - json_data['engines'].remove(engine_object) - - if remove: - return remove_engine_name_to_path(json_data, engine_path) - - if not engine_path.is_dir(): - logger.error(f'Engine path {engine_path} does not exist.') - return 1 - - engine_json = engine_path / 'engine.json' - if not validation.valid_o3de_engine_json(engine_json): - logger.error(f'Engine json {engine_json} is not valid.') - return 1 - - engine_object = {} - engine_object.update({'path': engine_path.as_posix()}) - - json_data.setdefault('engines', []).insert(0, engine_object) - - return add_engine_name_to_path(json_data, engine_path, force) - - def register_o3de_object_path(json_data: dict, o3de_object_path: str or pathlib.Path, o3de_object_key: str, @@ -343,7 +306,7 @@ def register_o3de_object_path(json_data: dict, try: paths_to_remove.append(o3de_object_path.relative_to(save_path.parent)) except ValueError: - pass # It is OK relative path cannot be formed + pass # It is not an error if a relative path cannot be formed manifest_data[o3de_object_key] = list(filter(lambda p: pathlib.Path(p) not in paths_to_remove, manifest_data.setdefault(o3de_object_key, []))) @@ -358,7 +321,7 @@ def register_o3de_object_path(json_data: dict, manifest_json_path = o3de_object_path / o3de_json_filename if validation_func and not validation_func(manifest_json_path): - logger.error(f'o3de json {manifest_json_path} is not valid.') + logger.error(f'Manifest at path {manifest_json_path} is not valid.') return 1 # if there is a save path make it relative the directory containing o3de object json file @@ -374,6 +337,27 @@ def register_o3de_object_path(json_data: dict, return 0 +def register_engine_path(json_data: dict, + engine_path: pathlib.Path, + remove: bool = False, + force: bool = False) -> int: + # If the o3de_manifest.json 'engines' key is list containing dictionary entries, transform it to a list of strings + engine_list = json_data.get('engines', []) + + def transform_engine_dict_to_string(engine): return engine.get('path', '') if isinstance(engine, dict) else engine + json_data['engines'] = list(map(transform_engine_dict_to_string, engine_list)) + + result = register_o3de_object_path(json_data, engine_path, 'engines', 'engine.json', + validation.valid_o3de_engine_json, remove) + if result != 0: + return result + + if remove: + return remove_engine_name_to_path(json_data, engine_path) + + return add_engine_name_to_path(json_data, engine_path, force) + + def register_external_subdirectory(json_data: dict, external_subdir_path: pathlib.Path, remove: bool = False, @@ -677,37 +661,30 @@ def remove_invalid_o3de_projects(manifest_path: pathlib.Path = None) -> int: return result def remove_invalid_o3de_objects() -> None: - json_data = manifest.load_o3de_manifest() - - for engine_object in json_data.get('engines', []): - engine_path = engine_object.get('path', '') + for engine_path in manifest.get_engines(): if not validation.valid_o3de_engine_json(pathlib.Path(engine_path).resolve() / 'engine.json'): logger.warn(f"Engine path {engine_path} is invalid.") register(engine_path=engine_path, remove=True) remove_invalid_o3de_projects() - for gem in json_data.get('gems', []): - if not validation.valid_o3de_gem_json(pathlib.Path(gem).resolve() / 'gem.json'): - logger.warn(f"Gem path {gem} is invalid.") - register(gem_path=gem, remove=True) - - for external in json_data.get('external_subdirectories', []): + for external in manifest.get_external_subdirectories(): external = pathlib.Path(external).resolve() if not external.is_dir(): logger.warn(f"External subdirectory {external} is invalid.") register(engine_path=engine_path, external_subdir_path=external, remove=True) - for template in json_data.get('templates', []): + for template in manifest.get_templates(): if not validation.valid_o3de_template_json(pathlib.Path(template).resolve() / 'template.json'): logger.warn(f"Template path {template} is invalid.") register(template_path=template, remove=True) - for restricted in json_data.get('restricted', []): + for restricted in manifest.get_restricted(): if not validation.valid_o3de_restricted_json(pathlib.Path(restricted).resolve() / 'restricted.json'): logger.warn(f"Restricted path {restricted} is invalid.") register(restricted_path=restricted, remove=True) + json_data = manifest.load_o3de_manifest() default_engines_folder = pathlib.Path(json_data.get('default_engines_folder', manifest.get_o3de_engines_folder())).resolve() if not default_engines_folder.is_dir(): new_default_engines_folder = manifest.get_o3de_folder() / 'Engines' diff --git a/scripts/o3de/tests/CMakeLists.txt b/scripts/o3de/tests/CMakeLists.txt index 00d0774c45..1cd6eac7ee 100644 --- a/scripts/o3de/tests/CMakeLists.txt +++ b/scripts/o3de/tests/CMakeLists.txt @@ -65,4 +65,11 @@ ly_add_pytest( PATH ${CMAKE_CURRENT_LIST_DIR}/unit_test_engine_template.py TEST_SUITE smoke EXCLUDE_TEST_RUN_TARGET_FROM_IDE -) \ No newline at end of file +) + +ly_add_pytest( + NAME o3de_register_show + PATH ${CMAKE_CURRENT_LIST_DIR}/unit_test_print_registration.py + TEST_SUITE smoke + EXCLUDE_TEST_RUN_TARGET_FROM_IDE +) diff --git a/scripts/o3de/tests/unit_test_print_registration.py b/scripts/o3de/tests/unit_test_print_registration.py new file mode 100644 index 0000000000..7e3e75edcd --- /dev/null +++ b/scripts/o3de/tests/unit_test_print_registration.py @@ -0,0 +1,383 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +import argparse +import json +import logging +import pytest +import pathlib +from unittest.mock import patch + +from o3de import print_registration + + +TEST_PROJECT_JSON_PAYLOAD = ''' +{ + "project_name": "MinimalProject", + "origin": "The primary repo for MinimalProject goes here: i.e. http://www.mydomain.com", + "license": "What license MinimalProject uses goes here: i.e. https://opensource.org/licenses/MIT", + "display_name": "MinimalProject", + "summary": "A short description of MinimalProject.", + "canonical_tags": [ + "Project" + ], + "user_tags": [ + "MinimalProject" + ], + "icon_path": "preview.png", + "engine": "o3de-install", + "external_subdirectories": [ + "D:/TestGem" + ] +} +''' + +TEST_ENGINE_JSON_PAYLOAD = ''' +{ + "engine_name": "o3de", + "restricted_name": "o3de", + "FileVersion": 1, + "O3DEVersion": "0.0.0.0", + "O3DECopyrightYear": 2021, + "O3DEBuildNumber": 0, + "external_subdirectories": [ + "Gems/TestGem2" + ], + "projects": [ + ], + "templates": [ + "Templates/MinimalProject" + ] +} +''' + +TEST_GEM_JSON_PAYLOAD = ''' +{ + "gem_name": "TestGem", + "display_name": "TestGem", + "license": "What license TestGem uses goes here: i.e. https://opensource.org/licenses/MIT", + "origin": "The primary repo for TestGem goes here: i.e. http://www.mydomain.com", + "type": "Code", + "summary": "A short description of TestGem.", + "canonical_tags": [ + "Gem" + ], + "user_tags": [ + "TestGem" + ], + "icon_path": "preview.png", + "requirements": "" +} +''' + +TEST_TEMPLATE_JSON_PAYLOAD = ''' +{ + "template_name": "AssetGem", + "origin": "The primary repo for AssetGem goes here: i.e. http://www.mydomain.com", + "license": "What license AssetGem uses goes here: i.e. https://opensource.org/licenses/MIT", + "display_name": "AssetGem", + "summary": "A short description of AssetGem template.", + "canonical_tags": [], + "user_tags": [ + "AssetGem" + ], + "icon_path": "preview.png", + "copyFiles": [ + { + "file": "CMakeLists.txt", + "origin": "CMakeLists.txt", + "isTemplated": true, + "isOptional": false + }, + { + "file": "gem.json", + "origin": "gem.json", + "isTemplated": true, + "isOptional": false + }, + { + "file": "preview.png", + "origin": "preview.png", + "isTemplated": false, + "isOptional": false + } + ], + "createDirectories": [ + { + "dir": "Assets", + "origin": "Assets" + } + ] +} +''' + +TEST_RESTRICTED_JSON_PAYLOAD = ''' +{ + "restricted_name": "o3de" +} +''' + +TEST_O3DE_MANIFEST_JSON_PAYLOAD = ''' +{ + "o3de_manifest_name": "testuser", + "origin": "C:/Users/testuser/.o3de", + "default_engines_folder": "C:/Users/testuser/.o3de/Engines", + "default_projects_folder": "C:/Users/testuser/.o3de/Projects", + "default_gems_folder": "C:/Users/testuser/.o3de/Gems", + "default_templates_folder": "C:/Users/testuser/.o3de/Templates", + "default_restricted_folder": "C:/Users/testuser/.o3de/Restricted", + "default_third_party_folder": "C:/Users/testuser/.o3de/3rdParty", + "projects": [ + "D:/MinimalProject" + ], + "external_subdirectories": [], + "templates": [], + "restricted": [], + "repos": [], + "engines": [ + "D:/o3de/o3de" + ], + "engines_path": { + "o3de": "D:/o3de/o3de" + } +} +''' + +class TestPrintRegistration: + @staticmethod + def load_manifest_json(): + return json.loads(TEST_O3DE_MANIFEST_JSON_PAYLOAD) + + @staticmethod + def get_engine_json_data(engine_path: pathlib.Path = None): + return json.loads(TEST_ENGINE_JSON_PAYLOAD) + + @staticmethod + def get_project_json_data(project_path: pathlib.Path = None): + return json.loads(TEST_PROJECT_JSON_PAYLOAD) + + @staticmethod + def get_gem_json_data(gem_path: pathlib.Path = None): + return json.loads(TEST_GEM_JSON_PAYLOAD) + + @staticmethod + def get_template_json_data(template_path: pathlib.Path = None): + return json.loads(TEST_TEMPLATE_JSON_PAYLOAD) + + @staticmethod + def get_restricted_json_data(restricted_path: pathlib.Path = None): + return json.loads(TEST_RESTRICTED_JSON_PAYLOAD) + + @pytest.mark.parametrize("project_path, verbose", [ + pytest.param(None, 0), + pytest.param(None, 1), + pytest.param(pathlib.Path("D:/MinimalProject"), 0), + pytest.param(pathlib.Path("D:/MinimalProject"), 1) + ]) + def test_print_registration_no_option(self, project_path, verbose): + parser = argparse.ArgumentParser() + + # Register the registration script subparsers with the current argument parser + print_registration.add_parser_args(parser) + arg_list = [] + if project_path: + arg_list += ['--project-path', project_path.as_posix()] + if verbose: + arg_list += ['-' + 'v' * verbose] + test_args = parser.parse_args(arg_list) + + with patch('o3de.manifest.load_o3de_manifest', side_effect=self.load_manifest_json) as load_manifest_patch, \ + patch('o3de.manifest.get_engine_json_data', + side_effect=self.get_engine_json_data) as get_engine_json_data_patch, \ + patch('o3de.manifest.get_project_json_data', + side_effect=self.get_project_json_data) as get_project_json_patch, \ + patch('o3de.manifest.get_gem_json_data', side_effect=self.get_gem_json_data) as get_gem_json_patch, \ + patch('o3de.manifest.get_template_json_data', side_effect=self.get_template_json_data) as get_template_json_patch, \ + patch('o3de.manifest.get_restricted_json_data', side_effect=self.get_restricted_json_data) as get_json_patch: + result = print_registration._run_register_show(test_args) + assert result == 0 + + + @pytest.mark.parametrize("engine_arg_option, verbose", [ + pytest.param("--this-engine", 0), + pytest.param("--this-engine", 1), + pytest.param("--engines", 0), + pytest.param("--engines", 1) + ]) + def test_print_engine_registration(self, engine_arg_option, verbose): + parser = argparse.ArgumentParser() + + # Register the registration script subparsers with the current argument parser + print_registration.add_parser_args(parser) + arg_list = [engine_arg_option] + if verbose: + arg_list += ['-' + 'v' * verbose] + test_args = parser.parse_args(arg_list) + + + with patch('o3de.manifest.load_o3de_manifest', side_effect=self.load_manifest_json) as load_manifest_patch, \ + patch('o3de.manifest.get_engine_json_data', side_effect=self.get_engine_json_data) as get_engine_json_data_patch: + result = print_registration._run_register_show(test_args) + assert result == 0 + + + @pytest.mark.parametrize("arg_option, verbose", [ + pytest.param("--projects", 0), + pytest.param("--projects", 1), + pytest.param("--engine-projects", 0), + pytest.param("--engine-projects", 1), + pytest.param("--all-projects", 0), + pytest.param("--all-projects", 1) + ]) + def test_print_project_registration(self, arg_option, verbose): + parser = argparse.ArgumentParser() + + # Register the registration script subparsers with the current argument parser + print_registration.add_parser_args(parser) + arg_list = [arg_option] + if verbose: + arg_list += ['-' + 'v' * verbose] + test_args = parser.parse_args(arg_list) + + + with patch('o3de.manifest.load_o3de_manifest', side_effect=self.load_manifest_json) as load_manifest_patch, \ + patch('o3de.manifest.get_project_json_data', side_effect=self.get_project_json_data) as get_json_data_patch: + result = print_registration._run_register_show(test_args) + assert result == 0 + + + @pytest.mark.parametrize("arg_option, project_path, verbose", [ + pytest.param("--gems", None, 0), + pytest.param("--gems", None, 1), + pytest.param("--engine-gems", None, 0), + pytest.param("--engine-gems", None, 1), + pytest.param("--project-gems", pathlib.Path("D:/MinimalProject"), 0), + pytest.param("--project-gems", pathlib.Path("D:/MinimalProject"), 1), + pytest.param("--all-gems", pathlib.Path("D:/MinimalProject"), 0), + pytest.param("--all-gems", None, 0), + pytest.param("--all-gems", pathlib.Path("D:/MinimalProject"), 1), + pytest.param("--all-gems", None, 1) + ]) + def test_print_gem_registration(self, arg_option, project_path, verbose): + parser = argparse.ArgumentParser() + + # Register the registration script subparsers with the current argument parser + print_registration.add_parser_args(parser) + arg_list = [arg_option] + if project_path: + arg_list += ['--project-path', project_path.as_posix()] + if verbose: + arg_list += ['-' + 'v' * verbose] + test_args = parser.parse_args(arg_list) + + # Patch the manifest.py function to locate gem.json files in external subdirectories + # to just return a fake path to a single test gem + def get_gems_from_subdirectories(external_subdirs: list) -> list: + return ["D:/TestGem"] + + with patch('o3de.manifest.load_o3de_manifest', side_effect=self.load_manifest_json) as load_manifest_patch, \ + patch('o3de.manifest.get_gem_json_data', side_effect=self.get_gem_json_data) as get_json_patch, \ + patch('o3de.manifest.get_project_json_data', side_effect=self.get_project_json_data) as get_project_json_patch, \ + patch('o3de.manifest.get_gems_from_subdirectories', side_effect=get_gems_from_subdirectories) as get_gems_from_subdirs_patch, \ + patch('o3de.print_registration.get_project_path', return_value=project_path) as get_project_path_patch: + result = print_registration._run_register_show(test_args) + assert result == 0 + + + @pytest.mark.parametrize("arg_option, project_path, verbose", [ + pytest.param("--templates", None, 0), + pytest.param("--templates", None, 1), + pytest.param("--engine-templates", None, 0), + pytest.param("--engine-templates", None, 1), + pytest.param("--project-templates", pathlib.Path("D:/MinimalProject"), 0), + pytest.param("--project-templates", pathlib.Path("D:/MinimalProject"), 1), + pytest.param("--all-templates", pathlib.Path("D:/MinimalProject"), 0), + pytest.param("--all-templates", None, 0), + pytest.param("--all-templates", pathlib.Path("D:/MinimalProject"), 1), + pytest.param("--all-templates", None, 1) + ]) + def test_print_template_registration(self, arg_option, project_path, verbose): + parser = argparse.ArgumentParser() + + # Register the registration script subparsers with the current argument parser + print_registration.add_parser_args(parser) + arg_list = [arg_option] + if project_path: + arg_list += ['--project-path', project_path.as_posix()] + if verbose: + arg_list += ['-' + 'v' * verbose] + test_args = parser.parse_args(arg_list) + + + with patch('o3de.manifest.load_o3de_manifest', side_effect=self.load_manifest_json) as load_manifest_patch, \ + patch('o3de.manifest.get_template_json_data', side_effect=self.get_template_json_data) as get_json_patch, \ + patch('o3de.manifest.get_project_json_data', side_effect=self.get_project_json_data) as get_project_json_patch, \ + patch('o3de.print_registration.get_project_path', return_value=project_path) as get_project_path_patch: + result = print_registration._run_register_show(test_args) + assert result == 0 + + + @pytest.mark.parametrize("arg_option, project_path, verbose", [ + pytest.param("--restricted", None, 0), + pytest.param("--restricted", None, 1), + pytest.param("--engine-restricted", None, 0), + pytest.param("--engine-restricted", None, 1), + pytest.param("--project-restricted", pathlib.Path("D:/MinimalProject"), 0), + pytest.param("--project-restricted", pathlib.Path("D:/MinimalProject"), 1), + pytest.param("--all-restricted", pathlib.Path("D:/MinimalProject"), 0), + pytest.param("--all-restricted", None, 0), + pytest.param("--all-restricted", pathlib.Path("D:/MinimalProject"), 1), + pytest.param("--all-restricted", None, 1) + ]) + def test_print_restricted_registration(self, arg_option, project_path, verbose): + parser = argparse.ArgumentParser() + + # Register the registration script subparsers with the current argument parser + print_registration.add_parser_args(parser) + arg_list = [arg_option] + if project_path: + arg_list += ['--project-path', project_path.as_posix()] + if verbose: + arg_list += ['-' + 'v' * verbose] + test_args = parser.parse_args(arg_list) + + with patch('o3de.manifest.load_o3de_manifest', side_effect=self.load_manifest_json) as load_manifest_patch, \ + patch('o3de.manifest.get_restricted_json_data', side_effect=self.get_restricted_json_data) as get_json_patch, \ + patch('o3de.manifest.get_project_json_data', side_effect=self.get_project_json_data) as get_project_json_patch, \ + patch('o3de.print_registration.get_project_path', return_value=project_path) as get_project_path_patch: + result = print_registration._run_register_show(test_args) + assert result == 0 + + + # Setting --verbose with the --*external-subdirectories option doesn't result in any additional output + # So it is only parameterized as 0 + @pytest.mark.parametrize("arg_option, project_path, verbose", [ + pytest.param("--external-subdirectories", None, 0), + pytest.param("--engine-external-subdirectories", None, 0), + pytest.param("--project-external-subdirectories", pathlib.Path("D:/MinimalProject"), 0), + pytest.param("--all-external-subdirectories", pathlib.Path("D:/MinimalProject"), 0), + pytest.param("--all-external-subdirectories", None, 0), + ]) + def test_print_external_subdirectories_registration(self, arg_option, project_path, verbose): + parser = argparse.ArgumentParser() + + # Register the registration script subparsers with the current argument parser + print_registration.add_parser_args(parser) + arg_list = [arg_option] + if project_path: + arg_list += ['--project-path', project_path.as_posix()] + if verbose: + arg_list += ['-' + 'v' * verbose] + test_args = parser.parse_args(arg_list) + + with patch('o3de.manifest.load_o3de_manifest', side_effect=self.load_manifest_json) as load_manifest_patch, \ + patch('o3de.manifest.get_project_json_data', + side_effect=self.get_project_json_data) as get_project_json_patch, \ + patch('o3de.print_registration.get_project_path', return_value=project_path) as get_project_path_patch: + result = print_registration._run_register_show(test_args) + assert result == 0