ScriptAutomationSystemComponent.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  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 <ScriptAutomationSystemComponent.h>
  9. #include <ScriptAutomationScriptBindings.h>
  10. #include <ImageComparisonConfig.h>
  11. #include <AzCore/Asset/AssetCommon.h>
  12. #include <AzCore/Asset/AssetManager.h>
  13. #include <AzCore/Component/ComponentApplication.h>
  14. #include <AzCore/Console/IConsole.h>
  15. #include <AzCore/IO/FileIO.h>
  16. #include <AzCore/RTTI/BehaviorContext.h>
  17. #include <AzCore/Script/ScriptAsset.h>
  18. #include <AzCore/Script/ScriptContext.h>
  19. #include <AzCore/Script/ScriptSystemBus.h>
  20. #include <AzCore/Serialization/EditContext.h>
  21. #include <AzCore/Serialization/EditContextConstants.inl>
  22. #include <AzCore/Serialization/SerializeContext.h>
  23. #include <AzFramework/API/ApplicationAPI.h>
  24. #include <AzFramework/Components/ConsoleBus.h>
  25. #include <AzFramework/Script/ScriptComponent.h>
  26. namespace AZ::ScriptAutomation
  27. {
  28. namespace
  29. {
  30. AZ::Data::Asset<AZ::ScriptAsset> LoadScriptAssetFromPath(const char* productPath, AZ::ScriptContext& context)
  31. {
  32. AZ::IO::FixedMaxPath resolvedPath;
  33. AZ::IO::FileIOBase::GetInstance()->ResolvePath(resolvedPath, productPath);
  34. AZ::IO::FileIOStream inputStream;
  35. if (inputStream.Open(resolvedPath.c_str(), AZ::IO::OpenMode::ModeRead))
  36. {
  37. AzFramework::ScriptCompileRequest compileRequest;
  38. compileRequest.m_sourceFile = resolvedPath.c_str();
  39. compileRequest.m_input = &inputStream;
  40. auto outcome = AzFramework::CompileScript(compileRequest, context);
  41. inputStream.Close();
  42. if (outcome.IsSuccess())
  43. {
  44. AZ::Uuid id = AZ::Uuid::CreateName(productPath);
  45. AZ::Data::Asset<AZ::ScriptAsset> scriptAsset = AZ::Data::AssetManager::Instance().FindOrCreateAsset<AZ::ScriptAsset>(AZ::Data::AssetId(id)
  46. , AZ::Data::AssetLoadBehavior::Default);
  47. scriptAsset.Get()->m_data = compileRequest.m_luaScriptDataOut;
  48. return scriptAsset;
  49. }
  50. else
  51. {
  52. AZ_Assert(false, "Failed to compile script asset '%s'. Reason: '%s'", resolvedPath.c_str(), outcome.GetError().c_str());
  53. return {};
  54. }
  55. }
  56. else
  57. {
  58. AZ_Assert(false, "Unable to find product asset '%s'. Has the source asset finished building?", resolvedPath.c_str());
  59. return {};
  60. }
  61. }
  62. void LuaErrorLog([[maybe_unused]] ScriptContext* context, ScriptContext::ErrorType error, [[maybe_unused]] const char* message)
  63. {
  64. switch(error)
  65. {
  66. case ScriptContext::ErrorType::Log:
  67. AZ_Info("ScriptAutomation", "Lua log: %s", message);
  68. break;
  69. case ScriptContext::ErrorType::Warning:
  70. AZ_Warning("ScriptAutomation", false, "Lua warning: %s", message);
  71. break;
  72. case ScriptContext::ErrorType::Error:
  73. AZ_Error("ScriptAutomation", false, "Lua error: %s", message);
  74. break;
  75. }
  76. }
  77. } // namespace
  78. void ExecuteLuaScript(const AZ::ConsoleCommandContainer& arguments)
  79. {
  80. auto scriptAuto = ScriptAutomationInterface::Get();
  81. if (!scriptAuto)
  82. {
  83. AZ_Error("ScriptAutomation", false, "There is no ScriptAutomation instance registered to the interface.");
  84. return;
  85. }
  86. const char* scriptPath = arguments[0].data();
  87. scriptAuto->ActivateScript(scriptPath);
  88. }
  89. AZ_CONSOLEFREEFUNC(ExecuteLuaScript, AZ::ConsoleFunctorFlags::Null, "Execute a Lua script");
  90. void ScriptAutomationSystemComponent::ActivateScript(const char* scriptPath)
  91. {
  92. m_isStarted = false;
  93. m_automationScript = scriptPath;
  94. AZ::TickBus::Handler::BusConnect();
  95. }
  96. void ScriptAutomationSystemComponent::DeactivateScripts()
  97. {
  98. m_isStarted = false;
  99. m_automationScript = "";
  100. AZStd::queue<ScriptAutomationRequests::ScriptOperation> empty;
  101. empty.swap(m_scriptOperations); // clear the script operations
  102. m_scriptPaused = false;
  103. m_scriptIdleFrames = 0;
  104. m_scriptIdleSeconds = 0.0f;
  105. m_scriptPauseTimeout = 0.0f;
  106. // continue to tick so end of script is handled
  107. }
  108. void ScriptAutomationSystemComponent::Reflect(AZ::ReflectContext* context)
  109. {
  110. if (AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context))
  111. {
  112. serialize->Class<ScriptAutomationSystemComponent, AZ::Component>()
  113. ->Version(0);
  114. if (AZ::EditContext* ec = serialize->GetEditContext())
  115. {
  116. ec->Class<ScriptAutomationSystemComponent>("ScriptAutomation", "Provides a mechanism for automating various tasks through Lua scripting in the game launchers")
  117. ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
  118. ->Attribute(AZ::Edit::Attributes::AutoExpand, true);
  119. }
  120. }
  121. ScriptAutomation::ImageComparisonConfig::Reflect(context);
  122. }
  123. void ScriptAutomationSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
  124. {
  125. provided.push_back(AutomationServiceCrc);
  126. }
  127. void ScriptAutomationSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
  128. {
  129. incompatible.push_back(AutomationServiceCrc);
  130. }
  131. void ScriptAutomationSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required)
  132. {
  133. }
  134. void ScriptAutomationSystemComponent::GetDependentServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& dependent)
  135. {
  136. }
  137. ScriptAutomationSystemComponent::ScriptAutomationSystemComponent()
  138. {
  139. if (ScriptAutomationInterface::Get() == nullptr)
  140. {
  141. ScriptAutomationInterface::Register(this);
  142. }
  143. }
  144. ScriptAutomationSystemComponent::~ScriptAutomationSystemComponent()
  145. {
  146. if (ScriptAutomationInterface::Get() == this)
  147. {
  148. ScriptAutomationInterface::Unregister(this);
  149. }
  150. }
  151. void ScriptAutomationSystemComponent::SetIdleFrames(int numFrames)
  152. {
  153. AZ_Assert(m_scriptIdleFrames <= 0, "m_scriptIdleFrames is being stomped");
  154. m_scriptIdleFrames = numFrames;
  155. }
  156. void ScriptAutomationSystemComponent::SetIdleSeconds(float numSeconds)
  157. {
  158. AZ_Assert(m_scriptIdleSeconds <= 0, "m_scriptIdleSeconds is being stomped");
  159. m_scriptIdleSeconds = numSeconds;
  160. }
  161. void ScriptAutomationSystemComponent::SetFrameCaptureId(AZ::Render::FrameCaptureId frameCaptureId)
  162. {
  163. // FrameCapture system supports multiple active frame captures, Script Automation would need changes to support more than 1 active at a time.
  164. AZ_Assert(m_scriptFrameCaptureId == AZ::Render::InvalidFrameCaptureId, "Attempting to start a frame capture while one is in progress");
  165. m_scriptFrameCaptureId = frameCaptureId;
  166. AZ::Render::FrameCaptureNotificationBus::Handler::BusConnect(frameCaptureId);
  167. }
  168. void ScriptAutomationSystemComponent::StartProfilingCapture()
  169. {
  170. AZ::Render::ProfilingCaptureNotificationBus::Handler::BusConnect();
  171. }
  172. void ScriptAutomationSystemComponent::Activate()
  173. {
  174. ScriptAutomationRequestBus::Handler::BusConnect();
  175. m_scriptContext = AZStd::make_unique<AZ::ScriptContext>();
  176. m_scriptBehaviorContext = AZStd::make_unique<AZ::BehaviorContext>();
  177. ReflectScriptBindings(m_scriptBehaviorContext.get());
  178. m_scriptContext->BindTo(m_scriptBehaviorContext.get());
  179. m_scriptContext->SetErrorHook(LuaErrorLog);
  180. AZ::ComponentApplication* application = nullptr;
  181. AZ::ComponentApplicationBus::BroadcastResult(application, &AZ::ComponentApplicationBus::Events::GetApplication);
  182. if (application)
  183. {
  184. constexpr const char* automationSuiteSwitch = "run-automation-suite";
  185. constexpr const char* automationExitSwitch = "exit-on-automation-end";
  186. auto commandLine = application->GetAzCommandLine();
  187. if (commandLine->HasSwitch(automationSuiteSwitch))
  188. {
  189. m_exitOnFinish = commandLine->HasSwitch(automationExitSwitch);
  190. ActivateScript(commandLine->GetSwitchValue(automationSuiteSwitch, 0).c_str());
  191. }
  192. }
  193. }
  194. void ScriptAutomationSystemComponent::Deactivate()
  195. {
  196. DeactivateScripts();
  197. m_scriptBehaviorContext = nullptr;
  198. m_scriptContext = nullptr;
  199. ScriptAutomationRequestBus::Handler::BusDisconnect();
  200. }
  201. void ScriptAutomationSystemComponent::OnTick(float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time)
  202. {
  203. if (!m_isStarted)
  204. {
  205. m_isStarted = true;
  206. ExecuteScript(m_automationScript.c_str());
  207. ScriptAutomationNotificationBus::Broadcast(&ScriptAutomationNotificationBus::Events::OnAutomationStarted);
  208. }
  209. while (true)
  210. {
  211. if (m_scriptPaused)
  212. {
  213. m_scriptPauseTimeout -= deltaTime;
  214. if (m_scriptPauseTimeout < 0)
  215. {
  216. AZ_Error("ScriptAutomation", false, "Script pause timed out. Continuing...");
  217. m_scriptPaused = false;
  218. }
  219. else
  220. {
  221. break;
  222. }
  223. }
  224. if (m_scriptIdleFrames > 0)
  225. {
  226. m_scriptIdleFrames--;
  227. break;
  228. }
  229. if (m_scriptIdleSeconds > 0)
  230. {
  231. m_scriptIdleSeconds -= deltaTime;
  232. break;
  233. }
  234. if (!m_scriptOperations.empty()) // may be looping waiting for final pause to finish
  235. {
  236. // Execute the next operation
  237. m_scriptOperations.front()();
  238. m_scriptOperations.pop();
  239. }
  240. if (m_scriptOperations.empty())
  241. {
  242. if(!m_scriptPaused) // final operation may have paused, wait for it to complete or time out
  243. {
  244. DeactivateScripts();
  245. ScriptAutomationNotificationBus::Broadcast(&ScriptAutomationNotificationBus::Events::OnAutomationFinished);
  246. if (m_exitOnFinish)
  247. {
  248. AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::ExitMainLoop);
  249. }
  250. }
  251. break;
  252. }
  253. }
  254. }
  255. AZ::BehaviorContext* ScriptAutomationSystemComponent::GetAutomationContext()
  256. {
  257. return m_scriptBehaviorContext.get();
  258. }
  259. void ScriptAutomationSystemComponent::PauseAutomation(float timeout)
  260. {
  261. m_scriptPaused = true;
  262. m_scriptPauseTimeout = AZ::GetMax(timeout, m_scriptPauseTimeout);
  263. }
  264. void ScriptAutomationSystemComponent::ResumeAutomation()
  265. {
  266. AZ_Warning("ScriptAutomation", m_scriptPaused, "Script is not paused");
  267. m_scriptPaused = false;
  268. }
  269. void ScriptAutomationSystemComponent::QueueScriptOperation(ScriptAutomationRequests::ScriptOperation&& operation)
  270. {
  271. m_scriptOperations.push(AZStd::move(operation));
  272. }
  273. void ScriptAutomationSystemComponent::ExecuteScript(const char* scriptFilePath)
  274. {
  275. AZ::Data::Asset<AZ::ScriptAsset> scriptAsset = LoadScriptAssetFromPath(scriptFilePath, *m_scriptContext.get());
  276. AZStd::string localScriptFilePath = scriptFilePath;
  277. if (!scriptAsset)
  278. {
  279. #if AZ_ENABLE_TRACING
  280. // Push an error operation on the back of the queue instead of reporting it immediately so it doesn't get lost
  281. // in front of a bunch of queued m_scriptOperations.
  282. QueueScriptOperation([localScriptFilePath]()
  283. {
  284. AZ_Error("ScriptAutomation", false, "Script: Could not find or load script asset '%s'.", localScriptFilePath.c_str());
  285. }
  286. );
  287. #endif // AZ_ENABLE_TRACING
  288. return;
  289. }
  290. #if AZ_ENABLE_TRACING
  291. QueueScriptOperation([localScriptFilePath]()
  292. {
  293. AZ_Printf("ScriptAutomation", "Running script '%s'...\n", localScriptFilePath.c_str());
  294. }
  295. );
  296. #endif // AZ_ENABLE_TRACING
  297. if (!m_scriptContext->Execute(scriptAsset->m_data.GetScriptBuffer().data(), localScriptFilePath.c_str(), scriptAsset->m_data.GetScriptBuffer().size()))
  298. {
  299. #if AZ_ENABLE_TRACING
  300. // Push an error operation on the back of the queue instead of reporting it immediately so it doesn't get lost
  301. // in front of a bunch of queued m_scriptOperations.
  302. QueueScriptOperation([localScriptFilePath]()
  303. {
  304. AZ_Error("ScriptAutomation", false, "Script: Error running script '%s'.", localScriptFilePath.c_str());
  305. }
  306. );
  307. #endif // AZ_ENABLE_TRACING
  308. }
  309. }
  310. const ImageComparisonToleranceLevel* ScriptAutomationSystemComponent::FindToleranceLevel(const AZStd::string& name)
  311. {
  312. return m_imageComparisonSettings.FindToleranceLevel(name);
  313. }
  314. void ScriptAutomationSystemComponent::OnCaptureQueryTimestampFinished([[maybe_unused]] bool result, [[maybe_unused]] const AZStd::string& info)
  315. {
  316. AZ::Render::ProfilingCaptureNotificationBus::Handler::BusDisconnect();
  317. ResumeAutomation();
  318. }
  319. void ScriptAutomationSystemComponent::OnCaptureCpuFrameTimeFinished([[maybe_unused]] bool result, [[maybe_unused]] const AZStd::string& info)
  320. {
  321. AZ::Render::ProfilingCaptureNotificationBus::Handler::BusDisconnect();
  322. ResumeAutomation();
  323. }
  324. void ScriptAutomationSystemComponent::OnCaptureQueryPipelineStatisticsFinished([[maybe_unused]] bool result, [[maybe_unused]] const AZStd::string& info)
  325. {
  326. AZ::Render::ProfilingCaptureNotificationBus::Handler::BusDisconnect();
  327. ResumeAutomation();
  328. }
  329. void ScriptAutomationSystemComponent::OnCaptureBenchmarkMetadataFinished([[maybe_unused]] bool result, [[maybe_unused]] const AZStd::string& info)
  330. {
  331. AZ::Render::ProfilingCaptureNotificationBus::Handler::BusDisconnect();
  332. ResumeAutomation();
  333. }
  334. void ScriptAutomationSystemComponent::OnFrameCaptureFinished(AZ::Render::FrameCaptureResult result, const AZStd::string &info)
  335. {
  336. m_scriptFrameCaptureId = AZ::Render::InvalidFrameCaptureId;
  337. AZ::Render::FrameCaptureNotificationBus::Handler::BusDisconnect();
  338. ResumeAutomation();
  339. // This is checking for the exact scenario that results from an HDR setup. The goal is to add a very specific and prominent message that will
  340. // alert users to a common issue and what action to take. Any other Format issues will be reported by FrameCaptureSystemComponent with a
  341. // "Can't save image with format %s to a ppm file" message.
  342. if (result == AZ::Render::FrameCaptureResult::UnsupportedFormat && info.find(AZ::RHI::ToString(AZ::RHI::Format::R10G10B10A2_UNORM)) != AZStd::string::npos)
  343. {
  344. AZ_Assert(false, "ScriptAutomation Screen Capture - HDR Not Supported, Screen capture to image is not supported from RGB10A2 display format. Please change the system configuration to disable the HDR display feature.");
  345. }
  346. }
  347. void ScriptAutomationSystemComponent::LoadLevel(const char* levelName)
  348. {
  349. AZ_Assert(!m_levelLoading, "Attempting to load a level while still waiting for a level to load");
  350. m_levelLoading = true;
  351. m_levelName = levelName;
  352. AzFramework::LevelSystemLifecycleNotificationBus::Handler::BusConnect();
  353. PauseAutomation();
  354. auto loadLevelString = AZStd::string::format("LoadLevel %s", levelName);
  355. AzFramework::ConsoleRequestBus::Broadcast(
  356. &AzFramework::ConsoleRequests::ExecuteConsoleCommand, loadLevelString.c_str());
  357. }
  358. void ScriptAutomationSystemComponent::OnLevelNotFound(const char* levelName)
  359. {
  360. if (m_levelName == levelName)
  361. {
  362. m_levelLoading = false;
  363. m_levelName = "";
  364. AZ_Error("ScriptAutomation", false, "Level not found \"%s\"", levelName);
  365. AzFramework::LevelSystemLifecycleNotificationBus::Handler::BusDisconnect();
  366. DeactivateScripts();
  367. }
  368. }
  369. void ScriptAutomationSystemComponent::OnLoadingComplete(const char* levelName)
  370. {
  371. if (m_levelName == levelName)
  372. {
  373. m_levelLoading = false;
  374. AzFramework::LevelSystemLifecycleNotificationBus::Handler::BusDisconnect();
  375. ResumeAutomation();
  376. }
  377. }
  378. void ScriptAutomationSystemComponent::OnLoadingError(const char* levelName, [[maybe_unused]] const char* error)
  379. {
  380. if (m_levelName == levelName)
  381. {
  382. m_levelLoading = false;
  383. m_levelName = "";
  384. AZ_Error("ScriptAutomation", false, "Failed to load level \"%s\", error \"%s\"", levelName, error);
  385. AzFramework::LevelSystemLifecycleNotificationBus::Handler::BusDisconnect();
  386. DeactivateScripts();
  387. }
  388. }
  389. } // namespace AZ::ScriptAutomation