457 lines
15 KiB
C++
457 lines
15 KiB
C++
|
// Copyright Epic Games, Inc. All Rights Reserved.
|
||
|
|
||
|
#include "LyraExperienceManagerComponent.h"
|
||
|
#include "Net/UnrealNetwork.h"
|
||
|
#include "LyraExperienceDefinition.h"
|
||
|
#include "LyraExperienceActionSet.h"
|
||
|
#include "LyraExperienceManager.h"
|
||
|
#include "GameFeaturesSubsystem.h"
|
||
|
#include "System/LyraAssetManager.h"
|
||
|
#include "GameFeatureAction.h"
|
||
|
#include "GameFeaturesSubsystemSettings.h"
|
||
|
#include "TimerManager.h"
|
||
|
#include "Settings/LyraSettingsLocal.h"
|
||
|
#include "LyraLogChannels.h"
|
||
|
|
||
|
//@TODO: Async load the experience definition itself
|
||
|
//@TODO: Handle failures explicitly (go into a 'completed but failed' state rather than check()-ing)
|
||
|
//@TODO: Do the action phases at the appropriate times instead of all at once
|
||
|
//@TODO: Support deactivating an experience and do the unloading actions
|
||
|
//@TODO: Think about what deactivation/cleanup means for preloaded assets
|
||
|
//@TODO: Handle deactivating game features, right now we 'leak' them enabled
|
||
|
// (for a client moving from experience to experience we actually want to diff the requirements and only unload some, not unload everything for them to just be immediately reloaded)
|
||
|
//@TODO: Handle both built-in and URL-based plugins (search for colon?)
|
||
|
|
||
|
namespace LyraConsoleVariables
|
||
|
{
|
||
|
static float ExperienceLoadRandomDelayMin = 0.0f;
|
||
|
static FAutoConsoleVariableRef CVarExperienceLoadRandomDelayMin(
|
||
|
TEXT("lyra.chaos.ExperienceDelayLoad.MinSecs"),
|
||
|
ExperienceLoadRandomDelayMin,
|
||
|
TEXT("This value (in seconds) will be added as a delay of load completion of the experience (along with the random value lyra.chaos.ExperienceDelayLoad.RandomSecs)"),
|
||
|
ECVF_Default);
|
||
|
|
||
|
static float ExperienceLoadRandomDelayRange = 0.0f;
|
||
|
static FAutoConsoleVariableRef CVarExperienceLoadRandomDelayRange(
|
||
|
TEXT("lyra.chaos.ExperienceDelayLoad.RandomSecs"),
|
||
|
ExperienceLoadRandomDelayRange,
|
||
|
TEXT("A random amount of time between 0 and this value (in seconds) will be added as a delay of load completion of the experience (along with the fixed value lyra.chaos.ExperienceDelayLoad.MinSecs)"),
|
||
|
ECVF_Default);
|
||
|
|
||
|
float GetExperienceLoadDelayDuration()
|
||
|
{
|
||
|
return FMath::Max(0.0f, ExperienceLoadRandomDelayMin + FMath::FRand() * ExperienceLoadRandomDelayRange);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
ULyraExperienceManagerComponent::ULyraExperienceManagerComponent(const FObjectInitializer& ObjectInitializer)
|
||
|
: Super(ObjectInitializer)
|
||
|
{
|
||
|
SetIsReplicatedByDefault(true);
|
||
|
}
|
||
|
|
||
|
#if WITH_SERVER_CODE
|
||
|
void ULyraExperienceManagerComponent::ServerSetCurrentExperience(FPrimaryAssetId ExperienceId)
|
||
|
{
|
||
|
ULyraAssetManager& AssetManager = ULyraAssetManager::Get();
|
||
|
FSoftObjectPath AssetPath = AssetManager.GetPrimaryAssetPath(ExperienceId);
|
||
|
TSubclassOf<ULyraExperienceDefinition> AssetClass = Cast<UClass>(AssetPath.TryLoad());
|
||
|
check(AssetClass);
|
||
|
const ULyraExperienceDefinition* Experience = GetDefault<ULyraExperienceDefinition>(AssetClass);
|
||
|
|
||
|
check(Experience != nullptr);
|
||
|
check(CurrentExperience == nullptr);
|
||
|
CurrentExperience = Experience;
|
||
|
StartExperienceLoad();
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
void ULyraExperienceManagerComponent::CallOrRegister_OnExperienceLoaded_HighPriority(FOnLyraExperienceLoaded::FDelegate&& Delegate)
|
||
|
{
|
||
|
if (IsExperienceLoaded())
|
||
|
{
|
||
|
Delegate.Execute(CurrentExperience);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
OnExperienceLoaded_HighPriority.Add(MoveTemp(Delegate));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void ULyraExperienceManagerComponent::CallOrRegister_OnExperienceLoaded(FOnLyraExperienceLoaded::FDelegate&& Delegate)
|
||
|
{
|
||
|
if (IsExperienceLoaded())
|
||
|
{
|
||
|
Delegate.Execute(CurrentExperience);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
OnExperienceLoaded.Add(MoveTemp(Delegate));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void ULyraExperienceManagerComponent::CallOrRegister_OnExperienceLoaded_LowPriority(FOnLyraExperienceLoaded::FDelegate&& Delegate)
|
||
|
{
|
||
|
if (IsExperienceLoaded())
|
||
|
{
|
||
|
Delegate.Execute(CurrentExperience);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
OnExperienceLoaded_LowPriority.Add(MoveTemp(Delegate));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const ULyraExperienceDefinition* ULyraExperienceManagerComponent::GetCurrentExperienceChecked() const
|
||
|
{
|
||
|
check(LoadState == ELyraExperienceLoadState::Loaded);
|
||
|
check(CurrentExperience != nullptr);
|
||
|
return CurrentExperience;
|
||
|
}
|
||
|
|
||
|
bool ULyraExperienceManagerComponent::IsExperienceLoaded() const
|
||
|
{
|
||
|
return (LoadState == ELyraExperienceLoadState::Loaded) && (CurrentExperience != nullptr);
|
||
|
}
|
||
|
|
||
|
void ULyraExperienceManagerComponent::OnRep_CurrentExperience()
|
||
|
{
|
||
|
StartExperienceLoad();
|
||
|
}
|
||
|
|
||
|
void ULyraExperienceManagerComponent::StartExperienceLoad()
|
||
|
{
|
||
|
check(CurrentExperience != nullptr);
|
||
|
check(LoadState == ELyraExperienceLoadState::Unloaded);
|
||
|
|
||
|
UE_LOG(LogLyraExperience, Log, TEXT("EXPERIENCE: StartExperienceLoad(CurrentExperience = %s, %s)"),
|
||
|
*CurrentExperience->GetPrimaryAssetId().ToString(),
|
||
|
*GetClientServerContextString(this));
|
||
|
|
||
|
LoadState = ELyraExperienceLoadState::Loading;
|
||
|
|
||
|
ULyraAssetManager& AssetManager = ULyraAssetManager::Get();
|
||
|
|
||
|
TSet<FPrimaryAssetId> BundleAssetList;
|
||
|
TSet<FSoftObjectPath> RawAssetList;
|
||
|
|
||
|
BundleAssetList.Add(CurrentExperience->GetPrimaryAssetId());
|
||
|
for (const TObjectPtr<ULyraExperienceActionSet>& ActionSet : CurrentExperience->ActionSets)
|
||
|
{
|
||
|
if (ActionSet != nullptr)
|
||
|
{
|
||
|
BundleAssetList.Add(ActionSet->GetPrimaryAssetId());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Load assets associated with the experience
|
||
|
|
||
|
TArray<FName> BundlesToLoad;
|
||
|
BundlesToLoad.Add(FLyraBundles::Equipped);
|
||
|
|
||
|
//@TODO: Centralize this client/server stuff into the LyraAssetManager
|
||
|
const ENetMode OwnerNetMode = GetOwner()->GetNetMode();
|
||
|
const bool bLoadClient = GIsEditor || (OwnerNetMode != NM_DedicatedServer);
|
||
|
const bool bLoadServer = GIsEditor || (OwnerNetMode != NM_Client);
|
||
|
if (bLoadClient)
|
||
|
{
|
||
|
BundlesToLoad.Add(UGameFeaturesSubsystemSettings::LoadStateClient);
|
||
|
}
|
||
|
if (bLoadServer)
|
||
|
{
|
||
|
BundlesToLoad.Add(UGameFeaturesSubsystemSettings::LoadStateServer);
|
||
|
}
|
||
|
|
||
|
const TSharedPtr<FStreamableHandle> BundleLoadHandle = AssetManager.ChangeBundleStateForPrimaryAssets(BundleAssetList.Array(), BundlesToLoad, {}, false, FStreamableDelegate(), FStreamableManager::AsyncLoadHighPriority);
|
||
|
const TSharedPtr<FStreamableHandle> RawLoadHandle = AssetManager.LoadAssetList(RawAssetList.Array(), FStreamableDelegate(), FStreamableManager::AsyncLoadHighPriority, TEXT("StartExperienceLoad()"));
|
||
|
|
||
|
// If both async loads are running, combine them
|
||
|
TSharedPtr<FStreamableHandle> Handle = nullptr;
|
||
|
if (BundleLoadHandle.IsValid() && RawLoadHandle.IsValid())
|
||
|
{
|
||
|
Handle = AssetManager.GetStreamableManager().CreateCombinedHandle({ BundleLoadHandle, RawLoadHandle });
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Handle = BundleLoadHandle.IsValid() ? BundleLoadHandle : RawLoadHandle;
|
||
|
}
|
||
|
|
||
|
FStreamableDelegate OnAssetsLoadedDelegate = FStreamableDelegate::CreateUObject(this, &ThisClass::OnExperienceLoadComplete);
|
||
|
if (!Handle.IsValid() || Handle->HasLoadCompleted())
|
||
|
{
|
||
|
// Assets were already loaded, call the delegate now
|
||
|
FStreamableHandle::ExecuteDelegate(OnAssetsLoadedDelegate);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Handle->BindCompleteDelegate(OnAssetsLoadedDelegate);
|
||
|
|
||
|
Handle->BindCancelDelegate(FStreamableDelegate::CreateLambda([OnAssetsLoadedDelegate]()
|
||
|
{
|
||
|
OnAssetsLoadedDelegate.ExecuteIfBound();
|
||
|
}));
|
||
|
}
|
||
|
|
||
|
// This set of assets gets preloaded, but we don't block the start of the experience based on it
|
||
|
TSet<FPrimaryAssetId> PreloadAssetList;
|
||
|
//@TODO: Determine assets to preload (but not blocking-ly)
|
||
|
if (PreloadAssetList.Num() > 0)
|
||
|
{
|
||
|
AssetManager.ChangeBundleStateForPrimaryAssets(PreloadAssetList.Array(), BundlesToLoad, {});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void ULyraExperienceManagerComponent::OnExperienceLoadComplete()
|
||
|
{
|
||
|
check(LoadState == ELyraExperienceLoadState::Loading);
|
||
|
check(CurrentExperience != nullptr);
|
||
|
|
||
|
UE_LOG(LogLyraExperience, Log, TEXT("EXPERIENCE: OnExperienceLoadComplete(CurrentExperience = %s, %s)"),
|
||
|
*CurrentExperience->GetPrimaryAssetId().ToString(),
|
||
|
*GetClientServerContextString(this));
|
||
|
|
||
|
// find the URLs for our GameFeaturePlugins - filtering out dupes and ones that don't have a valid mapping
|
||
|
GameFeaturePluginURLs.Reset();
|
||
|
|
||
|
auto CollectGameFeaturePluginURLs = [This=this](const UPrimaryDataAsset* Context, const TArray<FString>& FeaturePluginList)
|
||
|
{
|
||
|
for (const FString& PluginName : FeaturePluginList)
|
||
|
{
|
||
|
FString PluginURL;
|
||
|
if (UGameFeaturesSubsystem::Get().GetPluginURLForBuiltInPluginByName(PluginName, /*out*/ PluginURL))
|
||
|
{
|
||
|
This->GameFeaturePluginURLs.AddUnique(PluginURL);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
ensureMsgf(false, TEXT("OnExperienceLoadComplete failed to find plugin URL from PluginName %s for experience %s - fix data, ignoring for this run"), *PluginName, *Context->GetPrimaryAssetId().ToString());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// // Add in our extra plugin
|
||
|
// if (!CurrentPlaylistData->GameFeaturePluginToActivateUntilDownloadedContentIsPresent.IsEmpty())
|
||
|
// {
|
||
|
// FString PluginURL;
|
||
|
// if (UGameFeaturesSubsystem::Get().GetPluginURLForBuiltInPluginByName(CurrentPlaylistData->GameFeaturePluginToActivateUntilDownloadedContentIsPresent, PluginURL))
|
||
|
// {
|
||
|
// GameFeaturePluginURLs.AddUnique(PluginURL);
|
||
|
// }
|
||
|
// }
|
||
|
};
|
||
|
|
||
|
CollectGameFeaturePluginURLs(CurrentExperience, CurrentExperience->GameFeaturesToEnable);
|
||
|
for (const TObjectPtr<ULyraExperienceActionSet>& ActionSet : CurrentExperience->ActionSets)
|
||
|
{
|
||
|
if (ActionSet != nullptr)
|
||
|
{
|
||
|
CollectGameFeaturePluginURLs(ActionSet, ActionSet->GameFeaturesToEnable);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Load and activate the features
|
||
|
NumGameFeaturePluginsLoading = GameFeaturePluginURLs.Num();
|
||
|
if (NumGameFeaturePluginsLoading > 0)
|
||
|
{
|
||
|
LoadState = ELyraExperienceLoadState::LoadingGameFeatures;
|
||
|
for (const FString& PluginURL : GameFeaturePluginURLs)
|
||
|
{
|
||
|
ULyraExperienceManager::NotifyOfPluginActivation(PluginURL);
|
||
|
UGameFeaturesSubsystem::Get().LoadAndActivateGameFeaturePlugin(PluginURL, FGameFeaturePluginLoadComplete::CreateUObject(this, &ThisClass::OnGameFeaturePluginLoadComplete));
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
OnExperienceFullLoadCompleted();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void ULyraExperienceManagerComponent::OnGameFeaturePluginLoadComplete(const UE::GameFeatures::FResult& Result)
|
||
|
{
|
||
|
// decrement the number of plugins that are loading
|
||
|
NumGameFeaturePluginsLoading--;
|
||
|
|
||
|
if (NumGameFeaturePluginsLoading == 0)
|
||
|
{
|
||
|
OnExperienceFullLoadCompleted();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void ULyraExperienceManagerComponent::OnExperienceFullLoadCompleted()
|
||
|
{
|
||
|
check(LoadState != ELyraExperienceLoadState::Loaded);
|
||
|
|
||
|
// Insert a random delay for testing (if configured)
|
||
|
if (LoadState != ELyraExperienceLoadState::LoadingChaosTestingDelay)
|
||
|
{
|
||
|
const float DelaySecs = LyraConsoleVariables::GetExperienceLoadDelayDuration();
|
||
|
if (DelaySecs > 0.0f)
|
||
|
{
|
||
|
FTimerHandle DummyHandle;
|
||
|
|
||
|
LoadState = ELyraExperienceLoadState::LoadingChaosTestingDelay;
|
||
|
GetWorld()->GetTimerManager().SetTimer(DummyHandle, this, &ThisClass::OnExperienceFullLoadCompleted, DelaySecs, /*bLooping=*/ false);
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
LoadState = ELyraExperienceLoadState::ExecutingActions;
|
||
|
|
||
|
// Execute the actions
|
||
|
FGameFeatureActivatingContext Context;
|
||
|
|
||
|
// Only apply to our specific world context if set
|
||
|
const FWorldContext* ExistingWorldContext = GEngine->GetWorldContextFromWorld(GetWorld());
|
||
|
if (ExistingWorldContext)
|
||
|
{
|
||
|
Context.SetRequiredWorldContextHandle(ExistingWorldContext->ContextHandle);
|
||
|
}
|
||
|
|
||
|
auto ActivateListOfActions = [&Context](const TArray<UGameFeatureAction*>& ActionList)
|
||
|
{
|
||
|
for (UGameFeatureAction* Action : ActionList)
|
||
|
{
|
||
|
if (Action != nullptr)
|
||
|
{
|
||
|
//@TODO: The fact that these don't take a world are potentially problematic in client-server PIE
|
||
|
// The current behavior matches systems like gameplay tags where loading and registering apply to the entire process,
|
||
|
// but actually applying the results to actors is restricted to a specific world
|
||
|
Action->OnGameFeatureRegistering();
|
||
|
Action->OnGameFeatureLoading();
|
||
|
Action->OnGameFeatureActivating(Context);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
ActivateListOfActions(CurrentExperience->Actions);
|
||
|
for (const TObjectPtr<ULyraExperienceActionSet>& ActionSet : CurrentExperience->ActionSets)
|
||
|
{
|
||
|
if (ActionSet != nullptr)
|
||
|
{
|
||
|
ActivateListOfActions(ActionSet->Actions);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
LoadState = ELyraExperienceLoadState::Loaded;
|
||
|
|
||
|
OnExperienceLoaded_HighPriority.Broadcast(CurrentExperience);
|
||
|
OnExperienceLoaded_HighPriority.Clear();
|
||
|
|
||
|
OnExperienceLoaded.Broadcast(CurrentExperience);
|
||
|
OnExperienceLoaded.Clear();
|
||
|
|
||
|
OnExperienceLoaded_LowPriority.Broadcast(CurrentExperience);
|
||
|
OnExperienceLoaded_LowPriority.Clear();
|
||
|
|
||
|
// Apply any necessary scalability settings
|
||
|
#if !UE_SERVER
|
||
|
ULyraSettingsLocal::Get()->OnExperienceLoaded();
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
void ULyraExperienceManagerComponent::OnActionDeactivationCompleted()
|
||
|
{
|
||
|
check(IsInGameThread());
|
||
|
++NumObservedPausers;
|
||
|
|
||
|
if (NumObservedPausers == NumExpectedPausers)
|
||
|
{
|
||
|
OnAllActionsDeactivated();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void ULyraExperienceManagerComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||
|
{
|
||
|
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||
|
|
||
|
DOREPLIFETIME(ThisClass, CurrentExperience);
|
||
|
}
|
||
|
|
||
|
void ULyraExperienceManagerComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||
|
{
|
||
|
Super::EndPlay(EndPlayReason);
|
||
|
|
||
|
// deactivate any features this experience loaded
|
||
|
//@TODO: This should be handled FILO as well
|
||
|
for (const FString& PluginURL : GameFeaturePluginURLs)
|
||
|
{
|
||
|
if (ULyraExperienceManager::RequestToDeactivatePlugin(PluginURL))
|
||
|
{
|
||
|
UGameFeaturesSubsystem::Get().DeactivateGameFeaturePlugin(PluginURL);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//@TODO: Ensure proper handling of a partially-loaded state too
|
||
|
if (LoadState == ELyraExperienceLoadState::Loaded)
|
||
|
{
|
||
|
LoadState = ELyraExperienceLoadState::Deactivating;
|
||
|
|
||
|
// Make sure we won't complete the transition prematurely if someone registers as a pauser but fires immediately
|
||
|
NumExpectedPausers = INDEX_NONE;
|
||
|
NumObservedPausers = 0;
|
||
|
|
||
|
// Deactivate and unload the actions
|
||
|
FGameFeatureDeactivatingContext Context(FSimpleDelegate::CreateUObject(this, &ThisClass::OnActionDeactivationCompleted));
|
||
|
|
||
|
const FWorldContext* ExistingWorldContext = GEngine->GetWorldContextFromWorld(GetWorld());
|
||
|
if (ExistingWorldContext)
|
||
|
{
|
||
|
Context.SetRequiredWorldContextHandle(ExistingWorldContext->ContextHandle);
|
||
|
}
|
||
|
|
||
|
auto DeactivateListOfActions = [&Context](const TArray<UGameFeatureAction*>& ActionList)
|
||
|
{
|
||
|
for (UGameFeatureAction* Action : ActionList)
|
||
|
{
|
||
|
if (Action)
|
||
|
{
|
||
|
Action->OnGameFeatureDeactivating(Context);
|
||
|
Action->OnGameFeatureUnregistering();
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
DeactivateListOfActions(CurrentExperience->Actions);
|
||
|
for (const TObjectPtr<ULyraExperienceActionSet>& ActionSet : CurrentExperience->ActionSets)
|
||
|
{
|
||
|
if (ActionSet != nullptr)
|
||
|
{
|
||
|
DeactivateListOfActions(ActionSet->Actions);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
NumExpectedPausers = Context.GetNumPausers();
|
||
|
|
||
|
if (NumExpectedPausers > 0)
|
||
|
{
|
||
|
UE_LOG(LogLyraExperience, Error, TEXT("Actions that have asynchronous deactivation aren't fully supported yet in Lyra experiences"));
|
||
|
}
|
||
|
|
||
|
if (NumExpectedPausers == NumObservedPausers)
|
||
|
{
|
||
|
OnAllActionsDeactivated();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bool ULyraExperienceManagerComponent::ShouldShowLoadingScreen(FString& OutReason) const
|
||
|
{
|
||
|
if (LoadState != ELyraExperienceLoadState::Loaded)
|
||
|
{
|
||
|
OutReason = TEXT("Experience still loading");
|
||
|
return true;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void ULyraExperienceManagerComponent::OnAllActionsDeactivated()
|
||
|
{
|
||
|
//@TODO: We actually only deactivated and didn't fully unload...
|
||
|
LoadState = ELyraExperienceLoadState::Unloaded;
|
||
|
CurrentExperience = nullptr;
|
||
|
//@TODO: GEngine->ForceGarbageCollection(true);
|
||
|
}
|