From 2f4120cdfbbd736d18fdc5f3187a94f6640ed28b Mon Sep 17 00:00:00 2001 From: puvvadar Date: Mon, 3 May 2021 13:07:11 -0700 Subject: [PATCH 01/22] Update Ctrl+G logic to account for prefab processing status and timing --- .../PrefabEditorEntityOwnershipInterface.h | 2 + .../PrefabEditorEntityOwnershipService.cpp | 5 ++ .../PrefabEditorEntityOwnershipService.h | 2 + Gems/Multiplayer/Code/CMakeLists.txt | 1 + .../Code/Include/IMultiplayerTools.h | 39 ++++++++++++ .../MultiplayerEditorSystemComponent.cpp | 63 ++++++++++++------- .../Editor/MultiplayerEditorSystemComponent.h | 12 +++- .../Code/Source/MultiplayerToolsModule.cpp | 35 ++++------- .../Code/Source/MultiplayerToolsModule.h | 27 ++++++++ .../Pipeline/NetworkPrefabProcessor.cpp | 3 + .../Code/multiplayer_tools_files.cmake | 1 + 11 files changed, 142 insertions(+), 48 deletions(-) create mode 100644 Gems/Multiplayer/Code/Include/IMultiplayerTools.h diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h index 19c236f509..4476876b59 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h @@ -46,6 +46,8 @@ namespace AzToolsFramework virtual Prefab::InstanceOptionalReference GetRootPrefabInstance() = 0; + virtual const AZStd::vector>& GetPlayInEditorAssetData() = 0; + virtual bool LoadFromStream(AZ::IO::GenericStream& stream, AZStd::string_view filename) = 0; virtual bool SaveToStream(AZ::IO::GenericStream& stream, AZStd::string_view filename) = 0; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp index 81233069a9..e81d6bf08e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp @@ -321,6 +321,11 @@ namespace AzToolsFramework return *m_rootInstance; } + const AZStd::vector>& PrefabEditorEntityOwnershipService::GetPlayInEditorAssetData() + { + return m_playInEditorData.m_assets; + } + void PrefabEditorEntityOwnershipService::OnEntityRemoved(AZ::EntityId entityId) { AzFramework::SliceEntityRequestBus::MultiHandler::BusDisconnect(entityId); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.h index 9c483e61c5..cf62220e67 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.h @@ -195,6 +195,8 @@ namespace AzToolsFramework AZ::IO::PathView filePath, Prefab::InstanceOptionalReference instanceToParentUnder) override; Prefab::InstanceOptionalReference GetRootPrefabInstance() override; + + const AZStd::vector>& GetPlayInEditorAssetData() override; ////////////////////////////////////////////////////////////////////////// void OnEntityRemoved(AZ::EntityId entityId); diff --git a/Gems/Multiplayer/Code/CMakeLists.txt b/Gems/Multiplayer/Code/CMakeLists.txt index 4eeee15c47..46f56ef315 100644 --- a/Gems/Multiplayer/Code/CMakeLists.txt +++ b/Gems/Multiplayer/Code/CMakeLists.txt @@ -119,6 +119,7 @@ if (PAL_TRAIT_BUILD_HOST_TOOLS) BUILD_DEPENDENCIES PRIVATE Gem::Multiplayer.Editor.Static + Gem::Multiplayer.Tools ) endif() diff --git a/Gems/Multiplayer/Code/Include/IMultiplayerTools.h b/Gems/Multiplayer/Code/Include/IMultiplayerTools.h new file mode 100644 index 0000000000..c621808f7a --- /dev/null +++ b/Gems/Multiplayer/Code/Include/IMultiplayerTools.h @@ -0,0 +1,39 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +#pragma once + +#include + +namespace Multiplayer +{ + //! IMultiplayer provides insight into the Multiplayer session and its Agents + class IMultiplayerTools + { + public: + // NetworkPrefabProcessor is the only class that should be setting process network prefab status + friend class NetworkPrefabProcessor; + + AZ_RTTI(IMultiplayerTools, "{E8A80EAB-29CB-4E3B-A0B2-FFCB37060FB0}"); + + virtual ~IMultiplayerTools() = default; + + //! Returns if network prefab processing has created currently active or pending spawnables + //! @return If network prefab processing has created currently active or pending spawnables + virtual bool DidProcessNetworkPrefabs() = 0; + + private: + //! Sets if network prefab processing has created currently active or pending spawnables + //! @param didProcessNetPrefabs if network prefab processing has created currently active or pending spawnables + virtual void SetDidProcessNetworkPrefabs(bool didProcessNetPrefabs) = 0; + }; +} diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp index aec3e7870f..0850b858a2 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp @@ -10,12 +10,14 @@ * */ +#include #include #include #include #include #include #include +#include namespace Multiplayer { @@ -57,12 +59,14 @@ namespace Multiplayer void MultiplayerEditorSystemComponent::Activate() { + AzFramework::GameEntityContextEventBus::Handler::BusConnect(); AzToolsFramework::EditorEvents::Bus::Handler::BusConnect(); } void MultiplayerEditorSystemComponent::Deactivate() { AzToolsFramework::EditorEvents::Bus::Handler::BusDisconnect(); + AzFramework::GameEntityContextEventBus::Handler::BusDisconnect(); } void MultiplayerEditorSystemComponent::NotifyRegisterViews() @@ -77,11 +81,42 @@ namespace Multiplayer { switch (event) { - case eNotify_OnBeginGameMode: - { + case eNotify_OnQuit: + AZ_Warning("Multiplayer Editor", m_editor != nullptr, "Multiplayer Editor received On Quit without an Editor pointer."); + if (m_editor) + { + m_editor->UnregisterNotifyListener(this); + m_editor = nullptr; + } + [[fallthrough]]; + case eNotify_OnEndGameMode: + AZ::TickBus::Handler::BusDisconnect(); + // Kill the configured server if it's active + if (m_serverProcess) + { + m_serverProcess->TerminateProcess(0); + m_serverProcess = nullptr; + } + break; + } + } + + void MultiplayerEditorSystemComponent::OnGameEntitiesStarted() + { + // BeginGameMode and Prefab Processing have completed at this point + IMultiplayerTools* mpTools = AZ::Interface::Get(); + if (editorsv_enabled && mpTools != nullptr && mpTools->DidProcessNetworkPrefabs()) + { AZ::TickBus::Handler::BusConnect(); - if (editorsv_enabled) + auto prefabEditorEntityOwnershipInterface = AZ::Interface::Get(); + if (!prefabEditorEntityOwnershipInterface) + { + AZ_Error("MultiplayerEditor", prefabEditorEntityOwnershipInterface != nullptr, "PrefabEditorEntityOwnershipInterface unavailable"); + } + const AZStd::vector>& assetData = prefabEditorEntityOwnershipInterface->GetPlayInEditorAssetData(); + + if (assetData.size() > 0) { // Assemble the server's path AZ::CVarFixedString serverProcess = editorsv_process; @@ -111,33 +146,13 @@ namespace Multiplayer // Start the configured server if it's available AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo; - processLaunchInfo.m_commandlineParameters = - AZStd::string::format("\"%s\"", serverPath.c_str()); + processLaunchInfo.m_commandlineParameters = AZStd::string::format("\"%s\"", serverPath.c_str()); processLaunchInfo.m_showWindow = true; processLaunchInfo.m_processPriority = AzFramework::ProcessPriority::PROCESSPRIORITY_NORMAL; m_serverProcess = AzFramework::ProcessWatcher::LaunchProcess( processLaunchInfo, AzFramework::ProcessCommunicationType::COMMUNICATOR_TYPE_NONE); } - break; - } - case eNotify_OnQuit: - AZ_Warning("Multiplayer Editor", m_editor != nullptr, "Multiplayer Editor received On Quit without an Editor pointer."); - if (m_editor) - { - m_editor->UnregisterNotifyListener(this); - m_editor = nullptr; - } - [[fallthrough]]; - case eNotify_OnEndGameMode: - AZ::TickBus::Handler::BusDisconnect(); - // Kill the configured server if it's active - if (m_serverProcess) - { - m_serverProcess->TerminateProcess(0); - m_serverProcess = nullptr; - } - break; } } diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h index 8c18a2e57a..31ecdf83a3 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -34,6 +35,7 @@ namespace Multiplayer class MultiplayerEditorSystemComponent final : public AZ::Component , private AZ::TickBus::Handler + , private AzFramework::GameEntityContextEventBus::Handler , private AzToolsFramework::EditorEvents::Bus::Handler , private IEditorNotifyListener { @@ -66,8 +68,16 @@ namespace Multiplayer void OnTick(float deltaTime, AZ::ScriptTimePoint time) override; int GetTickOrder() override; //! @} - //! + + //! EditorEvents::Handler overrides + //! @{ void OnEditorNotifyEvent(EEditorNotifyEvent event) override; + //! @} + + //! GameEntityContextEventBus::Handler overrides + //! @{ + void OnGameEntitiesStarted() override; + //! @} IEditor* m_editor = nullptr; AzFramework::ProcessWatcher* m_serverProcess = nullptr; diff --git a/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.cpp b/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.cpp index 5a223d6214..a5df3c1dc5 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.cpp @@ -18,32 +18,21 @@ namespace Multiplayer { - //! Multiplayer Tools system component provides serialize context reflection for tools-only systems. - class MultiplayerToolsSystemComponent final - : public AZ::Component + + void MultiplayerToolsSystemComponent::Reflect(AZ::ReflectContext* context) { - public: - AZ_COMPONENT(MultiplayerToolsSystemComponent, "{65AF5342-0ECE-423B-B646-AF55A122F72B}"); + NetworkPrefabProcessor::Reflect(context); + } - static void Reflect(AZ::ReflectContext* context) - { - NetworkPrefabProcessor::Reflect(context); - } + bool MultiplayerToolsSystemComponent::DidProcessNetworkPrefabs() + { + return m_didProcessNetPrefabs; + } - MultiplayerToolsSystemComponent() = default; - ~MultiplayerToolsSystemComponent() override = default; - - /// AZ::Component overrides. - void Activate() override - { - - } - - void Deactivate() override - { - - } - }; + void MultiplayerToolsSystemComponent::SetDidProcessNetworkPrefabs(bool didProcessNetPrefabs) + { + m_didProcessNetPrefabs = didProcessNetPrefabs; + } MultiplayerToolsModule::MultiplayerToolsModule() : AZ::Module() diff --git a/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.h b/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.h index 823bd63a1d..82d0415c5a 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.h @@ -12,10 +12,37 @@ #pragma once +#include #include +#include namespace Multiplayer { + class MultiplayerToolsSystemComponent final + : public AZ::Component + , public IMultiplayerTools + { + public: + AZ_COMPONENT(MultiplayerToolsSystemComponent, "{65AF5342-0ECE-423B-B646-AF55A122F72B}"); + + static void Reflect(AZ::ReflectContext* context); + + MultiplayerToolsSystemComponent() = default; + ~MultiplayerToolsSystemComponent() override = default; + + /// AZ::Component overrides. + void Activate() override {}; + + void Deactivate() override {}; + + bool DidProcessNetworkPrefabs() override; + + private: + void SetDidProcessNetworkPrefabs(bool didProcessNetPrefabs) override; + + bool m_didProcessNetPrefabs = false; + }; + class MultiplayerToolsModule : public AZ::Module { diff --git a/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp b/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp index 2006272135..8528b3d564 100644 --- a/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp +++ b/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,8 @@ namespace Multiplayer void NetworkPrefabProcessor::Process(PrefabProcessorContext& context) { + IMultiplayerTools* mpTools = AZ::Interface::Get(); + mpTools->SetDidProcessNetworkPrefabs(false); context.ListPrefabs([&context](AZStd::string_view prefabName, PrefabDom& prefab) { ProcessPrefab(context, prefabName, prefab); }); diff --git a/Gems/Multiplayer/Code/multiplayer_tools_files.cmake b/Gems/Multiplayer/Code/multiplayer_tools_files.cmake index 1be02fd999..12f12479ba 100644 --- a/Gems/Multiplayer/Code/multiplayer_tools_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_tools_files.cmake @@ -10,6 +10,7 @@ # set(FILES + Include/IMultiplayerTools.h Source/Multiplayer_precompiled.cpp Source/Multiplayer_precompiled.h Source/Pipeline/NetworkPrefabProcessor.cpp From aa51233536a55816324da9d41fff6afe4e3970c9 Mon Sep 17 00:00:00 2001 From: puvvadar Date: Thu, 6 May 2021 16:18:13 -0700 Subject: [PATCH 02/22] Add Asset serialization for Ctrl+G and related net interfaces --- .../AzNetworking/TcpTransport/TcpSocket.cpp | 4 +- .../AutoGen/Multiplayer.AutoPackets.xml | 6 ++ .../MultiplayerEditorSystemComponent.cpp | 93 +++++++++++++++++-- .../Editor/MultiplayerEditorSystemComponent.h | 2 + .../Source/MultiplayerSystemComponent.cpp | 21 +++++ .../Code/Source/MultiplayerSystemComponent.h | 4 +- .../Code/Source/MultiplayerToolsModule.cpp | 10 ++ .../Code/Source/MultiplayerToolsModule.h | 5 +- 8 files changed, 132 insertions(+), 13 deletions(-) diff --git a/Code/Framework/AzNetworking/AzNetworking/TcpTransport/TcpSocket.cpp b/Code/Framework/AzNetworking/AzNetworking/TcpTransport/TcpSocket.cpp index 78020cb09d..e8c30d0816 100644 --- a/Code/Framework/AzNetworking/AzNetworking/TcpTransport/TcpSocket.cpp +++ b/Code/Framework/AzNetworking/AzNetworking/TcpTransport/TcpSocket.cpp @@ -116,8 +116,8 @@ namespace AzNetworking int32_t TcpSocket::Receive(uint8_t* outData, uint32_t size) const { - AZ_Assert(size > 0, "Invalid data size for send"); - AZ_Assert(outData != nullptr, "NULL data pointer passed to send"); + AZ_Assert(size > 0, "Invalid data size for receive"); + AZ_Assert(outData != nullptr, "NULL data pointer passed to receive"); if (!IsOpen()) { return SocketOpResultErrorNotOpen; diff --git a/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml b/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml index 5de466899c..21faefa880 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml @@ -57,4 +57,10 @@ + + + + + + diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp index 0850b858a2..15f00bdf80 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp @@ -10,23 +10,32 @@ * */ +#include #include +#include +#include #include #include +#include #include #include #include #include +#include #include namespace Multiplayer { + static const AZStd::string_view s_networkInterfaceName("MultiplayerEditorServerInterface"); + using namespace AzNetworking; AZ_CVAR(bool, editorsv_enabled, false, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Whether Editor launching a local server to connect to is supported"); AZ_CVAR(AZ::CVarFixedString, editorsv_process, "", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The server executable that should be run. Empty to use the current project's ServerLauncher"); + AZ_CVAR(AZ::CVarFixedString, sv_serveraddr, "127.0.0.1", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The address of the server to connect to"); + AZ_CVAR(uint16_t, sv_port, 30091, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port that the multiplayer editor gem will bind to for traffic"); void MultiplayerEditorSystemComponent::Reflect(AZ::ReflectContext* context) { @@ -61,6 +70,16 @@ namespace Multiplayer { AzFramework::GameEntityContextEventBus::Handler::BusConnect(); AzToolsFramework::EditorEvents::Bus::Handler::BusConnect(); + + // Setup a network interface handled by MultiplayerSystemComponent + if (m_editorNetworkInterface == nullptr) + { + AZ::Entity* systemEntity = this->GetEntity(); + MultiplayerSystemComponent* mpSysComponent = systemEntity->FindComponent(); + + m_editorNetworkInterface = AZ::Interface::Get()->CreateNetworkInterface( + AZ::Name(s_networkInterfaceName), ProtocolType::Tcp, TrustZone::ExternalClientToServer, *mpSysComponent); + } } void MultiplayerEditorSystemComponent::Deactivate() @@ -97,25 +116,52 @@ namespace Multiplayer m_serverProcess->TerminateProcess(0); m_serverProcess = nullptr; } + if (m_editorNetworkInterface) + { + // Disconnect the interface, connection management will clean it up + m_editorNetworkInterface->Disconnect(m_editorConnId, AzNetworking::DisconnectReason::TerminatedByUser); + m_editorConnId = AzNetworking::InvalidConnectionId; + } break; } } void MultiplayerEditorSystemComponent::OnGameEntitiesStarted() { + auto prefabEditorEntityOwnershipInterface = AZ::Interface::Get(); + if (!prefabEditorEntityOwnershipInterface) + { + AZ_Error("MultiplayerEditor", prefabEditorEntityOwnershipInterface != nullptr, "PrefabEditorEntityOwnershipInterface unavailable"); + } + const AZStd::vector>& assetData = prefabEditorEntityOwnershipInterface->GetPlayInEditorAssetData(); + + AZStd::vector buffer; + AZ::IO::ByteContainerStream byteStream(&buffer); + + // Serialize Asset information and AssetData into a potentially large buffer + for (auto asset : assetData) + { + AZ::Data::AssetId assetId = asset.GetId(); + AZ::Data::AssetType assetType = asset.GetType(); + const AZStd::string& assetHint = asset.GetHint(); + AZ::IO::SizeType assetHintSize = assetHint.size(); + AZ::Data::AssetLoadBehavior assetLoadBehavior = asset.GetAutoLoadBehavior(); + + byteStream.Write(sizeof(AZ::Data::AssetId), reinterpret_cast(&assetId)); + byteStream.Write(sizeof(AZ::Data::AssetType), reinterpret_cast(&assetType)); + byteStream.Write(sizeof(assetHintSize), reinterpret_cast(&assetHintSize)); + byteStream.Write(assetHint.size(), assetHint.c_str()); + byteStream.Write(sizeof(AZ::Data::AssetLoadBehavior), reinterpret_cast(&assetLoadBehavior)); + + AZ::Utils::SaveObjectToStream(byteStream, AZ::DataStream::ST_BINARY, asset.GetData(), asset.GetData()->GetType()); + } + // BeginGameMode and Prefab Processing have completed at this point IMultiplayerTools* mpTools = AZ::Interface::Get(); if (editorsv_enabled && mpTools != nullptr && mpTools->DidProcessNetworkPrefabs()) { AZ::TickBus::Handler::BusConnect(); - auto prefabEditorEntityOwnershipInterface = AZ::Interface::Get(); - if (!prefabEditorEntityOwnershipInterface) - { - AZ_Error("MultiplayerEditor", prefabEditorEntityOwnershipInterface != nullptr, "PrefabEditorEntityOwnershipInterface unavailable"); - } - const AZStd::vector>& assetData = prefabEditorEntityOwnershipInterface->GetPlayInEditorAssetData(); - if (assetData.size() > 0) { // Assemble the server's path @@ -154,6 +200,39 @@ namespace Multiplayer processLaunchInfo, AzFramework::ProcessCommunicationType::COMMUNICATOR_TYPE_NONE); } } + + // Now that the server has launched, attempt to connect the NetworkInterface + const AZ::CVarFixedString remoteAddress = sv_serveraddr; + m_editorConnId = m_editorNetworkInterface->Connect( + AzNetworking::IpAddress(remoteAddress.c_str(), sv_port, AzNetworking::ProtocolType::Tcp)); + + // Read the buffer into EditorServerInit packets until we've flushed the whole thing + byteStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN); + + while (byteStream.GetCurPos() < byteStream.GetLength()) + { + MultiplayerPackets::EditorServerInit packet; + AzNetworking::TcpPacketEncodingBuffer& outBuffer = packet.ModifyAssetData(); + + // Size the packet's buffer appropriately + size_t readSize = TcpPacketEncodingBuffer::GetCapacity(); + size_t byteStreamSize = byteStream.GetLength() - byteStream.GetCurPos(); + if (byteStreamSize < readSize) + { + readSize = byteStreamSize; + } + + outBuffer.Resize(readSize); + byteStream.Read(readSize, outBuffer.GetBuffer()); + + // If we've run out of buffer, mark that we're done + if (byteStream.GetCurPos() == byteStream.GetLength()) + { + packet.SetLastUpdate(true); + } + m_editorNetworkInterface->SendReliablePacket(m_editorConnId, packet); + } + } void MultiplayerEditorSystemComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h index 31ecdf83a3..d43d8747b9 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h @@ -81,5 +81,7 @@ namespace Multiplayer IEditor* m_editor = nullptr; AzFramework::ProcessWatcher* m_serverProcess = nullptr; + AzNetworking::ConnectionId m_editorConnId; + AzNetworking::INetworkInterface* m_editorNetworkInterface = nullptr; }; } diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index 03c661ab03..3c1e142d96 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -57,7 +57,9 @@ namespace Multiplayer using namespace AzNetworking; static const AZStd::string_view s_networkInterfaceName("MultiplayerNetworkInterface"); + static const AZStd::string_view s_networkEditorInterfaceName("MultiplayerEditorNetworkInterface"); static constexpr uint16_t DefaultServerPort = 30090; + static constexpr uint16_t DefaultServerEditorPort = 30091; AZ_CVAR(uint16_t, cl_clientport, 0, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port to bind to for game traffic when connecting to a remote host, a value of 0 will select any available port"); AZ_CVAR(AZ::CVarFixedString, cl_serveraddr, "127.0.0.1", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The address of the remote server or host to connect to"); @@ -397,6 +399,19 @@ namespace Multiplayer return false; } + bool MultiplayerSystemComponent::HandleRequest + ( + [[maybe_unused]] AzNetworking::IConnection* connection, + [[maybe_unused]] const IPacketHeader& packetHeader, + [[maybe_unused]] MultiplayerPackets::EditorServerInit& packet + ) + { +#if !defined(_RELEASE) + // Support Editor Server Init for all non-release targets +#endif + return true; + } + ConnectResult MultiplayerSystemComponent::ValidateConnect ( [[maybe_unused]] const IpAddress& remoteAddress, @@ -492,6 +507,12 @@ namespace Multiplayer { if (multiplayerType == MultiplayerAgentType::ClientServer || multiplayerType == MultiplayerAgentType::DedicatedServer) { +#if !defined(_RELEASE) + m_networkEditorInterface = AZ::Interface::Get()->CreateNetworkInterface( + AZ::Name(s_networkEditorInterfaceName), ProtocolType::Tcp, TrustZone::ExternalClientToServer, *this); + m_networkEditorInterface->Listen(DefaultServerEditorPort); +#endif + m_initEvent.Signal(m_networkInterface); const AZ::Aabb worldBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-16384.0f), AZ::Vector3(16384.0f)); diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h index f25e530b61..e77fa129b0 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h @@ -72,7 +72,8 @@ namespace Multiplayer bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerPackets::NotifyClientMigration& packet); bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerPackets::EntityMigration& packet); bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerPackets::ReadyForEntityUpdates& packet); - + bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerPackets::EditorServerInit& packet); + //! IConnectionListener interface //! @{ AzNetworking::ConnectResult ValidateConnect(const AzNetworking::IpAddress& remoteAddress, const AzNetworking::IPacketHeader& packetHeader, AzNetworking::ISerializer& serializer) override; @@ -109,6 +110,7 @@ namespace Multiplayer AZ_CONSOLEFUNC(MultiplayerSystemComponent, DumpStats, AZ::ConsoleFunctorFlags::Null, "Dumps stats for the current multiplayer session"); AzNetworking::INetworkInterface* m_networkInterface = nullptr; + AzNetworking::INetworkInterface* m_networkEditorInterface = nullptr; AZ::ConsoleCommandInvokedEvent::Handler m_consoleCommandHandler; AZ::ThreadSafeDeque m_cvarCommands; diff --git a/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.cpp b/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.cpp index a5df3c1dc5..ae91999a0c 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.cpp @@ -24,6 +24,16 @@ namespace Multiplayer NetworkPrefabProcessor::Reflect(context); } + void MultiplayerToolsSystemComponent::Activate() + { + AZ::Interface::Register(this); + } + + void MultiplayerToolsSystemComponent::Deactivate() + { + AZ::Interface::Unregister(this); + } + bool MultiplayerToolsSystemComponent::DidProcessNetworkPrefabs() { return m_didProcessNetPrefabs; diff --git a/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.h b/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.h index 82d0415c5a..181a971150 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.h @@ -31,9 +31,8 @@ namespace Multiplayer ~MultiplayerToolsSystemComponent() override = default; /// AZ::Component overrides. - void Activate() override {}; - - void Deactivate() override {}; + void Activate() override; + void Deactivate() override; bool DidProcessNetworkPrefabs() override; From 8a39f9f1b474640b4b525423b0ec3fa9c32c187e Mon Sep 17 00:00:00 2001 From: puvvadar Date: Thu, 13 May 2021 15:31:37 -0700 Subject: [PATCH 03/22] Streamline MP Ctrl+G logic via MultiplayerEditorConnection --- .../AzNetworking/TcpTransport/TcpSocket.cpp | 2 +- .../UdpTransport/UdpNetworkInterface.cpp | 2 +- .../AzNetworking/UdpTransport/UdpSocket.cpp | 2 +- Gems/Multiplayer/Code/CMakeLists.txt | 25 +-- .../AutoGen/Multiplayer.AutoPackets.xml | 7 +- .../AutoGen/MultiplayerEditor.AutoPackets.xml | 14 ++ .../Editor/MultiplayerEditorConnection.cpp | 162 ++++++++++++++ .../Editor/MultiplayerEditorConnection.h | 55 +++++ .../Editor/MultiplayerEditorDispatcher.cpp | 21 -- .../Editor/MultiplayerEditorDispatcher.h | 36 ---- .../{ => Editor}/MultiplayerEditorGem.cpp | 2 +- .../{ => Editor}/MultiplayerEditorGem.h | 0 .../MultiplayerEditorSystemComponent.cpp | 199 ++++++++---------- .../Editor/MultiplayerEditorSystemComponent.h | 13 +- .../Source/MultiplayerSystemComponent.cpp | 42 ++-- .../Code/Source/MultiplayerSystemComponent.h | 8 +- .../Pipeline/NetworkPrefabProcessor.cpp | 4 + .../Code/multiplayer_editor_files.cmake | 2 - .../multiplayer_editor_shared_files.cmake | 4 +- Gems/Multiplayer/Code/multiplayer_files.cmake | 3 + 20 files changed, 359 insertions(+), 244 deletions(-) create mode 100644 Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml create mode 100644 Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp create mode 100644 Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h delete mode 100644 Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorDispatcher.cpp delete mode 100644 Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorDispatcher.h rename Gems/Multiplayer/Code/Source/{ => Editor}/MultiplayerEditorGem.cpp (97%) rename Gems/Multiplayer/Code/Source/{ => Editor}/MultiplayerEditorGem.h (100%) diff --git a/Code/Framework/AzNetworking/AzNetworking/TcpTransport/TcpSocket.cpp b/Code/Framework/AzNetworking/AzNetworking/TcpTransport/TcpSocket.cpp index e8c30d0816..70000dc9a8 100644 --- a/Code/Framework/AzNetworking/AzNetworking/TcpTransport/TcpSocket.cpp +++ b/Code/Framework/AzNetworking/AzNetworking/TcpTransport/TcpSocket.cpp @@ -176,7 +176,7 @@ namespace AzNetworking if (::bind(aznumeric_cast(m_socketFd), (const sockaddr*)&hints, sizeof(hints)) != 0) { const int32_t error = GetLastNetworkError(); - AZLOG_ERROR("Failed to bind socket (%d:%s)", error, GetNetworkErrorDesc(error)); + AZLOG_ERROR("Failed to bind TCP socket to port %u (%d:%s)", uint32_t(port), error, GetNetworkErrorDesc(error)); return false; } diff --git a/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpNetworkInterface.cpp b/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpNetworkInterface.cpp index 870545e6c8..5676d48150 100644 --- a/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpNetworkInterface.cpp +++ b/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpNetworkInterface.cpp @@ -162,7 +162,7 @@ namespace AzNetworking const UdpReaderThread::ReceivedPackets* packets = m_readerThread.GetReceivedPackets(m_socket.get()); if (packets == nullptr) { - AZ_Assert(false, "nullptr was retrieved for the received packet buffer, check that the socket has been registered with the reader thread"); + // Socket is not yet registered with the reader thread and is likely still pending, try again later return; } diff --git a/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpSocket.cpp b/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpSocket.cpp index e642c87623..cbb5f8e6c0 100644 --- a/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpSocket.cpp +++ b/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpSocket.cpp @@ -82,7 +82,7 @@ namespace AzNetworking if (::bind(static_cast(m_socketFd), (const sockaddr *)&hints, sizeof(hints)) != 0) { const int32_t error = GetLastNetworkError(); - AZLOG_ERROR("Failed to bind socket to port %u (%d:%s)", uint32_t(port), error, GetNetworkErrorDesc(error)); + AZLOG_ERROR("Failed to bind UDP socket to port %u (%d:%s)", uint32_t(port), error, GetNetworkErrorDesc(error)); return false; } } diff --git a/Gems/Multiplayer/Code/CMakeLists.txt b/Gems/Multiplayer/Code/CMakeLists.txt index 46f56ef315..3f0c2936b7 100644 --- a/Gems/Multiplayer/Code/CMakeLists.txt +++ b/Gems/Multiplayer/Code/CMakeLists.txt @@ -77,12 +77,12 @@ if (PAL_TRAIT_BUILD_HOST_TOOLS) AZ::AzToolsFramework Gem::Multiplayer.Static ) - + ly_add_target( - NAME Multiplayer.Editor.Static STATIC + NAME Multiplayer.Editor GEM_MODULE NAMESPACE Gem FILES_CMAKE - multiplayer_editor_files.cmake + multiplayer_editor_shared_files.cmake COMPILE_DEFINITIONS PUBLIC MULTIPLAYER_EDITOR @@ -94,7 +94,7 @@ if (PAL_TRAIT_BUILD_HOST_TOOLS) PUBLIC Include BUILD_DEPENDENCIES - PUBLIC + PRIVATE Legacy::CryCommon Legacy::Editor.Headers AZ::AzCore @@ -102,23 +102,6 @@ if (PAL_TRAIT_BUILD_HOST_TOOLS) AZ::AzNetworking AZ::AzToolsFramework Gem::Multiplayer.Static - ) - - ly_add_target( - NAME Multiplayer.Editor GEM_MODULE - NAMESPACE Gem - FILES_CMAKE - multiplayer_editor_shared_files.cmake - INCLUDE_DIRECTORIES - PRIVATE - . - Source - ${pal_source_dir} - PUBLIC - Include - BUILD_DEPENDENCIES - PRIVATE - Gem::Multiplayer.Editor.Static Gem::Multiplayer.Tools ) diff --git a/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml b/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml index 84a7207f0c..633218c215 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml @@ -59,10 +59,5 @@ - - - - - - + diff --git a/Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml b/Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml new file mode 100644 index 0000000000..986860e822 --- /dev/null +++ b/Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp new file mode 100644 index 0000000000..dcf9c134da --- /dev/null +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp @@ -0,0 +1,162 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Multiplayer +{ + using namespace AzNetworking; + + static const AZStd::string_view s_networkInterfaceName("MultiplayerNetworkInterface"); + static const AZStd::string_view s_networkEditorInterfaceName("MultiplayerEditorNetworkInterface"); + static constexpr AZStd::string_view DefaultEditorIp = "127.0.0.1"; + static constexpr uint16_t DefaultServerPort = 30090; + static constexpr uint16_t DefaultServerEditorPort = 30091; + + static AZStd::vector buffer; + static AZ::IO::ByteContainerStream> s_byteStream(&buffer); + + AZ_CVAR(bool, editorsv_isDedicated, false, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Whether to init as a server expecting data from an Editor. Do not modify unless you're sure of what you're doing."); + + MultiplayerEditorConnection::MultiplayerEditorConnection() + { + m_networkEditorInterface = AZ::Interface::Get()->CreateNetworkInterface( + AZ::Name(s_networkEditorInterfaceName), ProtocolType::Tcp, TrustZone::ExternalClientToServer, *this); + if (editorsv_isDedicated) + { + m_networkEditorInterface->Listen(DefaultServerEditorPort); + } + } + + bool MultiplayerEditorConnection::HandleRequest + ( + [[maybe_unused]] AzNetworking::IConnection* connection, + [[maybe_unused]] const IPacketHeader& packetHeader, + [[maybe_unused]] MultiplayerEditorPackets::EditorServerInit& packet + ) + { + // Editor Server Init is intended for non-release targets + if (!packet.GetLastUpdate()) + { + // More packets are expected, flush this to the buffer + s_byteStream.Write(TcpPacketEncodingBuffer::GetCapacity(), reinterpret_cast(packet.ModifyAssetData().GetBuffer())); + } + else + { + // This is the last expected packet, flush it to the buffer + s_byteStream.Write(packet.GetAssetData().GetSize(), reinterpret_cast(packet.ModifyAssetData().GetBuffer())); + + // Read all assets out of the buffer + s_byteStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN); + AZStd::vector> assetData; + while (s_byteStream.GetCurPos() < s_byteStream.GetLength()) + { + AZ::Data::AssetLoadBehavior assetLoadBehavior; + s_byteStream.Read(sizeof(AZ::Data::AssetLoadBehavior), reinterpret_cast(&assetLoadBehavior)); + + AZ::Data::AssetData* assetDatum = AZ::Utils::LoadObjectFromStream(s_byteStream, nullptr); + AZ::Data::Asset asset = AZ::Data::Asset(assetDatum, assetLoadBehavior); + + /* + // Register Asset to AssetManager + */ + + assetData.push_back(asset); + } + + // Now that we've deserialized, clear the byte stream + s_byteStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN); + s_byteStream.Truncate(); + + /* + // Hand-off our resultant assets + */ + + AZLOG_INFO("Editor Server completed asset receive, responding to Editor..."); + if (connection->SendReliablePacket(MultiplayerEditorPackets::EditorServerReady())) + { + // Setup the normal multiplayer connection + AZ::Interface::Get()->InitializeMultiplayer(MultiplayerAgentType::DedicatedServer); + INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(s_networkInterfaceName)); + networkInterface->Listen(DefaultServerPort); + + return true; + } + else + { + return false; + } + } + + return true; + } + + bool MultiplayerEditorConnection::HandleRequest + ( + [[maybe_unused]] AzNetworking::IConnection* connection, + [[maybe_unused]] const IPacketHeader& packetHeader, + [[maybe_unused]] MultiplayerEditorPackets::EditorServerReady& packet + ) + { + if (connection->GetConnectionRole() == ConnectionRole::Connector) + { + // Receiving this packet means Editor sync is done, disconnect + connection->Disconnect(AzNetworking::DisconnectReason::TerminatedByClient, AzNetworking::TerminationEndpoint::Local); + + // Connect the Editor to the local server for Multiplayer simulation + AZ::Interface::Get()->InitializeMultiplayer(MultiplayerAgentType::Client); + INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(s_networkInterfaceName)); + const IpAddress ipAddress(DefaultEditorIp.data(), DefaultServerEditorPort, networkInterface->GetType()); + networkInterface->Connect(ipAddress); + } + return true; + } + + ConnectResult MultiplayerEditorConnection::ValidateConnect + ( + [[maybe_unused]] const IpAddress& remoteAddress, + [[maybe_unused]] const IPacketHeader& packetHeader, + [[maybe_unused]] ISerializer& serializer + ) + { + return ConnectResult::Accepted; + } + + void MultiplayerEditorConnection::OnConnect([[maybe_unused]] AzNetworking::IConnection* connection) + { + ; + } + + bool MultiplayerEditorConnection::OnPacketReceived(AzNetworking::IConnection* connection, const IPacketHeader& packetHeader, ISerializer& serializer) + { + return MultiplayerEditorPackets::DispatchPacket(connection, packetHeader, serializer, *this); + } + + void MultiplayerEditorConnection::OnPacketLost([[maybe_unused]] IConnection* connection, [[maybe_unused]] PacketId packetId) + { + ; + } + + void MultiplayerEditorConnection::OnDisconnect([[maybe_unused]] AzNetworking::IConnection* connection, [[maybe_unused]] DisconnectReason reason, [[maybe_unused]] TerminationEndpoint endpoint) + { + ; + } +} diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h new file mode 100644 index 0000000000..3621e3aee6 --- /dev/null +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h @@ -0,0 +1,55 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AzNetworking +{ + class INetworkInterface; +} + +namespace Multiplayer +{ + //! MultiplayerEditorConnection is a connection listener to synchronize the Editor and a local server it launches + class MultiplayerEditorConnection final + : public AzNetworking::IConnectionListener + { + public: + MultiplayerEditorConnection(); + ~MultiplayerEditorConnection() = default; + + bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerEditorPackets::EditorServerInit& packet); + bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerEditorPackets::EditorServerReady& packet); + + //! IConnectionListener interface + //! @{ + AzNetworking::ConnectResult ValidateConnect(const AzNetworking::IpAddress& remoteAddress, const AzNetworking::IPacketHeader& packetHeader, AzNetworking::ISerializer& serializer) override; + void OnConnect(AzNetworking::IConnection* connection) override; + bool OnPacketReceived(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, AzNetworking::ISerializer& serializer) override; + void OnPacketLost(AzNetworking::IConnection* connection, AzNetworking::PacketId packetId) override; + void OnDisconnect(AzNetworking::IConnection* connection, AzNetworking::DisconnectReason reason, AzNetworking::TerminationEndpoint endpoint) override; + //! @} + + private: + + AzNetworking::INetworkInterface* m_networkEditorInterface = nullptr; + }; +} diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorDispatcher.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorDispatcher.cpp deleted file mode 100644 index 470aa61cd0..0000000000 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorDispatcher.cpp +++ /dev/null @@ -1,21 +0,0 @@ -/* - * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or - * its licensors. - * - * For complete copyright and license terms please see the LICENSE at the root of this - * distribution (the "License"). All use of this software is governed by the License, - * or, if provided, by the license below or the license accompanying this file. Do not - * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * - */ - -#include - -namespace Multiplayer -{ - MultiplayerEditorDispatcher::MultiplayerEditorDispatcher() - { - ; - } -} diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorDispatcher.h b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorDispatcher.h deleted file mode 100644 index c1058dc8a0..0000000000 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorDispatcher.h +++ /dev/null @@ -1,36 +0,0 @@ -/* -* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or -* its licensors. -* -* For complete copyright and license terms please see the LICENSE at the root of this -* distribution (the "License"). All use of this software is governed by the License, -* or, if provided, by the license below or the license accompanying this file. Do not -* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* -*/ - -#pragma once - -#include - -#include -#include -#include -#include - -#include - - -namespace Multiplayer -{ - //! MultiplayerEditorDispatcher is responsible for dispatching delta from the Editor to an Editor launched local server - class MultiplayerEditorDispatcher final - { - public: - MultiplayerEditorDispatcher(); - ~MultiplayerEditorDispatcher() = default; - - private: - }; -} diff --git a/Gems/Multiplayer/Code/Source/MultiplayerEditorGem.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorGem.cpp similarity index 97% rename from Gems/Multiplayer/Code/Source/MultiplayerEditorGem.cpp rename to Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorGem.cpp index 97c5e4d105..ae38ef1d6d 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerEditorGem.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorGem.cpp @@ -13,7 +13,7 @@ #include #include #include -#include +#include #include #include diff --git a/Gems/Multiplayer/Code/Source/MultiplayerEditorGem.h b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorGem.h similarity index 100% rename from Gems/Multiplayer/Code/Source/MultiplayerEditorGem.h rename to Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorGem.h diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp index 15f00bdf80..4c3e8200a1 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp @@ -26,16 +26,16 @@ namespace Multiplayer { - static const AZStd::string_view s_networkInterfaceName("MultiplayerEditorServerInterface"); + static const AZStd::string_view s_networkEditorInterfaceName("MultiplayerEditorNetworkInterface"); using namespace AzNetworking; - AZ_CVAR(bool, editorsv_enabled, false, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, + AZ_CVAR(bool, editorsv_enabled, true, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Whether Editor launching a local server to connect to is supported"); AZ_CVAR(AZ::CVarFixedString, editorsv_process, "", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The server executable that should be run. Empty to use the current project's ServerLauncher"); - AZ_CVAR(AZ::CVarFixedString, sv_serveraddr, "127.0.0.1", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The address of the server to connect to"); - AZ_CVAR(uint16_t, sv_port, 30091, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port that the multiplayer editor gem will bind to for traffic"); + AZ_CVAR(AZ::CVarFixedString, editorsv_serveraddr, "127.0.0.1", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The address of the server to connect to"); + AZ_CVAR(uint16_t, editorsv_port, 30091, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port that the multiplayer editor gem will bind to for traffic"); void MultiplayerEditorSystemComponent::Reflect(AZ::ReflectContext* context) { @@ -70,16 +70,6 @@ namespace Multiplayer { AzFramework::GameEntityContextEventBus::Handler::BusConnect(); AzToolsFramework::EditorEvents::Bus::Handler::BusConnect(); - - // Setup a network interface handled by MultiplayerSystemComponent - if (m_editorNetworkInterface == nullptr) - { - AZ::Entity* systemEntity = this->GetEntity(); - MultiplayerSystemComponent* mpSysComponent = systemEntity->FindComponent(); - - m_editorNetworkInterface = AZ::Interface::Get()->CreateNetworkInterface( - AZ::Name(s_networkInterfaceName), ProtocolType::Tcp, TrustZone::ExternalClientToServer, *mpSysComponent); - } } void MultiplayerEditorSystemComponent::Deactivate() @@ -109,18 +99,16 @@ namespace Multiplayer } [[fallthrough]]; case eNotify_OnEndGameMode: - AZ::TickBus::Handler::BusDisconnect(); // Kill the configured server if it's active if (m_serverProcess) { m_serverProcess->TerminateProcess(0); m_serverProcess = nullptr; } - if (m_editorNetworkInterface) + INetworkInterface* editorNetworkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(s_networkEditorInterfaceName)); + if (editorNetworkInterface) { - // Disconnect the interface, connection management will clean it up - m_editorNetworkInterface->Disconnect(m_editorConnId, AzNetworking::DisconnectReason::TerminatedByUser); - m_editorConnId = AzNetworking::InvalidConnectionId; + editorNetworkInterface->Disconnect(m_editorConnId, AzNetworking::DisconnectReason::TerminatedByClient); } break; } @@ -133,116 +121,95 @@ namespace Multiplayer { AZ_Error("MultiplayerEditor", prefabEditorEntityOwnershipInterface != nullptr, "PrefabEditorEntityOwnershipInterface unavailable"); } - const AZStd::vector>& assetData = prefabEditorEntityOwnershipInterface->GetPlayInEditorAssetData(); - - AZStd::vector buffer; - AZ::IO::ByteContainerStream byteStream(&buffer); - - // Serialize Asset information and AssetData into a potentially large buffer - for (auto asset : assetData) - { - AZ::Data::AssetId assetId = asset.GetId(); - AZ::Data::AssetType assetType = asset.GetType(); - const AZStd::string& assetHint = asset.GetHint(); - AZ::IO::SizeType assetHintSize = assetHint.size(); - AZ::Data::AssetLoadBehavior assetLoadBehavior = asset.GetAutoLoadBehavior(); - - byteStream.Write(sizeof(AZ::Data::AssetId), reinterpret_cast(&assetId)); - byteStream.Write(sizeof(AZ::Data::AssetType), reinterpret_cast(&assetType)); - byteStream.Write(sizeof(assetHintSize), reinterpret_cast(&assetHintSize)); - byteStream.Write(assetHint.size(), assetHint.c_str()); - byteStream.Write(sizeof(AZ::Data::AssetLoadBehavior), reinterpret_cast(&assetLoadBehavior)); - - AZ::Utils::SaveObjectToStream(byteStream, AZ::DataStream::ST_BINARY, asset.GetData(), asset.GetData()->GetType()); - } // BeginGameMode and Prefab Processing have completed at this point IMultiplayerTools* mpTools = AZ::Interface::Get(); if (editorsv_enabled && mpTools != nullptr && mpTools->DidProcessNetworkPrefabs()) { - AZ::TickBus::Handler::BusConnect(); - - if (assetData.size() > 0) - { - // Assemble the server's path - AZ::CVarFixedString serverProcess = editorsv_process; - if (serverProcess.empty()) - { - // If enabled but no process name is supplied, try this project's ServerLauncher - serverProcess = AZ::Utils::GetProjectName() + ".ServerLauncher"; - } - - AZ::IO::FixedMaxPathString serverPath = AZ::Utils::GetExecutableDirectory(); - if (!serverProcess.contains(AZ_TRAIT_OS_PATH_SEPARATOR)) - { - // If only the process name is specified, append that as well - serverPath.append(AZ_TRAIT_OS_PATH_SEPARATOR + serverProcess); - } - else - { - // If any path was already specified, then simply assign - serverPath = serverProcess; - } - - if (!serverProcess.ends_with(AZ_TRAIT_OS_EXECUTABLE_EXTENSION)) - { - // Add this platform's exe extension if it's not specified - serverPath.append(AZ_TRAIT_OS_EXECUTABLE_EXTENSION); - } - - // Start the configured server if it's available - AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo; - processLaunchInfo.m_commandlineParameters = AZStd::string::format("\"%s\"", serverPath.c_str()); - processLaunchInfo.m_showWindow = true; - processLaunchInfo.m_processPriority = AzFramework::ProcessPriority::PROCESSPRIORITY_NORMAL; - - m_serverProcess = AzFramework::ProcessWatcher::LaunchProcess( - processLaunchInfo, AzFramework::ProcessCommunicationType::COMMUNICATOR_TYPE_NONE); - } - } - - // Now that the server has launched, attempt to connect the NetworkInterface - const AZ::CVarFixedString remoteAddress = sv_serveraddr; - m_editorConnId = m_editorNetworkInterface->Connect( - AzNetworking::IpAddress(remoteAddress.c_str(), sv_port, AzNetworking::ProtocolType::Tcp)); - - // Read the buffer into EditorServerInit packets until we've flushed the whole thing - byteStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN); + const AZStd::vector>& assetData = prefabEditorEntityOwnershipInterface->GetPlayInEditorAssetData(); - while (byteStream.GetCurPos() < byteStream.GetLength()) - { - MultiplayerPackets::EditorServerInit packet; - AzNetworking::TcpPacketEncodingBuffer& outBuffer = packet.ModifyAssetData(); + AZStd::vector buffer; + AZ::IO::ByteContainerStream byteStream(&buffer); - // Size the packet's buffer appropriately - size_t readSize = TcpPacketEncodingBuffer::GetCapacity(); - size_t byteStreamSize = byteStream.GetLength() - byteStream.GetCurPos(); - if (byteStreamSize < readSize) + // Serialize Asset information and AssetData into a potentially large buffer + for (auto asset : assetData) { - readSize = byteStreamSize; + AZ::Data::AssetLoadBehavior assetLoadBehavior = asset.GetAutoLoadBehavior(); + byteStream.Write(sizeof(AZ::Data::AssetLoadBehavior), reinterpret_cast(&assetLoadBehavior)); + + AZ::Utils::SaveObjectToStream(byteStream, AZ::DataStream::ST_BINARY, asset.GetData(), asset.GetData()->GetType()); } - outBuffer.Resize(readSize); - byteStream.Read(readSize, outBuffer.GetBuffer()); - - // If we've run out of buffer, mark that we're done - if (byteStream.GetCurPos() == byteStream.GetLength()) + // Assemble the server's path + AZ::CVarFixedString serverProcess = editorsv_process; + if (serverProcess.empty()) { - packet.SetLastUpdate(true); + // If enabled but no process name is supplied, try this project's ServerLauncher + serverProcess = AZ::Utils::GetProjectName() + ".ServerLauncher"; + } + + AZ::IO::FixedMaxPathString serverPath = AZ::Utils::GetExecutableDirectory(); + if (!serverProcess.contains(AZ_TRAIT_OS_PATH_SEPARATOR)) + { + // If only the process name is specified, append that as well + serverPath.append(AZ_TRAIT_OS_PATH_SEPARATOR + serverProcess); + } + else + { + // If any path was already specified, then simply assign + serverPath = serverProcess; + } + + if (!serverProcess.ends_with(AZ_TRAIT_OS_EXECUTABLE_EXTENSION)) + { + // Add this platform's exe extension if it's not specified + serverPath.append(AZ_TRAIT_OS_EXECUTABLE_EXTENSION); + } + + // Start the configured server if it's available + AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo; + processLaunchInfo.m_commandlineParameters = AZStd::string::format("\"%s\" --editorsv_isDedicated true", serverPath.c_str()); + processLaunchInfo.m_showWindow = true; + processLaunchInfo.m_processPriority = AzFramework::ProcessPriority::PROCESSPRIORITY_NORMAL; + + // Launch the Server and give it a few seconds to boot up + m_serverProcess = AzFramework::ProcessWatcher::LaunchProcess( + processLaunchInfo, AzFramework::ProcessCommunicationType::COMMUNICATOR_TYPE_NONE); + AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(15000)); + + // Now that the server has launched, attempt to connect the NetworkInterface + const AZ::CVarFixedString remoteAddress = editorsv_serveraddr; + INetworkInterface* editorNetworkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(s_networkEditorInterfaceName)); + m_editorConnId = editorNetworkInterface->Connect( + AzNetworking::IpAddress(remoteAddress.c_str(), editorsv_port, AzNetworking::ProtocolType::Tcp)); + + // Read the buffer into EditorServerInit packets until we've flushed the whole thing + byteStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN); + + while (byteStream.GetCurPos() < byteStream.GetLength()) + { + MultiplayerEditorPackets::EditorServerInit packet; + AzNetworking::TcpPacketEncodingBuffer& outBuffer = packet.ModifyAssetData(); + + // Size the packet's buffer appropriately + size_t readSize = TcpPacketEncodingBuffer::GetCapacity(); + size_t byteStreamSize = byteStream.GetLength() - byteStream.GetCurPos(); + if (byteStreamSize < readSize) + { + readSize = byteStreamSize; + } + + outBuffer.Resize(readSize); + byteStream.Read(readSize, outBuffer.GetBuffer()); + + // If we've run out of buffer, mark that we're done + if (byteStream.GetCurPos() == byteStream.GetLength()) + { + packet.SetLastUpdate(true); + } + editorNetworkInterface->SendReliablePacket(m_editorConnId, packet); } - m_editorNetworkInterface->SendReliablePacket(m_editorConnId, packet); } } - - void MultiplayerEditorSystemComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) - { - - } - - int MultiplayerEditorSystemComponent::GetTickOrder() - { - // Tick immediately after the network system component - return AZ::TICK_PLACEMENT + 1; - } } diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h index d43d8747b9..569092e981 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h @@ -14,6 +14,8 @@ #include +#include + #include #include #include @@ -34,7 +36,6 @@ namespace Multiplayer //! Multiplayer system component wraps the bridging logic between the game and transport layer. class MultiplayerEditorSystemComponent final : public AZ::Component - , private AZ::TickBus::Handler , private AzFramework::GameEntityContextEventBus::Handler , private AzToolsFramework::EditorEvents::Bus::Handler , private IEditorNotifyListener @@ -61,14 +62,7 @@ namespace Multiplayer void NotifyRegisterViews() override; //! @} - private: - - //! AZ::TickBus::Handler overrides. - //! @{ - void OnTick(float deltaTime, AZ::ScriptTimePoint time) override; - int GetTickOrder() override; - //! @} - + private: //! EditorEvents::Handler overrides //! @{ void OnEditorNotifyEvent(EEditorNotifyEvent event) override; @@ -82,6 +76,5 @@ namespace Multiplayer IEditor* m_editor = nullptr; AzFramework::ProcessWatcher* m_serverProcess = nullptr; AzNetworking::ConnectionId m_editorConnId; - AzNetworking::INetworkInterface* m_editorNetworkInterface = nullptr; }; } diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index 5dfdd86607..e277b394ff 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -23,6 +23,9 @@ #include #include #include +#include +#include +#include namespace AZ::ConsoleTypeHelpers { @@ -60,6 +63,8 @@ namespace Multiplayer static const AZStd::string_view s_networkEditorInterfaceName("MultiplayerEditorNetworkInterface"); static constexpr uint16_t DefaultServerPort = 30090; static constexpr uint16_t DefaultServerEditorPort = 30091; + //static AZStd::vector buffer; + //static AZ::IO::ByteContainerStream> s_byteStream(&buffer); AZ_CVAR(uint16_t, cl_clientport, 0, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port to bind to for game traffic when connecting to a remote host, a value of 0 will select any available port"); AZ_CVAR(AZ::CVarFixedString, cl_serveraddr, "127.0.0.1", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The address of the remote server or host to connect to"); @@ -107,7 +112,7 @@ namespace Multiplayer AZ::ConsoleInvokedFrom invokedFrom ) { OnConsoleCommandInvoked(command, args, flags, invokedFrom); }) { - ; + } void MultiplayerSystemComponent::Activate() @@ -401,19 +406,6 @@ namespace Multiplayer return false; } - bool MultiplayerSystemComponent::HandleRequest - ( - [[maybe_unused]] AzNetworking::IConnection* connection, - [[maybe_unused]] const IPacketHeader& packetHeader, - [[maybe_unused]] MultiplayerPackets::EditorServerInit& packet - ) - { -#if !defined(_RELEASE) - // Support Editor Server Init for all non-release targets -#endif - return true; - } - ConnectResult MultiplayerSystemComponent::ValidateConnect ( [[maybe_unused]] const IpAddress& remoteAddress, @@ -447,13 +439,19 @@ namespace Multiplayer // TODO: This needs to be set to the players autonomous proxy ------------v NetworkEntityHandle controlledEntity = GetNetworkEntityTracker()->Get(NetEntityId{ 0 }); - if (connection->GetUserData() == nullptr) // Only add user data if the connect event handler has not already done so + if (controlledEntity.GetEntity() != nullptr) { - connection->SetUserData(new ServerToClientConnectionData(connection, *this, controlledEntity)); - } + if (connection->GetUserData() == nullptr) // Only add user data if the connect event handler has not already done so + { + connection->SetUserData(new ServerToClientConnectionData(connection, *this, controlledEntity)); + } - AZStd::unique_ptr window = AZStd::make_unique(controlledEntity, connection); - reinterpret_cast(connection->GetUserData())->GetReplicationManager().SetReplicationWindow(AZStd::move(window)); + AZStd::unique_ptr window = + AZStd::make_unique(controlledEntity, connection); + reinterpret_cast(connection->GetUserData()) + ->GetReplicationManager() + .SetReplicationWindow(AZStd::move(window)); + } } else { @@ -509,12 +507,6 @@ namespace Multiplayer { if (multiplayerType == MultiplayerAgentType::ClientServer || multiplayerType == MultiplayerAgentType::DedicatedServer) { -#if !defined(_RELEASE) - m_networkEditorInterface = AZ::Interface::Get()->CreateNetworkInterface( - AZ::Name(s_networkEditorInterfaceName), ProtocolType::Tcp, TrustZone::ExternalClientToServer, *this); - m_networkEditorInterface->Listen(DefaultServerEditorPort); -#endif - m_initEvent.Signal(m_networkInterface); const AZ::Aabb worldBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-16384.0f), AZ::Vector3(16384.0f)); diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h index af5791e5aa..c49e5367e6 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h @@ -16,10 +16,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -72,7 +74,7 @@ namespace Multiplayer bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerPackets::NotifyClientMigration& packet); bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerPackets::EntityMigration& packet); bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerPackets::ReadyForEntityUpdates& packet); - bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerPackets::EditorServerInit& packet); + //bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerPackets::EditorServerInit& packet); //! IConnectionListener interface //! @{ @@ -125,5 +127,9 @@ namespace Multiplayer AZ::TimeMs m_lastReplicatedHostTimeMs = AZ::TimeMs{ 0 }; HostFrameId m_lastReplicatedHostFrameId = InvalidHostFrameId; + +#if !defined(_RELEASE) + MultiplayerEditorConnection m_editorConnectionListener; +#endif }; } diff --git a/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp b/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp index c2b0c07976..bc6e5710fc 100644 --- a/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp +++ b/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp @@ -35,6 +35,10 @@ namespace Multiplayer context.ListPrefabs([&context](AZStd::string_view prefabName, PrefabDom& prefab) { ProcessPrefab(context, prefabName, prefab); }); + if (context.GetProcessedObjects().size() > 0) + { + mpTools->SetDidProcessNetworkPrefabs(true); + } } void NetworkPrefabProcessor::Reflect(AZ::ReflectContext* context) diff --git a/Gems/Multiplayer/Code/multiplayer_editor_files.cmake b/Gems/Multiplayer/Code/multiplayer_editor_files.cmake index ce3e3227e0..5714be5dfb 100644 --- a/Gems/Multiplayer/Code/multiplayer_editor_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_editor_files.cmake @@ -10,6 +10,4 @@ # set(FILES - Source/Editor/MultiplayerEditorDispatcher.cpp - Source/Editor/MultiplayerEditorDispatcher.h ) diff --git a/Gems/Multiplayer/Code/multiplayer_editor_shared_files.cmake b/Gems/Multiplayer/Code/multiplayer_editor_shared_files.cmake index 2d5611d4b6..3fb76061b8 100644 --- a/Gems/Multiplayer/Code/multiplayer_editor_shared_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_editor_shared_files.cmake @@ -12,8 +12,8 @@ set(FILES Source/MultiplayerGem.cpp Source/MultiplayerGem.h - Source/MultiplayerEditorGem.cpp - Source/MultiplayerEditorGem.h + Source/Editor/MultiplayerEditorGem.cpp + Source/Editor/MultiplayerEditorGem.h Source/Editor/MultiplayerEditorSystemComponent.cpp Source/Editor/MultiplayerEditorSystemComponent.h ) diff --git a/Gems/Multiplayer/Code/multiplayer_files.cmake b/Gems/Multiplayer/Code/multiplayer_files.cmake index 1f4e57ae43..b8fd842426 100644 --- a/Gems/Multiplayer/Code/multiplayer_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_files.cmake @@ -33,6 +33,7 @@ set(FILES Source/AutoGen/AutoComponentTypes_Source.jinja Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml Source/AutoGen/Multiplayer.AutoPackets.xml + Source/AutoGen/MultiplayerEditor.AutoPackets.xml Source/AutoGen/NetworkTransformComponent.AutoComponent.xml Source/Components/LocalPredictionPlayerInputComponent.cpp Source/Components/LocalPredictionPlayerInputComponent.h @@ -52,6 +53,8 @@ set(FILES Source/ConnectionData/ServerToClientConnectionData.cpp Source/ConnectionData/ServerToClientConnectionData.h Source/ConnectionData/ServerToClientConnectionData.inl + Source/Editor/MultiplayerEditorConnection.cpp + Source/Editor/MultiplayerEditorConnection.h Source/EntityDomains/FullOwnershipEntityDomain.cpp Source/EntityDomains/FullOwnershipEntityDomain.h Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp From 467caa61759b12426ba8bbd969b4c89aed21c5a1 Mon Sep 17 00:00:00 2001 From: puvvadar Date: Thu, 13 May 2021 15:42:21 -0700 Subject: [PATCH 04/22] Some whitespace and comment cleanup --- Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp | 2 +- Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index e277b394ff..867a9d3c2e 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -112,7 +112,7 @@ namespace Multiplayer AZ::ConsoleInvokedFrom invokedFrom ) { OnConsoleCommandInvoked(command, args, flags, invokedFrom); }) { - + ; } void MultiplayerSystemComponent::Activate() diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h index c49e5367e6..eba6813169 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h @@ -74,8 +74,7 @@ namespace Multiplayer bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerPackets::NotifyClientMigration& packet); bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerPackets::EntityMigration& packet); bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerPackets::ReadyForEntityUpdates& packet); - //bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerPackets::EditorServerInit& packet); - + //! IConnectionListener interface //! @{ AzNetworking::ConnectResult ValidateConnect(const AzNetworking::IpAddress& remoteAddress, const AzNetworking::IPacketHeader& packetHeader, AzNetworking::ISerializer& serializer) override; From 74ea093f71096c124ec25ca39630f5abbea2bdde Mon Sep 17 00:00:00 2001 From: puvvadar Date: Thu, 13 May 2021 15:43:07 -0700 Subject: [PATCH 05/22] More comment cleanup --- Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index 867a9d3c2e..f920e7ed74 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -63,8 +63,6 @@ namespace Multiplayer static const AZStd::string_view s_networkEditorInterfaceName("MultiplayerEditorNetworkInterface"); static constexpr uint16_t DefaultServerPort = 30090; static constexpr uint16_t DefaultServerEditorPort = 30091; - //static AZStd::vector buffer; - //static AZ::IO::ByteContainerStream> s_byteStream(&buffer); AZ_CVAR(uint16_t, cl_clientport, 0, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port to bind to for game traffic when connecting to a remote host, a value of 0 will select any available port"); AZ_CVAR(AZ::CVarFixedString, cl_serveraddr, "127.0.0.1", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The address of the remote server or host to connect to"); From 1915b97c16e7837bf325e60b58f608b04f5428f5 Mon Sep 17 00:00:00 2001 From: puvvadar Date: Sat, 15 May 2021 12:58:30 -0700 Subject: [PATCH 06/22] Cleanup server launch, misc. MP consts, and register Editor Spawnable assets server side --- .../AzCore/AzCore/Asset/AssetCommon.h | 13 ++ .../PrefabEditorEntityOwnershipInterface.h | 2 + .../Code/Include/MultiplayerConstants.h | 32 +++++ .../Editor/MultiplayerEditorConnection.cpp | 87 +++++++++---- .../MultiplayerEditorSystemComponent.cpp | 114 +++++++++++------- .../Source/MultiplayerSystemComponent.cpp | 16 +-- .../Pipeline/NetworkPrefabProcessor.cpp | 9 +- Gems/Multiplayer/Code/multiplayer_files.cmake | 1 + 8 files changed, 191 insertions(+), 83 deletions(-) create mode 100644 Gems/Multiplayer/Code/Include/MultiplayerConstants.h diff --git a/Code/Framework/AzCore/AzCore/Asset/AssetCommon.h b/Code/Framework/AzCore/AzCore/Asset/AssetCommon.h index c8a245af68..ff1cd13023 100644 --- a/Code/Framework/AzCore/AzCore/Asset/AssetCommon.h +++ b/Code/Framework/AzCore/AzCore/Asset/AssetCommon.h @@ -308,6 +308,8 @@ namespace AZ Asset(AssetLoadBehavior loadBehavior = AssetLoadBehavior::Default); /// Create an asset from a valid asset data (created asset), might not be loaded or currently loading. Asset(AssetData* assetData, AssetLoadBehavior loadBehavior); + /// Create an asset from a valid asset data (created asset) and set the asset id for both, might not be loaded or currently loading. + Asset(const AZ::Data::AssetId& id, AssetData* assetData, AssetLoadBehavior loadBehavior); /// Initialize asset pointer with id, type, and hint. No data construction will occur until QueueLoad is called. Asset(const AZ::Data::AssetId& id, const AZ::Data::AssetType& type, const AZStd::string& hint = AZStd::string()); @@ -788,6 +790,17 @@ namespace AZ SetData(assetData); } + //========================================================================= + template + Asset::Asset(const AssetId& id, AssetData* assetData, AssetLoadBehavior loadBehavior) + : m_assetId(id) + , m_assetType(azrtti_typeid()) + , m_loadBehavior(loadBehavior) + { + assetData->m_assetId = id; + SetData(assetData); + } + //========================================================================= template Asset::Asset(const AssetId& id, const AZ::Data::AssetType& type, const AZStd::string& hint) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h index 4476876b59..2afabd16a9 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h @@ -46,6 +46,8 @@ namespace AzToolsFramework virtual Prefab::InstanceOptionalReference GetRootPrefabInstance() = 0; + //! Get all Assets generated by Prefab processing when entering Play-In Editor mode (Ctrl+G) + //! /return The vector of Assets generated by Prefab processing virtual const AZStd::vector>& GetPlayInEditorAssetData() = 0; virtual bool LoadFromStream(AZ::IO::GenericStream& stream, AZStd::string_view filename) = 0; diff --git a/Gems/Multiplayer/Code/Include/MultiplayerConstants.h b/Gems/Multiplayer/Code/Include/MultiplayerConstants.h new file mode 100644 index 0000000000..892691177a --- /dev/null +++ b/Gems/Multiplayer/Code/Include/MultiplayerConstants.h @@ -0,0 +1,32 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace Multiplayer +{ + static constexpr AZStd::string_view MPNetworkInterfaceName("MultiplayerNetworkInterface"); + static constexpr AZStd::string_view MPEditorInterfaceName("MultiplayerEditorNetworkInterface"); + + static constexpr AZStd::string_view LocalHost("127.0.0.1"); + static constexpr uint16_t DefaultServerPort = 30090; + static constexpr uint16_t DefaultServerEditorPort = 30091; + +} + diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp index dcf9c134da..c97c66544e 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp @@ -11,26 +11,22 @@ */ #include +#include #include #include #include #include -#include +#include #include #include #include #include +#include namespace Multiplayer { using namespace AzNetworking; - static const AZStd::string_view s_networkInterfaceName("MultiplayerNetworkInterface"); - static const AZStd::string_view s_networkEditorInterfaceName("MultiplayerEditorNetworkInterface"); - static constexpr AZStd::string_view DefaultEditorIp = "127.0.0.1"; - static constexpr uint16_t DefaultServerPort = 30090; - static constexpr uint16_t DefaultServerEditorPort = 30091; - static AZStd::vector buffer; static AZ::IO::ByteContainerStream> s_byteStream(&buffer); @@ -39,10 +35,16 @@ namespace Multiplayer MultiplayerEditorConnection::MultiplayerEditorConnection() { m_networkEditorInterface = AZ::Interface::Get()->CreateNetworkInterface( - AZ::Name(s_networkEditorInterfaceName), ProtocolType::Tcp, TrustZone::ExternalClientToServer, *this); + AZ::Name(MPEditorInterfaceName), ProtocolType::Tcp, TrustZone::ExternalClientToServer, *this); if (editorsv_isDedicated) { - m_networkEditorInterface->Listen(DefaultServerEditorPort); + uint16_t editorServerPort = DefaultServerEditorPort; + if (auto console = AZ::Interface::Get(); console) + { + console->GetCvarValue("editorsv_port", editorServerPort); + } + AZ_Assert(m_networkEditorInterface, "MP Editor Network Interface was unregistered before Editor Server could start listening."); + m_networkEditorInterface->Listen(editorServerPort); } } @@ -69,34 +71,56 @@ namespace Multiplayer AZStd::vector> assetData; while (s_byteStream.GetCurPos() < s_byteStream.GetLength()) { + AZ::Data::AssetId assetId; AZ::Data::AssetLoadBehavior assetLoadBehavior; + uint32_t hintSize; + AZStd::string assetHint; + s_byteStream.Read(sizeof(AZ::Data::AssetId), reinterpret_cast(&assetId)); s_byteStream.Read(sizeof(AZ::Data::AssetLoadBehavior), reinterpret_cast(&assetLoadBehavior)); + s_byteStream.Read(sizeof(uint32_t), reinterpret_cast(&hintSize)); + assetHint.resize(hintSize); + s_byteStream.Read(hintSize, assetHint.data()); + + size_t assetSize = s_byteStream.GetCurPos(); + AZ::Data::AssetData* assetDatum = AZ::Utils::LoadObjectFromStream(s_byteStream, nullptr); + assetSize = s_byteStream.GetCurPos() - assetSize; + AZ::Data::Asset asset = AZ::Data::Asset(assetId, assetDatum, assetLoadBehavior); + asset.SetHint(assetHint); + + AZ::Data::AssetInfo assetInfo; + assetInfo.m_assetId = asset.GetId(); + assetInfo.m_assetType = asset.GetType(); + assetInfo.m_relativePath = asset.GetHint(); + assetInfo.m_sizeBytes = assetSize; - AZ::Data::AssetData* assetDatum = AZ::Utils::LoadObjectFromStream(s_byteStream, nullptr); - AZ::Data::Asset asset = AZ::Data::Asset(assetDatum, assetLoadBehavior); - - /* // Register Asset to AssetManager - */ - - assetData.push_back(asset); + AZ::Data::AssetManager::Instance().AssignAssetData(asset); + AZ::Data::AssetCatalogRequestBus::Broadcast(&AZ::Data::AssetCatalogRequests::RegisterAsset, asset.GetId(), assetInfo); + + assetData.push_back(asset); } // Now that we've deserialized, clear the byte stream s_byteStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN); s_byteStream.Truncate(); - /* - // Hand-off our resultant assets - */ + // Load the level via the root spawnable tha was registered + AZ::CVarFixedString loadLevelString = "LoadLevel Root.spawnable"; + AZ::Interface::Get()->PerformCommand(loadLevelString.c_str()); AZLOG_INFO("Editor Server completed asset receive, responding to Editor..."); if (connection->SendReliablePacket(MultiplayerEditorPackets::EditorServerReady())) { // Setup the normal multiplayer connection AZ::Interface::Get()->InitializeMultiplayer(MultiplayerAgentType::DedicatedServer); - INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(s_networkInterfaceName)); - networkInterface->Listen(DefaultServerPort); + INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPNetworkInterfaceName)); + + uint16_t serverPort = DefaultServerPort; + if (auto console = AZ::Interface::Get(); console) + { + console->GetCvarValue("sv_port", serverPort); + } + networkInterface->Listen(serverPort); return true; } @@ -121,11 +145,22 @@ namespace Multiplayer // Receiving this packet means Editor sync is done, disconnect connection->Disconnect(AzNetworking::DisconnectReason::TerminatedByClient, AzNetworking::TerminationEndpoint::Local); - // Connect the Editor to the local server for Multiplayer simulation - AZ::Interface::Get()->InitializeMultiplayer(MultiplayerAgentType::Client); - INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(s_networkInterfaceName)); - const IpAddress ipAddress(DefaultEditorIp.data(), DefaultServerEditorPort, networkInterface->GetType()); - networkInterface->Connect(ipAddress); + if (auto console = AZ::Interface::Get(); console) + { + AZ::CVarFixedString remoteAddress; + uint16_t remotePort; + if (console->GetCvarValue("editorsv_serveraddr", remoteAddress) != AZ::GetValueResult::ConsoleVarNotFound && + console->GetCvarValue("editorsv_port", remotePort) != AZ::GetValueResult::ConsoleVarNotFound) + { + // Connect the Editor to the editor server for Multiplayer simulation + AZ::Interface::Get()->InitializeMultiplayer(MultiplayerAgentType::Client); + INetworkInterface* networkInterface = + AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPNetworkInterfaceName)); + + const IpAddress ipAddress(remoteAddress.c_str(), remotePort, networkInterface->GetType()); + networkInterface->Connect(ipAddress); + } + } } return true; } diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp index 4c3e8200a1..89cb816695 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -26,16 +27,16 @@ namespace Multiplayer { - static const AZStd::string_view s_networkEditorInterfaceName("MultiplayerEditorNetworkInterface"); - using namespace AzNetworking; AZ_CVAR(bool, editorsv_enabled, true, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Whether Editor launching a local server to connect to is supported"); + AZ_CVAR(bool, editorsv_launch, false, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, + "Whether Editor should launch a server when the server address is localhost"); AZ_CVAR(AZ::CVarFixedString, editorsv_process, "", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The server executable that should be run. Empty to use the current project's ServerLauncher"); - AZ_CVAR(AZ::CVarFixedString, editorsv_serveraddr, "127.0.0.1", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The address of the server to connect to"); - AZ_CVAR(uint16_t, editorsv_port, 30091, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port that the multiplayer editor gem will bind to for traffic"); + AZ_CVAR(AZ::CVarFixedString, editorsv_serveraddr, LocalHost.data(), nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The address of the server to connect to"); + AZ_CVAR(uint16_t, editorsv_port, DefaultServerEditorPort, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port that the multiplayer editor gem will bind to for traffic"); void MultiplayerEditorSystemComponent::Reflect(AZ::ReflectContext* context) { @@ -105,7 +106,7 @@ namespace Multiplayer m_serverProcess->TerminateProcess(0); m_serverProcess = nullptr; } - INetworkInterface* editorNetworkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(s_networkEditorInterfaceName)); + INetworkInterface* editorNetworkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPEditorInterfaceName)); if (editorNetworkInterface) { editorNetworkInterface->Disconnect(m_editorConnId, AzNetworking::DisconnectReason::TerminatedByClient); @@ -114,6 +115,46 @@ namespace Multiplayer } } + void LaunchEditorServer(AzFramework::ProcessWatcher* outProcess) + { + // Assemble the server's path + AZ::CVarFixedString serverProcess = editorsv_process; + if (serverProcess.empty()) + { + // If enabled but no process name is supplied, try this project's ServerLauncher + serverProcess = AZ::Utils::GetProjectName() + ".ServerLauncher"; + } + + AZ::IO::FixedMaxPathString serverPath = AZ::Utils::GetExecutableDirectory(); + if (!serverProcess.contains(AZ_TRAIT_OS_PATH_SEPARATOR)) + { + // If only the process name is specified, append that as well + serverPath.append(AZ_TRAIT_OS_PATH_SEPARATOR + serverProcess); + } + else + { + // If any path was already specified, then simply assign + serverPath = serverProcess; + } + + if (!serverProcess.ends_with(AZ_TRAIT_OS_EXECUTABLE_EXTENSION)) + { + // Add this platform's exe extension if it's not specified + serverPath.append(AZ_TRAIT_OS_EXECUTABLE_EXTENSION); + } + + // Start the configured server if it's available + AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo; + processLaunchInfo.m_commandlineParameters = AZStd::string::format("\"%s\" --editorsv_isDedicated true", serverPath.c_str()); + processLaunchInfo.m_showWindow = true; + processLaunchInfo.m_processPriority = AzFramework::ProcessPriority::PROCESSPRIORITY_NORMAL; + + // Launch the Server and give it a few seconds to boot up + outProcess = AzFramework::ProcessWatcher::LaunchProcess( + processLaunchInfo, AzFramework::ProcessCommunicationType::COMMUNICATOR_TYPE_NONE); + AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(15000)); + } + void MultiplayerEditorSystemComponent::OnGameEntitiesStarted() { auto prefabEditorEntityOwnershipInterface = AZ::Interface::Get(); @@ -134,55 +175,38 @@ namespace Multiplayer // Serialize Asset information and AssetData into a potentially large buffer for (auto asset : assetData) { + AZ::Data::AssetId assetId = asset.GetId(); AZ::Data::AssetLoadBehavior assetLoadBehavior = asset.GetAutoLoadBehavior(); + AZStd::string assetHint = asset.GetHint(); + uint32_t hintSize = aznumeric_cast(assetHint.size()); + + byteStream.Write(sizeof(AZ::Data::AssetId), reinterpret_cast(&assetId)); byteStream.Write(sizeof(AZ::Data::AssetLoadBehavior), reinterpret_cast(&assetLoadBehavior)); - + byteStream.Write(sizeof(uint32_t), reinterpret_cast(&hintSize)); + byteStream.Write(assetHint.size(), assetHint.data()); AZ::Utils::SaveObjectToStream(byteStream, AZ::DataStream::ST_BINARY, asset.GetData(), asset.GetData()->GetType()); } - // Assemble the server's path - AZ::CVarFixedString serverProcess = editorsv_process; - if (serverProcess.empty()) - { - // If enabled but no process name is supplied, try this project's ServerLauncher - serverProcess = AZ::Utils::GetProjectName() + ".ServerLauncher"; - } - - AZ::IO::FixedMaxPathString serverPath = AZ::Utils::GetExecutableDirectory(); - if (!serverProcess.contains(AZ_TRAIT_OS_PATH_SEPARATOR)) - { - // If only the process name is specified, append that as well - serverPath.append(AZ_TRAIT_OS_PATH_SEPARATOR + serverProcess); - } - else - { - // If any path was already specified, then simply assign - serverPath = serverProcess; - } - - if (!serverProcess.ends_with(AZ_TRAIT_OS_EXECUTABLE_EXTENSION)) - { - // Add this platform's exe extension if it's not specified - serverPath.append(AZ_TRAIT_OS_EXECUTABLE_EXTENSION); - } - - // Start the configured server if it's available - AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo; - processLaunchInfo.m_commandlineParameters = AZStd::string::format("\"%s\" --editorsv_isDedicated true", serverPath.c_str()); - processLaunchInfo.m_showWindow = true; - processLaunchInfo.m_processPriority = AzFramework::ProcessPriority::PROCESSPRIORITY_NORMAL; - - // Launch the Server and give it a few seconds to boot up - m_serverProcess = AzFramework::ProcessWatcher::LaunchProcess( - processLaunchInfo, AzFramework::ProcessCommunicationType::COMMUNICATOR_TYPE_NONE); - AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(15000)); - - // Now that the server has launched, attempt to connect the NetworkInterface const AZ::CVarFixedString remoteAddress = editorsv_serveraddr; - INetworkInterface* editorNetworkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(s_networkEditorInterfaceName)); + if (editorsv_launch && LocalHost.compare(remoteAddress.c_str()) == 0) + { + LaunchEditorServer(m_serverProcess); + } + + // Now that the server has launched, attempt to connect the NetworkInterface + INetworkInterface* editorNetworkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPEditorInterfaceName)); + AZ_Assert(editorNetworkInterface, "MP Editor Network Interface was unregistered before Editor could connect."); m_editorConnId = editorNetworkInterface->Connect( AzNetworking::IpAddress(remoteAddress.c_str(), editorsv_port, AzNetworking::ProtocolType::Tcp)); + if (m_editorConnId == AzNetworking::InvalidConnectionId) + { + AZ_Warning( + "MultiplayerEditor", false, + "Could not connect to server targeted by Editor. If using a local server, check that it's built and editorsv_launch is true."); + return; + } + // Read the buffer into EditorServerInit packets until we've flushed the whole thing byteStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN); diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index f920e7ed74..a0a343f320 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -10,6 +10,7 @@ * */ +#include #include #include #include @@ -59,13 +60,8 @@ namespace Multiplayer { using namespace AzNetworking; - static const AZStd::string_view s_networkInterfaceName("MultiplayerNetworkInterface"); - static const AZStd::string_view s_networkEditorInterfaceName("MultiplayerEditorNetworkInterface"); - static constexpr uint16_t DefaultServerPort = 30090; - static constexpr uint16_t DefaultServerEditorPort = 30091; - AZ_CVAR(uint16_t, cl_clientport, 0, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port to bind to for game traffic when connecting to a remote host, a value of 0 will select any available port"); - AZ_CVAR(AZ::CVarFixedString, cl_serveraddr, "127.0.0.1", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The address of the remote server or host to connect to"); + AZ_CVAR(AZ::CVarFixedString, cl_serveraddr, LocalHost.data(), nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The address of the remote server or host to connect to"); AZ_CVAR(AZ::CVarFixedString, cl_serverpassword, "", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Optional server password"); AZ_CVAR(uint16_t, cl_serverport, DefaultServerPort, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port of the remote host to connect to for game traffic"); AZ_CVAR(uint16_t, sv_port, DefaultServerPort, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port that this multiplayer gem will bind to for game traffic"); @@ -116,7 +112,7 @@ namespace Multiplayer void MultiplayerSystemComponent::Activate() { AZ::TickBus::Handler::BusConnect(); - m_networkInterface = AZ::Interface::Get()->CreateNetworkInterface(AZ::Name(s_networkInterfaceName), sv_protocol, TrustZone::ExternalClientToServer, *this); + m_networkInterface = AZ::Interface::Get()->CreateNetworkInterface(AZ::Name(MPNetworkInterfaceName), sv_protocol, TrustZone::ExternalClientToServer, *this); m_consoleCommandHandler.Connect(AZ::Interface::Get()->GetConsoleCommandInvokedEvent()); AZ::Interface::Register(this); @@ -635,7 +631,7 @@ namespace Multiplayer { Multiplayer::MultiplayerAgentType serverType = sv_isDedicated ? MultiplayerAgentType::DedicatedServer : MultiplayerAgentType::ClientServer; AZ::Interface::Get()->InitializeMultiplayer(serverType); - INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(s_networkInterfaceName)); + INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPNetworkInterfaceName)); networkInterface->Listen(sv_port); } AZ_CONSOLEFREEFUNC(host, AZ::ConsoleFunctorFlags::DontReplicate, "Opens a multiplayer connection as a host for other clients to connect to"); @@ -643,7 +639,7 @@ namespace Multiplayer void connect([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments) { AZ::Interface::Get()->InitializeMultiplayer(MultiplayerAgentType::Client); - INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(s_networkInterfaceName)); + INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPNetworkInterfaceName)); if (arguments.size() < 1) { @@ -673,7 +669,7 @@ namespace Multiplayer void disconnect([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments) { AZ::Interface::Get()->InitializeMultiplayer(MultiplayerAgentType::Uninitialized); - INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(s_networkInterfaceName)); + INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPNetworkInterfaceName)); auto visitor = [](IConnection& connection) { connection.Disconnect(DisconnectReason::TerminatedByUser, TerminationEndpoint::Local); }; networkInterface->GetConnectionSet().VisitConnections(visitor); } diff --git a/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp b/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp index bc6e5710fc..137bd27794 100644 --- a/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp +++ b/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp @@ -31,11 +31,16 @@ namespace Multiplayer void NetworkPrefabProcessor::Process(PrefabProcessorContext& context) { IMultiplayerTools* mpTools = AZ::Interface::Get(); - mpTools->SetDidProcessNetworkPrefabs(false); + if (mpTools) + { + mpTools->SetDidProcessNetworkPrefabs(false); + } + context.ListPrefabs([&context](AZStd::string_view prefabName, PrefabDom& prefab) { ProcessPrefab(context, prefabName, prefab); }); - if (context.GetProcessedObjects().size() > 0) + + if (mpTools && context.GetProcessedObjects().size() > 0) { mpTools->SetDidProcessNetworkPrefabs(true); } diff --git a/Gems/Multiplayer/Code/multiplayer_files.cmake b/Gems/Multiplayer/Code/multiplayer_files.cmake index b8fd842426..ecde01e2d7 100644 --- a/Gems/Multiplayer/Code/multiplayer_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_files.cmake @@ -17,6 +17,7 @@ set(FILES Include/INetworkEntityManager.h Include/INetworkTime.h Include/IReplicationWindow.h + Include/MultiplayerConstants.h Include/MultiplayerStats.cpp Include/MultiplayerStats.h Include/MultiplayerTypes.h From 0e53c775162bd46ffd89245b1792e6bb98919a6d Mon Sep 17 00:00:00 2001 From: puvvadar Date: Sat, 15 May 2021 13:47:03 -0700 Subject: [PATCH 07/22] Fix some include paths --- .../Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml | 4 ++-- .../Code/Source/Editor/MultiplayerEditorConnection.cpp | 4 ++-- Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml b/Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml index 986860e822..9904eef83c 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml @@ -2,8 +2,8 @@ - - + + diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp index c97c66544e..545e17eff6 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp @@ -10,8 +10,8 @@ * */ -#include -#include +#include +#include #include #include #include diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index c8bdd80a39..fe90d68696 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -10,7 +10,7 @@ * */ -#include +#include #include #include #include From df68ab6c568a632b9591570454f5c8062a35448f Mon Sep 17 00:00:00 2001 From: puvvadar Date: Sun, 16 May 2021 15:30:00 -0700 Subject: [PATCH 08/22] Update headers in editor auto packets --- .../Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml b/Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml index 9904eef83c..8f55ecd2b8 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml @@ -3,7 +3,7 @@ - + From f94d0c99e72738d4add4b10e053e403bf67bba47 Mon Sep 17 00:00:00 2001 From: puvvadar Date: Mon, 17 May 2021 09:58:58 -0700 Subject: [PATCH 09/22] Cleanup connection order slightly --- .../Editor/MultiplayerEditorConnection.cpp | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp index 545e17eff6..731c865137 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp @@ -93,7 +93,7 @@ namespace Multiplayer assetInfo.m_relativePath = asset.GetHint(); assetInfo.m_sizeBytes = assetSize; - // Register Asset to AssetManager + // Register Asset to AssetManager AZ::Data::AssetManager::Instance().AssignAssetData(asset); AZ::Data::AssetCatalogRequestBus::Broadcast(&AZ::Data::AssetCatalogRequests::RegisterAsset, asset.GetId(), assetInfo); @@ -108,26 +108,19 @@ namespace Multiplayer AZ::CVarFixedString loadLevelString = "LoadLevel Root.spawnable"; AZ::Interface::Get()->PerformCommand(loadLevelString.c_str()); + // Setup the normal multiplayer connection + AZ::Interface::Get()->InitializeMultiplayer(MultiplayerAgentType::DedicatedServer); + INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPNetworkInterfaceName)); + + uint16_t serverPort = DefaultServerPort; + if (auto console = AZ::Interface::Get(); console) + { + console->GetCvarValue("sv_port", serverPort); + } + networkInterface->Listen(serverPort); + AZLOG_INFO("Editor Server completed asset receive, responding to Editor..."); - if (connection->SendReliablePacket(MultiplayerEditorPackets::EditorServerReady())) - { - // Setup the normal multiplayer connection - AZ::Interface::Get()->InitializeMultiplayer(MultiplayerAgentType::DedicatedServer); - INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPNetworkInterfaceName)); - - uint16_t serverPort = DefaultServerPort; - if (auto console = AZ::Interface::Get(); console) - { - console->GetCvarValue("sv_port", serverPort); - } - networkInterface->Listen(serverPort); - - return true; - } - else - { - return false; - } + return connection->SendReliablePacket(MultiplayerEditorPackets::EditorServerReady()); } return true; From 83a56ce71cdb62e0a815b3a0f8a1c1419f1c5221 Mon Sep 17 00:00:00 2001 From: puvvadar Date: Mon, 17 May 2021 12:53:15 -0700 Subject: [PATCH 10/22] Cleanup typo --- .../Code/Source/Editor/MultiplayerEditorConnection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp index 731c865137..e5f7ed4a68 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp @@ -104,7 +104,7 @@ namespace Multiplayer s_byteStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN); s_byteStream.Truncate(); - // Load the level via the root spawnable tha was registered + // Load the level via the root spawnable that was registered AZ::CVarFixedString loadLevelString = "LoadLevel Root.spawnable"; AZ::Interface::Get()->PerformCommand(loadLevelString.c_str()); From 6f3f5268d830736872a3ccdc31f03182b2cb91d0 Mon Sep 17 00:00:00 2001 From: puvvadar Date: Mon, 17 May 2021 14:06:20 -0700 Subject: [PATCH 11/22] Fix build error on unity builds --- .../Entity/PrefabEditorEntityOwnershipInterface.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h index 2afabd16a9..8412361657 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h @@ -12,6 +12,7 @@ #pragma once +#include #include #include #include From bb851943c8bb20d65ae81c43aeb0638b7414c369 Mon Sep 17 00:00:00 2001 From: puvvadar Date: Mon, 17 May 2021 15:16:35 -0700 Subject: [PATCH 12/22] Update unit test to account for missing dependency --- Gems/Multiplayer/Code/Tests/MultiplayerSystemTests.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Gems/Multiplayer/Code/Tests/MultiplayerSystemTests.cpp b/Gems/Multiplayer/Code/Tests/MultiplayerSystemTests.cpp index 2e0b9c0759..6ace4db592 100644 --- a/Gems/Multiplayer/Code/Tests/MultiplayerSystemTests.cpp +++ b/Gems/Multiplayer/Code/Tests/MultiplayerSystemTests.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -30,6 +31,7 @@ namespace UnitTest SetupAllocator(); AZ::NameDictionary::Create(); m_spawnableComponent = new AzFramework::SpawnableSystemComponent(); + m_netComponent = new AzNetworking::NetworkingSystemComponent(); m_mpComponent = new Multiplayer::MultiplayerSystemComponent(); m_initHandler = Multiplayer::SessionInitEvent::Handler([this](AzNetworking::INetworkInterface* value) { TestInitEvent(value); }); @@ -43,6 +45,7 @@ namespace UnitTest void TearDown() override { delete m_mpComponent; + delete m_netComponent; delete m_spawnableComponent; AZ::NameDictionary::Destroy(); TeardownAllocator(); @@ -71,6 +74,7 @@ namespace UnitTest Multiplayer::SessionShutdownEvent::Handler m_shutdownHandler; Multiplayer::ConnectionAcquiredEvent::Handler m_connAcquiredHandler; + AzNetworking::NetworkingSystemComponent* m_netComponent = nullptr; Multiplayer::MultiplayerSystemComponent* m_mpComponent = nullptr; AzFramework::SpawnableSystemComponent* m_spawnableComponent = nullptr; }; From 0d9b55bff25e26d86b7b904412432bfec92acf24 Mon Sep 17 00:00:00 2001 From: puvvadar Date: Mon, 17 May 2021 16:48:05 -0700 Subject: [PATCH 13/22] Move static buffer to member to prevent potential memory issues --- .../Editor/MultiplayerEditorConnection.cpp | 30 +++++++++---------- .../Editor/MultiplayerEditorConnection.h | 2 ++ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp index e5f7ed4a68..2fdd29c542 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp @@ -27,12 +27,10 @@ namespace Multiplayer { using namespace AzNetworking; - static AZStd::vector buffer; - static AZ::IO::ByteContainerStream> s_byteStream(&buffer); - AZ_CVAR(bool, editorsv_isDedicated, false, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Whether to init as a server expecting data from an Editor. Do not modify unless you're sure of what you're doing."); MultiplayerEditorConnection::MultiplayerEditorConnection() + : m_byteStream(&m_buffer) { m_networkEditorInterface = AZ::Interface::Get()->CreateNetworkInterface( AZ::Name(MPEditorInterfaceName), ProtocolType::Tcp, TrustZone::ExternalClientToServer, *this); @@ -59,31 +57,31 @@ namespace Multiplayer if (!packet.GetLastUpdate()) { // More packets are expected, flush this to the buffer - s_byteStream.Write(TcpPacketEncodingBuffer::GetCapacity(), reinterpret_cast(packet.ModifyAssetData().GetBuffer())); + m_byteStream.Write(TcpPacketEncodingBuffer::GetCapacity(), reinterpret_cast(packet.ModifyAssetData().GetBuffer())); } else { // This is the last expected packet, flush it to the buffer - s_byteStream.Write(packet.GetAssetData().GetSize(), reinterpret_cast(packet.ModifyAssetData().GetBuffer())); + m_byteStream.Write(packet.GetAssetData().GetSize(), reinterpret_cast(packet.ModifyAssetData().GetBuffer())); // Read all assets out of the buffer - s_byteStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN); + m_byteStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN); AZStd::vector> assetData; - while (s_byteStream.GetCurPos() < s_byteStream.GetLength()) + while (m_byteStream.GetCurPos() < m_byteStream.GetLength()) { AZ::Data::AssetId assetId; AZ::Data::AssetLoadBehavior assetLoadBehavior; uint32_t hintSize; AZStd::string assetHint; - s_byteStream.Read(sizeof(AZ::Data::AssetId), reinterpret_cast(&assetId)); - s_byteStream.Read(sizeof(AZ::Data::AssetLoadBehavior), reinterpret_cast(&assetLoadBehavior)); - s_byteStream.Read(sizeof(uint32_t), reinterpret_cast(&hintSize)); + m_byteStream.Read(sizeof(AZ::Data::AssetId), reinterpret_cast(&assetId)); + m_byteStream.Read(sizeof(AZ::Data::AssetLoadBehavior), reinterpret_cast(&assetLoadBehavior)); + m_byteStream.Read(sizeof(uint32_t), reinterpret_cast(&hintSize)); assetHint.resize(hintSize); - s_byteStream.Read(hintSize, assetHint.data()); + m_byteStream.Read(hintSize, assetHint.data()); - size_t assetSize = s_byteStream.GetCurPos(); - AZ::Data::AssetData* assetDatum = AZ::Utils::LoadObjectFromStream(s_byteStream, nullptr); - assetSize = s_byteStream.GetCurPos() - assetSize; + size_t assetSize = m_byteStream.GetCurPos(); + AZ::Data::AssetData* assetDatum = AZ::Utils::LoadObjectFromStream(m_byteStream, nullptr); + assetSize = m_byteStream.GetCurPos() - assetSize; AZ::Data::Asset asset = AZ::Data::Asset(assetId, assetDatum, assetLoadBehavior); asset.SetHint(assetHint); @@ -101,8 +99,8 @@ namespace Multiplayer } // Now that we've deserialized, clear the byte stream - s_byteStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN); - s_byteStream.Truncate(); + m_byteStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN); + m_byteStream.Truncate(); // Load the level via the root spawnable that was registered AZ::CVarFixedString loadLevelString = "LoadLevel Root.spawnable"; diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h index 3621e3aee6..d559651080 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h @@ -51,5 +51,7 @@ namespace Multiplayer private: AzNetworking::INetworkInterface* m_networkEditorInterface = nullptr; + AZStd::vector m_buffer; + AZ::IO::ByteContainerStream> m_byteStream; }; } From 93e267345fb2fb8954ec090e25296dc7c625c98e Mon Sep 17 00:00:00 2001 From: puvvadar Date: Mon, 17 May 2021 18:46:21 -0700 Subject: [PATCH 14/22] Address string/mem feedback plus some misc cleanup --- .../Multiplayer/MultiplayerConstants.h | 10 ++--- .../Editor/MultiplayerEditorConnection.cpp | 7 ++-- .../Source/Editor/MultiplayerEditorGem.cpp | 12 +++--- .../MultiplayerEditorSystemComponent.cpp | 38 ++++++++----------- .../Editor/MultiplayerEditorSystemComponent.h | 4 +- .../Source/MultiplayerSystemComponent.cpp | 20 +++++----- .../Pipeline/NetworkPrefabProcessor.cpp | 12 +++--- 7 files changed, 49 insertions(+), 54 deletions(-) diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerConstants.h b/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerConstants.h index 892691177a..b82fab91be 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerConstants.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerConstants.h @@ -21,12 +21,12 @@ namespace Multiplayer { - static constexpr AZStd::string_view MPNetworkInterfaceName("MultiplayerNetworkInterface"); - static constexpr AZStd::string_view MPEditorInterfaceName("MultiplayerEditorNetworkInterface"); + constexpr AZStd::string_view MPNetworkInterfaceName("MultiplayerNetworkInterface"); + constexpr AZStd::string_view MPEditorInterfaceName("MultiplayerEditorNetworkInterface"); - static constexpr AZStd::string_view LocalHost("127.0.0.1"); - static constexpr uint16_t DefaultServerPort = 30090; - static constexpr uint16_t DefaultServerEditorPort = 30091; + constexpr AZStd::string_view LocalHost("127.0.0.1"); + constexpr uint16_t DefaultServerPort = 30090; + constexpr uint16_t DefaultServerEditorPort = 30091; } diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp index 2fdd29c542..f88a314ea5 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp @@ -12,16 +12,17 @@ #include #include -#include +#include #include -#include -#include + #include #include #include #include #include #include +#include +#include namespace Multiplayer { diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorGem.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorGem.cpp index ae38ef1d6d..2fe0aabe5f 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorGem.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorGem.cpp @@ -10,13 +10,13 @@ * */ -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include -#include +#include namespace Multiplayer { diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp index d837b15b05..159f3b944b 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp @@ -13,13 +13,15 @@ #include #include #include + +#include +#include #include -#include -#include -#include + #include #include #include +#include #include #include #include @@ -31,11 +33,11 @@ namespace Multiplayer AZ_CVAR(bool, editorsv_enabled, true, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Whether Editor launching a local server to connect to is supported"); - AZ_CVAR(bool, editorsv_launch, false, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, + AZ_CVAR(bool, editorsv_launch, true, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Whether Editor should launch a server when the server address is localhost"); AZ_CVAR(AZ::CVarFixedString, editorsv_process, "", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The server executable that should be run. Empty to use the current project's ServerLauncher"); - AZ_CVAR(AZ::CVarFixedString, editorsv_serveraddr, LocalHost.data(), nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The address of the server to connect to"); + AZ_CVAR(AZ::CVarFixedString, editorsv_serveraddr, AZ::CVarFixedString(LocalHost), nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The address of the server to connect to"); AZ_CVAR(uint16_t, editorsv_port, DefaultServerEditorPort, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port that the multiplayer editor gem will bind to for traffic"); void MultiplayerEditorSystemComponent::Reflect(AZ::ReflectContext* context) @@ -115,34 +117,24 @@ namespace Multiplayer } } - void LaunchEditorServer(AzFramework::ProcessWatcher* outProcess) + AzFramework::ProcessWatcher* LaunchEditorServer() { // Assemble the server's path AZ::CVarFixedString serverProcess = editorsv_process; + AZ::IO::FixedMaxPath serverPath; if (serverProcess.empty()) { // If enabled but no process name is supplied, try this project's ServerLauncher serverProcess = AZ::Utils::GetProjectName() + ".ServerLauncher"; - } - AZ::IO::FixedMaxPathString serverPath = AZ::Utils::GetExecutableDirectory(); - if (!serverProcess.contains(AZ_TRAIT_OS_PATH_SEPARATOR)) - { - // If only the process name is specified, append that as well - serverPath.append(AZ_TRAIT_OS_PATH_SEPARATOR + serverProcess); + serverPath = AZ::Utils::GetExecutableDirectory(); + serverPath /= serverProcess + AZ_TRAIT_OS_EXECUTABLE_EXTENSION; } else { - // If any path was already specified, then simply assign serverPath = serverProcess; } - if (!serverProcess.ends_with(AZ_TRAIT_OS_EXECUTABLE_EXTENSION)) - { - // Add this platform's exe extension if it's not specified - serverPath.append(AZ_TRAIT_OS_EXECUTABLE_EXTENSION); - } - // Start the configured server if it's available AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo; processLaunchInfo.m_commandlineParameters = AZStd::string::format("\"%s\" --editorsv_isDedicated true", serverPath.c_str()); @@ -150,9 +142,11 @@ namespace Multiplayer processLaunchInfo.m_processPriority = AzFramework::ProcessPriority::PROCESSPRIORITY_NORMAL; // Launch the Server and give it a few seconds to boot up - outProcess = AzFramework::ProcessWatcher::LaunchProcess( + AzFramework::ProcessWatcher* outProcess = AzFramework::ProcessWatcher::LaunchProcess( processLaunchInfo, AzFramework::ProcessCommunicationType::COMMUNICATOR_TYPE_NONE); AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(15000)); + + return outProcess; } void MultiplayerEditorSystemComponent::OnGameEntitiesStarted() @@ -188,9 +182,9 @@ namespace Multiplayer } const AZ::CVarFixedString remoteAddress = editorsv_serveraddr; - if (editorsv_launch && LocalHost.compare(remoteAddress.c_str()) == 0) + if (editorsv_launch && LocalHost == remoteAddress) { - LaunchEditorServer(m_serverProcess); + m_serverProcess = LaunchEditorServer(); } // Now that the server has launched, attempt to connect the NetworkInterface diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h index 569092e981..81b138c675 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h @@ -14,18 +14,16 @@ #include -#include +#include #include #include #include #include - #include #include #include - namespace AzNetworking { class INetworkInterface; diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index c6a8458eae..aa6fe6e72c 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -11,15 +11,16 @@ */ #include -#include -#include -#include -#include -#include -#include -#include #include -#include + +#include +#include +#include +#include +#include +#include +#include + #include #include #include @@ -29,6 +30,7 @@ #include #include #include +#include namespace AZ::ConsoleTypeHelpers { @@ -63,7 +65,7 @@ namespace Multiplayer using namespace AzNetworking; AZ_CVAR(uint16_t, cl_clientport, 0, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port to bind to for game traffic when connecting to a remote host, a value of 0 will select any available port"); - AZ_CVAR(AZ::CVarFixedString, cl_serveraddr, LocalHost.data(), nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The address of the remote server or host to connect to"); + AZ_CVAR(AZ::CVarFixedString, cl_serveraddr, AZ::CVarFixedString(LocalHost), nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The address of the remote server or host to connect to"); AZ_CVAR(AZ::CVarFixedString, cl_serverpassword, "", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Optional server password"); AZ_CVAR(uint16_t, cl_serverport, DefaultServerPort, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port of the remote host to connect to for game traffic"); AZ_CVAR(uint16_t, sv_port, DefaultServerPort, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port that this multiplayer gem will bind to for game traffic"); diff --git a/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp b/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp index 8bea44e893..bd6899aad6 100644 --- a/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp +++ b/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp @@ -10,7 +10,11 @@ * */ -#include +#include +#include +#include +#include +#include #include #include @@ -18,10 +22,6 @@ #include #include #include -#include -#include -#include -#include namespace Multiplayer { @@ -40,7 +40,7 @@ namespace Multiplayer ProcessPrefab(context, prefabName, prefab); }); - if (mpTools && context.GetProcessedObjects().size() > 0) + if (mpTools && !context.GetProcessedObjects().empty()) { mpTools->SetDidProcessNetworkPrefabs(true); } From cf4e04ba573aad88417671a1424f3896cac96b6b Mon Sep 17 00:00:00 2001 From: puvvadar Date: Mon, 17 May 2021 18:52:40 -0700 Subject: [PATCH 15/22] Cleanup a few more headers --- .../Code/Source/Editor/MultiplayerEditorConnection.h | 3 ++- Gems/Multiplayer/Code/Source/MultiplayerToolsModule.cpp | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h index d559651080..d803a60744 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h @@ -12,6 +12,8 @@ #pragma once +#include + #include #include #include @@ -19,7 +21,6 @@ #include #include #include -#include namespace AzNetworking { diff --git a/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.cpp b/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.cpp index ae91999a0c..3646dd52a4 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.cpp @@ -10,9 +10,10 @@ * */ -#include -#include +#include +#include #include + #include #include From 920f85981de33e1ebf2bae5d548b3a6df41d1a6a Mon Sep 17 00:00:00 2001 From: puvvadar Date: Mon, 17 May 2021 18:55:00 -0700 Subject: [PATCH 16/22] Add another missed header file --- .../Code/Source/MultiplayerSystemComponent.h | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h index a48363abe4..cac64db89b 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h @@ -12,6 +12,12 @@ #pragma once +#include +#include +#include +#include +#include + #include #include #include @@ -20,11 +26,6 @@ #include #include #include -#include -#include -#include -#include -#include namespace AzNetworking { From 19316e422b7c42cce7b3ac6d52c5fbb9e9dc68a2 Mon Sep 17 00:00:00 2001 From: puvvadar Date: Tue, 18 May 2021 10:46:18 -0700 Subject: [PATCH 17/22] Check launch process exists before waiting for 15 seconds --- .../Code/Source/Editor/MultiplayerEditorSystemComponent.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp index 159f3b944b..a3d2551dd8 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp @@ -126,7 +126,6 @@ namespace Multiplayer { // If enabled but no process name is supplied, try this project's ServerLauncher serverProcess = AZ::Utils::GetProjectName() + ".ServerLauncher"; - serverPath = AZ::Utils::GetExecutableDirectory(); serverPath /= serverProcess + AZ_TRAIT_OS_EXECUTABLE_EXTENSION; } @@ -144,7 +143,10 @@ namespace Multiplayer // Launch the Server and give it a few seconds to boot up AzFramework::ProcessWatcher* outProcess = AzFramework::ProcessWatcher::LaunchProcess( processLaunchInfo, AzFramework::ProcessCommunicationType::COMMUNICATOR_TYPE_NONE); - AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(15000)); + if (outProcess) + { + AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(15000)); + } return outProcess; } From e03645f8161cfcb67400176e23cc3870c1aa872b Mon Sep 17 00:00:00 2001 From: puvvadar Date: Tue, 18 May 2021 10:48:39 -0700 Subject: [PATCH 18/22] Disable editorsv launch by default --- .../Code/Source/Editor/MultiplayerEditorSystemComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp index a3d2551dd8..3c361dab2a 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp @@ -31,7 +31,7 @@ namespace Multiplayer { using namespace AzNetworking; - AZ_CVAR(bool, editorsv_enabled, true, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, + AZ_CVAR(bool, editorsv_enabled, false, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Whether Editor launching a local server to connect to is supported"); AZ_CVAR(bool, editorsv_launch, true, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Whether Editor should launch a server when the server address is localhost"); From e2ade654fb30d12bdb1564f04f8700e3652532d8 Mon Sep 17 00:00:00 2001 From: puvvadar Date: Tue, 18 May 2021 13:52:28 -0700 Subject: [PATCH 19/22] Address misc feedback --- .../Components/LocalPredictionPlayerInputComponent.cpp | 4 ++-- .../Code/Source/Editor/MultiplayerEditorConnection.cpp | 6 ++---- .../Code/Source/Editor/MultiplayerEditorSystemComponent.cpp | 4 +--- Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp b/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp index 10a0d17e73..612601883c 100644 --- a/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp @@ -23,7 +23,7 @@ namespace Multiplayer { AZ_CVAR(AZ::TimeMs, cl_InputRateMs, AZ::TimeMs{ 33 }, nullptr, AZ::ConsoleFunctorFlags::Null, "Rate at which to sample and process client inputs"); AZ_CVAR(AZ::TimeMs, cl_MaxRewindHistoryMs, AZ::TimeMs{ 2000 }, nullptr, AZ::ConsoleFunctorFlags::Null, "Maximum number of milliseconds to keep for server correction rewind and replay"); -#ifndef _RELEASE +#ifndef AZ_RELEASE_BUILD AZ_CVAR(float, cl_DebugHackTimeMultiplier, 1.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "Scalar value used to simulate clock hacking cheats for validating bank time system and anticheat"); #endif @@ -477,7 +477,7 @@ namespace Multiplayer const double inputRate = static_cast(static_cast(cl_InputRateMs)) / 1000.0; const double maxRewindHistory = static_cast(static_cast(cl_MaxRewindHistoryMs)) / 1000.0; -#ifndef _RELEASE +#ifndef AZ_RELEASE_BUILD m_moveAccumulator += deltaTime * cl_DebugHackTimeMultiplier; #else m_moveAccumulator += deltaTime; diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp index f88a314ea5..f684e1f12f 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp @@ -71,11 +71,9 @@ namespace Multiplayer while (m_byteStream.GetCurPos() < m_byteStream.GetLength()) { AZ::Data::AssetId assetId; - AZ::Data::AssetLoadBehavior assetLoadBehavior; uint32_t hintSize; AZStd::string assetHint; m_byteStream.Read(sizeof(AZ::Data::AssetId), reinterpret_cast(&assetId)); - m_byteStream.Read(sizeof(AZ::Data::AssetLoadBehavior), reinterpret_cast(&assetLoadBehavior)); m_byteStream.Read(sizeof(uint32_t), reinterpret_cast(&hintSize)); assetHint.resize(hintSize); m_byteStream.Read(hintSize, assetHint.data()); @@ -83,7 +81,7 @@ namespace Multiplayer size_t assetSize = m_byteStream.GetCurPos(); AZ::Data::AssetData* assetDatum = AZ::Utils::LoadObjectFromStream(m_byteStream, nullptr); assetSize = m_byteStream.GetCurPos() - assetSize; - AZ::Data::Asset asset = AZ::Data::Asset(assetId, assetDatum, assetLoadBehavior); + AZ::Data::Asset asset = AZ::Data::Asset(assetId, assetDatum, AZ::Data::AssetLoadBehavior::NoLoad); asset.SetHint(assetHint); AZ::Data::AssetInfo assetInfo; @@ -104,7 +102,7 @@ namespace Multiplayer m_byteStream.Truncate(); // Load the level via the root spawnable that was registered - AZ::CVarFixedString loadLevelString = "LoadLevel Root.spawnable"; + const AZ::CVarFixedString loadLevelString = "LoadLevel Root.spawnable"; AZ::Interface::Get()->PerformCommand(loadLevelString.c_str()); // Setup the normal multiplayer connection diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp index 3c361dab2a..0a95f96ff7 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp @@ -169,15 +169,13 @@ namespace Multiplayer AZ::IO::ByteContainerStream byteStream(&buffer); // Serialize Asset information and AssetData into a potentially large buffer - for (auto asset : assetData) + for (auto& asset : assetData) { AZ::Data::AssetId assetId = asset.GetId(); - AZ::Data::AssetLoadBehavior assetLoadBehavior = asset.GetAutoLoadBehavior(); AZStd::string assetHint = asset.GetHint(); uint32_t hintSize = aznumeric_cast(assetHint.size()); byteStream.Write(sizeof(AZ::Data::AssetId), reinterpret_cast(&assetId)); - byteStream.Write(sizeof(AZ::Data::AssetLoadBehavior), reinterpret_cast(&assetLoadBehavior)); byteStream.Write(sizeof(uint32_t), reinterpret_cast(&hintSize)); byteStream.Write(assetHint.size(), assetHint.data()); AZ::Utils::SaveObjectToStream(byteStream, AZ::DataStream::ST_BINARY, asset.GetData(), asset.GetData()->GetType()); diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h index cac64db89b..1410a82a3c 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h @@ -129,7 +129,7 @@ namespace Multiplayer AZ::TimeMs m_lastReplicatedHostTimeMs = AZ::TimeMs{ 0 }; HostFrameId m_lastReplicatedHostFrameId = InvalidHostFrameId; -#if !defined(_RELEASE) +#if !defined(AZ_RELEASE_BUILD) MultiplayerEditorConnection m_editorConnectionListener; #endif }; From 55ecd8517d3b8769199b36edf000f69a356cf4fd Mon Sep 17 00:00:00 2001 From: puvvadar Date: Tue, 18 May 2021 14:57:27 -0700 Subject: [PATCH 20/22] Add assert to new Asset constructor to declare intent and safeguard ID overwrite --- Code/Framework/AzCore/AzCore/Asset/AssetCommon.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Code/Framework/AzCore/AzCore/Asset/AssetCommon.h b/Code/Framework/AzCore/AzCore/Asset/AssetCommon.h index 3666eae2d4..11f4531124 100644 --- a/Code/Framework/AzCore/AzCore/Asset/AssetCommon.h +++ b/Code/Framework/AzCore/AzCore/Asset/AssetCommon.h @@ -796,6 +796,7 @@ namespace AZ , m_assetType(azrtti_typeid()) , m_loadBehavior(loadBehavior) { + AZ_Assert(!assetData->m_assetId.IsValid(), "Asset data already has an ID set."); assetData->m_assetId = id; SetData(assetData); } From 350e5a0cd24c634bc7cd78036300937569c56333 Mon Sep 17 00:00:00 2001 From: puvvadar Date: Tue, 18 May 2021 15:22:22 -0700 Subject: [PATCH 21/22] Update to const auto& --- .../Code/Source/Editor/MultiplayerEditorSystemComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp index 0a95f96ff7..829fe7e495 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp @@ -169,7 +169,7 @@ namespace Multiplayer AZ::IO::ByteContainerStream byteStream(&buffer); // Serialize Asset information and AssetData into a potentially large buffer - for (auto& asset : assetData) + for (const auto& asset : assetData) { AZ::Data::AssetId assetId = asset.GetId(); AZStd::string assetHint = asset.GetHint(); From a2608e187b40424995ca69b9568a809d899a5b09 Mon Sep 17 00:00:00 2001 From: bosnichd Date: Tue, 18 May 2021 17:37:02 -0600 Subject: [PATCH 22/22] Resurrect error.log and error.dmp file output when the engine crashes (LYN-3873) (#811) Resurrect error.log and error.dmp file output when the engine crashes (LYN-3873). This was recently removed as part of 96b85e6813920554f7dfdc7752c1dd4452919b92 --- Code/CryEngine/CrySystem/DebugCallStack.cpp | 900 ++++++++++++++++++ Code/CryEngine/CrySystem/DebugCallStack.h | 95 ++ Code/CryEngine/CrySystem/DllMain.cpp | 11 + Code/CryEngine/CrySystem/IDebugCallStack.cpp | 275 ++++++ Code/CryEngine/CrySystem/IDebugCallStack.h | 90 ++ Code/CryEngine/CrySystem/System.h | 1 + Code/CryEngine/CrySystem/SystemInit.cpp | 19 + Code/CryEngine/CrySystem/SystemWin32.cpp | 5 + .../CrySystem/WindowsErrorReporting.cpp | 137 +++ .../CryEngine/CrySystem/crysystem_files.cmake | 5 + 10 files changed, 1538 insertions(+) create mode 100644 Code/CryEngine/CrySystem/DebugCallStack.cpp create mode 100644 Code/CryEngine/CrySystem/DebugCallStack.h create mode 100644 Code/CryEngine/CrySystem/IDebugCallStack.cpp create mode 100644 Code/CryEngine/CrySystem/IDebugCallStack.h create mode 100644 Code/CryEngine/CrySystem/WindowsErrorReporting.cpp diff --git a/Code/CryEngine/CrySystem/DebugCallStack.cpp b/Code/CryEngine/CrySystem/DebugCallStack.cpp new file mode 100644 index 0000000000..2a219ce674 --- /dev/null +++ b/Code/CryEngine/CrySystem/DebugCallStack.cpp @@ -0,0 +1,900 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ +// Original file Copyright Crytek GMBH or its affiliates, used under license. + +#include "CrySystem_precompiled.h" +#include "DebugCallStack.h" + +#if defined(WIN32) || defined(WIN64) + +#include +#include +#include "System.h" + +#include +#include + +#define VS_VERSION_INFO 1 +#define IDD_CRITICAL_ERROR 101 +#define IDB_CONFIRM_SAVE 102 +#define IDB_DONT_SAVE 103 +#define IDD_CONFIRM_SAVE_LEVEL 127 +#define IDB_CRASH_FACE 128 +#define IDD_EXCEPTION 245 +#define IDC_CALLSTACK 1001 +#define IDC_EXCEPTION_CODE 1002 +#define IDC_EXCEPTION_ADDRESS 1003 +#define IDC_EXCEPTION_MODULE 1004 +#define IDC_EXCEPTION_DESC 1005 +#define IDB_EXIT 1008 +#define IDB_IGNORE 1010 +__pragma(comment(lib, "version.lib")) + +//! Needs one external of DLL handle. +extern HMODULE gDLLHandle; + +#include + +#define MAX_PATH_LENGTH 1024 +#define MAX_SYMBOL_LENGTH 512 + +static HWND hwndException = 0; +static bool g_bUserDialog = true; // true=on crash show dialog box, false=supress user interaction + +static int PrintException(EXCEPTION_POINTERS* pex); + +static bool IsFloatingPointException(EXCEPTION_POINTERS* pex); + +extern LONG WINAPI CryEngineExceptionFilterWER(struct _EXCEPTION_POINTERS* pExceptionPointers); +extern LONG WINAPI CryEngineExceptionFilterMiniDump(struct _EXCEPTION_POINTERS* pExceptionPointers, const char* szDumpPath, MINIDUMP_TYPE mdumpValue); + +//============================================================================= +CONTEXT CaptureCurrentContext() +{ + CONTEXT context; + memset(&context, 0, sizeof(context)); + context.ContextFlags = CONTEXT_FULL; + RtlCaptureContext(&context); + + return context; +} + +LONG __stdcall CryUnhandledExceptionHandler(EXCEPTION_POINTERS* pex) +{ + return DebugCallStack::instance()->handleException(pex); +} + + +BOOL CALLBACK EnumModules( + PCSTR ModuleName, + DWORD64 BaseOfDll, + PVOID UserContext) +{ + DebugCallStack::TModules& modules = *static_cast(UserContext); + modules[(void*)BaseOfDll] = ModuleName; + + return TRUE; +} +//============================================================================= +// Class Statics +//============================================================================= + +// Return single instance of class. +IDebugCallStack* IDebugCallStack::instance() +{ + static DebugCallStack sInstance; + return &sInstance; +} + +//------------------------------------------------------------------------------------------------------------------------ +// Sets up the symbols for functions in the debug file. +//------------------------------------------------------------------------------------------------------------------------ +DebugCallStack::DebugCallStack() + : prevExceptionHandler(0) + , m_pSystem(0) + , m_nSkipNumFunctions(0) + , m_bCrash(false) + , m_szBugMessage(NULL) +{ +} + +DebugCallStack::~DebugCallStack() +{ +} + +void DebugCallStack::RemoveOldFiles() +{ + RemoveFile("error.log"); + RemoveFile("error.bmp"); + RemoveFile("error.dmp"); +} + +void DebugCallStack::RemoveFile(const char* szFileName) +{ + FILE* pFile = nullptr; + azfopen(&pFile, szFileName, "r"); + const bool bFileExists = (pFile != NULL); + + if (bFileExists) + { + fclose(pFile); + + WriteLineToLog("Removing file \"%s\"...", szFileName); + if (remove(szFileName) == 0) + { + WriteLineToLog("File successfully removed."); + } + else + { + WriteLineToLog("Couldn't remove file!"); + } + } +} + +void DebugCallStack::installErrorHandler(ISystem* pSystem) +{ + m_pSystem = pSystem; + prevExceptionHandler = (void*)SetUnhandledExceptionFilter(CryUnhandledExceptionHandler); +} + +////////////////////////////////////////////////////////////////////////// +void DebugCallStack::SetUserDialogEnable(const bool bUserDialogEnable) +{ + g_bUserDialog = bUserDialogEnable; +} + + +DWORD g_idDebugThreads[10]; +const char* g_nameDebugThreads[10]; +int g_nDebugThreads = 0; +volatile int g_lockThreadDumpList = 0; + +void MarkThisThreadForDebugging(const char* name) +{ + EBUS_EVENT(AZ::Debug::EventTraceDrillerSetupBus, SetThreadName, AZStd::this_thread::get_id(), name); + + WriteLock lock(g_lockThreadDumpList); + DWORD id = GetCurrentThreadId(); + if (g_nDebugThreads == sizeof(g_idDebugThreads) / sizeof(g_idDebugThreads[0])) + { + return; + } + for (int i = 0; i < g_nDebugThreads; i++) + { + if (g_idDebugThreads[i] == id) + { + return; + } + } + g_nameDebugThreads[g_nDebugThreads] = name; + g_idDebugThreads[g_nDebugThreads++] = id; + ((CSystem*)gEnv->pSystem)->EnableFloatExceptions(g_cvars.sys_float_exceptions); +} + +void UnmarkThisThreadFromDebugging() +{ + WriteLock lock(g_lockThreadDumpList); + DWORD id = GetCurrentThreadId(); + for (int i = g_nDebugThreads - 1; i >= 0; i--) + { + if (g_idDebugThreads[i] == id) + { + memmove(g_idDebugThreads + i, g_idDebugThreads + i + 1, (g_nDebugThreads - 1 - i) * sizeof(g_idDebugThreads[0])); + memmove(g_nameDebugThreads + i, g_nameDebugThreads + i + 1, (g_nDebugThreads - 1 - i) * sizeof(g_nameDebugThreads[0])); + --g_nDebugThreads; + } + } +} + +extern int prev_sys_float_exceptions; +void UpdateFPExceptionsMaskForThreads() +{ + int mask = -iszero(g_cvars.sys_float_exceptions); + CONTEXT ctx; + for (int i = 0; i < g_nDebugThreads; i++) + { + if (g_idDebugThreads[i] != GetCurrentThreadId()) + { + HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, TRUE, g_idDebugThreads[i]); + ctx.ContextFlags = CONTEXT_ALL; + SuspendThread(hThread); + GetThreadContext(hThread, &ctx); +#ifndef WIN64 + (ctx.FloatSave.ControlWord |= 7) &= ~5 | mask; + (*(WORD*)(ctx.ExtendedRegisters + 24) |= 0x280) &= ~0x280 | mask; +#else + (ctx.FltSave.ControlWord |= 7) &= ~5 | mask; + (ctx.FltSave.MxCsr |= 0x280) &= ~0x280 | mask; +#endif + SetThreadContext(hThread, &ctx); + ResumeThread(hThread); + } + } +} + +////////////////////////////////////////////////////////////////////////// +int DebugCallStack::handleException(EXCEPTION_POINTERS* exception_pointer) +{ + if (gEnv == NULL) + { + return EXCEPTION_EXECUTE_HANDLER; + } + + ResetFPU(exception_pointer); + + prev_sys_float_exceptions = 0; + const int cached_sys_float_exceptions = g_cvars.sys_float_exceptions; + + ((CSystem*)gEnv->pSystem)->EnableFloatExceptions(0); + + if (g_cvars.sys_WER) + { + gEnv->pLog->FlushAndClose(); + return CryEngineExceptionFilterWER(exception_pointer); + } + + if (g_cvars.sys_no_crash_dialog) + { + DWORD dwMode = SetErrorMode(SEM_NOGPFAULTERRORBOX); + SetErrorMode(dwMode | SEM_NOGPFAULTERRORBOX); + } + + m_bCrash = true; + + if (g_cvars.sys_no_crash_dialog) + { + DWORD dwMode = SetErrorMode(SEM_NOGPFAULTERRORBOX); + SetErrorMode(dwMode | SEM_NOGPFAULTERRORBOX); + } + + static bool firstTime = true; + + if (g_cvars.sys_dump_aux_threads) + { + for (int i = 0; i < g_nDebugThreads; i++) + { + if (g_idDebugThreads[i] != GetCurrentThreadId()) + { + SuspendThread(OpenThread(THREAD_ALL_ACCESS, TRUE, g_idDebugThreads[i])); + } + } + } + + // uninstall our exception handler. + SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)prevExceptionHandler); + + if (!firstTime) + { + WriteLineToLog("Critical Exception! Called Multiple Times!"); + gEnv->pLog->FlushAndClose(); + // Exception called more then once. + return EXCEPTION_EXECUTE_HANDLER; + } + + // Print exception info: + { + char excCode[80]; + char excAddr[80]; + WriteLineToLog(""); + sprintf_s(excAddr, "0x%04X:0x%p", exception_pointer->ContextRecord->SegCs, exception_pointer->ExceptionRecord->ExceptionAddress); + sprintf_s(excCode, "0x%08X", exception_pointer->ExceptionRecord->ExceptionCode); + WriteLineToLog("Exception: %s, at Address: %s", excCode, excAddr); + } + + firstTime = false; + + const int ret = SubmitBug(exception_pointer); + + if (ret != IDB_IGNORE) + { + CryEngineExceptionFilterWER(exception_pointer); + } + + gEnv->pLog->FlushAndClose(); + + if (exception_pointer->ExceptionRecord->ExceptionFlags & EXCEPTION_NONCONTINUABLE) + { + // This is non continuable exception. abort application now. + exit(exception_pointer->ExceptionRecord->ExceptionCode); + } + + //typedef long (__stdcall *ExceptionFunc)(EXCEPTION_POINTERS*); + //ExceptionFunc prevFunc = (ExceptionFunc)prevExceptionHandler; + //return prevFunc( (EXCEPTION_POINTERS*)exception_pointer ); + if (ret == IDB_EXIT) + { + // Immediate exit. + // on windows, exit() and _exit() do all sorts of things, unfortuantely + // TerminateProcess is the only way to die. + TerminateProcess(GetCurrentProcess(), exception_pointer->ExceptionRecord->ExceptionCode); // we crashed, so don't return a zero exit code! + // on linux based systems, _exit will not call ATEXIT and other things, which makes it more suitable for termination in an emergency such + // as an unhandled exception. + // however, this function is a windows exception handler. + } + else if (ret == IDB_IGNORE) + { +#ifndef WIN64 + exception_pointer->ContextRecord->FloatSave.StatusWord &= ~31; + exception_pointer->ContextRecord->FloatSave.ControlWord |= 7; + (*(WORD*)(exception_pointer->ContextRecord->ExtendedRegisters + 24) &= 31) |= 0x1F80; +#else + exception_pointer->ContextRecord->FltSave.StatusWord &= ~31; + exception_pointer->ContextRecord->FltSave.ControlWord |= 7; + (exception_pointer->ContextRecord->FltSave.MxCsr &= 31) |= 0x1F80; +#endif + firstTime = true; + prevExceptionHandler = (void*)SetUnhandledExceptionFilter(CryUnhandledExceptionHandler); + g_cvars.sys_float_exceptions = cached_sys_float_exceptions; + ((CSystem*)gEnv->pSystem)->EnableFloatExceptions(g_cvars.sys_float_exceptions); + return EXCEPTION_CONTINUE_EXECUTION; + } + + // Continue; + return EXCEPTION_EXECUTE_HANDLER; +} + +void DebugCallStack::ReportBug(const char* szErrorMessage) +{ + WriteLineToLog("Reporting bug: %s", szErrorMessage); + + m_szBugMessage = szErrorMessage; + m_context = CaptureCurrentContext(); + SubmitBug(NULL); + m_szBugMessage = NULL; +} + +void DebugCallStack::dumpCallStack(std::vector& funcs) +{ + WriteLineToLog("============================================================================="); + int len = (int)funcs.size(); + for (int i = 0; i < len; i++) + { + const char* str = funcs[i].c_str(); + WriteLineToLog("%2d) %s", len - i, str); + } + WriteLineToLog("============================================================================="); +} + + +////////////////////////////////////////////////////////////////////////// +void DebugCallStack::LogExceptionInfo(EXCEPTION_POINTERS* pex) +{ + string path(""); + if ((gEnv) && (gEnv->pFileIO)) + { + const char* logAlias = gEnv->pFileIO->GetAlias("@log@"); + if (!logAlias) + { + logAlias = gEnv->pFileIO->GetAlias("@root@"); + } + if (logAlias) + { + path = logAlias; + path += "/"; + } + } + + string fileName = path; + fileName += "error.log"; + + struct stat fileInfo; + string timeStamp; + string backupPath; + if (gEnv->IsDedicated()) + { + backupPath = PathUtil::ToUnixPath(PathUtil::AddSlash(path + "DumpBackups")); + gEnv->pFileIO->CreatePath(backupPath.c_str()); + + if (stat(fileName.c_str(), &fileInfo) == 0) + { + // Backup log + tm creationTime; + localtime_s(&creationTime, &fileInfo.st_mtime); + char tempBuffer[32]; + strftime(tempBuffer, sizeof(tempBuffer), "%d %b %Y (%H %M %S)", &creationTime); + timeStamp = tempBuffer; + + string backupFileName = backupPath + timeStamp + " error.log"; + CopyFile(fileName.c_str(), backupFileName.c_str(), true); + } + } + + FILE* f = nullptr; + azfopen(&f, fileName.c_str(), "wt"); + + static char errorString[s_iCallStackSize]; + errorString[0] = 0; + + // Time and Version. + char versionbuf[1024]; + azstrcpy(versionbuf, AZ_ARRAY_SIZE(versionbuf), ""); + PutVersion(versionbuf, AZ_ARRAY_SIZE(versionbuf)); + cry_strcat(errorString, versionbuf); + cry_strcat(errorString, "\n"); + + char excCode[MAX_WARNING_LENGTH]; + char excAddr[80]; + char desc[1024]; + char excDesc[MAX_WARNING_LENGTH]; + + // make sure the mouse cursor is visible + ShowCursor(TRUE); + + const char* excName; + if (m_bIsFatalError || !pex) + { + const char* const szMessage = m_bIsFatalError ? s_szFatalErrorCode : m_szBugMessage; + excName = szMessage; + cry_strcpy(excCode, szMessage); + cry_strcpy(excAddr, ""); + cry_strcpy(desc, ""); + cry_strcpy(m_excModule, ""); + cry_strcpy(excDesc, szMessage); + } + else + { + sprintf_s(excAddr, "0x%04X:0x%p", pex->ContextRecord->SegCs, pex->ExceptionRecord->ExceptionAddress); + sprintf_s(excCode, "0x%08X", pex->ExceptionRecord->ExceptionCode); + excName = TranslateExceptionCode(pex->ExceptionRecord->ExceptionCode); + cry_strcpy(desc, ""); + sprintf_s(excDesc, "%s\r\n%s", excName, desc); + + + if (pex->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) + { + if (pex->ExceptionRecord->NumberParameters > 1) + { + ULONG_PTR iswrite = pex->ExceptionRecord->ExceptionInformation[0]; + DWORD64 accessAddr = pex->ExceptionRecord->ExceptionInformation[1]; + if (iswrite) + { + sprintf_s(desc, "Attempt to write data to address 0x%08llu\r\nThe memory could not be \"written\"", accessAddr); + } + else + { + sprintf_s(desc, "Attempt to read from address 0x%08llu\r\nThe memory could not be \"read\"", accessAddr); + } + } + } + } + + + WriteLineToLog("Exception Code: %s", excCode); + WriteLineToLog("Exception Addr: %s", excAddr); + WriteLineToLog("Exception Module: %s", m_excModule); + WriteLineToLog("Exception Name : %s", excName); + WriteLineToLog("Exception Description: %s", desc); + + + cry_strcpy(m_excDesc, excDesc); + cry_strcpy(m_excAddr, excAddr); + cry_strcpy(m_excCode, excCode); + + + char errs[32768]; + sprintf_s(errs, "Exception Code: %s\nException Addr: %s\nException Module: %s\nException Description: %s, %s\n", + excCode, excAddr, m_excModule, excName, desc); + + + cry_strcat(errs, "\nCall Stack Trace:\n"); + + std::vector funcs; + { + AZ::Debug::StackFrame frames[25]; + AZ::Debug::SymbolStorage::StackLine lines[AZ_ARRAY_SIZE(frames)]; + unsigned int numFrames = AZ::Debug::StackRecorder::Record(frames, AZ_ARRAY_SIZE(frames), 3); + if (numFrames) + { + AZ::Debug::SymbolStorage::DecodeFrames(frames, numFrames, lines); + for (unsigned int i = 0; i < numFrames; i++) + { + funcs.push_back(lines[i]); + } + } + dumpCallStack(funcs); + // Fill call stack. + char str[s_iCallStackSize]; + cry_strcpy(str, ""); + for (unsigned int i = 0; i < funcs.size(); i++) + { + char temp[s_iCallStackSize]; + sprintf_s(temp, "%2zd) %s", funcs.size() - i, (const char*)funcs[i].c_str()); + cry_strcat(str, temp); + cry_strcat(str, "\r\n"); + cry_strcat(errs, temp); + cry_strcat(errs, "\n"); + } + cry_strcpy(m_excCallstack, str); + } + + cry_strcat(errorString, errs); + + if (f) + { + fwrite(errorString, strlen(errorString), 1, f); + { + if (g_cvars.sys_dump_aux_threads) + { + for (int i = 0; i < g_nDebugThreads; i++) + { + if (g_idDebugThreads[i] != GetCurrentThreadId()) + { + fprintf(f, "\n\nSuspended thread (%s):\n", g_nameDebugThreads[i]); + HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, TRUE, g_idDebugThreads[i]); + + // mirrors the AZ::Debug::Trace::PrintCallstack() functionality, but prints to a file + { + AZ::Debug::StackFrame frames[10]; + + // Without StackFrame explicit alignment frames array is aligned to 4 bytes + // which causes the stack tracing to fail. + AZ::Debug::SymbolStorage::StackLine lines[AZ_ARRAY_SIZE(frames)]; + + unsigned int numFrames = AZ::Debug::StackRecorder::Record(frames, AZ_ARRAY_SIZE(frames), 0, hThread); + if (numFrames) + { + AZ::Debug::SymbolStorage::DecodeFrames(frames, numFrames, lines); + for (unsigned int i2 = 0; i2 < numFrames; ++i2) + { + fprintf(f, "%2d) %s\n", numFrames - i2, lines[i2]); + } + } + } + + ResumeThread(hThread); + } + } + } + } + fflush(f); + fclose(f); + } + + if (pex) + { + MINIDUMP_TYPE mdumpValue; + bool bDump = true; + switch (g_cvars.sys_dump_type) + { + case 0: + bDump = false; + break; + case 1: + mdumpValue = MiniDumpNormal; + break; + case 2: + mdumpValue = (MINIDUMP_TYPE)(MiniDumpWithIndirectlyReferencedMemory | MiniDumpWithDataSegs); + break; + case 3: + mdumpValue = MiniDumpWithFullMemory; + break; + default: + mdumpValue = (MINIDUMP_TYPE)g_cvars.sys_dump_type; + break; + } + if (bDump) + { + fileName = path + "error.dmp"; + + if (gEnv->IsDedicated() && stat(fileName.c_str(), &fileInfo) == 0) + { + // Backup dump (use timestamp from error.log if available) + if (timeStamp.empty()) + { + tm creationTime; + localtime_s(&creationTime, &fileInfo.st_mtime); + char tempBuffer[32]; + strftime(tempBuffer, sizeof(tempBuffer), "%d %b %Y (%H %M %S)", &creationTime); + timeStamp = tempBuffer; + } + + string backupFileName = backupPath + timeStamp + " error.dmp"; + CopyFile(fileName.c_str(), backupFileName.c_str(), true); + } + + CryEngineExceptionFilterMiniDump(pex, fileName.c_str(), mdumpValue); + } + } + + //if no crash dialog don't even submit the bug + if (m_postBackupProcess && g_cvars.sys_no_crash_dialog == 0 && g_bUserDialog) + { + m_postBackupProcess(); + } + else + { + // lawsonn: Disabling the JIRA-based crash reporter for now + // we'll need to deal with it our own way, pending QA. + // if you're customizing the engine this is also your opportunity to deal with it. + if (g_cvars.sys_no_crash_dialog != 0 || !g_bUserDialog) + { + // ------------ place custom crash handler here --------------------- + // it should launch an executable! + /// by this time, error.bmp will be in the engine root folder + // error.log and error.dmp will also be present in the engine root folder + // if your error dumper wants those, it should zip them up and send them or offer to do so. + // ------------------------------------------------------------------ + } + } + const bool bQuitting = !gEnv || !gEnv->pSystem || gEnv->pSystem->IsQuitting(); + + //[AlexMcC|16.04.10] When the engine is shutting down, MessageBox doesn't display a box + // and immediately returns IDYES. Avoid this by just not trying to save if we're quitting. + // Don't ask to save if this isn't a real crash (a real crash has exception pointers) + if (g_cvars.sys_no_crash_dialog == 0 && g_bUserDialog && gEnv->IsEditor() && !bQuitting && pex) + { + BackupCurrentLevel(); + + const INT_PTR res = DialogBoxParam(gDLLHandle, MAKEINTRESOURCE(IDD_CONFIRM_SAVE_LEVEL), NULL, DebugCallStack::ConfirmSaveDialogProc, NULL); + if (res == IDB_CONFIRM_SAVE) + { + if (SaveCurrentLevel()) + { + MessageBox(NULL, "Level has been successfully saved!\r\nPress Ok to terminate Editor.", "Save", MB_OK); + } + else + { + MessageBox(NULL, "Error saving level.\r\nPress Ok to terminate Editor.", "Save", MB_OK | MB_ICONWARNING); + } + } + } + + if (g_cvars.sys_no_crash_dialog != 0 || !g_bUserDialog) + { + // terminate immediately - since we're in a crash, there is no point unwinding stack, we've already done access violation or worse. + // calling exit will only cause further death down the line... + TerminateProcess(GetCurrentProcess(), pex->ExceptionRecord->ExceptionCode); + } +} + + +INT_PTR CALLBACK DebugCallStack::ExceptionDialogProc(HWND hwndDlg, UINT message, WPARAM wParam, LPARAM lParam) +{ + static EXCEPTION_POINTERS* pex; + + static char errorString[32768] = ""; + + switch (message) + { + case WM_INITDIALOG: + { + pex = (EXCEPTION_POINTERS*)lParam; + HWND h; + + if (pex->ExceptionRecord->ExceptionFlags & EXCEPTION_NONCONTINUABLE) + { + // Disable continue button for non continuable exceptions. + //h = GetDlgItem( hwndDlg,IDB_CONTINUE ); + //if (h) EnableWindow( h,FALSE ); + } + + DebugCallStack* pDCS = static_cast(DebugCallStack::instance()); + + h = GetDlgItem(hwndDlg, IDC_EXCEPTION_DESC); + if (h) + { + SendMessage(h, EM_REPLACESEL, FALSE, (LONG_PTR)pDCS->m_excDesc); + } + + h = GetDlgItem(hwndDlg, IDC_EXCEPTION_CODE); + if (h) + { + SendMessage(h, EM_REPLACESEL, FALSE, (LONG_PTR)pDCS->m_excCode); + } + + h = GetDlgItem(hwndDlg, IDC_EXCEPTION_MODULE); + if (h) + { + SendMessage(h, EM_REPLACESEL, FALSE, (LONG_PTR)pDCS->m_excModule); + } + + h = GetDlgItem(hwndDlg, IDC_EXCEPTION_ADDRESS); + if (h) + { + SendMessage(h, EM_REPLACESEL, FALSE, (LONG_PTR)pDCS->m_excAddr); + } + + // Fill call stack. + HWND callStack = GetDlgItem(hwndDlg, IDC_CALLSTACK); + if (callStack) + { + SendMessage(callStack, WM_SETTEXT, FALSE, (LPARAM)pDCS->m_excCallstack); + } + + if (hwndException) + { + DestroyWindow(hwndException); + hwndException = 0; + } + + if (IsFloatingPointException(pex)) + { + EnableWindow(GetDlgItem(hwndDlg, IDB_IGNORE), TRUE); + } + } + break; + + case WM_COMMAND: + switch (LOWORD(wParam)) + { + case IDB_EXIT: + case IDB_IGNORE: + // Fall through. + + EndDialog(hwndDlg, wParam); + return TRUE; + } + } + return FALSE; +} + +INT_PTR CALLBACK DebugCallStack::ConfirmSaveDialogProc(HWND hwndDlg, UINT message, WPARAM wParam, [[maybe_unused]] LPARAM lParam) +{ + switch (message) + { + case WM_INITDIALOG: + { + // The user might be holding down the spacebar while the engine crashes. + // If we don't remove keyboard focus from this dialog, the keypress will + // press the default button before the dialog actually appears, even if + // the user has already released the key, which is bad. + SetFocus(NULL); + } break; + case WM_COMMAND: + { + switch (LOWORD(wParam)) + { + case IDB_CONFIRM_SAVE: // Fall through + case IDB_DONT_SAVE: + { + EndDialog(hwndDlg, wParam); + return TRUE; + } + } + } break; + } + + return FALSE; +} + +bool DebugCallStack::BackupCurrentLevel() +{ + CSystem* pSystem = static_cast(m_pSystem); + if (pSystem && pSystem->GetUserCallback()) + { + return pSystem->GetUserCallback()->OnBackupDocument(); + } + + return false; +} + +bool DebugCallStack::SaveCurrentLevel() +{ + CSystem* pSystem = static_cast(m_pSystem); + if (pSystem && pSystem->GetUserCallback()) + { + return pSystem->GetUserCallback()->OnSaveDocument(); + } + + return false; +} + +int DebugCallStack::SubmitBug(EXCEPTION_POINTERS* exception_pointer) +{ + int ret = IDB_EXIT; + + assert(!hwndException); + + RemoveOldFiles(); + + AZ::Debug::Trace::PrintCallstack("", 2); + + LogExceptionInfo(exception_pointer); + + if (IsFloatingPointException(exception_pointer)) + { + //! Print exception dialog. + ret = PrintException(exception_pointer); + } + + return ret; +} + +void DebugCallStack::ResetFPU(EXCEPTION_POINTERS* pex) +{ + if (IsFloatingPointException(pex)) + { + // How to reset FPU: http://www.experts-exchange.com/Programming/System/Windows__Programming/Q_10310953.html + _clearfp(); +#ifndef WIN64 + pex->ContextRecord->FloatSave.ControlWord |= 0x2F; + pex->ContextRecord->FloatSave.StatusWord &= ~0x8080; +#endif + } +} + +string DebugCallStack::GetModuleNameForAddr(void* addr) +{ + if (m_modules.empty()) + { + return "[unknown]"; + } + + if (addr < m_modules.begin()->first) + { + return "[unknown]"; + } + + TModules::const_iterator it = m_modules.begin(); + TModules::const_iterator end = m_modules.end(); + for (; ++it != end; ) + { + if (addr < it->first) + { + return (--it)->second; + } + } + + //if address is higher than the last module, we simply assume it is in the last module. + return m_modules.rbegin()->second; +} + +void DebugCallStack::GetProcNameForAddr(void* addr, string& procName, void*& baseAddr, string& filename, int& line) +{ + AZ::Debug::SymbolStorage::StackLine func, file, module; + AZ::Debug::SymbolStorage::FindFunctionFromIP(addr, &func, &file, &module, line, baseAddr); + procName = func; + filename = file; +} + +string DebugCallStack::GetCurrentFilename() +{ + char fullpath[MAX_PATH_LENGTH + 1]; + GetModuleFileName(NULL, fullpath, MAX_PATH_LENGTH); + return fullpath; +} + +static bool IsFloatingPointException(EXCEPTION_POINTERS* pex) +{ + if (!pex) + { + return false; + } + + DWORD exceptionCode = pex->ExceptionRecord->ExceptionCode; + switch (exceptionCode) + { + case EXCEPTION_FLT_DENORMAL_OPERAND: + case EXCEPTION_FLT_DIVIDE_BY_ZERO: + case EXCEPTION_FLT_INEXACT_RESULT: + case EXCEPTION_FLT_INVALID_OPERATION: + case EXCEPTION_FLT_OVERFLOW: + case EXCEPTION_FLT_UNDERFLOW: + case STATUS_FLOAT_MULTIPLE_FAULTS: + case STATUS_FLOAT_MULTIPLE_TRAPS: + return true; + + default: + return false; + } +} + +int DebugCallStack::PrintException(EXCEPTION_POINTERS* exception_pointer) +{ + return (int)DialogBoxParam(gDLLHandle, MAKEINTRESOURCE(IDD_CRITICAL_ERROR), NULL, DebugCallStack::ExceptionDialogProc, (LPARAM)exception_pointer); +} + +#else +void MarkThisThreadForDebugging(const char*) {} +void UnmarkThisThreadFromDebugging() {} +void UpdateFPExceptionsMaskForThreads() {} +#endif //WIN32 diff --git a/Code/CryEngine/CrySystem/DebugCallStack.h b/Code/CryEngine/CrySystem/DebugCallStack.h new file mode 100644 index 0000000000..c37e6ba0d4 --- /dev/null +++ b/Code/CryEngine/CrySystem/DebugCallStack.h @@ -0,0 +1,95 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ +// Original file Copyright Crytek GMBH or its affiliates, used under license. + +#ifndef CRYINCLUDE_CRYSYSTEM_DEBUGCALLSTACK_H +#define CRYINCLUDE_CRYSYSTEM_DEBUGCALLSTACK_H +#pragma once + + +#include "IDebugCallStack.h" + +#if defined (WIN32) || defined (WIN64) + +//! Limits the maximal number of functions in call stack. +const int MAX_DEBUG_STACK_ENTRIES_FILE_DUMP = 12; + +struct ISystem; + +//!============================================================================ +//! +//! DebugCallStack class, capture call stack information from symbol files. +//! +//!============================================================================ +class DebugCallStack + : public IDebugCallStack +{ +public: + DebugCallStack(); + virtual ~DebugCallStack(); + + ISystem* GetSystem() { return m_pSystem; }; + + virtual string GetModuleNameForAddr(void* addr); + virtual void GetProcNameForAddr(void* addr, string& procName, void*& baseAddr, string& filename, int& line); + virtual string GetCurrentFilename(); + + void installErrorHandler(ISystem* pSystem); + virtual int handleException(EXCEPTION_POINTERS* exception_pointer); + + virtual void ReportBug(const char*); + + void dumpCallStack(std::vector& functions); + + void SetUserDialogEnable(const bool bUserDialogEnable); + + typedef std::map TModules; +protected: + static void RemoveOldFiles(); + static void RemoveFile(const char* szFileName); + + static int PrintException(EXCEPTION_POINTERS* exception_pointer); + static INT_PTR CALLBACK ExceptionDialogProc(HWND hwndDlg, UINT message, WPARAM wParam, LPARAM lParam); + static INT_PTR CALLBACK ConfirmSaveDialogProc(HWND hwndDlg, UINT message, WPARAM wParam, LPARAM lParam); + + void LogExceptionInfo(EXCEPTION_POINTERS* exception_pointer); + bool BackupCurrentLevel(); + bool SaveCurrentLevel(); + int SubmitBug(EXCEPTION_POINTERS* exception_pointer); + void ResetFPU(EXCEPTION_POINTERS* pex); + + static const int s_iCallStackSize = 32768; + + char m_excLine[256]; + char m_excModule[128]; + + char m_excDesc[MAX_WARNING_LENGTH]; + char m_excCode[MAX_WARNING_LENGTH]; + char m_excAddr[80]; + char m_excCallstack[s_iCallStackSize]; + + void* prevExceptionHandler; + + bool m_bCrash; + const char* m_szBugMessage; + + ISystem* m_pSystem; + + int m_nSkipNumFunctions; + CONTEXT m_context; + + TModules m_modules; +}; + +#endif //WIN32 + +#endif // CRYINCLUDE_CRYSYSTEM_DEBUGCALLSTACK_H diff --git a/Code/CryEngine/CrySystem/DllMain.cpp b/Code/CryEngine/CrySystem/DllMain.cpp index 7fd620835b..53593821d9 100644 --- a/Code/CryEngine/CrySystem/DllMain.cpp +++ b/Code/CryEngine/CrySystem/DllMain.cpp @@ -14,6 +14,7 @@ #include "CrySystem_precompiled.h" #include "System.h" #include +#include "DebugCallStack.h" #if defined(AZ_RESTRICTED_PLATFORM) #undef AZ_RESTRICTED_SECTION @@ -87,6 +88,16 @@ CRYSYSTEM_API ISystem* CreateSystemInterface(const SSystemInitParams& startupPar startupParams.pUserCallback->OnSystemConnect(pSystem); } +#if defined(WIN32) + // Environment Variable to signal we don't want to override our exception handler - our crash report system will set this + auto envVar = AZ::Environment::FindVariable("ExceptionHandlerIsSet"); + const bool handlerIsSet = (envVar && *envVar); + if (!handlerIsSet) + { + ((DebugCallStack*)IDebugCallStack::instance())->installErrorHandler(pSystem); + } +#endif + bool retVal = false; { AZ::Debug::StartupLogSinkReporter initLogSink; diff --git a/Code/CryEngine/CrySystem/IDebugCallStack.cpp b/Code/CryEngine/CrySystem/IDebugCallStack.cpp new file mode 100644 index 0000000000..c14dd2b0da --- /dev/null +++ b/Code/CryEngine/CrySystem/IDebugCallStack.cpp @@ -0,0 +1,275 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ +// Original file Copyright Crytek GMBH or its affiliates, used under license. + +// Description : A multiplatform base class for handling errors and collecting call stacks + + +#include "CrySystem_precompiled.h" +#include "IDebugCallStack.h" +#include "System.h" +#include +#include +#include +#include +//#if !defined(LINUX) + +#include + +const char* const IDebugCallStack::s_szFatalErrorCode = "FATAL_ERROR"; + +IDebugCallStack::IDebugCallStack() + : m_bIsFatalError(false) + , m_postBackupProcess(0) + , m_memAllocFileHandle(AZ::IO::InvalidHandle) +{ +} + +IDebugCallStack::~IDebugCallStack() +{ + StopMemLog(); +} + +#if AZ_LEGACY_CRYSYSTEM_TRAIT_DEBUGCALLSTACK_SINGLETON +IDebugCallStack* IDebugCallStack::instance() +{ + static IDebugCallStack sInstance; + return &sInstance; +} +#endif + +void IDebugCallStack::FileCreationCallback(void (* postBackupProcess)()) +{ + m_postBackupProcess = postBackupProcess; +} +////////////////////////////////////////////////////////////////////////// +void IDebugCallStack::LogCallstack() +{ + AZ::Debug::Trace::PrintCallstack("", 2); +} + +const char* IDebugCallStack::TranslateExceptionCode(DWORD dwExcept) +{ + switch (dwExcept) + { +#if AZ_LEGACY_CRYSYSTEM_TRAIT_DEBUGCALLSTACK_TRANSLATE + case EXCEPTION_ACCESS_VIOLATION: + return "EXCEPTION_ACCESS_VIOLATION"; + break; + case EXCEPTION_DATATYPE_MISALIGNMENT: + return "EXCEPTION_DATATYPE_MISALIGNMENT"; + break; + case EXCEPTION_BREAKPOINT: + return "EXCEPTION_BREAKPOINT"; + break; + case EXCEPTION_SINGLE_STEP: + return "EXCEPTION_SINGLE_STEP"; + break; + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: + return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED"; + break; + case EXCEPTION_FLT_DENORMAL_OPERAND: + return "EXCEPTION_FLT_DENORMAL_OPERAND"; + break; + case EXCEPTION_FLT_DIVIDE_BY_ZERO: + return "EXCEPTION_FLT_DIVIDE_BY_ZERO"; + break; + case EXCEPTION_FLT_INEXACT_RESULT: + return "EXCEPTION_FLT_INEXACT_RESULT"; + break; + case EXCEPTION_FLT_INVALID_OPERATION: + return "EXCEPTION_FLT_INVALID_OPERATION"; + break; + case EXCEPTION_FLT_OVERFLOW: + return "EXCEPTION_FLT_OVERFLOW"; + break; + case EXCEPTION_FLT_STACK_CHECK: + return "EXCEPTION_FLT_STACK_CHECK"; + break; + case EXCEPTION_FLT_UNDERFLOW: + return "EXCEPTION_FLT_UNDERFLOW"; + break; + case EXCEPTION_INT_DIVIDE_BY_ZERO: + return "EXCEPTION_INT_DIVIDE_BY_ZERO"; + break; + case EXCEPTION_INT_OVERFLOW: + return "EXCEPTION_INT_OVERFLOW"; + break; + case EXCEPTION_PRIV_INSTRUCTION: + return "EXCEPTION_PRIV_INSTRUCTION"; + break; + case EXCEPTION_IN_PAGE_ERROR: + return "EXCEPTION_IN_PAGE_ERROR"; + break; + case EXCEPTION_ILLEGAL_INSTRUCTION: + return "EXCEPTION_ILLEGAL_INSTRUCTION"; + break; + case EXCEPTION_NONCONTINUABLE_EXCEPTION: + return "EXCEPTION_NONCONTINUABLE_EXCEPTION"; + break; + case EXCEPTION_STACK_OVERFLOW: + return "EXCEPTION_STACK_OVERFLOW"; + break; + case EXCEPTION_INVALID_DISPOSITION: + return "EXCEPTION_INVALID_DISPOSITION"; + break; + case EXCEPTION_GUARD_PAGE: + return "EXCEPTION_GUARD_PAGE"; + break; + case EXCEPTION_INVALID_HANDLE: + return "EXCEPTION_INVALID_HANDLE"; + break; + //case EXCEPTION_POSSIBLE_DEADLOCK: return "EXCEPTION_POSSIBLE_DEADLOCK"; break ; + + case STATUS_FLOAT_MULTIPLE_FAULTS: + return "STATUS_FLOAT_MULTIPLE_FAULTS"; + break; + case STATUS_FLOAT_MULTIPLE_TRAPS: + return "STATUS_FLOAT_MULTIPLE_TRAPS"; + break; + + +#endif + default: + return "Unknown"; + break; + } +} + +void IDebugCallStack::PutVersion(char* str, size_t length) +{ +AZ_PUSH_DISABLE_WARNING(4996, "-Wunknown-warning-option") + + if (!gEnv || !gEnv->pSystem) + { + return; + } + + char sFileVersion[128]; + gEnv->pSystem->GetFileVersion().ToString(sFileVersion, sizeof(sFileVersion)); + + char sProductVersion[128]; + gEnv->pSystem->GetProductVersion().ToString(sProductVersion, sizeof(sFileVersion)); + + + //! Get time. + time_t ltime; + time(<ime); + tm* today = localtime(<ime); + + char s[1024]; + //! Use strftime to build a customized time string. + strftime(s, 128, "Logged at %#c\n", today); + azstrcat(str, length, s); + sprintf_s(s, "FileVersion: %s\n", sFileVersion); + azstrcat(str, length, s); + sprintf_s(s, "ProductVersion: %s\n", sProductVersion); + azstrcat(str, length, s); + + if (gEnv->pLog) + { + const char* logfile = gEnv->pLog->GetFileName(); + if (logfile) + { + sprintf (s, "LogFile: %s\n", logfile); + azstrcat(str, length, s); + } + } + + AZ::IO::FixedMaxPathString projectPath = AZ::Utils::GetProjectPath(); + azstrcat(str, length, "ProjectDir: "); + azstrcat(str, length, projectPath.c_str()); + azstrcat(str, length, "\n"); + +#if AZ_LEGACY_CRYSYSTEM_TRAIT_DEBUGCALLSTACK_APPEND_MODULENAME + GetModuleFileNameA(NULL, s, sizeof(s)); + + // Log EXE filename only if possible (not full EXE path which could contain sensitive info) + AZStd::string exeName; + if (AZ::StringFunc::Path::GetFullFileName(s, exeName)) + { + azstrcat(str, length, "Executable: "); + azstrcat(str, length, exeName.c_str()); + +# ifdef AZ_DEBUG_BUILD + azstrcat(str, length, " (debug: yes"); +# else + azstrcat(str, length, " (debug: no"); +# endif + } +#endif +AZ_POP_DISABLE_WARNING +} + + +//Crash the application, in this way the debug callstack routine will be called and it will create all the necessary files (error.log, dump, and eventually screenshot) +void IDebugCallStack::FatalError(const char* description) +{ + m_bIsFatalError = true; + WriteLineToLog(description); + +#ifndef _RELEASE + bool bShowDebugScreen = g_cvars.sys_no_crash_dialog == 0; + // showing the debug screen is not safe when not called from mainthread + // it normally leads to a infinity recursion followed by a stack overflow, preventing + // useful call stacks, thus they are disabled + bShowDebugScreen = bShowDebugScreen && gEnv->mMainThreadId == CryGetCurrentThreadId(); + if (bShowDebugScreen) + { + EBUS_EVENT(AZ::NativeUI::NativeUIRequestBus, DisplayOkDialog, "Open 3D Engine Fatal Error", description, false); + } +#endif + +#if defined(WIN32) || !defined(_RELEASE) + int* p = 0x0; + PREFAST_SUPPRESS_WARNING(6011) * p = 1; // we're intentionally crashing here +#endif +} + +void IDebugCallStack::WriteLineToLog(const char* format, ...) +{ + va_list ArgList; + char szBuffer[MAX_WARNING_LENGTH]; + va_start(ArgList, format); + vsnprintf_s(szBuffer, sizeof(szBuffer), sizeof(szBuffer) - 1, format, ArgList); + cry_strcat(szBuffer, "\n"); + szBuffer[sizeof(szBuffer) - 1] = '\0'; + va_end(ArgList); + + AZ::IO::HandleType fileHandle = AZ::IO::InvalidHandle; + AZ::IO::FileIOBase::GetDirectInstance()->Open("@Log@\\error.log", AZ::IO::GetOpenModeFromStringMode("a+t"), fileHandle); + if (fileHandle != AZ::IO::InvalidHandle) + { + AZ::IO::FileIOBase::GetDirectInstance()->Write(fileHandle, szBuffer, strlen(szBuffer)); + AZ::IO::FileIOBase::GetDirectInstance()->Flush(fileHandle); + AZ::IO::FileIOBase::GetDirectInstance()->Close(fileHandle); + } +} + +////////////////////////////////////////////////////////////////////////// +void IDebugCallStack::StartMemLog() +{ + AZ::IO::FileIOBase::GetDirectInstance()->Open("@Log@\\memallocfile.log", AZ::IO::OpenMode::ModeWrite, m_memAllocFileHandle); + + assert(m_memAllocFileHandle != AZ::IO::InvalidHandle); +} + +////////////////////////////////////////////////////////////////////////// +void IDebugCallStack::StopMemLog() +{ + if (m_memAllocFileHandle != AZ::IO::InvalidHandle) + { + AZ::IO::FileIOBase::GetDirectInstance()->Close(m_memAllocFileHandle); + m_memAllocFileHandle = AZ::IO::InvalidHandle; + } +} +//#endif //!defined(LINUX) diff --git a/Code/CryEngine/CrySystem/IDebugCallStack.h b/Code/CryEngine/CrySystem/IDebugCallStack.h new file mode 100644 index 0000000000..f181b73913 --- /dev/null +++ b/Code/CryEngine/CrySystem/IDebugCallStack.h @@ -0,0 +1,90 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ +// Original file Copyright Crytek GMBH or its affiliates, used under license. + +// Description : A multiplatform base class for handling errors and collecting call stacks + +#ifndef CRYINCLUDE_CRYSYSTEM_IDEBUGCALLSTACK_H +#define CRYINCLUDE_CRYSYSTEM_IDEBUGCALLSTACK_H +#pragma once + +#include "System.h" + +#if AZ_LEGACY_CRYSYSTEM_TRAIT_FORWARD_EXCEPTION_POINTERS +struct EXCEPTION_POINTERS; +#endif +//! Limits the maximal number of functions in call stack. +enum +{ + MAX_DEBUG_STACK_ENTRIES = 80 +}; + +class IDebugCallStack +{ +public: + // Returns single instance of DebugStack + static IDebugCallStack* instance(); + + virtual int handleException([[maybe_unused]] EXCEPTION_POINTERS* exception_pointer){return 0; } + + // returns the module name of a given address + virtual string GetModuleNameForAddr([[maybe_unused]] void* addr) { return "[unknown]"; } + + // returns the function name of a given address together with source file and line number (if available) of a given address + virtual void GetProcNameForAddr(void* addr, string& procName, void*& baseAddr, string& filename, int& line) + { + filename = "[unknown]"; + line = 0; + baseAddr = addr; +#if defined(PLATFORM_64BIT) + procName.Format("[%016llX]", addr); +#else + procName.Format("[%08X]", addr); +#endif + } + + // returns current filename + virtual string GetCurrentFilename() { return "[unknown]"; } + + //! Dumps Current Call Stack to log. + virtual void LogCallstack(); + //triggers a fatal error, so the DebugCallstack can create the error.log and terminate the application + void FatalError(const char*); + + //Reports a bug and continues execution + virtual void ReportBug(const char*) {} + + virtual void FileCreationCallback(void (* postBackupProcess)()); + + static void WriteLineToLog(const char* format, ...); + + virtual void StartMemLog(); + virtual void StopMemLog(); + +protected: + IDebugCallStack(); + virtual ~IDebugCallStack(); + + static const char* TranslateExceptionCode(DWORD dwExcept); + static void PutVersion(char* str, size_t length); + + bool m_bIsFatalError; + static const char* const s_szFatalErrorCode; + + void (* m_postBackupProcess)(); + + AZ::IO::HandleType m_memAllocFileHandle; +}; + + + +#endif // CRYINCLUDE_CRYSYSTEM_IDEBUGCALLSTACK_H diff --git a/Code/CryEngine/CrySystem/System.h b/Code/CryEngine/CrySystem/System.h index 631d84d934..b91b1ba059 100644 --- a/Code/CryEngine/CrySystem/System.h +++ b/Code/CryEngine/CrySystem/System.h @@ -208,6 +208,7 @@ struct SSystemCVars int sys_no_crash_dialog; int sys_no_error_report_window; int sys_dump_aux_threads; + int sys_WER; int sys_dump_type; int sys_ai; int sys_entitysystem; diff --git a/Code/CryEngine/CrySystem/SystemInit.cpp b/Code/CryEngine/CrySystem/SystemInit.cpp index a2c21ea04c..25d6b9c601 100644 --- a/Code/CryEngine/CrySystem/SystemInit.cpp +++ b/Code/CryEngine/CrySystem/SystemInit.cpp @@ -121,6 +121,10 @@ # include #endif +#ifdef WIN32 +extern LONG WINAPI CryEngineExceptionFilterWER(struct _EXCEPTION_POINTERS* pExceptionPointers); +#endif + #if defined(AZ_RESTRICTED_PLATFORM) #define AZ_RESTRICTED_SECTION SYSTEMINIT_CPP_SECTION_14 #include AZ_RESTRICTED_FILE(SystemInit_cpp) @@ -1484,6 +1488,13 @@ AZ_POP_DISABLE_WARNING InlineInitializationProcessing("CSystem::Init LoadConfigurations"); +#ifdef WIN32 + if (g_cvars.sys_WER) + { + SetUnhandledExceptionFilter(CryEngineExceptionFilterWER); + } +#endif + ////////////////////////////////////////////////////////////////////////// // Localization ////////////////////////////////////////////////////////////////////////// @@ -2020,6 +2031,14 @@ void CSystem::CreateSystemVars() REGISTER_CVAR2("sys_update_profile_time", &g_cvars.sys_update_profile_time, 1.0f, 0, "Time to keep updates timings history for."); REGISTER_CVAR2("sys_no_crash_dialog", &g_cvars.sys_no_crash_dialog, m_bNoCrashDialog, VF_NULL, "Whether to disable the crash dialog window"); REGISTER_CVAR2("sys_no_error_report_window", &g_cvars.sys_no_error_report_window, m_bNoErrorReportWindow, VF_NULL, "Whether to disable the error report list"); +#if defined(_RELEASE) + if (!gEnv->IsDedicated()) + { + REGISTER_CVAR2("sys_WER", &g_cvars.sys_WER, 1, 0, "Enables Windows Error Reporting"); + } +#else + REGISTER_CVAR2("sys_WER", &g_cvars.sys_WER, 0, 0, "Enables Windows Error Reporting"); +#endif #ifdef USE_HTTP_WEBSOCKETS REGISTER_CVAR2("sys_simple_http_base_port", &g_cvars.sys_simple_http_base_port, 1880, VF_REQUIRE_APP_RESTART, diff --git a/Code/CryEngine/CrySystem/SystemWin32.cpp b/Code/CryEngine/CrySystem/SystemWin32.cpp index c974365bfc..eb6aa17532 100644 --- a/Code/CryEngine/CrySystem/SystemWin32.cpp +++ b/Code/CryEngine/CrySystem/SystemWin32.cpp @@ -46,6 +46,8 @@ #include #endif +#include "IDebugCallStack.h" + #if defined(APPLE) || defined(LINUX) #include #endif @@ -355,6 +357,7 @@ void CSystem::FatalError(const char* format, ...) } // Dump callstack. + IDebugCallStack::instance()->FatalError(szBuffer); #endif CryDebugBreak(); @@ -396,6 +399,8 @@ void CSystem::ReportBug([[maybe_unused]] const char* format, ...) va_start(ArgList, format); azvsnprintf(szBuffer + strlen(sPrefix), MAX_WARNING_LENGTH - strlen(sPrefix), format, ArgList); va_end(ArgList); + + IDebugCallStack::instance()->ReportBug(szBuffer); #endif } diff --git a/Code/CryEngine/CrySystem/WindowsErrorReporting.cpp b/Code/CryEngine/CrySystem/WindowsErrorReporting.cpp new file mode 100644 index 0000000000..feaffd42aa --- /dev/null +++ b/Code/CryEngine/CrySystem/WindowsErrorReporting.cpp @@ -0,0 +1,137 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ +// Original file Copyright Crytek GMBH or its affiliates, used under license. + +// Description : Support for Windows Error Reporting (WER) + + +#include "CrySystem_precompiled.h" + +#ifdef WIN32 + +#include "System.h" +#include +#include +#include "errorrep.h" +#include "ISystem.h" + +#include + +static WCHAR szPath[MAX_PATH + 1]; +static WCHAR szFR[] = L"\\System32\\FaultRep.dll"; + +WCHAR* GetFullPathToFaultrepDll(void) +{ + UINT rc = GetSystemWindowsDirectoryW(szPath, ARRAYSIZE(szPath)); + if (rc == 0 || rc > ARRAYSIZE(szPath) - ARRAYSIZE(szFR) - 1) + { + return NULL; + } + + wcscat_s(szPath, szFR); + return szPath; +} + + +typedef BOOL (WINAPI * MINIDUMPWRITEDUMP)(HANDLE hProcess, DWORD dwPid, HANDLE hFile, MINIDUMP_TYPE DumpType, + CONST PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, + CONST PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, + CONST PMINIDUMP_CALLBACK_INFORMATION CallbackParam + ); + +////////////////////////////////////////////////////////////////////////// +LONG WINAPI CryEngineExceptionFilterMiniDump(struct _EXCEPTION_POINTERS* pExceptionPointers, const char* szDumpPath, MINIDUMP_TYPE DumpType) +{ + // note: In debug mode, this dll is loaded on startup anyway, so this should not incur an additional load unless it crashes + // very early during startup. + + fflush(nullptr); // according to MSDN on fflush, calling fflush on null flushes all buffers. + HMODULE hndDBGHelpDLL = LoadLibraryA("DBGHELP.DLL"); + + if (!hndDBGHelpDLL) + { + CryLogAlways("Failed to record DMP file: Could not open DBGHELP.DLL"); + return EXCEPTION_CONTINUE_SEARCH; + } + + MINIDUMPWRITEDUMP dumpFnPtr = (MINIDUMPWRITEDUMP)::GetProcAddress(hndDBGHelpDLL, "MiniDumpWriteDump"); + if (!dumpFnPtr) + { + CryLogAlways("Failed to record DMP file: Unable to find MiniDumpWriteDump in DBGHELP.DLL"); + return EXCEPTION_CONTINUE_SEARCH; + } + + HANDLE hFile = ::CreateFile(szDumpPath, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (hFile == INVALID_HANDLE_VALUE) + { + CryLogAlways("Failed to record DMP file: could not open file '%s' for writing - error code: %d", szDumpPath, GetLastError()); + return EXCEPTION_CONTINUE_SEARCH; + } + + _MINIDUMP_EXCEPTION_INFORMATION ExInfo; + ExInfo.ThreadId = ::GetCurrentThreadId(); + ExInfo.ExceptionPointers = pExceptionPointers; + ExInfo.ClientPointers = NULL; + + BOOL bOK = dumpFnPtr(GetCurrentProcess(), GetCurrentProcessId(), hFile, DumpType, &ExInfo, NULL, NULL); + ::CloseHandle(hFile); + + if (bOK) + { + CryLogAlways("Successfully recorded DMP file: '%s'", szDumpPath); + return EXCEPTION_EXECUTE_HANDLER; // SUCCESS! you can execute your handlers now + } + else + { + CryLogAlways("Failed to record DMP file: '%s' - error code: %d", szDumpPath, GetLastError()); + } + + return EXCEPTION_CONTINUE_SEARCH; +} + +////////////////////////////////////////////////////////////////////////// +LONG WINAPI CryEngineExceptionFilterWER(struct _EXCEPTION_POINTERS* pExceptionPointers) +{ + if (g_cvars.sys_WER > 1) + { + char szScratch [_MAX_PATH]; + const char* szDumpPath = gEnv->pCryPak->AdjustFileName("@log@/CE2Dump.dmp", szScratch, AZ_ARRAY_SIZE(szScratch), 0); + + MINIDUMP_TYPE mdumpValue = (MINIDUMP_TYPE)(MiniDumpNormal); + if (g_cvars.sys_WER > 1) + { + mdumpValue = (MINIDUMP_TYPE)(g_cvars.sys_WER - 2); + } + + return CryEngineExceptionFilterMiniDump(pExceptionPointers, szDumpPath, mdumpValue); + } + + LONG lRet = EXCEPTION_CONTINUE_SEARCH; + WCHAR* psz = GetFullPathToFaultrepDll(); + if (psz) + { + HMODULE hFaultRepDll = LoadLibraryW(psz); + if (hFaultRepDll) + { + pfn_REPORTFAULT pfn = (pfn_REPORTFAULT)GetProcAddress(hFaultRepDll, "ReportFault"); + if (pfn) + { + pfn(pExceptionPointers, 0); + lRet = EXCEPTION_EXECUTE_HANDLER; + } + FreeLibrary(hFaultRepDll); + } + } + return lRet; +} + +#endif // WIN32 diff --git a/Code/CryEngine/CrySystem/crysystem_files.cmake b/Code/CryEngine/CrySystem/crysystem_files.cmake index 6a56339b85..84250de95b 100644 --- a/Code/CryEngine/CrySystem/crysystem_files.cmake +++ b/Code/CryEngine/CrySystem/crysystem_files.cmake @@ -15,6 +15,8 @@ set(FILES CmdLineArg.cpp ConsoleBatchFile.cpp ConsoleHelpGen.cpp + DebugCallStack.cpp + IDebugCallStack.cpp Log.cpp System.cpp SystemCFG.cpp @@ -31,6 +33,8 @@ set(FILES CmdLineArg.h ConsoleBatchFile.h ConsoleHelpGen.h + DebugCallStack.h + IDebugCallStack.h Log.h SimpleStringPool.h CrySystem_precompiled.h @@ -72,4 +76,5 @@ set(FILES ViewSystem/ViewSystem.cpp ViewSystem/ViewSystem.h CrySystem_precompiled.cpp + WindowsErrorReporting.cpp )