2025-01-06 21:29:37 +00:00
// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action.
# include "GameModes/OLSExperienceManagerComponent.h"
2025-01-08 05:30:09 +00:00
# include "GameFeatureAction.h"
# include "GameModes/OLSExperienceManager.h"
# include "GameFeaturesSubsystem.h"
# include "GameFeatureAction.h"
# include "GameFeaturesSubsystemSettings.h"
# include "TimerManager.h"
# include "DataAssets/OLSExperienceDefinitionPrimaryDataAsset.h"
# include "Net/UnrealNetwork.h"
# include "Systems/OLSAssetManager.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 OLSConsoleVariables
{
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 ) ;
}
}
UOLSExperienceManagerComponent : : UOLSExperienceManagerComponent ( const FObjectInitializer & objectInitializer ) : Super ( objectInitializer )
{
SetIsReplicatedByDefault ( true ) ;
}
void UOLSExperienceManagerComponent : : 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 ( UOLSExperienceManager : : RequestToDeactivatePlugin ( PluginURL ) )
{
UGameFeaturesSubsystem : : Get ( ) . DeactivateGameFeaturePlugin ( PluginURL ) ;
}
}
//@TODO: Ensure proper handling of a partially-loaded state too
if ( LoadState = = EOLSExperienceLoadState : : Loaded )
{
LoadState = EOLSExperienceLoadState : : 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 ( TEXT ( " " ) , [ this ] ( FStringView ) { this - > 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 ) ;
// @Todo implement this.
// for (const TObjectPtr<UOLSExperienceActionSet>& ActionSet : CurrentExperience->ActionSets)
// {
// if (ActionSet != nullptr)
// {
// DeactivateListOfActions(ActionSet->Actions);
// }
// }
NumExpectedPausers = context . GetNumPausers ( ) ;
if ( NumExpectedPausers > 0 )
{
// @Todo replace this with our custom.
// UE_LOG(LogLyraExperience, Error, TEXT("Actions that have asynchronous deactivation aren't fully supported yet in Lyra experiences"));
}
if ( NumExpectedPausers = = NumObservedPausers )
{
OnAllActionsDeactivated ( ) ;
}
}
}
void UOLSExperienceManagerComponent : : GetLifetimeReplicatedProps ( TArray < FLifetimeProperty > & OutLifetimeProps ) const
{
Super : : GetLifetimeReplicatedProps ( OutLifetimeProps ) ;
DOREPLIFETIME ( ThisClass , CurrentExperience ) ;
}
bool UOLSExperienceManagerComponent : : ShouldShowLoadingScreen ( FString & outReason ) const
{
if ( LoadState ! = EOLSExperienceLoadState : : Loaded )
{
outReason = TEXT ( " Experience still loading " ) ;
return true ;
}
return false ;
}
const UOLSExperienceDefinitionPrimaryDataAsset * UOLSExperienceManagerComponent : : GetCurrentExperienceChecked ( ) const
{
check ( LoadState = = EOLSExperienceLoadState : : Loaded ) ;
check ( CurrentExperience ! = nullptr ) ;
return CurrentExperience ;
}
void UOLSExperienceManagerComponent : : SetCurrentExperience ( FPrimaryAssetId experienceId )
{
UOLSAssetManager & assetManager = UOLSAssetManager : : Get ( ) ;
FSoftObjectPath assetPath = assetManager . GetPrimaryAssetPath ( experienceId ) ;
TSubclassOf < UOLSExperienceDefinitionPrimaryDataAsset > assetClass = Cast < UClass > ( assetPath . TryLoad ( ) ) ;
check ( assetClass ) ;
const UOLSExperienceDefinitionPrimaryDataAsset * experience = GetDefault < UOLSExperienceDefinitionPrimaryDataAsset > ( assetClass ) ;
check ( experience ! = nullptr ) ;
check ( CurrentExperience = = nullptr ) ;
CurrentExperience = experience ;
StartExperienceLoad ( ) ;
}
bool UOLSExperienceManagerComponent : : IsExperienceLoaded ( ) const
{
return ( LoadState = = EOLSExperienceLoadState : : Loaded ) & & ( CurrentExperience ! = nullptr ) ;
}
void UOLSExperienceManagerComponent : : CallOrRegister_OnExperienceLoaded_HighPriority (
FOnOLSExperienceLoaded : : FDelegate & & delegate )
{
if ( IsExperienceLoaded ( ) )
{
delegate . Execute ( CurrentExperience ) ;
}
else
{
OnExperienceLoaded_HighPriority . Add ( MoveTemp ( delegate ) ) ;
}
}
void UOLSExperienceManagerComponent : : CallOrRegister_OnExperienceLoaded ( FOnOLSExperienceLoaded : : FDelegate & & delegate )
{
if ( IsExperienceLoaded ( ) )
{
delegate . Execute ( CurrentExperience ) ;
}
else
{
OnExperienceLoaded . Add ( MoveTemp ( delegate ) ) ;
}
}
void UOLSExperienceManagerComponent : : CallOrRegister_OnExperienceLoaded_LowPriority (
FOnOLSExperienceLoaded : : FDelegate & & delegate )
{
if ( IsExperienceLoaded ( ) )
{
delegate . Execute ( CurrentExperience ) ;
}
else
{
OnExperienceLoaded_LowPriority . Add ( MoveTemp ( delegate ) ) ;
}
}
void UOLSExperienceManagerComponent : : OnRep_CurrentExperience ( )
{
StartExperienceLoad ( ) ;
}
void UOLSExperienceManagerComponent : : StartExperienceLoad ( )
{
check ( CurrentExperience ! = nullptr ) ;
check ( LoadState = = EOLSExperienceLoadState : : Unloaded ) ;
// @Todo replace this by custom log.
// UE_LOG(LogLyraExperience, Log, TEXT("EXPERIENCE: StartExperienceLoad(CurrentExperience = %s, %s)"),
// *CurrentExperience->GetPrimaryAssetId().ToString(),
// *GetClientServerContextString(this));
LoadState = EOLSExperienceLoadState : : Loading ;
UOLSAssetManager & assetManager = UOLSAssetManager : : Get ( ) ;
TSet < FPrimaryAssetId > bundleAssetList ;
TSet < FSoftObjectPath > rawAssetList ;
bundleAssetList . Add ( CurrentExperience - > GetPrimaryAssetId ( ) ) ;
// @Todo implement this
// 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 ( FOLSBundles : : Equipped ) ;
//@TODO: Centralize this client/server stuff into the OLSAssetManager
const ENetMode ownerNetMode = GetOwner ( ) - > GetNetMode ( ) ;
const bool shouldLoadClient = GIsEditor | | ( ownerNetMode ! = NM_DedicatedServer ) ;
const bool shouldLoadServer = GIsEditor | | ( ownerNetMode ! = NM_Client ) ;
if ( shouldLoadClient )
{
bundlesToLoad . Add ( UGameFeaturesSubsystemSettings : : LoadStateClient ) ;
}
if ( shouldLoadServer )
{
bundlesToLoad . Add ( UGameFeaturesSubsystemSettings : : LoadStateServer ) ;
}
TSharedPtr < FStreamableHandle > bundleLoadHandle = nullptr ;
if ( bundleAssetList . Num ( ) > 0 )
{
bundleLoadHandle = assetManager . ChangeBundleStateForPrimaryAssets ( bundleAssetList . Array ( ) , bundlesToLoad , { } , false , FStreamableDelegate ( ) , FStreamableManager : : AsyncLoadHighPriority ) ;
}
TSharedPtr < FStreamableHandle > rawLoadHandle = nullptr ;
if ( rawAssetList . Num ( ) > 0 )
{
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 UOLSExperienceManagerComponent : : OnExperienceLoadComplete ( )
{
check ( LoadState = = EOLSExperienceLoadState : : Loading ) ;
check ( CurrentExperience ! = nullptr ) ;
// @Todo replace this by our custom.
// 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 ( ) . GetPluginURLByName ( 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().GetPluginURLByName(CurrentPlaylistData->GameFeaturePluginToActivateUntilDownloadedContentIsPresent, PluginURL))
// {
// GameFeaturePluginURLs.AddUnique(PluginURL);
// }
// }
} ;
collectGameFeaturePluginURLs ( CurrentExperience , CurrentExperience - > GameFeaturesToEnable ) ;
// @Todo implement this.
// 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 = EOLSExperienceLoadState : : LoadingGameFeatures ;
for ( const FString & pluginURL : GameFeaturePluginURLs )
{
UOLSExperienceManager : : NotifyOfPluginActivation ( pluginURL ) ;
UGameFeaturesSubsystem : : Get ( ) . LoadAndActivateGameFeaturePlugin (
pluginURL ,
FGameFeaturePluginLoadComplete : : CreateUObject ( this , & ThisClass : : OnGameFeaturePluginLoadComplete ) ) ;
}
}
else
{
OnExperienceFullLoadCompleted ( ) ;
}
}
void UOLSExperienceManagerComponent : : OnGameFeaturePluginLoadComplete ( const UE : : GameFeatures : : FResult & result )
{
// decrement the number of plugins that are loading
NumGameFeaturePluginsLoading - - ;
if ( NumGameFeaturePluginsLoading = = 0 )
{
OnExperienceFullLoadCompleted ( ) ;
}
}
void UOLSExperienceManagerComponent : : OnExperienceFullLoadCompleted ( )
{
check ( LoadState ! = EOLSExperienceLoadState : : Loaded ) ;
// Insert a random delay for testing (if configured)
if ( LoadState ! = EOLSExperienceLoadState : : LoadingChaosTestingDelay )
{
const float delaySecs = OLSConsoleVariables : : GetExperienceLoadDelayDuration ( ) ;
if ( delaySecs > 0.0f )
{
FTimerHandle dummyHandle ;
LoadState = EOLSExperienceLoadState : : LoadingChaosTestingDelay ;
GetWorld ( ) - > GetTimerManager ( ) . SetTimer ( dummyHandle , this , & ThisClass : : OnExperienceFullLoadCompleted , delaySecs , /*bLooping=*/ false ) ;
return ;
}
}
LoadState = EOLSExperienceLoadState : : 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 ) ;
// @Todo implement this
// for (const TObjectPtr<ULyraExperienceActionSet>& ActionSet : CurrentExperience->ActionSets)
// {
// if (ActionSet != nullptr)
// {
// activateListOfActions(ActionSet->Actions);
// }
// }
LoadState = EOLSExperienceLoadState : : 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
// @Todo implement this
// UOLSSettingsLocal::Get()->OnExperienceLoaded();
# endif
}
void UOLSExperienceManagerComponent : : OnActionDeactivationCompleted ( )
{
check ( IsInGameThread ( ) ) ;
+ + NumObservedPausers ;
if ( NumObservedPausers = = NumExpectedPausers )
{
OnAllActionsDeactivated ( ) ;
}
}
void UOLSExperienceManagerComponent : : OnAllActionsDeactivated ( )
{
//@TODO: We actually only deactivated and didn't fully unload...
LoadState = EOLSExperienceLoadState : : Unloaded ;
CurrentExperience = nullptr ;
//@TODO: GEngine->ForceGarbageCollection(true);
}