CryEditPy.cpp 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include "EditorDefs.h"
  9. #include "CryEdit.h"
  10. // Qt
  11. #include <QTimer>
  12. // AzCore
  13. #include <AzCore/Component/TickBus.h>
  14. #include <AzCore/Console/IConsole.h>
  15. #include <AzCore/Utils/Utils.h>
  16. // AzToolsFramework
  17. #include <AzToolsFramework/API/EditorPythonConsoleBus.h>
  18. #include <AzToolsFramework/API/EditorPythonRunnerRequestsBus.h>
  19. // Editor
  20. #include "Core/QtEditorApplication.h"
  21. #include "CheckOutDialog.h"
  22. #include "GameEngine.h"
  23. #include "ViewManager.h"
  24. #include "EditorViewportCamera.h"
  25. // Atom
  26. #include <Atom/RPI.Public/ViewportContextBus.h>
  27. #include <Atom/RPI.Public/ViewportContext.h>
  28. //////////////////////////////////////////////////////////////////////////
  29. namespace
  30. {
  31. void PyRunFile(const AZ::ConsoleCommandContainer& args)
  32. {
  33. if (args.empty())
  34. {
  35. // We expect at least "pyRunFile" and the filename
  36. AZ_Warning("editor", false, "The pyRunFile requires a file script name.");
  37. return;
  38. }
  39. else if (args.size() == 1)
  40. {
  41. // If we only have "pyRunFile filename", there are no args to pass through.
  42. AzToolsFramework::EditorPythonRunnerRequestBus::Broadcast(
  43. &AzToolsFramework::EditorPythonRunnerRequestBus::Events::ExecuteByFilename,
  44. args.front().data());
  45. }
  46. else
  47. {
  48. // We have "pyRunFile filename x y z", so copy everything past filename into a new vector
  49. AZStd::vector<AZStd::string_view> pythonArgs;
  50. AZStd::transform(args.begin() + 1, args.end(), std::back_inserter(pythonArgs), [](auto&& value) { return value; });
  51. AzToolsFramework::EditorPythonRunnerRequestBus::Broadcast(
  52. &AzToolsFramework::EditorPythonRunnerRequestBus::Events::ExecuteByFilenameWithArgs,
  53. args[0],
  54. pythonArgs);
  55. }
  56. }
  57. AZ_CONSOLEFREEFUNC("pyRunFile", PyRunFile, AZ::ConsoleFunctorFlags::Null, "Runs the Python script from the console.");
  58. //! We have explicitly not exposed this CloseCurrentLevel API to Python Scripting since the Editor
  59. //! doesn't officially support it (it doesn't exist in the File menu). It is used for cases
  60. //! where a level with legacy entities are unable to be converted and so the level that has
  61. //! been opened needs to be closed, but it hasn't been fully tested for a normal workflow.
  62. void CloseCurrentLevel()
  63. {
  64. CCryEditDoc* currentLevel = GetIEditor()->GetDocument();
  65. if (currentLevel && currentLevel->IsDocumentReady())
  66. {
  67. // This closes the current document (level)
  68. currentLevel->OnNewDocument();
  69. // Then we need to tell the game engine there is no level to render anymore
  70. if (GetIEditor()->GetGameEngine())
  71. {
  72. GetIEditor()->GetGameEngine()->SetLevelPath("");
  73. GetIEditor()->GetGameEngine()->SetLevelLoaded(false);
  74. CViewManager* pViewManager = GetIEditor()->GetViewManager();
  75. CViewport* pGameViewport = pViewManager ? pViewManager->GetGameViewport() : nullptr;
  76. if (pGameViewport)
  77. {
  78. pGameViewport->SetViewTM(Matrix34::CreateIdentity());
  79. }
  80. }
  81. }
  82. }
  83. AZStd::string PyGetGameFolderAsString()
  84. {
  85. return Path::GetEditingGameDataFolder();
  86. }
  87. AZStd::string PyGetBuildFolderAsString()
  88. {
  89. AZ::IO::FixedMaxPath projectBuildPath = AZ::Utils::GetExecutableDirectory();
  90. projectBuildPath = projectBuildPath.RemoveFilename(); // profile
  91. projectBuildPath = projectBuildPath.RemoveFilename(); // bin
  92. return AZStd::string(projectBuildPath.c_str());
  93. }
  94. bool PyOpenLevel(const char* pLevelName)
  95. {
  96. const char* oldExtension = EditorUtils::LevelFile::GetOldCryFileExtension();
  97. const char* defaultExtension = EditorUtils::LevelFile::GetDefaultFileExtension();
  98. QString levelPath = pLevelName;
  99. if (!QFile::exists(levelPath))
  100. {
  101. // if the input path can't be found, let's automatically add on the game folder and the levels
  102. QString levelsDir = QString("%1/%2").arg(Path::GetEditingGameDataFolder().c_str()).arg("Levels");
  103. // now let's check if they pre-pended directories (Samples/SomeLevelName)
  104. QString levelFileName = levelPath;
  105. QStringList splitLevelPath = levelPath.contains('/') ? levelPath.split('/') : levelPath.split('\\');
  106. if (splitLevelPath.length() > 1)
  107. {
  108. // take the last one as the level directory name and the level file name in that directory
  109. levelFileName = splitLevelPath.last();
  110. }
  111. levelPath = levelsDir / levelPath / levelFileName;
  112. // make sure the level path includes the cry extension, if needed
  113. if (!levelFileName.endsWith(oldExtension) && !levelFileName.endsWith(defaultExtension))
  114. {
  115. QString newLevelPath = levelPath + defaultExtension;
  116. QString oldLevelPath = levelPath + oldExtension;
  117. // Check if there is a .cry file, otherwise assume it is a new .ly file
  118. if (QFileInfo(oldLevelPath).exists())
  119. {
  120. levelPath = oldLevelPath;
  121. }
  122. else
  123. {
  124. levelPath = newLevelPath;
  125. }
  126. }
  127. if (!QFile::exists(levelPath))
  128. {
  129. return false;
  130. }
  131. }
  132. const bool addToMostRecentFileList = false;
  133. auto newDocument = CCryEditApp::instance()->OpenDocumentFile(levelPath.toUtf8().data(),
  134. addToMostRecentFileList, COpenSameLevelOptions::ReopenLevelIfSame);
  135. return newDocument != nullptr && !newDocument->IsLevelLoadFailed();
  136. }
  137. bool PyOpenLevelNoPrompt(const char* pLevelName)
  138. {
  139. GetIEditor()->GetDocument()->SetModifiedFlag(false);
  140. return PyOpenLevel(pLevelName);
  141. }
  142. bool PyReloadCurrentLevel()
  143. {
  144. if (GetIEditor()->IsLevelLoaded())
  145. {
  146. // Close the current level so that the subsequent call to open the same level will be allowed
  147. QString currentLevelPath = GetIEditor()->GetDocument()->GetLevelPathName();
  148. CloseCurrentLevel();
  149. return PyOpenLevel(currentLevelPath.toUtf8().constData());
  150. }
  151. return false;
  152. }
  153. int PyCreateLevel(const char* templateName, const char* levelName, [[maybe_unused]] int resolution, [[maybe_unused]] int unitSize, [[maybe_unused]] bool bUseTerrain)
  154. {
  155. QString qualifiedName;
  156. return CCryEditApp::instance()->CreateLevel(templateName, levelName, qualifiedName);
  157. }
  158. int PyCreateLevelNoPrompt(const char* templateName, const char* levelName, [[maybe_unused]] int heightmapResolution, [[maybe_unused]] int heightmapUnitSize,
  159. [[maybe_unused]] int terrainExportTextureSize, [[maybe_unused]] bool useTerrain)
  160. {
  161. // If a level was open, ignore any unsaved changes if it had been modified
  162. if (GetIEditor()->IsLevelLoaded())
  163. {
  164. GetIEditor()->GetDocument()->SetModifiedFlag(false);
  165. }
  166. QString qualifiedName;
  167. return CCryEditApp::instance()->CreateLevel(templateName, levelName, qualifiedName);
  168. }
  169. const char* PyGetCurrentLevelName()
  170. {
  171. // Using static member to capture temporary data
  172. static AZ::IO::FixedMaxPathString tempLevelName;
  173. tempLevelName = GetIEditor()->GetGameEngine()->GetLevelName().toUtf8().data();
  174. return tempLevelName.c_str();
  175. }
  176. const char* PyGetCurrentLevelPath()
  177. {
  178. // Using static member to capture temporary data
  179. static AZ::IO::FixedMaxPathString tempLevelPath;
  180. tempLevelPath = GetIEditor()->GetGameEngine()->GetLevelPath().toUtf8().data();
  181. return tempLevelPath.c_str();
  182. }
  183. void Command_LoadPlugins()
  184. {
  185. GetIEditor()->LoadPlugins();
  186. }
  187. AZ::Vector3 PyGetCurrentViewPosition()
  188. {
  189. if (const auto viewportContextRequests = AZ::RPI::ViewportContextRequests::Get())
  190. {
  191. AZ::RPI::ConstViewportContextPtr viewportContext = viewportContextRequests->GetDefaultViewportContext();
  192. return viewportContext->GetCameraTransform().GetTranslation();
  193. }
  194. return AZ::Vector3();
  195. }
  196. AZ::Vector3 PyGetCurrentViewRotation()
  197. {
  198. if (const auto viewportContextRequests = AZ::RPI::ViewportContextRequests::Get())
  199. {
  200. AZ::RPI::ConstViewportContextPtr viewportContext = viewportContextRequests->GetDefaultViewportContext();
  201. return viewportContext->GetCameraTransform().GetRotation().GetEulerDegrees();
  202. }
  203. return AZ::Vector3();
  204. }
  205. void PySetCurrentViewPosition(float x, float y, float z)
  206. {
  207. auto viewportContextRequests = AZ::RPI::ViewportContextRequests::Get();
  208. if (viewportContextRequests)
  209. {
  210. AZ::RPI::ViewportContextPtr viewportContext = viewportContextRequests->GetDefaultViewportContext();
  211. AZ::Transform transform = viewportContext->GetCameraTransform();
  212. transform.SetTranslation(x, y, z);
  213. viewportContextRequests->GetDefaultViewportContext()->SetCameraTransform(transform);
  214. }
  215. }
  216. void PySetCurrentViewRotation(float x, [[maybe_unused]] float y, float z)
  217. {
  218. auto viewportContextRequests = AZ::RPI::ViewportContextRequests::Get();
  219. if (viewportContextRequests)
  220. {
  221. AZ::RPI::ViewportContextPtr viewportContext = viewportContextRequests->GetDefaultViewportContext();
  222. AZ::Transform transform = viewportContext->GetCameraTransform();
  223. transform.SetRotation(AZ::Quaternion::CreateFromEulerAnglesDegrees(AZ::Vector3(x, y, z)));
  224. viewportContextRequests->GetDefaultViewportContext()->SetCameraTransform(transform);
  225. }
  226. }
  227. }
  228. namespace
  229. {
  230. void PyLaunchLUAEditor(const char* files)
  231. {
  232. CCryEditApp::instance()->OpenLUAEditor(files);
  233. }
  234. bool PyCheckOutDialogEnableForAll(bool isEnable)
  235. {
  236. return CCheckOutDialog::EnableForAll(isEnable);
  237. }
  238. }
  239. namespace
  240. {
  241. bool g_runScriptResult = false; // true -> success, false -> failure
  242. }
  243. namespace
  244. {
  245. void PySetResultToSuccess()
  246. {
  247. g_runScriptResult = true;
  248. }
  249. void PySetResultToFailure()
  250. {
  251. g_runScriptResult = false;
  252. }
  253. void PyIdleEnable(bool enable)
  254. {
  255. if (Editor::EditorQtApplication::instance())
  256. {
  257. Editor::EditorQtApplication::instance()->EnableOnIdle(enable);
  258. }
  259. }
  260. bool PyIdleIsEnabled()
  261. {
  262. if (!Editor::EditorQtApplication::instance())
  263. {
  264. return false;
  265. }
  266. return Editor::EditorQtApplication::instance()->OnIdleEnabled();
  267. }
  268. void PyIdleWait(double timeInSec)
  269. {
  270. const bool wasIdleEnabled = PyIdleIsEnabled();
  271. if (!wasIdleEnabled)
  272. {
  273. PyIdleEnable(true);
  274. }
  275. clock_t start = clock();
  276. do
  277. {
  278. QEventLoop loop;
  279. QTimer::singleShot(timeInSec * 1000, &loop, &QEventLoop::quit);
  280. loop.exec();
  281. } while ((double)(clock() - start) / CLOCKS_PER_SEC < timeInSec);
  282. if (!wasIdleEnabled)
  283. {
  284. PyIdleEnable(false);
  285. }
  286. }
  287. void PyIdleWaitFrames(uint32 frames)
  288. {
  289. struct Ticker : public AZ::TickBus::Handler
  290. {
  291. Ticker(QEventLoop* loop, uint32 targetFrames) : m_loop(loop), m_targetFrames(targetFrames)
  292. {
  293. AZ::TickBus::Handler::BusConnect();
  294. }
  295. ~Ticker() override
  296. {
  297. AZ::TickBus::Handler::BusDisconnect();
  298. }
  299. void OnTick(float deltaTime, AZ::ScriptTimePoint time) override
  300. {
  301. AZ_UNUSED(deltaTime);
  302. AZ_UNUSED(time);
  303. if (++m_elapsedFrames >= m_targetFrames)
  304. {
  305. m_loop->quit();
  306. }
  307. }
  308. QEventLoop* m_loop = nullptr;
  309. uint32 m_elapsedFrames = 0;
  310. uint32 m_targetFrames = 0;
  311. };
  312. QEventLoop loop;
  313. Ticker ticker(&loop, frames);
  314. loop.exec();
  315. }
  316. }
  317. inline namespace Commands
  318. {
  319. int PyGetConfigPlatform()
  320. {
  321. return static_cast<int>(GetIEditor()->GetEditorConfigPlatform());
  322. }
  323. bool PyAttachDebugger()
  324. {
  325. return AZ::Debug::Trace::AttachDebugger();
  326. }
  327. bool PyWaitForDebugger(float timeoutSeconds = -1.f)
  328. {
  329. return AZ::Debug::Trace::WaitForDebugger(timeoutSeconds);
  330. }
  331. AZStd::string PyGetFileAlias(AZStd::string alias)
  332. {
  333. return AZ::IO::FileIOBase::GetInstance()->GetAlias(alias.c_str());
  334. }
  335. }
  336. namespace AzToolsFramework
  337. {
  338. void CryEditPythonHandler::Reflect(AZ::ReflectContext* context)
  339. {
  340. if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
  341. {
  342. // this will put these methods into the 'azlmbr.legacy.general' module
  343. auto addLegacyGeneral = [](AZ::BehaviorContext::GlobalMethodBuilder methodBuilder)
  344. {
  345. methodBuilder->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  346. ->Attribute(AZ::Script::Attributes::Category, "Legacy/Editor")
  347. ->Attribute(AZ::Script::Attributes::Module, "legacy.general");
  348. };
  349. addLegacyGeneral(behaviorContext->Method("open_level", ::PyOpenLevel, nullptr, "Opens a level."));
  350. addLegacyGeneral(behaviorContext->Method("open_level_no_prompt", ::PyOpenLevelNoPrompt, nullptr, "Opens a level. Doesn't prompt user about saving a modified level."));
  351. addLegacyGeneral(behaviorContext->Method("reload_current_level", ::PyReloadCurrentLevel, nullptr, "Re-loads the current level. If no level is loaded, then does nothing."));
  352. addLegacyGeneral(behaviorContext->Method("create_level", ::PyCreateLevel, nullptr, "Creates a level with the parameters of 'templateName', 'levelName', 'resolution', 'unitSize' and 'bUseTerrain'."));
  353. addLegacyGeneral(behaviorContext->Method("create_level_no_prompt", ::PyCreateLevelNoPrompt, nullptr, "Creates a level with the parameters of 'templateName','levelName', 'resolution', 'unitSize' and 'bUseTerrain'."));
  354. addLegacyGeneral(behaviorContext->Method("get_game_folder", PyGetGameFolderAsString, nullptr, "Gets the path to the Game folder of current project."));
  355. addLegacyGeneral(behaviorContext->Method("get_build_folder", PyGetBuildFolderAsString, nullptr, "Gets the build folder path of current project."));
  356. addLegacyGeneral(behaviorContext->Method("get_current_level_name", PyGetCurrentLevelName, nullptr, "Gets the name of the current level."));
  357. addLegacyGeneral(behaviorContext->Method("get_current_level_path", PyGetCurrentLevelPath, nullptr, "Gets the fully specified path of the current level."));
  358. addLegacyGeneral(behaviorContext->Method("load_all_plugins", ::Command_LoadPlugins, nullptr, "Loads all available plugins."));
  359. addLegacyGeneral(behaviorContext->Method("get_current_view_position", PyGetCurrentViewPosition, nullptr, "Returns the position of the current view as a Vec3."));
  360. addLegacyGeneral(behaviorContext->Method("get_current_view_rotation", PyGetCurrentViewRotation, nullptr, "Returns the rotation of the current view as a Vec3 of Euler angles in degrees."));
  361. addLegacyGeneral(behaviorContext->Method("set_current_view_position", PySetCurrentViewPosition, nullptr, "Sets the position of the current view as given x, y, z coordinates."));
  362. 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."));
  363. addLegacyGeneral(behaviorContext->Method("export_to_engine", CCryEditApp::Command_ExportToEngine, nullptr, "Exports the current level to the engine."));
  364. addLegacyGeneral(behaviorContext->Method("get_config_platform", PyGetConfigPlatform, nullptr, "Gets the system config platform."));
  365. addLegacyGeneral(behaviorContext->Method("set_result_to_success", PySetResultToSuccess, nullptr, "Sets the result of a script execution to success. Used only for Sandbox AutoTests."));
  366. addLegacyGeneral(behaviorContext->Method("set_result_to_failure", PySetResultToFailure, nullptr, "Sets the result of a script execution to failure. Used only for Sandbox AutoTests."));
  367. addLegacyGeneral(behaviorContext->Method("idle_enable", PyIdleEnable, nullptr, "Enables/Disables idle processing for the Editor. Primarily used for auto-testing."));
  368. addLegacyGeneral(behaviorContext->Method("is_idle_enabled", PyIdleIsEnabled, nullptr, "Returns whether or not idle processing is enabled for the Editor. Primarily used for auto-testing."));
  369. addLegacyGeneral(behaviorContext->Method("idle_is_enabled", PyIdleIsEnabled, nullptr, "Returns whether or not idle processing is enabled for the Editor. Primarily used for auto-testing."));
  370. addLegacyGeneral(behaviorContext->Method("idle_wait", PyIdleWait, nullptr, "Waits idling for a given seconds. Primarily used for auto-testing."));
  371. addLegacyGeneral(behaviorContext->Method("idle_wait_frames", PyIdleWaitFrames, nullptr, "Waits idling for a frames. Primarily used for auto-testing."));
  372. 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."));
  373. addLegacyGeneral(behaviorContext->Method("attach_debugger", PyAttachDebugger, nullptr, "Prompts for attaching the debugger"));
  374. addLegacyGeneral(behaviorContext->Method("wait_for_debugger", PyWaitForDebugger, behaviorContext->MakeDefaultValues(-1.f), "Pauses this thread execution until the debugger has been attached"));
  375. addLegacyGeneral(behaviorContext->Method("get_file_alias", PyGetFileAlias, nullptr, "Retrieves path for IO alias"));
  376. // this will put these methods into the 'azlmbr.legacy.checkout_dialog' module
  377. auto addCheckoutDialog = [](AZ::BehaviorContext::GlobalMethodBuilder methodBuilder)
  378. {
  379. methodBuilder->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  380. ->Attribute(AZ::Script::Attributes::Category, "Legacy/CheckoutDialog")
  381. ->Attribute(AZ::Script::Attributes::Module, "legacy.checkout_dialog");
  382. };
  383. 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."));
  384. behaviorContext->EnumProperty<ESystemConfigPlatform::CONFIG_INVALID_PLATFORM>("SystemConfigPlatform_InvalidPlatform")
  385. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
  386. behaviorContext->EnumProperty<ESystemConfigPlatform::CONFIG_PC>("SystemConfigPlatform_Pc")
  387. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
  388. behaviorContext->EnumProperty<ESystemConfigPlatform::CONFIG_MAC>("SystemConfigPlatform_Mac")
  389. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
  390. behaviorContext->EnumProperty<ESystemConfigPlatform::CONFIG_OSX_METAL>("SystemConfigPlatform_OsxMetal")
  391. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
  392. behaviorContext->EnumProperty<ESystemConfigPlatform::CONFIG_ANDROID>("SystemConfigPlatform_Android")
  393. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
  394. behaviorContext->EnumProperty<ESystemConfigPlatform::CONFIG_IOS>("SystemConfigPlatform_Ios")
  395. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
  396. behaviorContext->EnumProperty<ESystemConfigPlatform::CONFIG_PROVO>("SystemConfigPlatform_Provo")
  397. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
  398. }
  399. }
  400. }