123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469 |
- /*
- * Copyright (c) Contributors to the Open 3D Engine Project.
- * For complete copyright and license terms please see the LICENSE at the root of this distribution.
- *
- * SPDX-License-Identifier: Apache-2.0 OR MIT
- *
- */
- #include "EditorDefs.h"
- #include "CryEdit.h"
- // Qt
- #include <QTimer>
- // AzCore
- #include <AzCore/Component/TickBus.h>
- #include <AzCore/Console/IConsole.h>
- #include <AzCore/Utils/Utils.h>
- // AzToolsFramework
- #include <AzToolsFramework/API/EditorPythonConsoleBus.h>
- #include <AzToolsFramework/API/EditorPythonRunnerRequestsBus.h>
- // Editor
- #include "Core/QtEditorApplication.h"
- #include "CheckOutDialog.h"
- #include "GameEngine.h"
- #include "ViewManager.h"
- #include "EditorViewportCamera.h"
- // Atom
- #include <Atom/RPI.Public/ViewportContextBus.h>
- #include <Atom/RPI.Public/ViewportContext.h>
- //////////////////////////////////////////////////////////////////////////
- namespace
- {
- void PyRunFile(const AZ::ConsoleCommandContainer& args)
- {
- if (args.empty())
- {
- // We expect at least "pyRunFile" and the filename
- AZ_Warning("editor", false, "The pyRunFile requires a file script name.");
- return;
- }
- else if (args.size() == 1)
- {
- // If we only have "pyRunFile filename", there are no args to pass through.
- AzToolsFramework::EditorPythonRunnerRequestBus::Broadcast(
- &AzToolsFramework::EditorPythonRunnerRequestBus::Events::ExecuteByFilename,
- args.front().data());
- }
- else
- {
- // We have "pyRunFile filename x y z", so copy everything past filename into a new vector
- AZStd::vector<AZStd::string_view> pythonArgs;
- AZStd::transform(args.begin() + 1, args.end(), std::back_inserter(pythonArgs), [](auto&& value) { return value; });
- AzToolsFramework::EditorPythonRunnerRequestBus::Broadcast(
- &AzToolsFramework::EditorPythonRunnerRequestBus::Events::ExecuteByFilenameWithArgs,
- args[0],
- pythonArgs);
- }
- }
- AZ_CONSOLEFREEFUNC("pyRunFile", PyRunFile, AZ::ConsoleFunctorFlags::Null, "Runs the Python script from the console.");
- //! We have explicitly not exposed this CloseCurrentLevel API to Python Scripting since the Editor
- //! doesn't officially support it (it doesn't exist in the File menu). It is used for cases
- //! where a level with legacy entities are unable to be converted and so the level that has
- //! been opened needs to be closed, but it hasn't been fully tested for a normal workflow.
- void CloseCurrentLevel()
- {
- CCryEditDoc* currentLevel = GetIEditor()->GetDocument();
- if (currentLevel && currentLevel->IsDocumentReady())
- {
- // This closes the current document (level)
- currentLevel->OnNewDocument();
- // Then we need to tell the game engine there is no level to render anymore
- if (GetIEditor()->GetGameEngine())
- {
- GetIEditor()->GetGameEngine()->SetLevelPath("");
- GetIEditor()->GetGameEngine()->SetLevelLoaded(false);
- CViewManager* pViewManager = GetIEditor()->GetViewManager();
- CViewport* pGameViewport = pViewManager ? pViewManager->GetGameViewport() : nullptr;
- if (pGameViewport)
- {
- pGameViewport->SetViewTM(Matrix34::CreateIdentity());
- }
- }
- }
- }
- AZStd::string PyGetGameFolderAsString()
- {
- return Path::GetEditingGameDataFolder();
- }
- AZStd::string PyGetBuildFolderAsString()
- {
- AZ::IO::FixedMaxPath projectBuildPath = AZ::Utils::GetExecutableDirectory();
- projectBuildPath = projectBuildPath.RemoveFilename(); // profile
- projectBuildPath = projectBuildPath.RemoveFilename(); // bin
- return AZStd::string(projectBuildPath.c_str());
- }
- bool PyOpenLevel(const char* pLevelName)
- {
- const char* oldExtension = EditorUtils::LevelFile::GetOldCryFileExtension();
- const char* defaultExtension = EditorUtils::LevelFile::GetDefaultFileExtension();
- QString levelPath = pLevelName;
- if (!QFile::exists(levelPath))
- {
- // if the input path can't be found, let's automatically add on the game folder and the levels
- QString levelsDir = QString("%1/%2").arg(Path::GetEditingGameDataFolder().c_str()).arg("Levels");
- // now let's check if they pre-pended directories (Samples/SomeLevelName)
- QString levelFileName = levelPath;
- QStringList splitLevelPath = levelPath.contains('/') ? levelPath.split('/') : levelPath.split('\\');
- if (splitLevelPath.length() > 1)
- {
- // take the last one as the level directory name and the level file name in that directory
- levelFileName = splitLevelPath.last();
- }
- levelPath = levelsDir / levelPath / levelFileName;
- // make sure the level path includes the cry extension, if needed
- if (!levelFileName.endsWith(oldExtension) && !levelFileName.endsWith(defaultExtension))
- {
- QString newLevelPath = levelPath + defaultExtension;
- QString oldLevelPath = levelPath + oldExtension;
- // Check if there is a .cry file, otherwise assume it is a new .ly file
- if (QFileInfo(oldLevelPath).exists())
- {
- levelPath = oldLevelPath;
- }
- else
- {
- levelPath = newLevelPath;
- }
- }
- if (!QFile::exists(levelPath))
- {
- return false;
- }
- }
- const bool addToMostRecentFileList = false;
- auto newDocument = CCryEditApp::instance()->OpenDocumentFile(levelPath.toUtf8().data(),
- addToMostRecentFileList, COpenSameLevelOptions::ReopenLevelIfSame);
- return newDocument != nullptr && !newDocument->IsLevelLoadFailed();
- }
- bool PyOpenLevelNoPrompt(const char* pLevelName)
- {
- GetIEditor()->GetDocument()->SetModifiedFlag(false);
- return PyOpenLevel(pLevelName);
- }
- bool PyReloadCurrentLevel()
- {
- if (GetIEditor()->IsLevelLoaded())
- {
- // Close the current level so that the subsequent call to open the same level will be allowed
- QString currentLevelPath = GetIEditor()->GetDocument()->GetLevelPathName();
- CloseCurrentLevel();
- return PyOpenLevel(currentLevelPath.toUtf8().constData());
- }
- return false;
- }
- int PyCreateLevel(const char* templateName, const char* levelName, [[maybe_unused]] int resolution, [[maybe_unused]] int unitSize, [[maybe_unused]] bool bUseTerrain)
- {
- QString qualifiedName;
- return CCryEditApp::instance()->CreateLevel(templateName, levelName, qualifiedName);
- }
- int PyCreateLevelNoPrompt(const char* templateName, const char* levelName, [[maybe_unused]] int heightmapResolution, [[maybe_unused]] int heightmapUnitSize,
- [[maybe_unused]] int terrainExportTextureSize, [[maybe_unused]] bool useTerrain)
- {
- // If a level was open, ignore any unsaved changes if it had been modified
- if (GetIEditor()->IsLevelLoaded())
- {
- GetIEditor()->GetDocument()->SetModifiedFlag(false);
- }
- QString qualifiedName;
- return CCryEditApp::instance()->CreateLevel(templateName, levelName, qualifiedName);
- }
- const char* PyGetCurrentLevelName()
- {
- // Using static member to capture temporary data
- static AZ::IO::FixedMaxPathString tempLevelName;
- tempLevelName = GetIEditor()->GetGameEngine()->GetLevelName().toUtf8().data();
- return tempLevelName.c_str();
- }
- const char* PyGetCurrentLevelPath()
- {
- // Using static member to capture temporary data
- static AZ::IO::FixedMaxPathString tempLevelPath;
- tempLevelPath = GetIEditor()->GetGameEngine()->GetLevelPath().toUtf8().data();
- return tempLevelPath.c_str();
- }
- void Command_LoadPlugins()
- {
- GetIEditor()->LoadPlugins();
- }
- AZ::Vector3 PyGetCurrentViewPosition()
- {
- if (const auto viewportContextRequests = AZ::RPI::ViewportContextRequests::Get())
- {
- AZ::RPI::ConstViewportContextPtr viewportContext = viewportContextRequests->GetDefaultViewportContext();
- return viewportContext->GetCameraTransform().GetTranslation();
- }
- return AZ::Vector3();
- }
- AZ::Vector3 PyGetCurrentViewRotation()
- {
- if (const auto viewportContextRequests = AZ::RPI::ViewportContextRequests::Get())
- {
- AZ::RPI::ConstViewportContextPtr viewportContext = viewportContextRequests->GetDefaultViewportContext();
- return viewportContext->GetCameraTransform().GetRotation().GetEulerDegrees();
- }
- return AZ::Vector3();
- }
- void PySetCurrentViewPosition(float x, float y, float z)
- {
- auto viewportContextRequests = AZ::RPI::ViewportContextRequests::Get();
- if (viewportContextRequests)
- {
- AZ::RPI::ViewportContextPtr viewportContext = viewportContextRequests->GetDefaultViewportContext();
- AZ::Transform transform = viewportContext->GetCameraTransform();
- transform.SetTranslation(x, y, z);
- viewportContextRequests->GetDefaultViewportContext()->SetCameraTransform(transform);
- }
- }
- void PySetCurrentViewRotation(float x, [[maybe_unused]] float y, float z)
- {
- auto viewportContextRequests = AZ::RPI::ViewportContextRequests::Get();
- if (viewportContextRequests)
- {
- AZ::RPI::ViewportContextPtr viewportContext = viewportContextRequests->GetDefaultViewportContext();
- AZ::Transform transform = viewportContext->GetCameraTransform();
- transform.SetRotation(AZ::Quaternion::CreateFromEulerAnglesDegrees(AZ::Vector3(x, y, z)));
- viewportContextRequests->GetDefaultViewportContext()->SetCameraTransform(transform);
- }
- }
- }
- namespace
- {
- void PyLaunchLUAEditor(const char* files)
- {
- CCryEditApp::instance()->OpenLUAEditor(files);
- }
- bool PyCheckOutDialogEnableForAll(bool isEnable)
- {
- return CCheckOutDialog::EnableForAll(isEnable);
- }
- }
- namespace
- {
- bool g_runScriptResult = false; // true -> success, false -> failure
- }
- namespace
- {
- void PySetResultToSuccess()
- {
- g_runScriptResult = true;
- }
- void PySetResultToFailure()
- {
- g_runScriptResult = false;
- }
- void PyIdleEnable(bool enable)
- {
- if (Editor::EditorQtApplication::instance())
- {
- Editor::EditorQtApplication::instance()->EnableOnIdle(enable);
- }
- }
- bool PyIdleIsEnabled()
- {
- if (!Editor::EditorQtApplication::instance())
- {
- return false;
- }
- return Editor::EditorQtApplication::instance()->OnIdleEnabled();
- }
- void PyIdleWait(double timeInSec)
- {
- const bool wasIdleEnabled = PyIdleIsEnabled();
- if (!wasIdleEnabled)
- {
- PyIdleEnable(true);
- }
- clock_t start = clock();
- do
- {
- QEventLoop loop;
- QTimer::singleShot(timeInSec * 1000, &loop, &QEventLoop::quit);
- loop.exec();
- } while ((double)(clock() - start) / CLOCKS_PER_SEC < timeInSec);
- if (!wasIdleEnabled)
- {
- PyIdleEnable(false);
- }
- }
- void PyIdleWaitFrames(uint32 frames)
- {
- struct Ticker : public AZ::TickBus::Handler
- {
- Ticker(QEventLoop* loop, uint32 targetFrames) : m_loop(loop), m_targetFrames(targetFrames)
- {
- AZ::TickBus::Handler::BusConnect();
- }
- ~Ticker() override
- {
- 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 m_elapsedFrames = 0;
- uint32 m_targetFrames = 0;
- };
- QEventLoop loop;
- Ticker ticker(&loop, frames);
- loop.exec();
- }
- }
- inline namespace Commands
- {
- int PyGetConfigPlatform()
- {
- return static_cast<int>(GetIEditor()->GetEditorConfigPlatform());
- }
- bool PyAttachDebugger()
- {
- return AZ::Debug::Trace::AttachDebugger();
- }
- bool PyWaitForDebugger(float timeoutSeconds = -1.f)
- {
- return AZ::Debug::Trace::WaitForDebugger(timeoutSeconds);
- }
- AZStd::string PyGetFileAlias(AZStd::string alias)
- {
- return AZ::IO::FileIOBase::GetInstance()->GetAlias(alias.c_str());
- }
- }
- namespace AzToolsFramework
- {
- void CryEditPythonHandler::Reflect(AZ::ReflectContext* context)
- {
- if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
- {
- // this will put these methods into the 'azlmbr.legacy.general' module
- auto addLegacyGeneral = [](AZ::BehaviorContext::GlobalMethodBuilder methodBuilder)
- {
- methodBuilder->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
- ->Attribute(AZ::Script::Attributes::Category, "Legacy/Editor")
- ->Attribute(AZ::Script::Attributes::Module, "legacy.general");
- };
- addLegacyGeneral(behaviorContext->Method("open_level", ::PyOpenLevel, nullptr, "Opens a level."));
- addLegacyGeneral(behaviorContext->Method("open_level_no_prompt", ::PyOpenLevelNoPrompt, nullptr, "Opens a level. Doesn't prompt user about saving a modified level."));
- addLegacyGeneral(behaviorContext->Method("reload_current_level", ::PyReloadCurrentLevel, nullptr, "Re-loads the current level. If no level is loaded, then does nothing."));
- addLegacyGeneral(behaviorContext->Method("create_level", ::PyCreateLevel, nullptr, "Creates a level with the parameters of 'templateName', 'levelName', 'resolution', 'unitSize' and 'bUseTerrain'."));
- addLegacyGeneral(behaviorContext->Method("create_level_no_prompt", ::PyCreateLevelNoPrompt, nullptr, "Creates a level with the parameters of 'templateName','levelName', 'resolution', 'unitSize' and 'bUseTerrain'."));
- addLegacyGeneral(behaviorContext->Method("get_game_folder", PyGetGameFolderAsString, nullptr, "Gets the path to the Game folder of current project."));
- addLegacyGeneral(behaviorContext->Method("get_build_folder", PyGetBuildFolderAsString, nullptr, "Gets the build folder path of current project."));
- addLegacyGeneral(behaviorContext->Method("get_current_level_name", PyGetCurrentLevelName, nullptr, "Gets the name of the current level."));
- addLegacyGeneral(behaviorContext->Method("get_current_level_path", PyGetCurrentLevelPath, nullptr, "Gets the fully specified path of the current level."));
- addLegacyGeneral(behaviorContext->Method("load_all_plugins", ::Command_LoadPlugins, nullptr, "Loads all available plugins."));
- addLegacyGeneral(behaviorContext->Method("get_current_view_position", PyGetCurrentViewPosition, nullptr, "Returns the position of the current view as a Vec3."));
- addLegacyGeneral(behaviorContext->Method("get_current_view_rotation", PyGetCurrentViewRotation, nullptr, "Returns the rotation of the current view as a Vec3 of Euler angles in degrees."));
- addLegacyGeneral(behaviorContext->Method("set_current_view_position", PySetCurrentViewPosition, nullptr, "Sets the position of the current view as given x, y, z coordinates."));
- addLegacyGeneral(behaviorContext->Method("set_current_view_rotation", PySetCurrentViewRotation, nullptr, "Sets the rotation of the current view as given x, y, z Euler angles in degrees."));
- addLegacyGeneral(behaviorContext->Method("export_to_engine", CCryEditApp::Command_ExportToEngine, nullptr, "Exports the current level to the engine."));
- addLegacyGeneral(behaviorContext->Method("get_config_platform", PyGetConfigPlatform, nullptr, "Gets the system config platform."));
- addLegacyGeneral(behaviorContext->Method("set_result_to_success", PySetResultToSuccess, nullptr, "Sets the result of a script execution to success. Used only for Sandbox AutoTests."));
- addLegacyGeneral(behaviorContext->Method("set_result_to_failure", PySetResultToFailure, nullptr, "Sets the result of a script execution to failure. Used only for Sandbox AutoTests."));
- addLegacyGeneral(behaviorContext->Method("idle_enable", PyIdleEnable, nullptr, "Enables/Disables idle processing for the Editor. Primarily used for auto-testing."));
- addLegacyGeneral(behaviorContext->Method("is_idle_enabled", PyIdleIsEnabled, nullptr, "Returns whether or not idle processing is enabled for the Editor. Primarily used for auto-testing."));
- addLegacyGeneral(behaviorContext->Method("idle_is_enabled", PyIdleIsEnabled, nullptr, "Returns whether or not idle processing is enabled for the Editor. Primarily used for auto-testing."));
- addLegacyGeneral(behaviorContext->Method("idle_wait", PyIdleWait, nullptr, "Waits idling for a given seconds. Primarily used for auto-testing."));
- addLegacyGeneral(behaviorContext->Method("idle_wait_frames", PyIdleWaitFrames, nullptr, "Waits idling for a frames. Primarily used for auto-testing."));
- addLegacyGeneral(behaviorContext->Method("launch_lua_editor", PyLaunchLUAEditor, nullptr, "Launches the Lua editor, may receive a list of space separate file paths, or an empty string to only open the editor."));
- addLegacyGeneral(behaviorContext->Method("attach_debugger", PyAttachDebugger, nullptr, "Prompts for attaching the debugger"));
- addLegacyGeneral(behaviorContext->Method("wait_for_debugger", PyWaitForDebugger, behaviorContext->MakeDefaultValues(-1.f), "Pauses this thread execution until the debugger has been attached"));
- addLegacyGeneral(behaviorContext->Method("get_file_alias", PyGetFileAlias, nullptr, "Retrieves path for IO alias"));
- // this will put these methods into the 'azlmbr.legacy.checkout_dialog' module
- auto addCheckoutDialog = [](AZ::BehaviorContext::GlobalMethodBuilder methodBuilder)
- {
- methodBuilder->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
- ->Attribute(AZ::Script::Attributes::Category, "Legacy/CheckoutDialog")
- ->Attribute(AZ::Script::Attributes::Module, "legacy.checkout_dialog");
- };
- addCheckoutDialog(behaviorContext->Method("enable_for_all", PyCheckOutDialogEnableForAll, nullptr, "Enables the 'Apply to all' button in the checkout dialog; useful for allowing the user to apply a decision to check out files to multiple, related operations."));
- behaviorContext->EnumProperty<ESystemConfigPlatform::CONFIG_INVALID_PLATFORM>("SystemConfigPlatform_InvalidPlatform")
- ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
- behaviorContext->EnumProperty<ESystemConfigPlatform::CONFIG_PC>("SystemConfigPlatform_Pc")
- ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
- behaviorContext->EnumProperty<ESystemConfigPlatform::CONFIG_MAC>("SystemConfigPlatform_Mac")
- ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
- behaviorContext->EnumProperty<ESystemConfigPlatform::CONFIG_OSX_METAL>("SystemConfigPlatform_OsxMetal")
- ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
- behaviorContext->EnumProperty<ESystemConfigPlatform::CONFIG_ANDROID>("SystemConfigPlatform_Android")
- ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
- behaviorContext->EnumProperty<ESystemConfigPlatform::CONFIG_IOS>("SystemConfigPlatform_Ios")
- ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
- behaviorContext->EnumProperty<ESystemConfigPlatform::CONFIG_PROVO>("SystemConfigPlatform_Provo")
- ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
- }
- }
- }
|