Files
Unreal_EmberWild/Source/EmberWild/Private/SessionSubsystem.cpp
T

479 lines
16 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;
}
// On ne voyage PAS tout de suite : on annonce, on laisse le controller
// courant fondre au noir, puis on part. Sans ce delai, accepter une
// invitation depuis le menu coupe l'image d'un coup -- le seul endroit du
// jeu ou une transition n'etait pas fondue.
PendingTravelURL = ConnectString;
OnTravelPending.Broadcast();
UGameInstance* OwningGameInstance = GetGameInstance();
if (TravelFadeDuration <= 0.f || !OwningGameInstance)
{
CommitPendingTravel();
return;
}
// Le TimerManager du GameInstance et non celui du monde : le monde courant
// est justement celui qu'on s'apprete a quitter.
OwningGameInstance->GetTimerManager().SetTimer(
TravelTimer, this, &USessionSubsystem::CommitPendingTravel,
TravelFadeDuration, /*bLoop=*/false);
}
void USessionSubsystem::CommitPendingTravel()
{
if (PendingTravelURL.IsEmpty())
{
return;
}
const FString TravelURL = MoveTemp(PendingTravelURL);
PendingTravelURL.Reset();
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]."), *TravelURL);
Controller->ClientTravel(TravelURL, 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