diff --git a/Mods/SML/Source/SML/Private/Network/NetworkHandler.cpp b/Mods/SML/Source/SML/Private/Network/NetworkHandler.cpp index 96947a91dc..20dd4b14db 100644 --- a/Mods/SML/Source/SML/Private/Network/NetworkHandler.cpp +++ b/Mods/SML/Source/SML/Private/Network/NetworkHandler.cpp @@ -33,8 +33,23 @@ FMessageEntry& UModNetworkHandler::RegisterMessageType(const FMessageType& Messa void UModNetworkHandler::CloseWithFailureMessage(UNetConnection* Connection, const FString& Message) { FString MutableMessage = Message; - FNetControlMessage::Send(Connection, MutableMessage); - Connection->FlushNet(true); + if (Connection->GetDriver()->ServerConnection == Connection) { + // We are a client. Nothing is listening for NMT_Failure on the remote + // so we must "send" ourselves the message. + // We cannot use GEngine->BroadcastNetworkFailure, followed by Connection->Close, + // because UPendingNetGame will then call BroadcastNetworkFailure again with the generic "lost connection" message + + // Build a fake NMT_Failure bunch, only needs to contain a string. + // We don't need to handle byte swapping, the Bunches handle that themselves. + FControlChannelOutBunch Bunch(Connection->Channels[0], false); + Bunch << MutableMessage; + FInBunch InBunch(Connection, Bunch.GetData(), Bunch.GetNumBits()); + Connection->GetDriver()->Notify->NotifyControlMessage(Connection, NMT_Failure, InBunch); + } else { + // We are the server. Send NMT_Failure with the message to the client. + FNetControlMessage::Send(Connection, MutableMessage); + Connection->FlushNet(true); + } } void UModNetworkHandler::SendMessage(UNetConnection* Connection, FMessageType MessageType, FString Data) { @@ -69,7 +84,7 @@ void UModNetworkHandler::SetConnectionSupportsModMessages(UNetConnection* Connec } Connection->FlushNet(true); - + ConnectionMetadata.PendingMessages.Empty(); GConnectionMetadata.AddAnnotation(Connection, ConnectionMetadata); } @@ -108,19 +123,26 @@ void UModNetworkHandler::ReceiveMessage(UNetConnection* Connection, const FStrin /** * SML handshake is done in the following way: * - * Server Client - * | SML_HELLO | - * |------------------------->| - * | SML_HELLO | bSupportsModMessageType = true - * |<-------------------------| - * bSupportsModMessageType = true | | - * | any pending mod messages | - * |<-------------------------| - * | SML_HELLO | - * |------------------------->| - * | any pending mod messages | - * |------------------------->| - * + * Server Client + * | NMT_HELLO | UE initiates connection + * OnClientInitialJoin_Server is called |<-------------------------| OnClientInitialJoin is called + * SML intercepts the NMT_HELLO and sends SML_HELLO | SML_HELLO | + * |------------------------->| + * UE responds to NMT_HELLO | NMT_CHALLENGE | + * |------------------------->| + * | SML_HELLO | SML receives SML_HELLO, responds with SML_HELLO + * |<-------------------------| SML sets bSupportsModMessageType = true + * | (queued SML messages) | SML sends all queued messages + * |<-------------------------| + * | NMT_LOGIN | UE responds to NMT_CHALLENGE + * |<-------------------------| + * SML receives SML_HELLO, responds with SML_HELLO | SML_HELLO | + * SML sets bSupportsModMessageType = true |------------------------->| + * SML sends all queued messages | (queued SML messages) | + * |------------------------->| + * UE responds to NMT_LOGIN | NMT_WELCOME | + * OnWelcomePlayer is called |------------------------->| OnWelcomePlayer_Client is called + * * If the client is not running SML, it will not respond to the SML_HELLO message, * so the server will not mark the connection as supporting mod messages, and never send any mod messages. * @@ -153,15 +175,25 @@ void UModNetworkHandler::InitializePatches() { }); auto MessageHandler = [=](auto& Call, void*, UNetConnection* Connection, uint8 MessageType, class FInBunch& Bunch) { + UModNetworkHandler* NetworkHandler = GEngine->GetEngineSubsystem(); if (MessageType == NMT_Hello) { // NMT_Hello is only received on the server, sent by UPendingNetGame::SendInitialJoin // Initiate the SML handshake FNetControlMessage::Send(Connection, GSML_HELLO); Connection->FlushNet(true); + + NetworkHandler->OnClientInitialJoin_Server().Broadcast(Connection); + } + + if (MessageType == NMT_Welcome) { + // NMT_Welcome is only received on the client, sent by UWorld::WelcomePlayer + + NetworkHandler->OnWelcomePlayer_Client().Broadcast(Connection); } if (MessageType == NMT_DebugText) { + // Part of the SML handshake, normally SML_HELLO is the only message ever sent on this channel. const int64 Pos = Bunch.GetPosBits(); FString Text; @@ -182,7 +214,6 @@ void UModNetworkHandler::InitializePatches() { if (MessageType == NMT_ModMessage) { FString ModId; int32 MessageId; FString Content; if (FNetControlMessage::Receive(Bunch, ModId, MessageId, Content)) { - UModNetworkHandler* NetworkHandler = GEngine->GetEngineSubsystem(); NetworkHandler->ReceiveMessage(Connection, ModId, MessageId, Content); Call.Cancel(); } diff --git a/Mods/SML/Source/SML/Private/Network/SMLNetworkManager.cpp b/Mods/SML/Source/SML/Private/Network/SMLNetworkManager.cpp index 1a5d61eb41..6b89be68ea 100644 --- a/Mods/SML/Source/SML/Private/Network/SMLNetworkManager.cpp +++ b/Mods/SML/Source/SML/Private/Network/SMLNetworkManager.cpp @@ -29,10 +29,13 @@ void FSMLNetworkManager::RegisterMessageTypeAndHandlers() { FMessageEntry& MessageEntry = NetworkHandler->RegisterMessageType(*MessageTypeModInit); MessageEntry.bServerHandled = true; + MessageEntry.bClientHandled = true; MessageEntry.MessageReceived.BindStatic(FSMLNetworkManager::HandleMessageReceived); NetworkHandler->OnClientInitialJoin().AddStatic(FSMLNetworkManager::HandleInitialClientJoin); + NetworkHandler->OnClientInitialJoin_Server().AddStatic(FSMLNetworkManager::HandleInitialClientJoin); NetworkHandler->OnWelcomePlayer().AddStatic(FSMLNetworkManager::HandleWelcomePlayer); + NetworkHandler->OnWelcomePlayer_Client().AddStatic(FSMLNetworkManager::HandleWelcomePlayer_Client); FGameModeEvents::GameModePostLoginEvent.AddStatic(FSMLNetworkManager::HandleGameModePostLogin); } @@ -53,7 +56,11 @@ void FSMLNetworkManager::HandleInitialClientJoin(UNetConnection* Connection) { } void FSMLNetworkManager::HandleWelcomePlayer(UWorld* World, UNetConnection* Connection) { - ValidateSMLConnectionData(Connection); + ValidateSMLConnectionData(Connection, true); +} + +void FSMLNetworkManager::HandleWelcomePlayer_Client(UNetConnection* Connection) { + ValidateSMLConnectionData(Connection, false); } void FSMLNetworkManager::HandleGameModePostLogin(AGameModeBase* GameMode, APlayerController* Controller) { @@ -75,7 +82,7 @@ void FSMLNetworkManager::HandleGameModePostLogin(AGameModeBase* GameMode, APlaye const UNetConnection* NetConnection = CastChecked(Controller->Player); const FConnectionMetadata ConnectionMetadata = GModConnectionMetadata.GetAndRemoveAnnotation( NetConnection ); - RemoteCallObject->ClientInstalledMods.Append(ConnectionMetadata.InstalledClientMods); + RemoteCallObject->ClientInstalledMods.Append(ConnectionMetadata.InstalledRemoteMods); } } } @@ -124,7 +131,7 @@ bool FSMLNetworkManager::HandleModListObject( FConnectionMetadata& Metadata, con FVersion ModVersion = FVersion{}; FString ErrorMessage; if ( ModVersion.ParseVersion(Pair.Value->AsString(), ErrorMessage) ) { - Metadata.InstalledClientMods.Add(Pair.Key, ModVersion); + Metadata.InstalledRemoteMods.Add(Pair.Key, ModVersion); } else { return false; } @@ -133,13 +140,14 @@ bool FSMLNetworkManager::HandleModListObject( FConnectionMetadata& Metadata, con return true; } -void FSMLNetworkManager::ValidateSMLConnectionData(UNetConnection* Connection) +void FSMLNetworkManager::ValidateSMLConnectionData(UNetConnection* Connection, bool IsServer) { const bool bAllowMissingMods = CVarSkipRemoteModListCheck.GetValueOnGameThread(); const FConnectionMetadata SMLMetadata = GModConnectionMetadata.GetAnnotation( Connection ); - TArray ClientMissingMods; + TArray RemoteMissingMods; - if (!SMLMetadata.bIsInitialized && !bAllowMissingMods ) { + if (!SMLMetadata.bIsInitialized && !bAllowMissingMods && IsServer ) { + // TODO: Is joining a modded server with a vanilla client safe? UModNetworkHandler::CloseWithFailureMessage(Connection, TEXT("This server is running Satisfactory Mod Loader, and your client doesn't have it installed.")); return; } @@ -148,39 +156,39 @@ void FSMLNetworkManager::ValidateSMLConnectionData(UNetConnection* Connection) { if ( UModLoadingLibrary* ModLoadingLibrary = GameInstance->GetSubsystem() ) { - // TODO: Do we want to check that the client doesn't have any extra mods compared to the server? - // Doing so would require the client passing the complete mod info to the server, rather than just the version - const TArray Mods = ModLoadingLibrary->GetLoadedMods(); for (const FModInfo& ModInfo : Mods) { - const FVersion* ClientVersion = SMLMetadata.InstalledClientMods.Find( ModInfo.Name ); + const FVersion* ClientVersion = SMLMetadata.InstalledRemoteMods.Find( ModInfo.Name ); const FString ModName = FString::Printf( TEXT("%s (%s)"), *ModInfo.FriendlyName, *ModInfo.Name ); if ( ClientVersion == nullptr ) { - if ( !ModInfo.bRequiredOnRemote ) + // If the mod is not required on the remote, we don't care if it's missing + // We also ignore SML in case we're joining a vanilla server with a modded client + // TODO: Is joining a modded server with a vanilla client safe? + if ( !ModInfo.bRequiredOnRemote || (ModInfo.Name == TEXT("SML") && !IsServer) ) { continue; //Server-side only mod } - ClientMissingMods.Add( ModName ); + RemoteMissingMods.Add( ModName ); continue; } const FVersionRange& RemoteVersion = ModInfo.RemoteVersionRange; if ( !RemoteVersion.Matches(*ClientVersion) ) { - const FString VersionText = FString::Printf( TEXT("required: %s, client: %s"), *RemoteVersion.ToString(), *ClientVersion->ToString() ); - ClientMissingMods.Add( FString::Printf( TEXT("%s: %s"), *ModName, *VersionText ) ); + const FString VersionText = FString::Printf( TEXT("required: %s, %s: %s"), *RemoteVersion.ToString(), IsServer ? TEXT("client") : TEXT("server"), *ClientVersion->ToString() ); + RemoteMissingMods.Add( FString::Printf( TEXT("%s: %s"), *ModName, *VersionText ) ); } } } } - if ( ClientMissingMods.Num() > 0 && !bAllowMissingMods ) + if ( RemoteMissingMods.Num() > 0 && !bAllowMissingMods ) { - const FString JoinedModList = FString::Join( ClientMissingMods, TEXT("\n") ); - const FString Reason = FString::Printf( TEXT("Client missing mods: %s"), *JoinedModList ); + const FString JoinedModList = FString::Join( RemoteMissingMods, TEXT("\n") ); + const FString Reason = FString::Printf( TEXT("%s missing mods: %s"), IsServer ? TEXT("Client") : TEXT("Server"), *JoinedModList ); UModNetworkHandler::CloseWithFailureMessage( Connection, Reason ); } } diff --git a/Mods/SML/Source/SML/Public/Network/NetworkHandler.h b/Mods/SML/Source/SML/Public/Network/NetworkHandler.h index df577c96e0..d08531c0e2 100644 --- a/Mods/SML/Source/SML/Public/Network/NetworkHandler.h +++ b/Mods/SML/Source/SML/Public/Network/NetworkHandler.h @@ -12,7 +12,7 @@ class UNetDriver; DECLARE_LOG_CATEGORY_EXTERN(LogModNetworkHandler, Log, All); DECLARE_DELEGATE_TwoParams(FMessageReceived, class UNetConnection* /*Connection*/, FString /*Data*/); DECLARE_MULTICAST_DELEGATE_TwoParams(FWelcomePlayer, UWorld* /*ServerWorld*/, class UNetConnection* /*Connection*/); -DECLARE_MULTICAST_DELEGATE_OneParam(FClientInitialJoin, class UNetConnection* /*Connection*/); +DECLARE_MULTICAST_DELEGATE_OneParam(FConnectionEvent, class UNetConnection* /*Connection*/); struct FMessageType { FString ModReference; @@ -42,22 +42,38 @@ class SML_API UModNetworkHandler : public UEngineSubsystem { private: TMap> MessageHandlers; FWelcomePlayer WelcomePlayerDelegate; - FClientInitialJoin ClientLoginDelegate; + FConnectionEvent WelcomePlayerDelegateClient; + FConnectionEvent ClientLoginDelegate; + FConnectionEvent ClientLoginDelegateServer; private: void ReceiveMessage(class UNetConnection* Connection, const FString& ModId, int32 MessageId, const FString& Content) const; public: /** - * Delegate called on server when he received client join request and welcomed new player - * You can send additional information to client here, or check information received by client - * before to perform any required validation + * Delegate called on server after it has received and validate the client join request, and has sent the world load information to the client + * You can send additional information to the client here, + * or check information received by client to perform any required validation */ FORCEINLINE FWelcomePlayer& OnWelcomePlayer() { return WelcomePlayerDelegate; } /** - * Delegate called when client has sent initial join request to remote side - * Here you can send additional information to be acknowledged by the server via SendMessage + * Delegate called on the client when it has received the welcome message from the server. + * This is called before the client processes the welcome message, so it has not yet begun the process of loading the map. + * You can send additional information to the server here, + * or check information received by the server to perform any required validation */ - FORCEINLINE FClientInitialJoin& OnClientInitialJoin() { return ClientLoginDelegate; } + FORCEINLINE FConnectionEvent& OnWelcomePlayer_Client() { return WelcomePlayerDelegateClient; } + + /** + * Delegate called on the client after it has sent initial join request to the server + * Here you can queue additional information to be sent to the server after the SML connection is established (see flowchart), using SendMessage + */ + FORCEINLINE FConnectionEvent& OnClientInitialJoin() { return ClientLoginDelegate; } + + /** + * Delegate called on the server when it has received initial join request from client + * Here you can queue additional information to be sent to the client after the SML connection is established (see flowchart), using SendMessage + */ + FORCEINLINE FConnectionEvent& OnClientInitialJoin_Server() { return ClientLoginDelegateServer; } /** * Register new mod message type and return message entry which can be used diff --git a/Mods/SML/Source/SML/Public/Network/SMLConnection/SMLNetworkManager.h b/Mods/SML/Source/SML/Public/Network/SMLConnection/SMLNetworkManager.h index 29858f3c9c..6ffef076ec 100644 --- a/Mods/SML/Source/SML/Public/Network/SMLConnection/SMLNetworkManager.h +++ b/Mods/SML/Source/SML/Public/Network/SMLConnection/SMLNetworkManager.h @@ -5,7 +5,7 @@ struct FConnectionMetadata { bool bIsInitialized{false}; - TMap InstalledClientMods; + TMap InstalledRemoteMods; FORCEINLINE bool IsDefault() const { return !bIsInitialized; } }; @@ -15,11 +15,14 @@ class SML_API FSMLNetworkManager { /** Handles SML message being received on the server side */ static void HandleMessageReceived(class UNetConnection* Connection, FString Data); - /** Handles Initial Join request on Client. Called after sending initial NMT_Hello message with endianness/basic network version to server */ - static void HandleInitialClientJoin(UNetConnection* Connection); + /** Handles Initial Join request on both the Client and the Server. Called after sending (client) or when receiving (server) initial NMT_Hello message with endianness/basic network version */ + static void HandleInitialClientJoin(UNetConnection* Connection); - /** Handles WelcomePlayer call on Server, which is called after key exchange and results in client getting NMT_Welcome */ - static void HandleWelcomePlayer(UWorld* World, UNetConnection* Connection); + /** Handles WelcomePlayer call on Server, which is called after key exchange and results in client getting NMT_Welcome */ + static void HandleWelcomePlayer(UWorld* World, UNetConnection* Connection); + + /** Handles WelcomePlayer call on the Client, which is called before the client starts loading the map specified by the server in NMT_Welcome */ + static void HandleWelcomePlayer_Client(UNetConnection* Connection); /** Handles AGameModeBase::PostLogin call, which is the first place where APlayerController is available and is called after NMT_Join is received from client */ static void HandleGameModePostLogin(class AGameModeBase* GameMode, class APlayerController* Controller); @@ -31,10 +34,10 @@ class SML_API FSMLNetworkManager { static bool HandleModListObject( FConnectionMetadata& Metadata, const FString& ModList); /** Ensures that Connection has required SML initialization data and kicks player off if it doesn't */ - static void ValidateSMLConnectionData(class UNetConnection* Connection); + static void ValidateSMLConnectionData(class UNetConnection* Connection, bool IsServer); private: friend class FSatisfactoryModLoader; static TSharedPtr MessageTypeModInit; static void RegisterMessageTypeAndHandlers(); -}; \ No newline at end of file +};