Skip to content

Commit

Permalink
Add client side check for mods missing on the server
Browse files Browse the repository at this point in the history
  • Loading branch information
mircearoata committed Dec 8, 2024
1 parent e0988f5 commit 87c519b
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 49 deletions.
65 changes: 48 additions & 17 deletions Mods/SML/Source/SML/Private/Network/NetworkHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,23 @@ FMessageEntry& UModNetworkHandler::RegisterMessageType(const FMessageType& Messa

void UModNetworkHandler::CloseWithFailureMessage(UNetConnection* Connection, const FString& Message) {
FString MutableMessage = Message;
FNetControlMessage<NMT_Failure>::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<NMT_Failure>::Send(Connection, MutableMessage);
Connection->FlushNet(true);
}
}

void UModNetworkHandler::SendMessage(UNetConnection* Connection, FMessageType MessageType, FString Data) {
Expand Down Expand Up @@ -69,7 +84,7 @@ void UModNetworkHandler::SetConnectionSupportsModMessages(UNetConnection* Connec
}

Connection->FlushNet(true);

ConnectionMetadata.PendingMessages.Empty();
GConnectionMetadata.AddAnnotation(Connection, ConnectionMetadata);
}
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -153,15 +175,25 @@ void UModNetworkHandler::InitializePatches() {
});

auto MessageHandler = [=](auto& Call, void*, UNetConnection* Connection, uint8 MessageType, class FInBunch& Bunch) {
UModNetworkHandler* NetworkHandler = GEngine->GetEngineSubsystem<UModNetworkHandler>();
if (MessageType == NMT_Hello) {
// NMT_Hello is only received on the server, sent by UPendingNetGame::SendInitialJoin
// Initiate the SML handshake

FNetControlMessage<NMT_DebugText>::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;
Expand All @@ -182,7 +214,6 @@ void UModNetworkHandler::InitializePatches() {
if (MessageType == NMT_ModMessage) {
FString ModId; int32 MessageId; FString Content;
if (FNetControlMessage<NMT_ModMessage>::Receive(Bunch, ModId, MessageId, Content)) {
UModNetworkHandler* NetworkHandler = GEngine->GetEngineSubsystem<UModNetworkHandler>();
NetworkHandler->ReceiveMessage(Connection, ModId, MessageId, Content);
Call.Cancel();
}
Expand Down
42 changes: 25 additions & 17 deletions Mods/SML/Source/SML/Private/Network/SMLNetworkManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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) {
Expand All @@ -75,7 +82,7 @@ void FSMLNetworkManager::HandleGameModePostLogin(AGameModeBase* GameMode, APlaye
const UNetConnection* NetConnection = CastChecked<UNetConnection>(Controller->Player);
const FConnectionMetadata ConnectionMetadata = GModConnectionMetadata.GetAndRemoveAnnotation( NetConnection );

RemoteCallObject->ClientInstalledMods.Append(ConnectionMetadata.InstalledClientMods);
RemoteCallObject->ClientInstalledMods.Append(ConnectionMetadata.InstalledRemoteMods);
}
}
}
Expand Down Expand Up @@ -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;
}
Expand All @@ -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<FString> ClientMissingMods;
TArray<FString> 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;
}
Expand All @@ -148,39 +156,39 @@ void FSMLNetworkManager::ValidateSMLConnectionData(UNetConnection* Connection)
{
if ( UModLoadingLibrary* ModLoadingLibrary = GameInstance->GetSubsystem<UModLoadingLibrary>() )
{
// 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<FModInfo> 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 );
}
}
32 changes: 24 additions & 8 deletions Mods/SML/Source/SML/Public/Network/NetworkHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -42,22 +42,38 @@ class SML_API UModNetworkHandler : public UEngineSubsystem {
private:
TMap<FString, TMap<int32, FMessageEntry>> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

struct FConnectionMetadata {
bool bIsInitialized{false};
TMap<FString, FVersion> InstalledClientMods;
TMap<FString, FVersion> InstalledRemoteMods;

FORCEINLINE bool IsDefault() const { return !bIsInitialized; }
};
Expand All @@ -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);
Expand All @@ -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<struct FMessageType> MessageTypeModInit;

static void RegisterMessageTypeAndHandlers();
};
};

0 comments on commit 87c519b

Please sign in to comment.