3
0

MultiplayerDebugSystemComponent.cpp 20 KB

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