diff --git a/Code/Framework/AzFramework/AzFramework/Session/ISessionHandlingRequests.h b/Code/Framework/AzFramework/AzFramework/Session/ISessionHandlingRequests.h index a0731626ef..d55f38f65c 100644 --- a/Code/Framework/AzFramework/AzFramework/Session/ISessionHandlingRequests.h +++ b/Code/Framework/AzFramework/AzFramework/Session/ISessionHandlingRequests.h @@ -46,7 +46,7 @@ namespace AzFramework }; //! ISessionHandlingClientRequests - //! The session handling events to invoke multiplayer component handle the work on client side + //! Requests made to the client to manage their membership in a session class ISessionHandlingClientRequests { public: @@ -63,14 +63,14 @@ namespace AzFramework virtual void RequestPlayerLeaveSession() = 0; }; - //! ISessionHandlingServerRequests - //! The session handling events to invoke server provider handle the work on server side - class ISessionHandlingServerRequests + //! ISessionProviderRequests + //! Requests made to the service providing server/fleet management by the server + class ISessionHandlingProviderRequests { public: - AZ_RTTI(ISessionHandlingServerRequests, "{4F0C17BA-F470-4242-A8CB-EC7EA805257C}"); - ISessionHandlingServerRequests() = default; - virtual ~ISessionHandlingServerRequests() = default; + AZ_RTTI(ISessionHandlingProviderRequests, "{4F0C17BA-F470-4242-A8CB-EC7EA805257C}"); + ISessionHandlingProviderRequests() = default; + virtual ~ISessionHandlingProviderRequests() = default; // Handle the destroy session process virtual void HandleDestroySession() = 0; @@ -84,9 +84,14 @@ namespace AzFramework // @param playerConnectionConfig The required properties to handle the player leave session process virtual void HandlePlayerLeaveSession(const PlayerConnectionConfig& playerConnectionConfig) = 0; - // Retrieves the file location of a pem-encoded TLS certificate + // Retrieves the file location of a pem-encoded TLS certificate for Client to Server communication // @return If successful, returns the file location of TLS certificate file; if not successful, returns // empty string. - virtual AZStd::string GetSessionCertificate() = 0; + virtual AZ::IO::Path GetExternalSessionCertificate() = 0; + + // Retrieves the file location of a pem-encoded TLS certificate for Server to Server communication + // @return If successful, returns the file location of TLS certificate file; if not successful, returns + // empty string. + virtual AZ::IO::Path GetInternalSessionCertificate() = 0; }; } // namespace AzFramework diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h b/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h index 579ca195e5..44472422c7 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h @@ -45,6 +45,7 @@ namespace Multiplayer AzNetworking::ByteBuffer<2048> m_userData; }; + using ClientDisconnectedEvent = AZ::Event<>; using ConnectionAcquiredEvent = AZ::Event; using SessionInitEvent = AZ::Event; using SessionShutdownEvent = AZ::Event; @@ -65,8 +66,12 @@ namespace Multiplayer //! @param state The state of this connection virtual void InitializeMultiplayer(MultiplayerAgentType state) = 0; + //! Adds a ClientDisconnectedEvent Handler which is invoked on the client when a disconnection occurs + //! @param handler The ClientDisconnectedEvent Handler to add + virtual void AddClientDisconnectedHandler(ClientDisconnectedEvent::Handler& handler) = 0; + //! Adds a ConnectionAcquiredEvent Handler which is invoked when a new endpoint connects to the session. - //! @param handler The SessionInitEvent Handler to add + //! @param handler The ConnectionAcquiredEvent Handler to add virtual void AddConnectionAcquiredHandler(ConnectionAcquiredEvent::Handler& handler) = 0; //! Adds a SessionInitEvent Handler which is invoked when a new network session starts. diff --git a/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml b/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml index 642832805d..ce8931107f 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml @@ -9,6 +9,7 @@ + diff --git a/Gems/Multiplayer/Code/Source/ConnectionData/ClientToServerConnectionData.cpp b/Gems/Multiplayer/Code/Source/ConnectionData/ClientToServerConnectionData.cpp index ee308f6ed8..3e2a7ca006 100644 --- a/Gems/Multiplayer/Code/Source/ConnectionData/ClientToServerConnectionData.cpp +++ b/Gems/Multiplayer/Code/Source/ConnectionData/ClientToServerConnectionData.cpp @@ -21,10 +21,12 @@ namespace Multiplayer ClientToServerConnectionData::ClientToServerConnectionData ( AzNetworking::IConnection* connection, - AzNetworking::IConnectionListener& connectionListener + AzNetworking::IConnectionListener& connectionListener, + const AZStd::string& providerTicket ) : m_connection(connection) , m_entityReplicationManager(*connection, connectionListener, EntityReplicationManager::Mode::LocalClientToRemoteServer) + , m_providerTicket(providerTicket) { m_entityReplicationManager.SetMaxRemoteEntitiesPendingCreationCount(cl_ClientMaxRemoteEntitiesPendingCreationCount); m_entityReplicationManager.SetEntityPendingRemovalMs(cl_ClientEntityReplicatorPendingRemovalTimeMs); diff --git a/Gems/Multiplayer/Code/Source/ConnectionData/ClientToServerConnectionData.h b/Gems/Multiplayer/Code/Source/ConnectionData/ClientToServerConnectionData.h index 2e7be47842..e343bc6dfe 100644 --- a/Gems/Multiplayer/Code/Source/ConnectionData/ClientToServerConnectionData.h +++ b/Gems/Multiplayer/Code/Source/ConnectionData/ClientToServerConnectionData.h @@ -24,7 +24,8 @@ namespace Multiplayer ClientToServerConnectionData ( AzNetworking::IConnection* connection, - AzNetworking::IConnectionListener& connectionListener + AzNetworking::IConnectionListener& connectionListener, + const AZStd::string& providerTicket = "" ); ~ClientToServerConnectionData() override; @@ -38,8 +39,12 @@ namespace Multiplayer void SetCanSendUpdates(bool canSendUpdates) override; //! @} + const AZStd::string& GetProviderTicket() const; + void SetProviderTicket(const AZStd::string&); + private: EntityReplicationManager m_entityReplicationManager; + AZStd::string m_providerTicket; AzNetworking::IConnection* m_connection = nullptr; bool m_canSendUpdates = true; }; diff --git a/Gems/Multiplayer/Code/Source/ConnectionData/ClientToServerConnectionData.inl b/Gems/Multiplayer/Code/Source/ConnectionData/ClientToServerConnectionData.inl index 6d4a332b6e..a5982dcdb8 100644 --- a/Gems/Multiplayer/Code/Source/ConnectionData/ClientToServerConnectionData.inl +++ b/Gems/Multiplayer/Code/Source/ConnectionData/ClientToServerConnectionData.inl @@ -21,4 +21,14 @@ namespace Multiplayer { m_canSendUpdates = canSendUpdates; } + + inline const AZStd::string& ClientToServerConnectionData::GetProviderTicket() const + { + return m_providerTicket; + } + + inline void ClientToServerConnectionData::SetProviderTicket(const AZStd::string& ticket) + { + m_providerTicket = ticket; + } } diff --git a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.h b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.h index faa11bc225..dda7c15d69 100644 --- a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.h +++ b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.h @@ -41,6 +41,8 @@ namespace Multiplayer NetworkEntityHandle GetPrimaryPlayerEntity(); const NetworkEntityHandle& GetPrimaryPlayerEntity() const; + const AZStd::string& GetProviderTicket() const; + void SetProviderTicket(const AZStd::string&); private: void OnControlledEntityRemove(); @@ -51,6 +53,7 @@ namespace Multiplayer NetworkEntityHandle m_controlledEntity; EntityStopEvent::Handler m_controlledEntityRemovedHandler; EntityServerMigrationEvent::Handler m_controlledEntityMigrationHandler; + AZStd::string m_providerTicket; AzNetworking::IConnection* m_connection = nullptr; bool m_canSendUpdates = false; }; diff --git a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.inl b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.inl index 0a4215a363..1fb104b311 100644 --- a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.inl +++ b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.inl @@ -32,4 +32,14 @@ namespace Multiplayer { return m_controlledEntity; } + + inline const AZStd::string& ServerToClientConnectionData::GetProviderTicket() const + { + return m_providerTicket; + } + + inline void ServerToClientConnectionData::SetProviderTicket(const AZStd::string& ticket) + { + m_providerTicket = ticket; + } } diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index 069ac49d96..e4256a875c 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -30,11 +30,12 @@ #include #include #include - #include +#include +#include +#include #include #include -#include #include @@ -116,6 +117,31 @@ namespace Multiplayer behaviorContext->Class(); behaviorContext->Class(); behaviorContext->Class(); + + behaviorContext->Class("MultiplayerSystemComponent") + ->Attribute(AZ::Script::Attributes::Module, "multiplayer") + ->Attribute(AZ::Script::Attributes::Category, "Multiplayer") + ->Method("GetOnClientDisconnectedEvent", [](AZ::EntityId id) -> AZ::Event<>* + { + AZ::Entity* entity = AZ::Interface::Get()->FindEntity(id); + if (!entity) + { + AZ_Warning("Multiplayer Property", false, "MultiplayerSystemComponent GetOnClientDisconnectedEvent failed. The entity with id %s doesn't exist, please provide a valid entity id.", id.ToString().c_str()) + return nullptr; + } + + MultiplayerSystemComponent* mpComponent = entity->FindComponent(); + if (!mpComponent) + { + AZ_Warning("Multiplayer Property", false, "MultiplayerSystemComponent GetOnClientDisconnected failed. Entity '%s' (id: %s) is missing MultiplayerSystemComponent, be sure to add MultiplayerSystemComponent to this entity.", entity->GetName().c_str(), id.ToString().c_str()) + return nullptr; + } + + return &mpComponent->m_clientDisconnectedEvent; + }) + ->Attribute( + AZ::Script::Attributes::AzEventDescription, + AZ::BehaviorAzEventDescription{"On Client Disconnected Event"}); } MultiplayerComponent::Reflect(context); @@ -151,9 +177,11 @@ namespace Multiplayer void MultiplayerSystemComponent::Activate() { AZ::TickBus::Handler::BusConnect(); + AzFramework::SessionNotificationBus::Handler::BusConnect(); 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); + AZ::Interface::Register(this); //! Register our gems multiplayer components to assign NetComponentIds RegisterMultiplayerComponents(); @@ -161,10 +189,90 @@ namespace Multiplayer void MultiplayerSystemComponent::Deactivate() { + AZ::Interface::Unregister(this); AZ::Interface::Unregister(this); + AzFramework::SessionNotificationBus::Handler::BusDisconnect(); AZ::TickBus::Handler::BusDisconnect(); } + bool MultiplayerSystemComponent::RequestPlayerJoinSession(const AzFramework::SessionConnectionConfig& config) + { + AZ::Interface::Get()->InitializeMultiplayer(MultiplayerAgentType::Client); + + AZStd::string hostname = config.m_dnsName.empty() ? config.m_ipAddress : config.m_dnsName; + const IpAddress ipAddress(hostname.c_str(), config.m_port, m_networkInterface->GetType()); + ConnectionId connectionId = m_networkInterface->Connect(ipAddress); + + AzNetworking::IConnection* connection = m_networkInterface->GetConnectionSet().GetConnection(connectionId); + if (connection->GetUserData() == nullptr) // Only add user data if the connect event handler has not already done so + { + connection->SetUserData(new ClientToServerConnectionData(connection, *this, config.m_playerSessionId)); + } + else + { + reinterpret_cast(connection->GetUserData())->SetProviderTicket(config.m_playerSessionId); + } + + return true; + } + + void MultiplayerSystemComponent::RequestPlayerLeaveSession() + { + if (GetAgentType() == MultiplayerAgentType::Client) + { + AZ::Interface::Get()->InitializeMultiplayer(MultiplayerAgentType::Uninitialized); + auto visitor = [](IConnection& connection) + { + connection.Disconnect(DisconnectReason::TerminatedByUser, TerminationEndpoint::Local); + }; + m_networkInterface->GetConnectionSet().VisitConnections(visitor); + } + } + + bool MultiplayerSystemComponent::OnSessionHealthCheck() + { + return true; + } + + bool MultiplayerSystemComponent::OnCreateSessionBegin(const AzFramework::SessionConfig& sessionConfig) + { + // Check if session manager has a certificate for us and pass it along if so + if (AZ::Interface::Get() != nullptr) + { + AZ::CVarFixedString externalCertPath = AZ::CVarFixedString( + AZ::Interface::Get()->GetExternalSessionCertificate().c_str()); + if (!externalCertPath.empty()) + { + AZ::CVarFixedString commandString = "net_SslExternalCertificateFile " + externalCertPath; + AZ::Interface::Get()->PerformCommand(commandString.c_str()); + } + + AZ::CVarFixedString internalCertPath = AZ::CVarFixedString( + AZ::Interface::Get()->GetInternalSessionCertificate().c_str()); + if (!internalCertPath.empty()) + { + AZ::CVarFixedString commandString = "net_SslInternalCertificateFile " + internalCertPath; + AZ::Interface::Get()->PerformCommand(commandString.c_str()); + } + } + + Multiplayer::MultiplayerAgentType serverType = sv_isDedicated ? MultiplayerAgentType::DedicatedServer : MultiplayerAgentType::ClientServer; + AZ::Interface::Get()->InitializeMultiplayer(serverType); + return m_networkInterface->Listen(sessionConfig.m_port); + } + + bool MultiplayerSystemComponent::OnDestroySessionBegin() + { + bool disconnectSuccessful = true; + IConnectionSet& connectionSet = m_networkInterface->GetConnectionSet(); + connectionSet.VisitConnections([&disconnectSuccessful](IConnection& connection) + { + bool didDisconnect = connection.Disconnect(DisconnectReason::TerminatedByServer, TerminationEndpoint::Remote); + disconnectSuccessful = disconnectSuccessful && didDisconnect; + }); + return disconnectSuccessful; + } + void MultiplayerSystemComponent::OnTick(float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) { const AZ::TimeMs deltaTimeMs = aznumeric_cast(static_cast(deltaTime * 1000.0f)); @@ -306,6 +414,22 @@ namespace Multiplayer [[maybe_unused]] MultiplayerPackets::Connect& packet ) { + // Validate our session with the provider if any + if (AZ::Interface::Get() != nullptr) + { + AzFramework::PlayerConnectionConfig config; + config.m_playerConnectionId = aznumeric_cast(connection->GetConnectionId()); + config.m_playerSessionId = packet.GetTicket(); + if(!AZ::Interface::Get()->ValidatePlayerJoinSession(config)) + { + auto visitor = [](IConnection& connection) { connection.Disconnect(DisconnectReason::TerminatedByUser, TerminationEndpoint::Local); }; + m_networkInterface->GetConnectionSet().VisitConnections(visitor); + return true; + } + + reinterpret_cast(connection->GetUserData())->SetProviderTicket(packet.GetTicket().c_str()); + } + if (connection->SendReliablePacket(MultiplayerPackets::Accept(InvalidHostId, sv_map))) { // Sync our console @@ -463,7 +587,12 @@ namespace Multiplayer if (connection->GetConnectionRole() == ConnectionRole::Connector) { AZLOG_INFO("New outgoing connection to remote address: %s", connection->GetRemoteAddress().GetString().c_str()); - connection->SendReliablePacket(MultiplayerPackets::Connect(0)); + AZ::CVarFixedString providerTicket; + if (connection->GetUserData() != nullptr) + { + providerTicket = reinterpret_cast(connection->GetUserData())->GetProviderTicket(); + } + connection->SendReliablePacket(MultiplayerPackets::Connect(0, providerTicket)); } else { @@ -517,11 +646,16 @@ namespace Multiplayer AZStd::string reasonString = ToString(reason); AZLOG_INFO("%s due to %s from remote address: %s", endpointString, reasonString.c_str(), connection->GetRemoteAddress().GetString().c_str()); - // The authority is shutting down its connection if (connection->GetConnectionRole() == ConnectionRole::Acceptor) { + // The authority is shutting down its connection m_shutdownEvent.Signal(m_networkInterface); } + else if (GetAgentType() == MultiplayerAgentType::Client && connection->GetConnectionRole() == ConnectionRole::Connector) + { + // The client is disconnecting + m_clientDisconnectedEvent.Signal(); + } // Clean up any multiplayer connection data we've bound to this connection instance if (connection->GetUserData() != nullptr) @@ -530,6 +664,30 @@ namespace Multiplayer delete connectionData; connection->SetUserData(nullptr); } + + // Signal to session management that a user has left the server + if (m_agentType == MultiplayerAgentType::DedicatedServer || m_agentType == MultiplayerAgentType::ClientServer) + { + if (AZ::Interface::Get() != nullptr && + connection->GetConnectionRole() == ConnectionRole::Connector) + { + AzFramework::PlayerConnectionConfig config; + config.m_playerConnectionId = aznumeric_cast(connection->GetConnectionId()); + config.m_playerSessionId = reinterpret_cast(connection->GetUserData())->GetProviderTicket(); + AZ::Interface::Get()->HandlePlayerLeaveSession(config); + } + } + + // Signal to session management when there are no remaining players in a dedicated server for potential cleanup + // We avoid this for client server as the host itself is a user + if (m_agentType == MultiplayerAgentType::DedicatedServer && connection->GetConnectionRole() == ConnectionRole::Connector) + { + if (AZ::Interface::Get() != nullptr + && m_networkInterface->GetConnectionSet().GetConnectionCount() == 0) + { + AZ::Interface::Get()->HandleDestroySession(); + } + } } MultiplayerAgentType MultiplayerSystemComponent::GetAgentType() const @@ -566,6 +724,11 @@ namespace Multiplayer AZLOG_INFO("Multiplayer operating in %s mode", GetEnumString(m_agentType)); } + void MultiplayerSystemComponent::AddClientDisconnectedHandler(ClientDisconnectedEvent::Handler& handler) + { + handler.Connect(m_clientDisconnectedEvent); + } + void MultiplayerSystemComponent::AddConnectionAcquiredHandler(ConnectionAcquiredEvent::Handler& handler) { handler.Connect(m_connAcquiredEvent); diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h index 3b1faafd40..0efef3ebe4 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h @@ -25,8 +25,15 @@ #include #include #include +#include +#include #include +namespace AzFramework +{ + struct SessionConfig; +} + namespace AzNetworking { class INetworkInterface; @@ -38,6 +45,8 @@ namespace Multiplayer class MultiplayerSystemComponent final : public AZ::Component , public AZ::TickBus::Handler + , public AzFramework::SessionNotificationBus::Handler + , public AzFramework::ISessionHandlingClientRequests , public AzNetworking::IConnectionListener , public IMultiplayer { @@ -58,6 +67,13 @@ namespace Multiplayer void Deactivate() override; //! @} + //! AzFramework::SessionNotificationBus::Handler overrides. + //! @{ + bool OnSessionHealthCheck() override; + bool OnCreateSessionBegin(const AzFramework::SessionConfig& sessionConfig) override; + bool OnDestroySessionBegin() override; + //! @} + //! AZ::TickBus::Handler overrides. //! @{ void OnTick(float deltaTime, AZ::ScriptTimePoint time) override; @@ -82,10 +98,17 @@ namespace Multiplayer void OnDisconnect(AzNetworking::IConnection* connection, AzNetworking::DisconnectReason reason, AzNetworking::TerminationEndpoint endpoint) override; //! @} + //! ISessionHandlingClientRequests interface + //! @{ + bool RequestPlayerJoinSession(const AzFramework::SessionConnectionConfig& sessionConnectionConfig) override; + void RequestPlayerLeaveSession() override; + //! @} + //! IMultiplayer interface //! @{ MultiplayerAgentType GetAgentType() const override; void InitializeMultiplayer(MultiplayerAgentType state) override; + void AddClientDisconnectedHandler(ClientDisconnectedEvent::Handler& handler) override; void AddConnectionAcquiredHandler(ConnectionAcquiredEvent::Handler& handler) override; void AddSessionInitHandler(SessionInitEvent::Handler& handler) override; void AddSessionShutdownHandler(SessionShutdownEvent::Handler& handler) override; @@ -121,6 +144,7 @@ namespace Multiplayer SessionInitEvent m_initEvent; SessionShutdownEvent m_shutdownEvent; ConnectionAcquiredEvent m_connAcquiredEvent; + ClientDisconnectedEvent m_clientDisconnectedEvent; AZ::TimeMs m_lastReplicatedHostTimeMs = AZ::TimeMs{ 0 }; HostFrameId m_lastReplicatedHostFrameId = InvalidHostFrameId;