OLS/Source/ols/Private/GameModes/OLSExperienceManagerComponent.cpp
LongLy 7415b74e8f Added CommonUser Plugins.
Implemented GameplayStackTags
2025-01-13 15:36:08 -07:00

475 lines
15 KiB
C++

// © 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"
#include "GameFeatureAction.h"
#include "GameModes/OLSExperienceManager.h"
#include "GameFeaturesSubsystem.h"
#include "GameFeatureAction.h"
#include "GameFeaturesSubsystemSettings.h"
#include "TimerManager.h"
#include "DataAssets/OLSExperienceActionSetDataAsset.h"
#include "DataAssets/OLSExperienceDefinitionDataAsset.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 UOLSExperienceDefinitionDataAsset* 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<UOLSExperienceDefinitionDataAsset> assetClass = Cast<UClass>(assetPath.TryLoad());
check(assetClass);
const UOLSExperienceDefinitionDataAsset* experience = GetDefault<UOLSExperienceDefinitionDataAsset>(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(
FOLSExperienceLoadedNativeDelegate::FDelegate&& delegate)
{
if (IsExperienceLoaded())
{
delegate.Execute(CurrentExperience);
}
else
{
OnExperienceLoaded_HighPriority.Add(MoveTemp(delegate));
}
}
void UOLSExperienceManagerComponent::CallOrRegister_OnExperienceLoaded(FOLSExperienceLoadedNativeDelegate::FDelegate&& delegate)
{
if (IsExperienceLoaded())
{
delegate.Execute(CurrentExperience);
}
else
{
OnExperienceLoaded.Add(MoveTemp(delegate));
}
}
void UOLSExperienceManagerComponent::CallOrRegister_OnExperienceLoaded_LowPriority(
FOLSExperienceLoadedNativeDelegate::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());
for (const TObjectPtr<UOLSExperienceActionSetDataAsset>& 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);
}