MultiplayerDebugSystemComponent.cpp 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  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 <Source/Debug/MultiplayerDebugSystemComponent.h>
  9. #include <AzCore/Asset/AssetManagerBus.h>
  10. #include <AzCore/Component/ComponentApplicationBus.h>
  11. #include <AzCore/Component/TickBus.h>
  12. #include <AzCore/Interface/Interface.h>
  13. #include <AzCore/Serialization/SerializeContext.h>
  14. #include <AzCore/StringFunc/StringFunc.h>
  15. #include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
  16. #include <AzNetworking/Framework/INetworking.h>
  17. #include <AzNetworking/Framework/INetworkInterface.h>
  18. #include <Multiplayer/IMultiplayer.h>
  19. #include <Multiplayer/MultiplayerConstants.h>
  20. #include <Multiplayer/MultiplayerPerformanceStats.h>
  21. #include <Multiplayer/MultiplayerMetrics.h>
  22. #include <Atom/Feature/ImGui/SystemBus.h>
  23. #include <ImGuiContextScope.h>
  24. #include <imgui/imgui.h>
  25. #include <imgui/imgui_internal.h>
  26. void OnDebugEntities_ShowBandwidth_Changed(const bool& showBandwidth);
  27. AZ_CVAR(bool, net_DebugEntities_ShowBandwidth, false, &OnDebugEntities_ShowBandwidth_Changed, AZ::ConsoleFunctorFlags::Null,
  28. "If true, prints bandwidth values over entities that use a considerable amount of network traffic");
  29. AZ_CVAR(uint16_t, net_DebutAuditTrail_HistorySize, 20, nullptr, AZ::ConsoleFunctorFlags::Null,
  30. "Length of networking debug Audit Trail");
  31. namespace Multiplayer
  32. {
  33. void MultiplayerDebugSystemComponent::Reflect(AZ::ReflectContext* context)
  34. {
  35. if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
  36. {
  37. serializeContext->Class<MultiplayerDebugSystemComponent, AZ::Component>()
  38. ->Version(1);
  39. }
  40. }
  41. void MultiplayerDebugSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
  42. {
  43. provided.push_back(AZ_CRC_CE("MultiplayerDebugSystemComponent"));
  44. }
  45. void MultiplayerDebugSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required)
  46. {
  47. ;
  48. }
  49. void MultiplayerDebugSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatbile)
  50. {
  51. incompatbile.push_back(AZ_CRC_CE("MultiplayerDebugSystemComponent"));
  52. }
  53. void MultiplayerDebugSystemComponent::Activate()
  54. {
  55. #ifdef IMGUI_ENABLED
  56. AZ::ComponentApplicationBus::Broadcast(&AZ::ComponentApplicationRequests::QueryApplicationType, m_applicationType);
  57. ImGui::ImGuiUpdateListenerBus::Handler::BusConnect();
  58. m_networkMetrics = AZStd::make_unique<MultiplayerDebugNetworkMetrics>();
  59. m_multiplayerMetrics = AZStd::make_unique<MultiplayerDebugMultiplayerMetrics>();
  60. #endif
  61. }
  62. void MultiplayerDebugSystemComponent::Deactivate()
  63. {
  64. #ifdef IMGUI_ENABLED
  65. ImGui::ImGuiUpdateListenerBus::Handler::BusDisconnect();
  66. m_auditTrailElems.clear();
  67. m_committedAuditTrail.clear();
  68. m_pendingAuditTrail.clear();
  69. m_filteredAuditTrail.clear();
  70. #endif
  71. }
  72. void MultiplayerDebugSystemComponent::ShowEntityBandwidthDebugOverlay()
  73. {
  74. #ifdef IMGUI_ENABLED
  75. m_reporter = AZStd::make_unique<MultiplayerDebugPerEntityReporter>();
  76. #endif
  77. }
  78. void MultiplayerDebugSystemComponent::HideEntityBandwidthDebugOverlay()
  79. {
  80. #ifdef IMGUI_ENABLED
  81. m_reporter.reset();
  82. #endif
  83. }
  84. void MultiplayerDebugSystemComponent::AddAuditEntry(
  85. [[maybe_unused]] const AuditCategory category,
  86. [[maybe_unused]] const ClientInputId inputId,
  87. [[maybe_unused]] const HostFrameId frameId,
  88. [[maybe_unused]] const AZStd::string& name,
  89. [[maybe_unused]] AZStd::vector<MultiplayerAuditingElement>&& entryDetails)
  90. {
  91. if (category == AuditCategory::Desync)
  92. {
  93. INCREMENT_PERFORMANCE_STAT(MultiplayerStat_DesyncCorrections);
  94. }
  95. #ifdef IMGUI_ENABLED
  96. while (m_auditTrailElems.size() >= net_DebutAuditTrail_HistorySize)
  97. {
  98. m_auditTrailElems.pop_back();
  99. }
  100. m_auditTrailElems.emplace_front(category, inputId, frameId, name, AZStd::move(entryDetails));
  101. if (category == AuditCategory::Desync)
  102. {
  103. while (m_auditTrailElems.size() > 0)
  104. {
  105. m_pendingAuditTrail.push_front(AZStd::move(m_auditTrailElems.back()));
  106. m_auditTrailElems.pop_back();
  107. }
  108. while (m_pendingAuditTrail.size() >= net_DebutAuditTrail_HistorySize)
  109. {
  110. m_pendingAuditTrail.pop_back();
  111. }
  112. }
  113. #endif
  114. }
  115. #ifdef IMGUI_ENABLED
  116. void MultiplayerDebugSystemComponent::OnImGuiMainMenuUpdate()
  117. {
  118. if (ImGui::BeginMenu("Multiplayer"))
  119. {
  120. ImGui::Checkbox("Networking Stats", &m_displayNetworkingStats);
  121. ImGui::Checkbox("Multiplayer Stats", &m_displayMultiplayerStats);
  122. ImGui::Checkbox("Multiplayer Entity Stats", &m_displayPerEntityStats);
  123. ImGui::Checkbox("Multiplayer Hierarchy Debugger", &m_displayHierarchyDebugger);
  124. ImGui::Checkbox("Multiplayer Audit Trail", &m_displayNetAuditTrail);
  125. if (auto multiplayerInterface = AZ::Interface<IMultiplayer>::Get(); multiplayerInterface && !m_applicationType.IsEditor())
  126. {
  127. if (auto console = AZ::Interface<AZ::IConsole>::Get())
  128. {
  129. const MultiplayerAgentType multiplayerAgentType = multiplayerInterface->GetAgentType();
  130. // Enable the host level selection menu if we're neither a host nor client, or if we are hosting, but haven't loaded a level yet.
  131. const bool isLevelLoaded = AzFramework::LevelSystemLifecycleInterface::Get()->IsLevelLoaded();
  132. const bool isHosting = (multiplayerAgentType == MultiplayerAgentType::ClientServer) || (multiplayerAgentType == MultiplayerAgentType::DedicatedServer);
  133. const bool enableHostLevelSelection = multiplayerAgentType == MultiplayerAgentType::Uninitialized || (isHosting && !isLevelLoaded);
  134. if (ImGui::BeginMenu(HostLevelMenuTitle, enableHostLevelSelection))
  135. {
  136. // Run through all the assets in the asset catalog and gather up the list of level assets
  137. AZ::Data::AssetType levelAssetType = azrtti_typeid<AzFramework::Spawnable>();
  138. AZStd::set<AZStd::string> multiplayerLevelFilePaths;
  139. auto enumerateCB =
  140. [levelAssetType, &multiplayerLevelFilePaths]([[maybe_unused]] const AZ::Data::AssetId id, const AZ::Data::AssetInfo& assetInfo)
  141. {
  142. // Skip everything that isn't a spawnable
  143. if (assetInfo.m_assetType != levelAssetType)
  144. {
  145. return;
  146. }
  147. // Skip non-network spawnables
  148. // A network spawnable is serialized to file as a ".network.spawnable". (See Multiplayer Gem's MultiplayerConstants.h)
  149. if (!assetInfo.m_relativePath.ends_with(Multiplayer::NetworkSpawnableFileExtension))
  150. {
  151. return;
  152. }
  153. // Skip spawnables not inside the levels folder
  154. if (!assetInfo.m_relativePath.starts_with("levels"))
  155. {
  156. return;
  157. }
  158. // Skip spawnables that live inside level folders, but isn't the level itself
  159. AZ::IO::PathView spawnableFilePath(assetInfo.m_relativePath);
  160. AZ::IO::PathView filenameSansExtension = spawnableFilePath.Stem().Stem(); // Just the filename without the .network.spawnable extension
  161. AZ::IO::PathView::const_iterator parentFolderName = spawnableFilePath.end();
  162. AZStd::advance(parentFolderName, -2);
  163. if (parentFolderName->Native() != filenameSansExtension.Native())
  164. {
  165. return;
  166. }
  167. AZStd::string multiplayerLevelFilePath = assetInfo.m_relativePath;
  168. AZ::StringFunc::Replace(multiplayerLevelFilePath, Multiplayer::NetworkFileExtension.data(), "");
  169. multiplayerLevelFilePaths.emplace(multiplayerLevelFilePath);
  170. };
  171. AZ::Data::AssetCatalogRequestBus::Broadcast(
  172. &AZ::Data::AssetCatalogRequestBus::Events::EnumerateAssets, nullptr, enumerateCB, nullptr);
  173. if (!multiplayerLevelFilePaths.empty())
  174. {
  175. int levelIndex = 0;
  176. for (const auto& multiplayerLevelFilePath : multiplayerLevelFilePaths)
  177. {
  178. auto levelMenuItem = AZStd::string::format("%d- %s", levelIndex, multiplayerLevelFilePath.c_str());
  179. if (ImGui::MenuItem(levelMenuItem.c_str()))
  180. {
  181. AZ::TickBus::QueueFunction(
  182. [console, multiplayerLevelFilePath, isHosting]()
  183. {
  184. auto loadLevelString = AZStd::string::format("LoadLevel %s", multiplayerLevelFilePath.c_str());
  185. if (!isHosting)
  186. {
  187. console->PerformCommand("host");
  188. }
  189. console->PerformCommand(loadLevelString.c_str());
  190. });
  191. }
  192. ++levelIndex;
  193. }
  194. }
  195. else
  196. {
  197. ImGui::MenuItem(NoMultiplayerLevelsFound);
  198. }
  199. ImGui::EndMenu();
  200. }
  201. // Disable the launch local client button if we're not hosting, or if even if we are hosting, but haven't loaded a level yet.
  202. if (!isHosting || !isLevelLoaded)
  203. {
  204. ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.6f);
  205. ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
  206. }
  207. if (ImGui::Button(LaunchLocalClientButtonTitle))
  208. {
  209. console->PerformCommand("sv_launch_local_client");
  210. }
  211. if (!isHosting || !isLevelLoaded)
  212. {
  213. ImGui::PopItemFlag();
  214. ImGui::PopStyleVar();
  215. }
  216. }
  217. }
  218. ImGui::EndMenu();
  219. }
  220. }
  221. void MultiplayerDebugSystemComponent::OnImGuiUpdate()
  222. {
  223. bool displaying = m_displayNetworkingStats || m_displayMultiplayerStats || m_displayPerEntityStats || m_displayHierarchyDebugger ||
  224. m_displayNetAuditTrail;
  225. if (displaying)
  226. {
  227. if (m_displayNetworkingStats)
  228. {
  229. if (ImGui::Begin("Networking Stats", &m_displayNetworkingStats, ImGuiWindowFlags_None))
  230. {
  231. m_networkMetrics->OnImGuiUpdate();
  232. }
  233. ImGui::End();
  234. }
  235. if (m_displayMultiplayerStats)
  236. {
  237. if (ImGui::Begin("Multiplayer Stats", &m_displayMultiplayerStats, ImGuiWindowFlags_None))
  238. {
  239. m_multiplayerMetrics->OnImGuiUpdate();
  240. }
  241. ImGui::End();
  242. }
  243. if (m_displayPerEntityStats)
  244. {
  245. if (ImGui::Begin("Multiplayer Per Entity Stats", &m_displayPerEntityStats, ImGuiWindowFlags_AlwaysAutoResize))
  246. {
  247. // This overrides @net_DebugNetworkEntity_ShowBandwidth value
  248. if (m_reporter == nullptr)
  249. {
  250. ShowEntityBandwidthDebugOverlay();
  251. }
  252. if (m_reporter)
  253. {
  254. m_reporter->OnImGuiUpdate();
  255. }
  256. }
  257. ImGui::End();
  258. }
  259. if (m_displayHierarchyDebugger)
  260. {
  261. if (ImGui::Begin("Multiplayer Hierarchy Debugger", &m_displayHierarchyDebugger))
  262. {
  263. if (m_hierarchyDebugger == nullptr)
  264. {
  265. m_hierarchyDebugger = AZStd::make_unique<MultiplayerDebugHierarchyReporter>();
  266. }
  267. if (m_hierarchyDebugger)
  268. {
  269. m_hierarchyDebugger->OnImGuiUpdate();
  270. }
  271. }
  272. ImGui::End();
  273. }
  274. else
  275. {
  276. if (m_hierarchyDebugger)
  277. {
  278. m_hierarchyDebugger.reset();
  279. }
  280. }
  281. if (m_displayNetAuditTrail)
  282. {
  283. if (ImGui::Begin("Multiplayer Audit Trail", &m_displayNetAuditTrail))
  284. {
  285. if (m_auditTrail == nullptr)
  286. {
  287. m_lastFilter = "";
  288. m_auditTrail = AZStd::make_unique<MultiplayerDebugAuditTrail>();
  289. m_committedAuditTrail = m_pendingAuditTrail;
  290. }
  291. if (m_auditTrail->TryPumpAuditTrail())
  292. {
  293. m_committedAuditTrail = m_pendingAuditTrail;
  294. }
  295. FilterAuditTrail();
  296. if (m_auditTrail)
  297. {
  298. if (m_filteredAuditTrail.size() > 0)
  299. {
  300. m_auditTrail->OnImGuiUpdate(m_filteredAuditTrail);
  301. }
  302. else
  303. {
  304. m_auditTrail->OnImGuiUpdate(m_committedAuditTrail);
  305. }
  306. }
  307. }
  308. ImGui::End();
  309. }
  310. else
  311. {
  312. if (m_auditTrail)
  313. {
  314. m_auditTrail.reset();
  315. }
  316. }
  317. }
  318. }
  319. void MultiplayerDebugSystemComponent::FilterAuditTrail()
  320. {
  321. if (!m_auditTrail)
  322. {
  323. return;
  324. }
  325. AZStd::string filter = m_auditTrail->GetAuditTrialFilter();
  326. if (m_filteredAuditTrail.size() > 0 && filter == m_lastFilter)
  327. {
  328. return;
  329. }
  330. m_lastFilter = filter;
  331. m_filteredAuditTrail.clear();
  332. if (filter.size() == 0)
  333. {
  334. return;
  335. }
  336. for (auto elem = m_committedAuditTrail.begin(); elem != m_committedAuditTrail.end(); ++elem)
  337. {
  338. const char* nodeTitle = "";
  339. switch (elem->m_category)
  340. {
  341. case AuditCategory::Desync:
  342. nodeTitle = MultiplayerDebugAuditTrail::DESYNC_TITLE;
  343. break;
  344. case AuditCategory::Input:
  345. nodeTitle = MultiplayerDebugAuditTrail::INPUT_TITLE;
  346. break;
  347. case AuditCategory::Event:
  348. nodeTitle = MultiplayerDebugAuditTrail::EVENT_TITLE;
  349. break;
  350. }
  351. // Events only have one item
  352. if (elem->m_category == AuditCategory::Event)
  353. {
  354. if (elem->m_children.size() > 0 && elem->m_children.front().m_elements.size() > 0)
  355. {
  356. if (AZStd::string::format(nodeTitle, elem->m_name.c_str()).contains(filter))
  357. {
  358. m_filteredAuditTrail.push_back(*elem);
  359. }
  360. else
  361. {
  362. AZStd::pair<AZStd::string, AZStd::string> cliServValues =
  363. elem->m_children.front().m_elements.front()->GetClientServerValues();
  364. if (AZStd::string::format(
  365. "%d %d %s %s", static_cast<uint16_t>(elem->m_inputId), static_cast<uint32_t>(elem->m_hostFrameId),
  366. cliServValues.first.c_str(), cliServValues.second.c_str())
  367. .contains(filter))
  368. {
  369. m_filteredAuditTrail.push_back(*elem);
  370. }
  371. }
  372. }
  373. }
  374. // Desyncs and inputs can contain multiple line items
  375. else
  376. {
  377. if (AZStd::string::format(nodeTitle, elem->m_name.c_str()).contains(filter))
  378. {
  379. m_filteredAuditTrail.push_back(*elem);
  380. }
  381. else if (AZStd::string::format("%hu %d", static_cast<uint16_t>(elem->m_inputId), static_cast<uint32_t>(elem->m_hostFrameId))
  382. .contains(filter))
  383. {
  384. m_filteredAuditTrail.push_back(*elem);
  385. }
  386. else
  387. {
  388. // Attempt to construct a filtered input
  389. Multiplayer::AuditTrailInput filteredInput(
  390. elem->m_category, elem->m_inputId, elem->m_hostFrameId, elem->m_name, {});
  391. for (const auto& child : elem->m_children)
  392. {
  393. if (child.m_name.contains(filter))
  394. {
  395. filteredInput.m_children.push_back(child);
  396. }
  397. else if (child.m_elements.size() > 0)
  398. {
  399. MultiplayerAuditingElement filteredChild;
  400. filteredChild.m_name = child.m_name;
  401. for (const auto& childElem : child.m_elements)
  402. {
  403. AZStd::pair<AZStd::string, AZStd::string> cliServValues = childElem->GetClientServerValues();
  404. if (AZStd::string::format(
  405. "%s %s %s", childElem->GetName().c_str(), cliServValues.first.c_str(),
  406. cliServValues.second.c_str())
  407. .contains(filter))
  408. {
  409. filteredChild.m_elements.push_back(childElem.get()->Clone());
  410. }
  411. }
  412. if (filteredChild.m_elements.size() > 0)
  413. {
  414. filteredInput.m_children.push_back(filteredChild);
  415. }
  416. }
  417. }
  418. if (filteredInput.m_children.size() > 0 || elem->m_category == AuditCategory::Desync)
  419. {
  420. m_filteredAuditTrail.push_back(filteredInput);
  421. }
  422. }
  423. }
  424. }
  425. }
  426. #endif
  427. }
  428. void OnDebugEntities_ShowBandwidth_Changed(const bool& showBandwidth)
  429. {
  430. if (showBandwidth)
  431. {
  432. AZ::Interface<Multiplayer::IMultiplayerDebug>::Get()->ShowEntityBandwidthDebugOverlay();
  433. }
  434. else
  435. {
  436. AZ::Interface<Multiplayer::IMultiplayerDebug>::Get()->HideEntityBandwidthDebugOverlay();
  437. }
  438. }