diff --git a/.gitignore b/.gitignore index 7f934cc..917aee4 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,7 @@ SourceArt/**/*.tga # Binary Files Binaries/* Plugins/*/Binaries/* +Plugins/Developer/ # Builds Build/* diff --git a/Plugins/MultiplayerSessions/Content/WBP_Menu.uasset b/Plugins/MultiplayerSessions/Content/WBP_Menu.uasset new file mode 100644 index 0000000..22c4fa6 Binary files /dev/null and b/Plugins/MultiplayerSessions/Content/WBP_Menu.uasset differ diff --git a/Plugins/MultiplayerSessions/MultiplayerSessions.uplugin b/Plugins/MultiplayerSessions/MultiplayerSessions.uplugin new file mode 100644 index 0000000..6c4eb6f --- /dev/null +++ b/Plugins/MultiplayerSessions/MultiplayerSessions.uplugin @@ -0,0 +1,34 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.0", + "FriendlyName": "MultiplayerSessions", + "Description": "A plugin for handling online multiplayer sessions", + "Category": "Other", + "CreatedBy": "Bert", + "CreatedByURL": "", + "DocsURL": "", + "MarketplaceURL": "", + "SupportURL": "", + "CanContainContent": true, + "IsBetaVersion": false, + "IsExperimentalVersion": false, + "Installed": false, + "Modules": [ + { + "Name": "MultiplayerSessions", + "Type": "Runtime", + "LoadingPhase": "Default" + } + ], + "Plugins": [ + { + "Name": "OnlineSubsystem", + "Enabled": true + }, + { + "Name": "OnlineSubsystemSteam", + "Enabled": true + } + ] +} \ No newline at end of file diff --git a/Plugins/MultiplayerSessions/Resources/Icon128.png b/Plugins/MultiplayerSessions/Resources/Icon128.png new file mode 100644 index 0000000..1231d4a Binary files /dev/null and b/Plugins/MultiplayerSessions/Resources/Icon128.png differ diff --git a/Plugins/MultiplayerSessions/Source/MultiplayerSessions/MultiplayerSessions.Build.cs b/Plugins/MultiplayerSessions/Source/MultiplayerSessions/MultiplayerSessions.Build.cs new file mode 100644 index 0000000..2a4fa9f --- /dev/null +++ b/Plugins/MultiplayerSessions/Source/MultiplayerSessions/MultiplayerSessions.Build.cs @@ -0,0 +1,58 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class MultiplayerSessions : ModuleRules +{ + public MultiplayerSessions(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new string[] { + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "OnlineSubsystem", + "OnlineSubsystemSteam", + "UMG", + "Slate", + "SlateCore" + // ... add other public dependencies that you statically link with here ... + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore" + // ... add private dependencies that you statically link with here ... + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + } +} diff --git a/Plugins/MultiplayerSessions/Source/MultiplayerSessions/Private/Menu.cpp b/Plugins/MultiplayerSessions/Source/MultiplayerSessions/Private/Menu.cpp new file mode 100644 index 0000000..0c7fd94 --- /dev/null +++ b/Plugins/MultiplayerSessions/Source/MultiplayerSessions/Private/Menu.cpp @@ -0,0 +1,186 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "Menu.h" +#include "Components/Button.h" +#include "MultiplayerSessionsSubsystem.h" +#include "OnlineSessionSettings.h" +#include "OnlineSubsystem.h" + +void UMenu::MenuSetup(int32 NumberOfPublicConnections, FString TypeOfMatch) +{ + NumPublicConnections = NumberOfPublicConnections; + MatchType = TypeOfMatch; + + AddToViewport(); + SetVisibility(ESlateVisibility::Visible); + bIsFocusable = true; + + UWorld* World = GetWorld(); + if (World) + { + APlayerController* PlayerController = World->GetFirstPlayerController(); + if (PlayerController) + { + FInputModeUIOnly InputModeData; + InputModeData.SetWidgetToFocus(TakeWidget()); + InputModeData.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock); + PlayerController->SetInputMode(InputModeData); + PlayerController->SetShowMouseCursor(true); + } + } + + UGameInstance* GameInstance = GetGameInstance(); + if (GetGameInstance()) + { + MultiplayerSessionsSubsystem = GameInstance->GetSubsystem(); + } + + if (MultiplayerSessionsSubsystem != nullptr) + { + MultiplayerSessionsSubsystem->MultiplayerOnCreateSessionComplete.AddDynamic(this, &ThisClass::OnCreateSession); + MultiplayerSessionsSubsystem->MultiplayerOnFindSessionsComplete.AddUObject(this, &ThisClass::OnFindSessions); + MultiplayerSessionsSubsystem->MultiplayerOnJoinSessionComplete.AddUObject(this, &ThisClass::OnJoinSession); + MultiplayerSessionsSubsystem->MultiplayerOnDestroySessionComplete.AddDynamic(this, &ThisClass::OnDestroySession); + MultiplayerSessionsSubsystem->MultiplayerOnStartSessionComplete.AddDynamic(this, &ThisClass::OnStartSession); + } +} + +bool UMenu::Initialize() +{ + if (!Super::Initialize()) + { + return false; + } + + if (HostButton) + { + HostButton->OnClicked.AddDynamic(this, &UMenu::HostButtonClicked); + } + + if (JoinButton) + { + JoinButton->OnClicked.AddDynamic(this, &UMenu::JoinButtonClicked); + } + + return true; +} + +void UMenu::OnLevelRemovedFromWorld(ULevel* InLevel, UWorld* InWorld) +{ + MenuTearDown(); + + Super::OnLevelRemovedFromWorld(InLevel, InWorld); +} + +void UMenu::OnCreateSession(bool bWasSuccessful) +{ + if (bWasSuccessful) + { + if (GEngine) + { + GEngine->AddOnScreenDebugMessage( + -1, + 15.f, + FColor::Green, + FString(TEXT("Session created successfully!"))); + } + + UWorld* World = GetWorld(); + if (World) + { + World->ServerTravel(FString("/Game/ThirdPerson/Maps/Lobby?listen")); + } + } + else + { + if (GEngine) + { + GEngine->AddOnScreenDebugMessage( + -1, + 15.f, + FColor::Red, + FString(TEXT("Failed to create session!"))); + } + } +} + +void UMenu::OnFindSessions(const TArray& SessionResults, bool bWasSuccessful) +{ + if (MultiplayerSessionsSubsystem == nullptr) + { + return; + } + + for (auto Result : SessionResults) + { + FString SettingsValue; + Result.Session.SessionSettings.Get(FName("MatchType"), SettingsValue); + if (SettingsValue == MatchType) + { + MultiplayerSessionsSubsystem->JoinSession(Result); + return; + } + } +} + +void UMenu::OnJoinSession(EOnJoinSessionCompleteResult::Type Result) +{ + IOnlineSubsystem* Subsystem = IOnlineSubsystem::Get(); + if (Subsystem) + { + IOnlineSessionPtr SessionInterface = Subsystem->GetSessionInterface(); + + if (SessionInterface.IsValid()) + { + FString Address; + SessionInterface->GetResolvedConnectString(NAME_GameSession, Address); + + APlayerController* PlayerController = GetGameInstance()->GetFirstLocalPlayerController(); + if (PlayerController) + { + PlayerController->ClientTravel(Address, ETravelType::TRAVEL_Absolute); + } + } + } +} + +void UMenu::OnDestroySession(bool bWasSuccessful) +{ +} + +void UMenu::OnStartSession(bool bWasSuccessful) +{ +} + +void UMenu::HostButtonClicked() +{ + if (MultiplayerSessionsSubsystem) + { + MultiplayerSessionsSubsystem->CreateSession(NumPublicConnections, MatchType); + } +} + +void UMenu::JoinButtonClicked() +{ + if (MultiplayerSessionsSubsystem) + { + MultiplayerSessionsSubsystem->FindSessions(10000); + } +} + +void UMenu::MenuTearDown() +{ + RemoveFromParent(); + UWorld* World = GetWorld(); + if (World) + { + APlayerController* PlayerController = World->GetFirstPlayerController(); + if (PlayerController) + { + FInputModeGameOnly InputModeData; + PlayerController->SetInputMode(InputModeData); + PlayerController->SetShowMouseCursor(false); + } + } +} diff --git a/Plugins/MultiplayerSessions/Source/MultiplayerSessions/Private/MultiplayerSessions.cpp b/Plugins/MultiplayerSessions/Source/MultiplayerSessions/Private/MultiplayerSessions.cpp new file mode 100644 index 0000000..42c4b08 --- /dev/null +++ b/Plugins/MultiplayerSessions/Source/MultiplayerSessions/Private/MultiplayerSessions.cpp @@ -0,0 +1,20 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "MultiplayerSessions.h" + +#define LOCTEXT_NAMESPACE "FMultiplayerSessionsModule" + +void FMultiplayerSessionsModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module +} + +void FMultiplayerSessionsModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FMultiplayerSessionsModule, MultiplayerSessions) \ No newline at end of file diff --git a/Plugins/MultiplayerSessions/Source/MultiplayerSessions/Private/MultiplayerSessionsSubsystem.cpp b/Plugins/MultiplayerSessions/Source/MultiplayerSessions/Private/MultiplayerSessionsSubsystem.cpp new file mode 100644 index 0000000..30561ca --- /dev/null +++ b/Plugins/MultiplayerSessions/Source/MultiplayerSessions/Private/MultiplayerSessionsSubsystem.cpp @@ -0,0 +1,153 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "MultiplayerSessionsSubsystem.h" + +#include "OnlineSubsystem.h" +#include "OnlineSessionSettings.h" + +UMultiplayerSessionsSubsystem::UMultiplayerSessionsSubsystem(): + CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionComplete)), + FindSessionsCompleteDelegate(FOnFindSessionsCompleteDelegate::CreateUObject(this, &ThisClass::OnFindSessionsComplete)), + JoinSessionCompleteDelegate(FOnJoinSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnJoinSessionComplete)), + DestroySessionCompleteDelegate(FOnDestroySessionCompleteDelegate::CreateUObject(this, &ThisClass::OnDestroySessionComplete)), + StartSessionCompleteDelegate(FOnStartSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnStartSessionComplete)) +{ + IOnlineSubsystem* Subsystem = IOnlineSubsystem::Get(); + if (Subsystem) + { + SessionInterface = Subsystem->GetSessionInterface(); + } + +} + +void UMultiplayerSessionsSubsystem::CreateSession(int32 NumPublicConnections, FString MatchType) +{ + if (!SessionInterface.IsValid()) + { + return; + } + + auto ExistingSession = SessionInterface->GetNamedSession(NAME_GameSession); + if (ExistingSession != nullptr) + { + SessionInterface->DestroySession(NAME_GameSession); + } + + // Store the delegate in a FDelegateHandle so we can later remove it from the delegate list + CreateSessionCompleteDelegateHandle = SessionInterface->AddOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegate); + + LastSessionSettings = MakeShareable(new FOnlineSessionSettings()); + LastSessionSettings->bIsLANMatch = IOnlineSubsystem::Get()->GetSubsystemName() == "NULL" ? true : false; + LastSessionSettings->NumPublicConnections = NumPublicConnections; + LastSessionSettings->bAllowJoinInProgress = true; + LastSessionSettings->bAllowJoinViaPresence = true; + LastSessionSettings->bShouldAdvertise = true; + LastSessionSettings->bUsesPresence = true; + LastSessionSettings->bUseLobbiesIfAvailable = true; // For UE5 when not finding sessions + LastSessionSettings->Set(FName("MatchType"), FString(MatchType), EOnlineDataAdvertisementType::ViaOnlineServiceAndPing); + LastSessionSettings->BuildUniqueId = 1; + + const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); + if (!SessionInterface->CreateSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, *LastSessionSettings)) + { + SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegateHandle); + + // Broadcast our own custom delegate + MultiplayerOnCreateSessionComplete.Broadcast(false); + } +} + +void UMultiplayerSessionsSubsystem::FindSessions(int32 MaxSearchResults) +{ + if (!SessionInterface.IsValid()) + { + return; + } + + FindSessionCompleteDelegateHandle = SessionInterface->AddOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteDelegate); + + LastSessionSearch = MakeShareable(new FOnlineSessionSearch); + LastSessionSearch->MaxSearchResults = MaxSearchResults; + LastSessionSearch->bIsLanQuery = IOnlineSubsystem::Get()->GetSubsystemName() == "NULL" ? true : false; + LastSessionSearch->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals); + + const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); + if (!SessionInterface->FindSessions(*LocalPlayer->GetPreferredUniqueNetId(), LastSessionSearch.ToSharedRef())) + { + SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle(FindSessionCompleteDelegateHandle); + + MultiplayerOnFindSessionsComplete.Broadcast(TArray(), false); + } +} + +void UMultiplayerSessionsSubsystem::JoinSession(const FOnlineSessionSearchResult& SessionResult) +{ + if (!SessionInterface.IsValid()) + { + MultiplayerOnJoinSessionComplete.Broadcast(EOnJoinSessionCompleteResult::UnknownError); + return; + } + + JoinSessionCompleteDelegateHandle = SessionInterface->AddOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegate); + + const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); + if (!SessionInterface->JoinSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, SessionResult)) + { + SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegateHandle); + + MultiplayerOnJoinSessionComplete.Broadcast(EOnJoinSessionCompleteResult::UnknownError); + } +} + +void UMultiplayerSessionsSubsystem::DestroySession() +{ +} + +void UMultiplayerSessionsSubsystem::StartSession() +{ +} + +void UMultiplayerSessionsSubsystem::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful) +{ + if (SessionInterface) + { + SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegateHandle); + } + + MultiplayerOnCreateSessionComplete.Broadcast(bWasSuccessful); +} + +void UMultiplayerSessionsSubsystem::OnFindSessionsComplete(bool bWasSuccessful) +{ + if (SessionInterface) + { + SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle(FindSessionCompleteDelegateHandle); + } + + if (LastSessionSearch->SearchResults.Num() <= 0) + { + MultiplayerOnFindSessionsComplete.Broadcast(TArray(), false); + return; + } + + MultiplayerOnFindSessionsComplete.Broadcast(LastSessionSearch->SearchResults, bWasSuccessful); +} + +void UMultiplayerSessionsSubsystem::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result) +{ + if (SessionInterface) + { + SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegateHandle); + } + + MultiplayerOnJoinSessionComplete.Broadcast(Result); +} + +void UMultiplayerSessionsSubsystem::OnDestroySessionComplete(FName SessionName, bool bWasSuccessful) +{ +} + +void UMultiplayerSessionsSubsystem::OnStartSessionComplete(FName SessionNAme, bool bWasSuccessful) +{ +} diff --git a/Plugins/MultiplayerSessions/Source/MultiplayerSessions/Public/Menu.h b/Plugins/MultiplayerSessions/Source/MultiplayerSessions/Public/Menu.h new file mode 100644 index 0000000..a53cbd5 --- /dev/null +++ b/Plugins/MultiplayerSessions/Source/MultiplayerSessions/Public/Menu.h @@ -0,0 +1,61 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "Interfaces/OnlineSessionInterface.h" +#include "Menu.generated.h" + +/** + * + */ +UCLASS() +class MULTIPLAYERSESSIONS_API UMenu : public UUserWidget +{ + GENERATED_BODY() + +public: + + UFUNCTION(BlueprintCallable) + void MenuSetup(int32 NumberOfPublicConnections = 4, FString TypeOfMatch = FString(TEXT("FreeForAll"))); + +protected: + + virtual bool Initialize() override; + virtual void OnLevelRemovedFromWorld(ULevel* InLevel, UWorld* InWorld) override; + + /* + * Callbacks fot the custom delegates on the MultiplayerSessionsSubsystem + */ + UFUNCTION() + void OnCreateSession(bool bWasSuccessful); + void OnFindSessions(const TArray& SessionResults, bool bWasSuccessful); + void OnJoinSession(EOnJoinSessionCompleteResult::Type Result); + UFUNCTION() + void OnDestroySession(bool bWasSuccessful); + UFUNCTION() + void OnStartSession(bool bWasSuccessful); + +private: + + UPROPERTY(meta=(BindWidget)) + class UButton* HostButton; + + UPROPERTY(meta=(BindWidget)) + UButton* JoinButton; + + UFUNCTION() + void HostButtonClicked(); + + UFUNCTION() + void JoinButtonClicked(); + + void MenuTearDown(); + + // The subsystem designed to handle all online session functionality + class UMultiplayerSessionsSubsystem* MultiplayerSessionsSubsystem; + + int32 NumPublicConnections {4}; + FString MatchType {TEXT("FreeForAll")}; +}; diff --git a/Plugins/MultiplayerSessions/Source/MultiplayerSessions/Public/MultiplayerSessions.h b/Plugins/MultiplayerSessions/Source/MultiplayerSessions/Public/MultiplayerSessions.h new file mode 100644 index 0000000..c32ff38 --- /dev/null +++ b/Plugins/MultiplayerSessions/Source/MultiplayerSessions/Public/MultiplayerSessions.h @@ -0,0 +1,15 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +class FMultiplayerSessionsModule : public IModuleInterface +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/Plugins/MultiplayerSessions/Source/MultiplayerSessions/Public/MultiplayerSessionsSubsystem.h b/Plugins/MultiplayerSessions/Source/MultiplayerSessions/Public/MultiplayerSessionsSubsystem.h new file mode 100644 index 0000000..7db1c11 --- /dev/null +++ b/Plugins/MultiplayerSessions/Source/MultiplayerSessions/Public/MultiplayerSessionsSubsystem.h @@ -0,0 +1,81 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "OnlineSessionSettings.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "Interfaces/OnlineSessionInterface.h" +#include "MultiplayerSessionsSubsystem.generated.h" + + +// Declaring our own custom delegates for the Menu class to bind callbacks to +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMultiplayerOnCreateSessionComplete, bool, bWasSuccessful); +DECLARE_MULTICAST_DELEGATE_TwoParams(FMultiplayerOnFindSessionsComplete, const TArray& SessionResults, bool bWasSuccessful); +DECLARE_MULTICAST_DELEGATE_OneParam(FMultiplayerOnJoinSessionComplete, EOnJoinSessionCompleteResult::Type Result) +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMultiplayerOnDestroySessionComplete, bool, bWasSuccessful); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMultiplayerOnStartSessionComplete, bool, bWasSuccessful); + +/** + * + */ +UCLASS() +class MULTIPLAYERSESSIONS_API UMultiplayerSessionsSubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + + UMultiplayerSessionsSubsystem(); + + /* + * To handle session functionality. The Menu class will call these + */ + void CreateSession(int32 NumPublicConnections, FString MatchType); + void FindSessions(int32 MaxSearchResults); + void JoinSession(const FOnlineSessionSearchResult& SessionResult); + void DestroySession(); + void StartSession(); + + /* + * Our own custom delegates for the Menu class to bind callbacks to + */ + FMultiplayerOnCreateSessionComplete MultiplayerOnCreateSessionComplete; + FMultiplayerOnFindSessionsComplete MultiplayerOnFindSessionsComplete; + FMultiplayerOnJoinSessionComplete MultiplayerOnJoinSessionComplete; + FMultiplayerOnDestroySessionComplete MultiplayerOnDestroySessionComplete; + FMultiplayerOnStartSessionComplete MultiplayerOnStartSessionComplete; + +protected: + + /* + * Internal callbacks for the delegates we'll add to the Online Session Interface delegate list. + * These don't need to be called outside this class + */ + void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful); + void OnFindSessionsComplete(bool bWasSuccessful); + void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result); + void OnDestroySessionComplete(FName SessionName, bool bWasSuccessful); + void OnStartSessionComplete(FName SessionNAme, bool bWasSuccessful); + +private: + + IOnlineSessionPtr SessionInterface; + TSharedPtr LastSessionSettings; + TSharedPtr LastSessionSearch; + + /* + * To add to the Online Session Interface delegate list. + * We'll bind our MultiplayerSessionSubsystem internal callbacks to these. + */ + FOnCreateSessionCompleteDelegate CreateSessionCompleteDelegate; + FDelegateHandle CreateSessionCompleteDelegateHandle; + FOnFindSessionsCompleteDelegate FindSessionsCompleteDelegate; + FDelegateHandle FindSessionCompleteDelegateHandle; + FOnJoinSessionCompleteDelegate JoinSessionCompleteDelegate; + FDelegateHandle JoinSessionCompleteDelegateHandle; + FOnDestroySessionCompleteDelegate DestroySessionCompleteDelegate; + FDelegateHandle DestroySessionCompleteDelegateHandle; + FOnStartSessionCompleteDelegate StartSessionCompleteDelegate; + FDelegateHandle StartSessionCompleteDelegateHandle; +};