@@ -8,6 +8,7 @@
|
||||
|
||||
#include <Multiplayer/MultiplayerConstants.h>
|
||||
#include <Multiplayer/Components/MultiplayerComponent.h>
|
||||
#include <Multiplayer/Components/NetworkHierarchyRootComponent.h>
|
||||
#include <MultiplayerSystemComponent.h>
|
||||
#include <ConnectionData/ClientToServerConnectionData.h>
|
||||
#include <ConnectionData/ServerToClientConnectionData.h>
|
||||
@@ -76,6 +77,7 @@ namespace Multiplayer
|
||||
"The address of the remote server or host to connect to");
|
||||
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");
|
||||
AZ_CVAR(uint16_t, sv_portRange, 999, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The range of ports the host will incrementally attempt to bind to when initializing");
|
||||
AZ_CVAR(AZ::CVarFixedString, sv_map, "nolevel", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The map the server should load");
|
||||
AZ_CVAR(ProtocolType, sv_protocol, ProtocolType::Udp, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "This flag controls whether we use TCP or UDP for game networking");
|
||||
AZ_CVAR(bool, sv_isDedicated, true, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Whether the host command creates an independent or client hosted server");
|
||||
@@ -168,6 +170,7 @@ namespace Multiplayer
|
||||
AZ::ConsoleFunctorFlags flags,
|
||||
AZ::ConsoleInvokedFrom invokedFrom
|
||||
) { OnConsoleCommandInvoked(command, args, flags, invokedFrom); })
|
||||
, m_autonomousEntityReplicatorCreatedHandler([this]([[maybe_unused]] NetEntityId netEntityId) { OnAutonomousEntityReplicatorCreated(); })
|
||||
{
|
||||
AZ::Interface<IMultiplayer>::Register(this);
|
||||
}
|
||||
@@ -205,8 +208,23 @@ namespace Multiplayer
|
||||
|
||||
bool MultiplayerSystemComponent::StartHosting(uint16_t port, bool isDedicated)
|
||||
{
|
||||
InitializeMultiplayer(isDedicated ? MultiplayerAgentType::DedicatedServer : MultiplayerAgentType::ClientServer);
|
||||
return m_networkInterface->Listen(port);
|
||||
if (port != sv_port)
|
||||
{
|
||||
sv_port = port;
|
||||
}
|
||||
|
||||
const uint16_t maxPort = sv_port + sv_portRange;
|
||||
while (sv_port <= maxPort)
|
||||
{
|
||||
if (m_networkInterface->Listen(sv_port))
|
||||
{
|
||||
InitializeMultiplayer(isDedicated ? MultiplayerAgentType::DedicatedServer : MultiplayerAgentType::ClientServer);
|
||||
return true;
|
||||
}
|
||||
AZLOG_WARN("Failed to start listening on port %u, port is in use?", static_cast<uint32_t>(sv_port));
|
||||
sv_port = sv_port + 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MultiplayerSystemComponent::Connect(const AZStd::string& remoteAddress, uint16_t port)
|
||||
@@ -328,6 +346,11 @@ namespace Multiplayer
|
||||
|
||||
void MultiplayerSystemComponent::OnTick(float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time)
|
||||
{
|
||||
if (bg_multiplayerDebugDraw)
|
||||
{
|
||||
m_networkEntityManager.DebugDraw();
|
||||
}
|
||||
|
||||
const AZ::TimeMs deltaTimeMs = aznumeric_cast<AZ::TimeMs>(static_cast<int32_t>(deltaTime * 1000.0f));
|
||||
const AZ::TimeMs serverRateMs = static_cast<AZ::TimeMs>(sv_serverSendRateMs);
|
||||
const float serverRateSeconds = static_cast<float>(serverRateMs) / 1000.0f;
|
||||
@@ -412,11 +435,6 @@ namespace Multiplayer
|
||||
{
|
||||
m_networkInterface->GetConnectionSet().VisitConnections(visitor);
|
||||
}
|
||||
|
||||
if (bg_multiplayerDebugDraw)
|
||||
{
|
||||
m_networkEntityManager.DebugDraw();
|
||||
}
|
||||
}
|
||||
|
||||
int MultiplayerSystemComponent::GetTickOrder()
|
||||
@@ -487,17 +505,39 @@ namespace Multiplayer
|
||||
auto visitor = [](IConnection& connection) { connection.Disconnect(DisconnectReason::TerminatedByUser, TerminationEndpoint::Local); };
|
||||
m_networkInterface->GetConnectionSet().VisitConnections(visitor);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
reinterpret_cast<ServerToClientConnectionData*>(connection->GetUserData())->SetProviderTicket(packet.GetTicket().c_str());
|
||||
|
||||
// Hosts will spawn a new default player prefab for the user that just connected
|
||||
if (GetAgentType() == MultiplayerAgentType::ClientServer
|
||||
|| GetAgentType() == MultiplayerAgentType::DedicatedServer)
|
||||
{
|
||||
// We use a temporary userId over the clients address so we can maintain client lookups even in the event of wifi handoff
|
||||
NetworkEntityHandle controlledEntity = SpawnDefaultPlayerPrefab(packet.GetTemporaryUserId());
|
||||
EnableAutonomousControl(controlledEntity, connection->GetConnectionId());
|
||||
|
||||
ServerToClientConnectionData* connectionData = reinterpret_cast<ServerToClientConnectionData*>(connection->GetUserData());
|
||||
AZStd::unique_ptr<IReplicationWindow> window = AZStd::make_unique<ServerToClientReplicationWindow>(controlledEntity, connection);
|
||||
connectionData->GetReplicationManager().SetReplicationWindow(AZStd::move(window));
|
||||
connectionData->SetControlledEntity(controlledEntity);
|
||||
|
||||
// If this is a migrate or rejoin, immediately ready the connection for updates
|
||||
if (packet.GetTemporaryUserId() != 0)
|
||||
{
|
||||
connectionData->SetCanSendUpdates(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (connection->SendReliablePacket(MultiplayerPackets::Accept(sv_map)))
|
||||
{
|
||||
reinterpret_cast<ServerToClientConnectionData*>(connection->GetUserData())->SetDidHandshake(true);
|
||||
|
||||
// Sync our console
|
||||
ConsoleReplicator consoleReplicator(connection);
|
||||
AZ::Interface<AZ::IConsole>::Get()->VisitRegisteredFunctors([&consoleReplicator](AZ::ConsoleFunctorBase* functor) { consoleReplicator.Visit(functor); });
|
||||
if (packet.GetTemporaryUserId() == 0)
|
||||
{
|
||||
// Sync our console
|
||||
ConsoleReplicator consoleReplicator(connection);
|
||||
AZ::Interface<AZ::IConsole>::Get()->VisitRegisteredFunctors([&consoleReplicator](AZ::ConsoleFunctorBase* functor) { consoleReplicator.Visit(functor); });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -511,10 +551,26 @@ namespace Multiplayer
|
||||
)
|
||||
{
|
||||
reinterpret_cast<ClientToServerConnectionData*>(connection->GetUserData())->SetDidHandshake(true);
|
||||
AZ::CVarFixedString commandString = "sv_map " + packet.GetMap();
|
||||
AZ::Interface<AZ::IConsole>::Get()->PerformCommand(commandString.c_str());
|
||||
AZ::CVarFixedString loadLevelString = "LoadLevel " + packet.GetMap();
|
||||
AZ::Interface<AZ::IConsole>::Get()->PerformCommand(loadLevelString.c_str());
|
||||
if (m_temporaryUserIdentifier == 0)
|
||||
{
|
||||
AZ::CVarFixedString commandString = "sv_map " + packet.GetMap();
|
||||
AZ::Interface<AZ::IConsole>::Get()->PerformCommand(commandString.c_str());
|
||||
AZ::CVarFixedString loadLevelString = "LoadLevel " + packet.GetMap();
|
||||
AZ::Interface<AZ::IConsole>::Get()->PerformCommand(loadLevelString.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Bypass map loading and immediately ready the connection for updates
|
||||
IConnectionData* connectionData = reinterpret_cast<IConnectionData*>(connection->GetUserData());
|
||||
if (connectionData)
|
||||
{
|
||||
connectionData->SetCanSendUpdates(true);
|
||||
|
||||
// @nt: TODO - delete once dropped RPC problem fixed
|
||||
// Connection has migrated, we are now waiting for the autonomous entity replicator to be created
|
||||
connectionData->GetReplicationManager().AddAutonomousEntityReplicatorCreatedHandler(m_autonomousEntityReplicatorCreatedHandler);
|
||||
}
|
||||
}
|
||||
|
||||
m_serverAcceptanceReceivedEvent.Signal();
|
||||
return true;
|
||||
@@ -637,13 +693,17 @@ namespace Multiplayer
|
||||
|
||||
// Store the temporary user identifier so we can transmit it with our next Connect packet
|
||||
// The new server will use this to re-attach our set of autonomous entities
|
||||
m_temporaryUserIdentifier = packet.GetTemporaryUserIdentifier();
|
||||
|
||||
// Disconnect our existing server connection
|
||||
auto visitor = [](IConnection& connection) { connection.Disconnect(DisconnectReason::ClientMigrated, TerminationEndpoint::Local); };
|
||||
m_networkInterface->GetConnectionSet().VisitConnections(visitor);
|
||||
AZLOG_INFO("Migrating to new server shard");
|
||||
m_clientMigrationStartEvent.Signal(packet.GetLastClientInputId());
|
||||
m_networkInterface->Connect(packet.GetRemoteServerAddress());
|
||||
if (m_networkInterface->Connect(packet.GetRemoteServerAddress()) == AzNetworking::InvalidConnectionId)
|
||||
{
|
||||
AZLOG_ERROR("Failed to connect to new host during client migration event");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -668,7 +728,7 @@ namespace Multiplayer
|
||||
providerTicket = m_pendingConnectionTickets.front();
|
||||
m_pendingConnectionTickets.pop();
|
||||
}
|
||||
connection->SendReliablePacket(MultiplayerPackets::Connect(0, providerTicket.c_str()));
|
||||
connection->SendReliablePacket(MultiplayerPackets::Connect(0, m_temporaryUserIdentifier, providerTicket.c_str()));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -681,20 +741,10 @@ namespace Multiplayer
|
||||
m_connectionAcquiredEvent.Signal(datum);
|
||||
}
|
||||
|
||||
// Hosts will spawn a new default player prefab for the user that just connected
|
||||
if (GetAgentType() == MultiplayerAgentType::ClientServer
|
||||
|| GetAgentType() == MultiplayerAgentType::DedicatedServer)
|
||||
{
|
||||
NetworkEntityHandle controlledEntity = SpawnDefaultPlayerPrefab();
|
||||
if (controlledEntity.Exists())
|
||||
{
|
||||
controlledEntity.GetNetBindComponent()->SetOwningConnectionId(connection->GetConnectionId());
|
||||
}
|
||||
controlledEntity.Activate();
|
||||
|
||||
connection->SetUserData(new ServerToClientConnectionData(connection, *this, controlledEntity));
|
||||
AZStd::unique_ptr<IReplicationWindow> window = AZStd::make_unique<ServerToClientReplicationWindow>(controlledEntity, connection);
|
||||
reinterpret_cast<ServerToClientConnectionData*>(connection->GetUserData())->GetReplicationManager().SetReplicationWindow(AZStd::move(window));
|
||||
connection->SetUserData(new ServerToClientConnectionData(connection, *this));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -716,9 +766,9 @@ namespace Multiplayer
|
||||
|
||||
void MultiplayerSystemComponent::OnDisconnect(AzNetworking::IConnection* connection, DisconnectReason reason, TerminationEndpoint endpoint)
|
||||
{
|
||||
const char* endpointString = (endpoint == TerminationEndpoint::Local) ? "Disconnecting" : "Remote host disconnected";
|
||||
const char* endpointString = (endpoint == TerminationEndpoint::Local) ? "Disconnecting" : "Remotely disconnected";
|
||||
AZStd::string reasonString = ToString(reason);
|
||||
AZLOG_INFO("%s due to %s from remote address: %s", endpointString, reasonString.c_str(), connection->GetRemoteAddress().GetString().c_str());
|
||||
AZLOG_INFO("%s from remote address %s due to %s", endpointString, connection->GetRemoteAddress().GetString().c_str(), reasonString.c_str());
|
||||
|
||||
// The client is disconnecting
|
||||
if (m_agentType == MultiplayerAgentType::Client)
|
||||
@@ -799,12 +849,8 @@ namespace Multiplayer
|
||||
// Spawn the default player for this host since the host is also a player (not a dedicated server)
|
||||
if (m_agentType == MultiplayerAgentType::ClientServer)
|
||||
{
|
||||
NetworkEntityHandle controlledEntity = SpawnDefaultPlayerPrefab();
|
||||
if (NetBindComponent* controlledEntityNetBindComponent = controlledEntity.GetNetBindComponent())
|
||||
{
|
||||
controlledEntityNetBindComponent->SetAllowAutonomy(true);
|
||||
}
|
||||
controlledEntity.Activate();
|
||||
NetworkEntityHandle controlledEntity = SpawnDefaultPlayerPrefab(0);
|
||||
EnableAutonomousControl(controlledEntity, AzNetworking::InvalidConnectionId);
|
||||
}
|
||||
|
||||
AZLOG_INFO("Multiplayer operating in %s mode", GetEnumString(m_agentType));
|
||||
@@ -855,9 +901,9 @@ namespace Multiplayer
|
||||
handler.Connect(m_shutdownEvent);
|
||||
}
|
||||
|
||||
void MultiplayerSystemComponent::SendNotifyClientMigrationEvent(const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId)
|
||||
void MultiplayerSystemComponent::SendNotifyClientMigrationEvent(AzNetworking::ConnectionId connectionId, const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId, NetEntityId controlledEntityId)
|
||||
{
|
||||
m_notifyClientMigrationEvent.Signal(hostId, userIdentifier, lastClientInputId);
|
||||
m_notifyClientMigrationEvent.Signal(connectionId, hostId, userIdentifier, lastClientInputId, controlledEntityId);
|
||||
}
|
||||
|
||||
void MultiplayerSystemComponent::SendNotifyEntityMigrationEvent(const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId)
|
||||
@@ -911,6 +957,22 @@ namespace Multiplayer
|
||||
return m_filterEntityManager;
|
||||
}
|
||||
|
||||
void MultiplayerSystemComponent::RegisterPlayerIdentifierForRejoin(uint64_t temporaryUserIdentifier, NetEntityId controlledEntityId)
|
||||
{
|
||||
m_playerRejoinData[temporaryUserIdentifier] = controlledEntityId;
|
||||
}
|
||||
|
||||
void MultiplayerSystemComponent::CompleteClientMigration(uint64_t temporaryUserIdentifier, AzNetworking::ConnectionId connectionId, const HostId& publicHostId, ClientInputId migratedClientInputId)
|
||||
{
|
||||
IConnection* connection = m_networkInterface->GetConnectionSet().GetConnection(connectionId);
|
||||
if (connection != nullptr) // Make sure the player has not disconnected since the start of migration
|
||||
{
|
||||
// Tell the client who to join
|
||||
MultiplayerPackets::ClientMigration clientMigration(publicHostId, temporaryUserIdentifier, migratedClientInputId);
|
||||
connection->SendReliablePacket(clientMigration);
|
||||
}
|
||||
}
|
||||
|
||||
void MultiplayerSystemComponent::SetShouldSpawnNetworkEntities(bool value)
|
||||
{
|
||||
m_spawnNetboundEntities = value;
|
||||
@@ -1041,6 +1103,13 @@ namespace Multiplayer
|
||||
m_cvarCommands.PushBackItem(AZStd::move(replicateString));
|
||||
}
|
||||
|
||||
void MultiplayerSystemComponent::OnAutonomousEntityReplicatorCreated()
|
||||
{
|
||||
m_autonomousEntityReplicatorCreatedHandler.Disconnect();
|
||||
//m_networkEntityManager.GetNetworkEntityAuthorityTracker()->ResetTimeoutTime(AZ::TimeMs{ 2000 });
|
||||
m_clientMigrationEndEvent.Signal();
|
||||
}
|
||||
|
||||
void MultiplayerSystemComponent::ExecuteConsoleCommandList(IConnection* connection, const AZStd::fixed_vector<Multiplayer::LongNetworkString, 32>& commands)
|
||||
{
|
||||
AZ::IConsole* console = AZ::Interface<AZ::IConsole>::Get();
|
||||
@@ -1052,15 +1121,25 @@ namespace Multiplayer
|
||||
}
|
||||
}
|
||||
|
||||
NetworkEntityHandle MultiplayerSystemComponent::SpawnDefaultPlayerPrefab()
|
||||
NetworkEntityHandle MultiplayerSystemComponent::SpawnDefaultPlayerPrefab(uint64_t temporaryUserIdentifier)
|
||||
{
|
||||
const auto node = m_playerRejoinData.find(temporaryUserIdentifier);
|
||||
if (node != m_playerRejoinData.end())
|
||||
{
|
||||
return m_networkEntityManager.GetNetworkEntityTracker()->Get(node->second);
|
||||
}
|
||||
|
||||
// make sure the player prefab path is lowercase (how it's stored in the cache folder)
|
||||
auto sv_defaultPlayerSpawnAssetLowerCase = static_cast<AZ::CVarFixedString>(sv_defaultPlayerSpawnAsset);
|
||||
AZStd::to_lower(sv_defaultPlayerSpawnAssetLowerCase.begin(), sv_defaultPlayerSpawnAssetLowerCase.end());
|
||||
|
||||
PrefabEntityId playerPrefabEntityId(AZ::Name(sv_defaultPlayerSpawnAssetLowerCase.c_str()));
|
||||
PrefabEntityId playerPrefabEntityId(AZ::Name(static_cast<AZ::CVarFixedString>(sv_defaultPlayerSpawnAssetLowerCase).c_str()));
|
||||
INetworkEntityManager::EntityList entityList = m_networkEntityManager.CreateEntitiesImmediate(playerPrefabEntityId, NetEntityRole::Authority, AZ::Transform::CreateIdentity(), Multiplayer::AutoActivate::DoNotActivate);
|
||||
|
||||
for (NetworkEntityHandle subEntity : entityList)
|
||||
{
|
||||
subEntity.Activate();
|
||||
}
|
||||
|
||||
NetworkEntityHandle controlledEntity;
|
||||
if (!entityList.empty())
|
||||
{
|
||||
@@ -1069,11 +1148,45 @@ namespace Multiplayer
|
||||
return controlledEntity;
|
||||
}
|
||||
|
||||
void MultiplayerSystemComponent::EnableAutonomousControl(NetworkEntityHandle entityHandle, AzNetworking::ConnectionId connectionId)
|
||||
{
|
||||
if (!entityHandle.Exists())
|
||||
{
|
||||
AZLOG_WARN("Attempting to enable autonomous control for an invalid entity");
|
||||
return;
|
||||
}
|
||||
|
||||
entityHandle.GetNetBindComponent()->SetOwningConnectionId(connectionId);
|
||||
if (connectionId == InvalidConnectionId)
|
||||
{
|
||||
entityHandle.GetNetBindComponent()->SetAllowAutonomy(true);
|
||||
}
|
||||
|
||||
auto* hierarchyComponent = entityHandle.FindComponent<NetworkHierarchyRootComponent>();
|
||||
if (hierarchyComponent != nullptr)
|
||||
{
|
||||
for (AZ::Entity* subEntity : hierarchyComponent->GetHierarchicalEntities())
|
||||
{
|
||||
NetworkEntityHandle subEntityHandle = NetworkEntityHandle(subEntity);
|
||||
NetBindComponent* subEntityNetBindComponent = subEntityHandle.GetNetBindComponent();
|
||||
|
||||
if (subEntityNetBindComponent != nullptr)
|
||||
{
|
||||
subEntityNetBindComponent->SetOwningConnectionId(connectionId);
|
||||
if (connectionId == InvalidConnectionId)
|
||||
{
|
||||
subEntityNetBindComponent->SetAllowAutonomy(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void host([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments)
|
||||
{
|
||||
if (!AZ::Interface<IMultiplayer>::Get()->StartHosting(sv_port, sv_isDedicated))
|
||||
{
|
||||
AZLOG_ERROR("Failed to start listening on port %u, port is in use?", static_cast<uint32_t>(sv_port));
|
||||
AZLOG_ERROR("Failed to start listening on any allocated port");
|
||||
}
|
||||
}
|
||||
AZ_CONSOLEFREEFUNC(host, AZ::ConsoleFunctorFlags::DontReplicate, "Opens a multiplayer connection as a host for other clients to connect to");
|
||||
|
||||
Reference in New Issue
Block a user