// 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& 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(); 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(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