448 lines
15 KiB
C++
448 lines
15 KiB
C++
// Fill out your copyright notice in the Description page of Project Settings.
|
|
|
|
|
|
#include "SessionSubsystem.h"
|
|
|
|
#include "GameFramework/PlayerController.h"
|
|
#include "Interfaces/OnlineExternalUIInterface.h"
|
|
#include "Kismet/GameplayStatics.h"
|
|
#include "Online/OnlineSessionNames.h"
|
|
#include "OnlineSessionSettings.h"
|
|
#include "OnlineSubsystem.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "Session"
|
|
|
|
// Namespace NOMME et non anonyme : le build unity concatene les .cpp du module,
|
|
// et deux constantes homonymes dans deux fichiers deviennent une redefinition
|
|
// qui ne sort qu'au premier rebuild complet.
|
|
namespace SessionSubsystemConstants
|
|
{
|
|
/**
|
|
* Cle plantee dans les reglages de session et redemandee a la recherche.
|
|
* Sans elle on tomberait sur les parties de n'importe quel autre jeu partageant
|
|
* l'AppId de test 480 (Spacewar), qui sont legion.
|
|
*/
|
|
const FName GameKeyName(TEXT("EMBERWILD"));
|
|
const TCHAR* GameKeyValue = TEXT("Coop");
|
|
|
|
/** Le service charge quand Steam n'est pas disponible. */
|
|
const FName NullServiceName(TEXT("NULL"));
|
|
}
|
|
|
|
void USessionSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
|
{
|
|
Super::Initialize(Collection);
|
|
|
|
IOnlineSubsystem* Online = IOnlineSubsystem::Get();
|
|
if (!Online)
|
|
{
|
|
UE_LOG(LogTemp, Warning, TEXT("USessionSubsystem : aucun OnlineSubsystem charge, la coop sera indisponible."));
|
|
return;
|
|
}
|
|
|
|
SessionInterface = Online->GetSessionInterface();
|
|
if (!SessionInterface.IsValid())
|
|
{
|
|
UE_LOG(LogTemp, Warning, TEXT("USessionSubsystem : le service [%s] n'expose pas d'interface de session."), *Online->GetSubsystemName().ToString());
|
|
return;
|
|
}
|
|
|
|
InviteAcceptedHandle = SessionInterface->AddOnSessionUserInviteAcceptedDelegate_Handle(
|
|
FOnSessionUserInviteAcceptedDelegate::CreateUObject(this, &USessionSubsystem::HandleInviteAccepted));
|
|
|
|
UE_LOG(LogTemp, Log, TEXT("USessionSubsystem : service en ligne [%s], mode LAN [%d]."),
|
|
*Online->GetSubsystemName().ToString(), IsLanMode() ? 1 : 0);
|
|
}
|
|
|
|
void USessionSubsystem::Deinitialize()
|
|
{
|
|
if (SessionInterface.IsValid())
|
|
{
|
|
ClearPendingDelegates();
|
|
SessionInterface->ClearOnSessionUserInviteAcceptedDelegate_Handle(InviteAcceptedHandle);
|
|
}
|
|
|
|
SearchSettings.Reset();
|
|
SessionInterface.Reset();
|
|
|
|
Super::Deinitialize();
|
|
}
|
|
|
|
bool USessionSubsystem::IsLanMode() const
|
|
{
|
|
const IOnlineSubsystem* Online = IOnlineSubsystem::Get();
|
|
return !Online || Online->GetSubsystemName() == SessionSubsystemConstants::NullServiceName;
|
|
}
|
|
|
|
FString USessionSubsystem::GetOnlineServiceName() const
|
|
{
|
|
const IOnlineSubsystem* Online = IOnlineSubsystem::Get();
|
|
return Online ? Online->GetSubsystemName().ToString() : TEXT("None");
|
|
}
|
|
|
|
bool USessionSubsystem::IsSessionActive() const
|
|
{
|
|
return SessionInterface.IsValid() && SessionInterface->GetNamedSession(NAME_GameSession) != nullptr;
|
|
}
|
|
|
|
int32 USessionSubsystem::GetFoundSessionCount() const
|
|
{
|
|
return SearchSettings.IsValid() ? SearchSettings->SearchResults.Num() : 0;
|
|
}
|
|
|
|
void USessionSubsystem::ClearPendingDelegates()
|
|
{
|
|
if (!SessionInterface.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteHandle);
|
|
SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteHandle);
|
|
SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteHandle);
|
|
SessionInterface->ClearOnDestroySessionCompleteDelegate_Handle(DestroySessionCompleteHandle);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Hebergement
|
|
// ----------------------------------------------------------------------------
|
|
|
|
void USessionSubsystem::HostSession(const TSoftObjectPtr<UWorld>& Level, int32 MaxPlayers)
|
|
{
|
|
if (!SessionInterface.IsValid())
|
|
{
|
|
OnHostFailed.Broadcast(LOCTEXT("HostNoService", "No online service available."));
|
|
return;
|
|
}
|
|
|
|
if (Level.IsNull())
|
|
{
|
|
UE_LOG(LogTemp, Error, TEXT("USessionSubsystem::HostSession : aucune map fournie."));
|
|
OnHostFailed.Broadcast(LOCTEXT("HostNoLevel", "No level configured for the game."));
|
|
return;
|
|
}
|
|
|
|
PendingHostLevel = Level;
|
|
PendingMaxPlayers = FMath::Clamp(MaxPlayers, 1, 16);
|
|
|
|
// Une session survit a un retour au menu si personne ne l'a detruite. Le
|
|
// moteur en refuse alors une seconde du meme nom, et l'echec est muet.
|
|
if (SessionInterface->GetNamedSession(NAME_GameSession) != nullptr)
|
|
{
|
|
bCreateAfterDestroy = true;
|
|
DestroySessionCompleteHandle = SessionInterface->AddOnDestroySessionCompleteDelegate_Handle(
|
|
FOnDestroySessionCompleteDelegate::CreateUObject(this, &USessionSubsystem::HandleDestroySessionComplete));
|
|
|
|
SessionInterface->DestroySession(NAME_GameSession);
|
|
return;
|
|
}
|
|
|
|
CreateSessionNow();
|
|
}
|
|
|
|
void USessionSubsystem::CreateSessionNow()
|
|
{
|
|
FOnlineSessionSettings Settings;
|
|
|
|
Settings.NumPublicConnections = PendingMaxPlayers;
|
|
Settings.NumPrivateConnections = 0;
|
|
Settings.bIsLANMatch = IsLanMode();
|
|
Settings.bShouldAdvertise = true;
|
|
Settings.bAllowJoinInProgress = true;
|
|
Settings.bAllowJoinViaPresence = true;
|
|
Settings.bAllowInvites = true;
|
|
Settings.bUsesPresence = true;
|
|
|
|
// LE drapeau a ne pas oublier. Steam ne route le trafic P2P que par un LOBBY :
|
|
// sans lui la session est bien creee, elle apparait meme dans une recherche,
|
|
// mais aucune connexion n'aboutit en dehors du LAN.
|
|
Settings.bUseLobbiesIfAvailable = true;
|
|
Settings.bUseLobbiesVoiceChatIfAvailable = false;
|
|
|
|
Settings.bUsesStats = false;
|
|
Settings.bAntiCheatProtected = false;
|
|
|
|
Settings.Set(SETTING_MAPNAME, PendingHostLevel.GetAssetName(), EOnlineDataAdvertisementType::ViaOnlineServiceAndPing);
|
|
Settings.Set(SessionSubsystemConstants::GameKeyName, FString(SessionSubsystemConstants::GameKeyValue), EOnlineDataAdvertisementType::ViaOnlineServiceAndPing);
|
|
|
|
CreateSessionCompleteHandle = SessionInterface->AddOnCreateSessionCompleteDelegate_Handle(
|
|
FOnCreateSessionCompleteDelegate::CreateUObject(this, &USessionSubsystem::HandleCreateSessionComplete));
|
|
|
|
// CreateSession rend false quand l'appel n'a meme pas pu partir : dans ce cas
|
|
// le callback ne viendra jamais, il faut donc debrancher et repondre ici.
|
|
if (!SessionInterface->CreateSession(0, NAME_GameSession, Settings))
|
|
{
|
|
SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteHandle);
|
|
UE_LOG(LogTemp, Error, TEXT("USessionSubsystem : CreateSession a ete refusee immediatement."));
|
|
OnHostFailed.Broadcast(LOCTEXT("HostRefused", "Could not create the session."));
|
|
}
|
|
}
|
|
|
|
void USessionSubsystem::HandleCreateSessionComplete(FName SessionName, bool bWasSuccessful)
|
|
{
|
|
SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteHandle);
|
|
|
|
if (!bWasSuccessful)
|
|
{
|
|
UE_LOG(LogTemp, Error, TEXT("USessionSubsystem : creation de la session [%s] echouee."), *SessionName.ToString());
|
|
OnHostFailed.Broadcast(LOCTEXT("HostFailed", "Could not create the session."));
|
|
return;
|
|
}
|
|
|
|
UE_LOG(LogTemp, Log, TEXT("USessionSubsystem : session [%s] creee pour %d joueurs."), *SessionName.ToString(), PendingMaxPlayers);
|
|
|
|
// "listen" fait de cette instance un serveur d'ecoute : l'hote joue ET
|
|
// heberge dans le meme process, ce qui est exactement le modele voulu pour
|
|
// une coop a quatre. Sans cette option la map s'ouvre en solo et les clients
|
|
// se heurtent a un port ferme.
|
|
UGameplayStatics::OpenLevelBySoftObjectPtr(GetGameInstance(), PendingHostLevel, /*bAbsolute=*/true, TEXT("listen"));
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Recherche
|
|
// ----------------------------------------------------------------------------
|
|
|
|
void USessionSubsystem::FindSessions(int32 MaxResults)
|
|
{
|
|
if (!SessionInterface.IsValid())
|
|
{
|
|
OnSearchFinished.Broadcast(false, 0);
|
|
return;
|
|
}
|
|
|
|
SearchSettings = MakeShared<FOnlineSessionSearch>();
|
|
SearchSettings->MaxSearchResults = FMath::Max(1, MaxResults);
|
|
SearchSettings->bIsLanQuery = IsLanMode();
|
|
|
|
// Cote Steam les parties sont des lobbies, pas des serveurs annonces : sans
|
|
// ce filtre la recherche part interroger le service de serveurs dedies et
|
|
// ne rend jamais rien. En LAN la notion n'existe pas, on l'omet.
|
|
if (!SearchSettings->bIsLanQuery)
|
|
{
|
|
SearchSettings->QuerySettings.Set(SEARCH_LOBBIES, true, EOnlineComparisonOp::Equals);
|
|
}
|
|
|
|
SearchSettings->QuerySettings.Set(SessionSubsystemConstants::GameKeyName, FString(SessionSubsystemConstants::GameKeyValue), EOnlineComparisonOp::Equals);
|
|
|
|
FindSessionsCompleteHandle = SessionInterface->AddOnFindSessionsCompleteDelegate_Handle(
|
|
FOnFindSessionsCompleteDelegate::CreateUObject(this, &USessionSubsystem::HandleFindSessionsComplete));
|
|
|
|
if (!SessionInterface->FindSessions(0, SearchSettings.ToSharedRef()))
|
|
{
|
|
SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteHandle);
|
|
OnSearchFinished.Broadcast(false, 0);
|
|
}
|
|
}
|
|
|
|
void USessionSubsystem::HandleFindSessionsComplete(bool bWasSuccessful)
|
|
{
|
|
SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteHandle);
|
|
|
|
const int32 Count = GetFoundSessionCount();
|
|
UE_LOG(LogTemp, Log, TEXT("USessionSubsystem : recherche terminee (succes %d), %d partie(s)."), bWasSuccessful ? 1 : 0, Count);
|
|
|
|
OnSearchFinished.Broadcast(bWasSuccessful, Count);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Connexion
|
|
// ----------------------------------------------------------------------------
|
|
|
|
void USessionSubsystem::JoinFoundSession(int32 ResultIndex)
|
|
{
|
|
if (!SearchSettings.IsValid() || !SearchSettings->SearchResults.IsValidIndex(ResultIndex))
|
|
{
|
|
OnJoinFailed.Broadcast(LOCTEXT("JoinNoSession", "No game found to join."));
|
|
return;
|
|
}
|
|
|
|
JoinSearchResult(SearchSettings->SearchResults[ResultIndex]);
|
|
}
|
|
|
|
void USessionSubsystem::EmberJoin()
|
|
{
|
|
UE_LOG(LogTemp, Log, TEXT("USessionSubsystem : EmberJoin, recherche d'une partie..."));
|
|
|
|
// On se branche pour un seul tour : la commande enchaine recherche puis
|
|
// connexion, ce que le menu ne fait plus depuis que l'on ne rejoint que
|
|
// par invitation.
|
|
OnSearchFinished.AddDynamic(this, &USessionSubsystem::HandleExecJoinSearchFinished);
|
|
FindSessions();
|
|
}
|
|
|
|
void USessionSubsystem::HandleExecJoinSearchFinished(bool bWasSuccessful, int32 SessionCount)
|
|
{
|
|
OnSearchFinished.RemoveDynamic(this, &USessionSubsystem::HandleExecJoinSearchFinished);
|
|
|
|
if (!bWasSuccessful || SessionCount <= 0)
|
|
{
|
|
UE_LOG(LogTemp, Warning, TEXT("USessionSubsystem : EmberJoin n'a trouve aucune partie."));
|
|
return;
|
|
}
|
|
|
|
JoinFoundSession(0);
|
|
}
|
|
|
|
bool USessionSubsystem::CanInvite() const
|
|
{
|
|
if (!IsSessionActive() || IsLanMode())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const IOnlineSubsystem* Online = IOnlineSubsystem::Get();
|
|
return Online && Online->GetExternalUIInterface().IsValid();
|
|
}
|
|
|
|
bool USessionSubsystem::ShowInviteUI()
|
|
{
|
|
IOnlineSubsystem* Online = IOnlineSubsystem::Get();
|
|
if (!Online)
|
|
{
|
|
UE_LOG(LogTemp, Warning, TEXT("USessionSubsystem : pas de service en ligne, invitation impossible."));
|
|
return false;
|
|
}
|
|
|
|
const IOnlineExternalUIPtr ExternalUI = Online->GetExternalUIInterface();
|
|
if (!ExternalUI.IsValid())
|
|
{
|
|
UE_LOG(LogTemp, Warning, TEXT("USessionSubsystem : le service [%s] n'a pas d'overlay."), *Online->GetSubsystemName().ToString());
|
|
return false;
|
|
}
|
|
|
|
// Sans session ouverte, l'overlay s'affiche mais l'ami invite arrive sur
|
|
// une partie qui n'existe pas. Mieux vaut refuser franchement.
|
|
if (!IsSessionActive())
|
|
{
|
|
UE_LOG(LogTemp, Warning, TEXT("USessionSubsystem : aucune session ouverte, invitation impossible."));
|
|
return false;
|
|
}
|
|
|
|
return ExternalUI->ShowInviteUI(0, NAME_GameSession);
|
|
}
|
|
|
|
void USessionSubsystem::JoinSearchResult(const FOnlineSessionSearchResult& Result)
|
|
{
|
|
if (!SessionInterface.IsValid())
|
|
{
|
|
OnJoinFailed.Broadcast(LOCTEXT("JoinNoService", "No online service available."));
|
|
return;
|
|
}
|
|
|
|
if (!Result.IsValid())
|
|
{
|
|
OnJoinFailed.Broadcast(LOCTEXT("JoinInvalid", "This game is no longer available."));
|
|
return;
|
|
}
|
|
|
|
// Rejoindre alors qu'on heberge deja laisserait deux sessions ouvertes et
|
|
// le voyage echouerait sans message.
|
|
if (SessionInterface->GetNamedSession(NAME_GameSession) != nullptr)
|
|
{
|
|
SessionInterface->DestroySession(NAME_GameSession);
|
|
}
|
|
|
|
JoinSessionCompleteHandle = SessionInterface->AddOnJoinSessionCompleteDelegate_Handle(
|
|
FOnJoinSessionCompleteDelegate::CreateUObject(this, &USessionSubsystem::HandleJoinSessionComplete));
|
|
|
|
if (!SessionInterface->JoinSession(0, NAME_GameSession, Result))
|
|
{
|
|
SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteHandle);
|
|
OnJoinFailed.Broadcast(LOCTEXT("JoinRefused", "Could not join this game."));
|
|
}
|
|
}
|
|
|
|
void USessionSubsystem::HandleJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result)
|
|
{
|
|
SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteHandle);
|
|
|
|
if (Result != EOnJoinSessionCompleteResult::Success)
|
|
{
|
|
UE_LOG(LogTemp, Error, TEXT("USessionSubsystem : JoinSession a echoue (code %d)."), static_cast<int32>(Result));
|
|
|
|
// On distingue le seul cas que le joueur peut corriger lui-meme du reste,
|
|
// qui ne lui apprendrait rien.
|
|
const FText Reason = (Result == EOnJoinSessionCompleteResult::SessionIsFull)
|
|
? LOCTEXT("JoinFull", "This game is full.")
|
|
: LOCTEXT("JoinFailed", "Could not join this game.");
|
|
|
|
OnJoinFailed.Broadcast(Reason);
|
|
return;
|
|
}
|
|
|
|
// L'adresse n'est connue qu'ici : c'est le service en ligne qui la fabrique,
|
|
// et sous Steam ce n'est meme pas une IP mais un identifiant de relais.
|
|
FString ConnectString;
|
|
if (!SessionInterface->GetResolvedConnectString(SessionName, ConnectString))
|
|
{
|
|
UE_LOG(LogTemp, Error, TEXT("USessionSubsystem : adresse de l'hote introuvable."));
|
|
OnJoinFailed.Broadcast(LOCTEXT("JoinNoAddress", "Could not reach the host."));
|
|
return;
|
|
}
|
|
|
|
APlayerController* Controller = GetGameInstance() ? GetGameInstance()->GetFirstLocalPlayerController() : nullptr;
|
|
if (!Controller)
|
|
{
|
|
OnJoinFailed.Broadcast(LOCTEXT("JoinNoController", "Could not reach the host."));
|
|
return;
|
|
}
|
|
|
|
UE_LOG(LogTemp, Log, TEXT("USessionSubsystem : voyage vers [%s]."), *ConnectString);
|
|
Controller->ClientTravel(ConnectString, TRAVEL_Absolute);
|
|
}
|
|
|
|
void USessionSubsystem::HandleInviteAccepted(const bool bWasSuccessful, const int32 ControllerId, FUniqueNetIdPtr UserId, const FOnlineSessionSearchResult& InviteResult)
|
|
{
|
|
if (!bWasSuccessful)
|
|
{
|
|
UE_LOG(LogTemp, Warning, TEXT("USessionSubsystem : invitation acceptee mais illisible."));
|
|
return;
|
|
}
|
|
|
|
UE_LOG(LogTemp, Log, TEXT("USessionSubsystem : invitation acceptee, connexion en cours."));
|
|
JoinSearchResult(InviteResult);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Sortie
|
|
// ----------------------------------------------------------------------------
|
|
|
|
void USessionSubsystem::LeaveSession()
|
|
{
|
|
if (!SessionInterface.IsValid() || SessionInterface->GetNamedSession(NAME_GameSession) == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Pas de reprise apres coup ici : on quitte, point.
|
|
bCreateAfterDestroy = false;
|
|
|
|
DestroySessionCompleteHandle = SessionInterface->AddOnDestroySessionCompleteDelegate_Handle(
|
|
FOnDestroySessionCompleteDelegate::CreateUObject(this, &USessionSubsystem::HandleDestroySessionComplete));
|
|
|
|
SessionInterface->DestroySession(NAME_GameSession);
|
|
}
|
|
|
|
void USessionSubsystem::HandleDestroySessionComplete(FName SessionName, bool bWasSuccessful)
|
|
{
|
|
SessionInterface->ClearOnDestroySessionCompleteDelegate_Handle(DestroySessionCompleteHandle);
|
|
|
|
if (!bCreateAfterDestroy)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bCreateAfterDestroy = false;
|
|
|
|
if (!bWasSuccessful)
|
|
{
|
|
UE_LOG(LogTemp, Error, TEXT("USessionSubsystem : impossible de detruire la session [%s], hebergement annule."), *SessionName.ToString());
|
|
OnHostFailed.Broadcast(LOCTEXT("HostStaleSession", "A previous game is still closing. Try again."));
|
|
return;
|
|
}
|
|
|
|
CreateSessionNow();
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|