diff --git a/Code/Framework/AzCore/AzCore/Component/ComponentApplication.cpp b/Code/Framework/AzCore/AzCore/Component/ComponentApplication.cpp index b8f84cb0ea..54c3edfcdc 100644 --- a/Code/Framework/AzCore/AzCore/Component/ComponentApplication.cpp +++ b/Code/Framework/AzCore/AzCore/Component/ComponentApplication.cpp @@ -121,9 +121,9 @@ namespace AZ return environment ? environment->Get() : nullptr; } - ComponentApplication::EventLoggerDeleter::EventLoggerDeleter() noexcept= default; + ComponentApplication::EventLoggerDeleter::EventLoggerDeleter() noexcept = default; ComponentApplication::EventLoggerDeleter::EventLoggerDeleter(bool skipDelete) noexcept - : m_skipDelete{skipDelete} + : m_skipDelete{ skipDelete } {} void ComponentApplication::EventLoggerDeleter::operator()(AZ::Debug::LocalFileEventLogger* ptr) { @@ -332,27 +332,27 @@ namespace AZ ->Value("Stack trace always", Debug::AllocationRecords::RECORD_FULL); ec->Class("System memory settings", "Settings for managing application memory usage") ->ClassElement(Edit::ClassElements::EditorData, "") - ->Attribute(Edit::Attributes::AutoExpand, true) + ->Attribute(Edit::Attributes::AutoExpand, true) ->DataElement(Edit::UIHandlers::CheckBox, &Descriptor::m_grabAllMemory, "Allocate all memory at startup", "Allocate all system memory at startup if enabled, or allocate as needed if disabled") ->DataElement(Edit::UIHandlers::CheckBox, &Descriptor::m_allocationRecords, "Record allocations", "Collect information on each allocation made for debugging purposes (ignored in Release builds)") ->DataElement(Edit::UIHandlers::CheckBox, &Descriptor::m_allocationRecordsSaveNames, "Record allocations with name saving", "Saves names/filenames information on each allocation made, useful for tracking down leaks in dynamic modules (ignored in Release builds)") ->DataElement(Edit::UIHandlers::CheckBox, &Descriptor::m_allocationRecordsAttemptDecodeImmediately, "Record allocations and attempt immediate decode", "Decode callstacks for each allocation when they occur, used for tracking allocations that fail decoding. Very expensive. (ignored in Release builds)") ->DataElement(Edit::UIHandlers::ComboBox, &Descriptor::m_recordingMode, "Stack recording mode", "Stack record mode. (Ignored in final builds)") ->DataElement(Edit::UIHandlers::SpinBox, &Descriptor::m_stackRecordLevels, "Stack entries to record", "Number of stack levels to record for each allocation (ignored in Release builds)") - ->Attribute(Edit::Attributes::Step, 1) - ->Attribute(Edit::Attributes::Max, 1024) + ->Attribute(Edit::Attributes::Step, 1) + ->Attribute(Edit::Attributes::Max, 1024) ->DataElement(Edit::UIHandlers::CheckBox, &Descriptor::m_autoIntegrityCheck, "Validate allocations", "Check allocations for integrity on each allocation/free (ignored in Release builds)") ->DataElement(Edit::UIHandlers::CheckBox, &Descriptor::m_markUnallocatedMemory, "Mark freed memory", "Set memory to 0xcd when a block is freed for debugging (ignored in Release builds)") ->DataElement(Edit::UIHandlers::CheckBox, &Descriptor::m_doNotUsePools, "Don't pool allocations", "Pipe pool allocations in system/tree heap (ignored in Release builds)") ->DataElement(Edit::UIHandlers::SpinBox, &Descriptor::m_pageSize, "Page size", "Memory page size in bytes (must be OS page size aligned)") - ->Attribute(Edit::Attributes::Step, 1024) + ->Attribute(Edit::Attributes::Step, 1024) ->DataElement(Edit::UIHandlers::SpinBox, &Descriptor::m_poolPageSize, "Pool page size", "Memory pool page size in bytes (must be a multiple of page size)") - ->Attribute(Edit::Attributes::Max, &Descriptor::m_pageSize) - ->Attribute(Edit::Attributes::Step, 1024) + ->Attribute(Edit::Attributes::Max, &Descriptor::m_pageSize) + ->Attribute(Edit::Attributes::Step, 1024) ->DataElement(Edit::UIHandlers::SpinBox, &Descriptor::m_memoryBlockAlignment, "Block alignment", "Memory block alignment in bytes (must be multiple of the page size)") - ->Attribute(Edit::Attributes::Step, &Descriptor::m_pageSize) + ->Attribute(Edit::Attributes::Step, &Descriptor::m_pageSize) ->DataElement(Edit::UIHandlers::SpinBox, &Descriptor::m_memoryBlocksByteSize, "Block size", "Memory block size in bytes (must be multiple of the page size)") - ->Attribute(Edit::Attributes::Step, &Descriptor::m_pageSize) + ->Attribute(Edit::Attributes::Step, &Descriptor::m_pageSize) ->DataElement(Edit::UIHandlers::SpinBox, &Descriptor::m_reservedOS, "OS reserved memory", "System memory reserved for OS (used only when 'Allocate all memory at startup' is true)") ->DataElement(Edit::UIHandlers::SpinBox, &Descriptor::m_reservedDebug, "Memory reserved for debugger", "System memory reserved for Debug allocator, like memory tracking (used only when 'Allocate all memory at startup' is true)") ->DataElement(Edit::UIHandlers::CheckBox, &Descriptor::m_enableDrilling, "Enable Driller", "Enable Drilling support for the application (ignored in Release builds)") @@ -419,11 +419,11 @@ namespace AZ } else { - azstrcpy(m_commandLineBuffer, AZ_ARRAY_SIZE(m_commandLineBuffer), "no_argv_supplied"); + azstrcpy(m_commandLineBuffer, AZ_ARRAY_SIZE(m_commandLineBuffer), "no_argv_supplied"); // use a "valid" value here. This is because Qt and potentially other third party libraries require // that ArgC be 'at least 1' and that (*argV)[0] be a valid pointer to a real null terminated string. - m_argC = 1; - m_argV = &m_commandLineBufferAddress; + m_argC = 1; + m_argV = &m_commandLineBufferAddress; } // Create the Event logger if it doesn't exist, otherwise reuse the one registered @@ -557,8 +557,8 @@ namespace AZ void ReportBadEngineRoot() { - AZStd::string errorMessage = {"Unable to determine a valid path to the engine.\n" - "Check parameters such as --project-path and --engine-path and make sure they are valid.\n"}; + AZStd::string errorMessage = { "Unable to determine a valid path to the engine.\n" + "Check parameters such as --project-path and --engine-path and make sure they are valid.\n" }; if (auto registry = AZ::SettingsRegistry::Get(); registry != nullptr) { AZ::SettingsRegistryInterface::FixedValueString filePathErrorStr; @@ -614,7 +614,6 @@ namespace AZ AZ_Assert(m_systemEntity, "SystemEntity failed to initialize!"); AddRequiredSystemComponents(m_systemEntity.get()); - //m_currentTime = GetElapsedTimeUs(); m_isStarted = true; return m_systemEntity.get(); } @@ -1372,38 +1371,44 @@ namespace AZ void ComponentApplication::Tick(float deltaOverride /*= -1.f*/) { - AZ_PROFILE_SCOPE(System, "Component application simulation tick"); - AZStd::chrono::system_clock::time_point now = AZStd::chrono::system_clock::now(); - m_deltaTime = 0.0f; - if (now >= m_currentTime) { - AZStd::chrono::duration delta = now - m_currentTime; - m_deltaTime = deltaOverride >= 0.f ? deltaOverride : delta.count(); - } - { - AZ_PROFILE_SCOPE(AzCore, "ComponentApplication::Tick:ExecuteQueuedEvents"); - TickBus::ExecuteQueuedEvents(); - } - m_currentTime = now; - { - AZ_PROFILE_SCOPE(AzCore, "ComponentApplication::Tick:OnTick"); - EBUS_EVENT(TickBus, OnTick, m_deltaTime, ScriptTimePoint(now)); - } + AZ_PROFILE_SCOPE(System, "Component application simulation tick"); - // If tick rate limiting is on, ensure (1 / g_simulation_tick_rate) ms has elapsed since the last frame, - // sleeping if there's still time remaining. - if (g_simulation_tick_rate > 0.f) - { - now = AZStd::chrono::system_clock::now(); + AZStd::chrono::system_clock::time_point now = AZStd::chrono::system_clock::now(); - // Work in microsecond durations here as that's the native measurement time for time_point - constexpr float microsecondsPerSecond = 1000.f * 1000.f; - const AZStd::chrono::microseconds timeBudgetPerTick(static_cast(microsecondsPerSecond / g_simulation_tick_rate)); - AZStd::chrono::microseconds timeUntilNextTick = m_currentTime + timeBudgetPerTick - now; + m_deltaTime = 0.0f; - if (timeUntilNextTick.count() > 0) + if (now >= m_currentTime) { - AZStd::this_thread::sleep_for(timeUntilNextTick); + AZStd::chrono::duration delta = now - m_currentTime; + m_deltaTime = deltaOverride >= 0.f ? deltaOverride : delta.count(); + } + + { + AZ_PROFILE_SCOPE(AzCore, "ComponentApplication::Tick:ExecuteQueuedEvents"); + TickBus::ExecuteQueuedEvents(); + } + m_currentTime = now; + { + AZ_PROFILE_SCOPE(AzCore, "ComponentApplication::Tick:OnTick"); + EBUS_EVENT(TickBus, OnTick, m_deltaTime, ScriptTimePoint(now)); + } + + // If tick rate limiting is on, ensure (1 / g_simulation_tick_rate) ms has elapsed since the last frame, + // sleeping if there's still time remaining. + if (g_simulation_tick_rate > 0.f) + { + now = AZStd::chrono::system_clock::now(); + + // Work in microsecond durations here as that's the native measurement time for time_point + constexpr float microsecondsPerSecond = 1000.f * 1000.f; + const AZStd::chrono::microseconds timeBudgetPerTick(static_cast(microsecondsPerSecond / g_simulation_tick_rate)); + AZStd::chrono::microseconds timeUntilNextTick = m_currentTime + timeBudgetPerTick - now; + + if (timeUntilNextTick.count() > 0) + { + AZStd::this_thread::sleep_for(timeUntilNextTick); + } } } } @@ -1555,5 +1560,4 @@ namespace AZ AZ::SettingsRegistryScriptUtils::ReflectSettingsRegistryToBehaviorContext(*behaviorContext); } } - } // namespace AZ diff --git a/Code/Framework/AzCore/AzCore/Component/ComponentApplication.h b/Code/Framework/AzCore/AzCore/Component/ComponentApplication.h index 969903cdc1..892563fb08 100644 --- a/Code/Framework/AzCore/AzCore/Component/ComponentApplication.h +++ b/Code/Framework/AzCore/AzCore/Component/ComponentApplication.h @@ -5,13 +5,13 @@ * SPDX-License-Identifier: Apache-2.0 OR MIT * */ + #pragma once #include #include #include #include -#include #include #include #include @@ -377,14 +377,14 @@ namespace AZ EntityRemovedEvent m_entityRemovedEvent; EntityAddedEvent m_entityActivatedEvent; EntityRemovedEvent m_entityDeactivatedEvent; - AZ::IConsole* m_console{}; + AZ::IConsole* m_console{}; Descriptor m_descriptor; bool m_isStarted{ false }; bool m_isSystemAllocatorOwner{ false }; bool m_isOSAllocatorOwner{ false }; bool m_ownsConsole{}; - void* m_fixedMemoryBlock{ nullptr }; //!< Pointer to the memory block allocator, so we can free it OnDestroy. - IAllocatorAllocate* m_osAllocator{ nullptr }; + void* m_fixedMemoryBlock{ nullptr }; //!< Pointer to the memory block allocator, so we can free it OnDestroy. + IAllocatorAllocate* m_osAllocator{ nullptr }; EntitySetType m_entities; AZ::IO::FixedMaxPath m_exeDirectory; AZ::IO::FixedMaxPath m_engineRoot; @@ -405,11 +405,11 @@ namespace AZ // we create a buffer that can be written to (up to AZ_MAX_PATH_LEN) and then // pack it with a single param. char m_commandLineBuffer[AZ_MAX_PATH_LEN]; - char* m_commandLineBufferAddress{ m_commandLineBuffer }; + char* m_commandLineBufferAddress{ m_commandLineBuffer }; StartupParameters m_startupParameters; - char** m_argV{ nullptr }; + char** m_argV{ nullptr }; int m_argC{ 0 }; AZ::CommandLine m_commandLine; // < Stores parsed command line supplied to the constructor diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h b/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h index 11f5ac7e47..021907fb7a 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h @@ -45,6 +45,7 @@ namespace Multiplayer using ClientMigrationStartEvent = AZ::Event; using ClientMigrationEndEvent = AZ::Event<>; using ClientDisconnectedEvent = AZ::Event<>; + using NotifyClientMigrationEvent = AZ::Event; using ConnectionAcquiredEvent = AZ::Event; using SessionInitEvent = AZ::Event; using SessionShutdownEvent = AZ::Event; @@ -80,34 +81,38 @@ namespace Multiplayer //! @param state The state of this connection virtual void InitializeMultiplayer(MultiplayerAgentType state) = 0; - //! Starts hosting a server + //! Starts hosting a server. //! @param port The port to listen for connection on //! @param isDedicated Whether the server is dedicated or client hosted //! @return if the application successfully started hosting virtual bool StartHosting(uint16_t port, bool isDedicated = true) = 0; - //! Connects to the specified IP as a Client + //! Connects to the specified IP as a Client. //! @param remoteAddress The domain or IP to connect to //! @param port The port to connect to //! @result if a connection was successfully created virtual bool Connect(AZStd::string remoteAddress, uint16_t port) = 0; - // Disconnects all multiplayer connections, stops listening on the server and invokes handlers appropriate to network context + // Disconnects all multiplayer connections, stops listening on the server and invokes handlers appropriate to network context. //! @param reason The reason for terminating connections virtual void Terminate(AzNetworking::DisconnectReason reason) = 0; - //! Adds a ClientMigrationStartEvent Handler which is invoked at the start of a client migration + //! Adds a ClientMigrationStartEvent Handler which is invoked at the start of a client migration. //! @param handler The ClientMigrationStartEvent Handler to add virtual void AddClientMigrationStartEventHandler(ClientMigrationStartEvent::Handler& handler) = 0; - //! Adds a ClientMigrationEndEvent Handler which is invoked when a client completes migration + //! Adds a ClientMigrationEndEvent Handler which is invoked when a client completes migration. //! @param handler The ClientMigrationEndEvent Handler to add virtual void AddClientMigrationEndEventHandler(ClientMigrationEndEvent::Handler& handler) = 0; - //! Adds a ClientDisconnectedEvent Handler which is invoked on the client when a disconnection occurs + //! 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 NotifyClientMigrationEvent Handler which is invoked when a client migrates from one host to another. + //! @param handler The NotifyClientMigrationEvent Handler to add + virtual void AddNotifyClientMigrationHandler(NotifyClientMigrationEvent::Handler& handler) = 0; + //! Adds a ConnectionAcquiredEvent Handler which is invoked when a new endpoint connects to the session. //! @param handler The ConnectionAcquiredEvent Handler to add virtual void AddConnectionAcquiredHandler(ConnectionAcquiredEvent::Handler& handler) = 0; @@ -120,7 +125,13 @@ namespace Multiplayer //! @param handler The SessionShutdownEvent handler to add virtual void AddSessionShutdownHandler(SessionShutdownEvent::Handler& handler) = 0; - //! Sends a packet telling if entity update messages can be sent + //! Signals a NotifyClientMigrationEvent with the provided parameters. + //! @param hostId the host id of the host the client is migrating to + //! @param userIdentifier the user identifier the client will provide the new host to validate identity + //! @param lastClientInputId the last processed clientInputId by the current host + virtual void SendNotifyClientMigrationEvent(HostId hostId, uint64_t userIdentifier, ClientInputId lastClientInputId); + + //! Sends a packet telling if entity update messages can be sent. //! @param readyForEntityUpdates Ready for entity updates or not virtual void SendReadyForEntityUpdates(bool readyForEntityUpdates) = 0; @@ -132,7 +143,7 @@ namespace Multiplayer //! @return the current server time in milliseconds virtual AZ::TimeMs GetCurrentHostTimeMs() const = 0; - //! Returns the current blend factor for client side interpolation + //! Returns the current blend factor for client side interpolation. //! This value is only relevant on the client and is used to smooth between host frames //! @return the current blend factor virtual float GetCurrentBlendFactor() const = 0; diff --git a/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Header.jinja b/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Header.jinja index 41f0fa1b02..5cfeb250fa 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Header.jinja +++ b/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Header.jinja @@ -403,6 +403,8 @@ namespace {{ Component.attrib['Namespace'] }} : public Multiplayer::MultiplayerController { public: + using ComponentType = {{ ComponentName }}; + {{ ControllerBaseName }}({{ ComponentName }}& owner); ~{{ ControllerBaseName }}() override = default; diff --git a/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml b/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml index c553bc5351..d7602b1a3b 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml @@ -40,8 +40,8 @@ - - - + + + diff --git a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.cpp b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.cpp index 5a9e8b4d4b..ad1bcb5130 100644 --- a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.cpp +++ b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.cpp @@ -7,7 +7,10 @@ */ #include +#include +#include #include +#include namespace Multiplayer { @@ -95,36 +98,31 @@ namespace Multiplayer [[maybe_unused]] AzNetworking::ConnectionId connectionId ) { - //Multiplayer::ServerAddrInfo serverAddr; - //if (gNovaGame->GetMultiplayerworkAgent().GetServerToServerNetwork().GetServerAddrInfoFromConnectionId(newConnectionId, serverAddr) == false) - //{ - // AZLOG_WARN("MigrateClient::Failed to find servershard address, userID:%d", static_cast(GetUserId())); - // return; - //} - // - //Multiplayer::GameTimePoint migratedClientGameTimePoint; - // - //if (m_ControlledEntity != nullptr) - //{ - // if (const PlayerNetworkInputComponent::Authority* pComponent = Multiplayer::FindController(m_ControlledEntity)) - // { - // migratedClientGameTimePoint = pComponent->GetLastInputId().GetServerGameTimePoint(); - // } - //} - // - // generate crypto-rand user identifier, send to both server and client so they can negotiate the autonomous entity to assume predictive control over after migration - //const uint64_t randomUserIdentifier = 0; - // - //// Tell the server a new client is about to join - //MultiplayerPackets::NotifyClientMigration notifyClientMigration(randomUserIdentifier); - //gNovaGame->GetMultiplayerworkAgent().GetServerToServerNetwork().SendReliablePacket(newConnectionId, notifyClientMigration); - // - //// Tell the client who to join - //MultiplayerPackets::ClientMigration clientMigration(randomUserIdentifier, serverAddr, migratedClientGameTimePoint); - //GetConnection()->SendReliablePacket(clientMigration); - // - //m_controlledEntity = nullptr; - //m_canSendUpdates = false; + AzNetworking::IpAddress serverAddress; + // serverAddress = GetHost(remoteHostId).GetAddress(); + + ClientInputId migratedClientInputId = ClientInputId{ 0 }; + if (m_controlledEntity != nullptr) + { + auto controller = m_controlledEntity.FindController(); + if (controller != nullptr) + { + migratedClientInputId = controller->GetLastInputId(); + } + } + + // Generate crypto-rand user identifier, send to both server and client so they can negotiate the autonomous entity to assume predictive control over after migration + const uint64_t randomUserIdentifier = AzNetworking::CryptoRand64(); + + // Tell the new host that a client is about to (re)join + GetMultiplayer()->SendNotifyClientMigrationEvent(remoteHostId, randomUserIdentifier, migratedClientInputId); + + // Tell the client who to join + MultiplayerPackets::ClientMigration clientMigration(serverAddress, randomUserIdentifier, migratedClientInputId); + GetConnection()->SendReliablePacket(clientMigration); + + m_controlledEntity = NetworkEntityHandle(); + m_canSendUpdates = false; } void ServerToClientConnectionData::OnGameplayStarted() diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index c28fb23cc4..dbc93834e8 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -617,7 +617,7 @@ namespace Multiplayer auto visitor = [](IConnection& connection) { connection.Disconnect(DisconnectReason::ClientMigrated, TerminationEndpoint::Local); }; m_networkInterface->GetConnectionSet().VisitConnections(visitor); AZLOG_INFO("Migrating to new server shard"); - //m_clientMigrateStartEvent(packet.GetLastInputGameTimeMs()); + m_clientMigrationStartEvent.Signal(ClientInputId{ 0 }); m_networkInterface->Connect(packet.GetRemoteServerAddress()); return true; } @@ -795,6 +795,11 @@ namespace Multiplayer handler.Connect(m_clientDisconnectedEvent); } + void MultiplayerSystemComponent::AddNotifyClientMigrationHandler(NotifyClientMigrationEvent::Handler& handler) + { + handler.Connect(m_notifyClientMigrationEvent); + } + void MultiplayerSystemComponent::AddConnectionAcquiredHandler(ConnectionAcquiredEvent::Handler& handler) { handler.Connect(m_connectionAcquiredEvent); @@ -810,6 +815,11 @@ namespace Multiplayer handler.Connect(m_shutdownEvent); } + void MultiplayerSystemComponent::SendNotifyClientMigrationEvent(HostId hostId, uint64_t userIdentifier, ClientInputId lastClientInputId) + { + m_notifyClientMigrationEvent.Signal(hostId, userIdentifier, lastClientInputId); + } + void MultiplayerSystemComponent::SendReadyForEntityUpdates(bool readyForEntityUpdates) { IConnectionSet& connectionSet = m_networkInterface->GetConnectionSet(); diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h index 40d806a0e0..bdf6511409 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h @@ -111,9 +111,11 @@ namespace Multiplayer void AddClientMigrationStartEventHandler(ClientMigrationStartEvent::Handler& handler) override; void AddClientMigrationEndEventHandler(ClientMigrationEndEvent::Handler& handler) override; void AddClientDisconnectedHandler(ClientDisconnectedEvent::Handler& handler) override; + void AddNotifyClientMigrationHandler(NotifyClientMigrationEvent::Handler& handler) override; void AddConnectionAcquiredHandler(ConnectionAcquiredEvent::Handler& handler) override; void AddSessionInitHandler(SessionInitEvent::Handler& handler) override; void AddSessionShutdownHandler(SessionShutdownEvent::Handler& handler) override; + void SendNotifyClientMigrationEvent(HostId hostId, uint64_t userIdentifier, ClientInputId lastClientInputId) override; void SendReadyForEntityUpdates(bool readyForEntityUpdates) override; AZ::TimeMs GetCurrentHostTimeMs() const override; float GetCurrentBlendFactor() const override; @@ -154,6 +156,7 @@ namespace Multiplayer ClientDisconnectedEvent m_clientDisconnectedEvent; ClientMigrationStartEvent m_clientMigrationStartEvent; ClientMigrationEndEvent m_clientMigrationEndEvent; + NotifyClientMigrationEvent m_notifyClientMigrationEvent; AZStd::queue m_pendingConnectionTickets;