2022-05-23 18:41:30 +00:00
// 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 ;
2022-09-13 07:18:28 +00:00
if ( UGameFeaturesSubsystem : : Get ( ) . GetPluginURLByName ( PluginName , /*out*/ PluginURL ) )
2022-05-23 18:41:30 +00:00
{
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;
2022-09-13 07:18:28 +00:00
// if (UGameFeaturesSubsystem::Get().GetPluginURLByName(CurrentPlaylistData->GameFeaturePluginToActivateUntilDownloadedContentIsPresent, PluginURL))
2022-05-23 18:41:30 +00:00
// {
// 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);
}