ScriptAutomationSystemComponent.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  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. [[maybe_unused]] AZStd::string localScriptFilePath = scriptFilePath;
  277. if (!scriptAsset)
  278. {
  279. // Push an error operation on the back of the queue instead of reporting it immediately so it doesn't get lost
  280. // in front of a bunch of queued m_scriptOperations.
  281. QueueScriptOperation([localScriptFilePath]()
  282. {
  283. AZ_Error("ScriptAutomation", false, "Script: Could not find or load script asset '%s'.", localScriptFilePath.c_str());
  284. }
  285. );
  286. return;
  287. }
  288. QueueScriptOperation([localScriptFilePath]()
  289. {
  290. AZ_Printf("ScriptAutomation", "Running script '%s'...\n", localScriptFilePath.c_str());
  291. }
  292. );
  293. if (!m_scriptContext->Execute(scriptAsset->m_data.GetScriptBuffer().data(), localScriptFilePath.c_str(), scriptAsset->m_data.GetScriptBuffer().size()))
  294. {
  295. // Push an error operation on the back of the queue instead of reporting it immediately so it doesn't get lost
  296. // in front of a bunch of queued m_scriptOperations.
  297. QueueScriptOperation([localScriptFilePath]()
  298. {
  299. AZ_Error("ScriptAutomation", false, "Script: Error running script '%s'.", localScriptFilePath.c_str());
  300. }
  301. );
  302. }
  303. }
  304. const ImageComparisonToleranceLevel* ScriptAutomationSystemComponent::FindToleranceLevel(const AZStd::string& name)
  305. {
  306. return m_imageComparisonSettings.FindToleranceLevel(name);
  307. }
  308. void ScriptAutomationSystemComponent::OnCaptureQueryTimestampFinished([[maybe_unused]] bool result, [[maybe_unused]] const AZStd::string& info)
  309. {
  310. AZ::Render::ProfilingCaptureNotificationBus::Handler::BusDisconnect();
  311. ResumeAutomation();
  312. }
  313. void ScriptAutomationSystemComponent::OnCaptureCpuFrameTimeFinished([[maybe_unused]] bool result, [[maybe_unused]] const AZStd::string& info)
  314. {
  315. AZ::Render::ProfilingCaptureNotificationBus::Handler::BusDisconnect();
  316. ResumeAutomation();
  317. }
  318. void ScriptAutomationSystemComponent::OnCaptureQueryPipelineStatisticsFinished([[maybe_unused]] bool result, [[maybe_unused]] const AZStd::string& info)
  319. {
  320. AZ::Render::ProfilingCaptureNotificationBus::Handler::BusDisconnect();
  321. ResumeAutomation();
  322. }
  323. void ScriptAutomationSystemComponent::OnCaptureBenchmarkMetadataFinished([[maybe_unused]] bool result, [[maybe_unused]] const AZStd::string& info)
  324. {
  325. AZ::Render::ProfilingCaptureNotificationBus::Handler::BusDisconnect();
  326. ResumeAutomation();
  327. }
  328. void ScriptAutomationSystemComponent::OnFrameCaptureFinished(AZ::Render::FrameCaptureResult result, const AZStd::string &info)
  329. {
  330. m_scriptFrameCaptureId = AZ::Render::InvalidFrameCaptureId;
  331. AZ::Render::FrameCaptureNotificationBus::Handler::BusDisconnect();
  332. ResumeAutomation();
  333. // 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
  334. // alert users to a common issue and what action to take. Any other Format issues will be reported by FrameCaptureSystemComponent with a
  335. // "Can't save image with format %s to a ppm file" message.
  336. if (result == AZ::Render::FrameCaptureResult::UnsupportedFormat && info.find(AZ::RHI::ToString(AZ::RHI::Format::R10G10B10A2_UNORM)) != AZStd::string::npos)
  337. {
  338. 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.");
  339. }
  340. }
  341. void ScriptAutomationSystemComponent::LoadLevel(const char* levelName)
  342. {
  343. AZ_Assert(!m_levelLoading, "Attempting to load a level while still waiting for a level to load");
  344. m_levelLoading = true;
  345. m_levelName = levelName;
  346. AzFramework::LevelSystemLifecycleNotificationBus::Handler::BusConnect();
  347. PauseAutomation();
  348. auto loadLevelString = AZStd::string::format("LoadLevel %s", levelName);
  349. AzFramework::ConsoleRequestBus::Broadcast(
  350. &AzFramework::ConsoleRequests::ExecuteConsoleCommand, loadLevelString.c_str());
  351. }
  352. void ScriptAutomationSystemComponent::OnLevelNotFound(const char* levelName)
  353. {
  354. if (m_levelName == levelName)
  355. {
  356. m_levelLoading = false;
  357. m_levelName = "";
  358. AZ_Error("ScriptAutomation", false, "Level not found \"%s\"", levelName);
  359. AzFramework::LevelSystemLifecycleNotificationBus::Handler::BusDisconnect();
  360. DeactivateScripts();
  361. }
  362. }
  363. void ScriptAutomationSystemComponent::OnLoadingComplete(const char* levelName)
  364. {
  365. if (m_levelName == levelName)
  366. {
  367. m_levelLoading = false;
  368. AzFramework::LevelSystemLifecycleNotificationBus::Handler::BusDisconnect();
  369. ResumeAutomation();
  370. }
  371. }
  372. void ScriptAutomationSystemComponent::OnLoadingError(const char* levelName, [[maybe_unused]] const char* error)
  373. {
  374. if (m_levelName == levelName)
  375. {
  376. m_levelLoading = false;
  377. m_levelName = "";
  378. AZ_Error("ScriptAutomation", false, "Failed to load level \"%s\", error \"%s\"", levelName, error);
  379. AzFramework::LevelSystemLifecycleNotificationBus::Handler::BusDisconnect();
  380. DeactivateScripts();
  381. }
  382. }
  383. } // namespace AZ::ScriptAutomation