// Copyright Epic Games, Inc. All Rights Reserved. #include "LyraGameplayCueManager.h" #include "LyraLogChannels.h" #include "GameplayCueSet.h" #include "AbilitySystemGlobals.h" #include "HAL/IConsoleManager.h" #include "GameplayTagsManager.h" #include "UObject/UObjectThreadContext.h" #include "System/LyraAssetManager.h" #include "Async/Async.h" ////////////////////////////////////////////////////////////////////// enum class ELyraEditorLoadMode { // Loads all cues upfront; longer loading speed in the editor but short PIE times and effects never fail to play LoadUpfront, // Outside of editor: Async loads as cue tag are registered // In editor: Async loads when cues are invoked // Note: This can cause some 'why didn't I see the effect for X' issues in PIE and is good for iteration speed but otherwise bad for designers PreloadAsCuesAreReferenced_GameOnly, // Async loads as cue tag are registered PreloadAsCuesAreReferenced }; namespace LyraGameplayCueManagerCvars { static FAutoConsoleCommand CVarDumpGameplayCues( TEXT("Lyra.DumpGameplayCues"), TEXT("Shows all assets that were loaded via LyraGameplayCueManager and are currently in memory."), FConsoleCommandWithArgsDelegate::CreateStatic(ULyraGameplayCueManager::DumpGameplayCues)); static ELyraEditorLoadMode LoadMode = ELyraEditorLoadMode::LoadUpfront; } const bool bPreloadEvenInEditor = true; ////////////////////////////////////////////////////////////////////// struct FGameplayCueTagThreadSynchronizeGraphTask : public FAsyncGraphTaskBase { TFunction TheTask; FGameplayCueTagThreadSynchronizeGraphTask(TFunction&& Task) : TheTask(MoveTemp(Task)) { } void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) { TheTask(); } ENamedThreads::Type GetDesiredThread() { return ENamedThreads::GameThread; } }; ////////////////////////////////////////////////////////////////////// ULyraGameplayCueManager::ULyraGameplayCueManager(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { } ULyraGameplayCueManager* ULyraGameplayCueManager::Get() { return Cast(UAbilitySystemGlobals::Get().GetGameplayCueManager()); } void ULyraGameplayCueManager::OnCreated() { Super::OnCreated(); UpdateDelayLoadDelegateListeners(); } void ULyraGameplayCueManager::LoadAlwaysLoadedCues() { if (ShouldDelayLoadGameplayCues()) { UGameplayTagsManager& TagManager = UGameplayTagsManager::Get(); //@TODO: Try to collect these by filtering GameplayCue. tags out of native gameplay tags? TArray AdditionalAlwaysLoadedCueTags; for (const FName& CueTagName : AdditionalAlwaysLoadedCueTags) { FGameplayTag CueTag = TagManager.RequestGameplayTag(CueTagName, /*ErrorIfNotFound=*/ false); if (CueTag.IsValid()) { ProcessTagToPreload(CueTag, nullptr); } else { UE_LOG(LogLyra, Warning, TEXT("ULyraGameplayCueManager::AdditionalAlwaysLoadedCueTags contains invalid tag %s"), *CueTagName.ToString()); } } } } bool ULyraGameplayCueManager::ShouldAsyncLoadRuntimeObjectLibraries() const { switch (LyraGameplayCueManagerCvars::LoadMode) { case ELyraEditorLoadMode::LoadUpfront: return true; case ELyraEditorLoadMode::PreloadAsCuesAreReferenced_GameOnly: #if WITH_EDITOR if (GIsEditor) { return false; } #endif break; case ELyraEditorLoadMode::PreloadAsCuesAreReferenced: break; } return !ShouldDelayLoadGameplayCues(); } bool ULyraGameplayCueManager::ShouldSyncLoadMissingGameplayCues() const { return false; } bool ULyraGameplayCueManager::ShouldAsyncLoadMissingGameplayCues() const { return true; } void ULyraGameplayCueManager::DumpGameplayCues(const TArray& Args) { ULyraGameplayCueManager* GCM = Cast(UAbilitySystemGlobals::Get().GetGameplayCueManager()); if (!GCM) { UE_LOG(LogLyra, Error, TEXT("DumpGameplayCues failed. No ULyraGameplayCueManager found.")); return; } const bool bIncludeRefs = Args.Contains(TEXT("Refs")); UE_LOG(LogLyra, Log, TEXT("=========== Dumping Always Loaded Gameplay Cue Notifies ===========")); for (UClass* CueClass : GCM->AlwaysLoadedCues) { UE_LOG(LogLyra, Log, TEXT(" %s"), *GetPathNameSafe(CueClass)); } UE_LOG(LogLyra, Log, TEXT("=========== Dumping Preloaded Gameplay Cue Notifies ===========")); for (UClass* CueClass : GCM->PreloadedCues) { TSet* ReferencerSet = GCM->PreloadedCueReferencers.Find(CueClass); int32 NumRefs = ReferencerSet ? ReferencerSet->Num() : 0; UE_LOG(LogLyra, Log, TEXT(" %s (%d refs)"), *GetPathNameSafe(CueClass), NumRefs); if (bIncludeRefs && ReferencerSet) { for (const FObjectKey& Ref : *ReferencerSet) { UObject* RefObject = Ref.ResolveObjectPtr(); UE_LOG(LogLyra, Log, TEXT(" ^- %s"), *GetPathNameSafe(RefObject)); } } } UE_LOG(LogLyra, Log, TEXT("=========== Dumping Gameplay Cue Notifies loaded on demand ===========")); int32 NumMissingCuesLoaded = 0; if (GCM->RuntimeGameplayCueObjectLibrary.CueSet) { for (const FGameplayCueNotifyData& CueData : GCM->RuntimeGameplayCueObjectLibrary.CueSet->GameplayCueData) { if (CueData.LoadedGameplayCueClass && !GCM->AlwaysLoadedCues.Contains(CueData.LoadedGameplayCueClass) && !GCM->PreloadedCues.Contains(CueData.LoadedGameplayCueClass)) { NumMissingCuesLoaded++; UE_LOG(LogLyra, Log, TEXT(" %s"), *CueData.LoadedGameplayCueClass->GetPathName()); } } } UE_LOG(LogLyra, Log, TEXT("=========== Gameplay Cue Notify summary ===========")); UE_LOG(LogLyra, Log, TEXT(" ... %d cues in always loaded list"), GCM->AlwaysLoadedCues.Num()); UE_LOG(LogLyra, Log, TEXT(" ... %d cues in preloaded list"), GCM->PreloadedCues.Num()); UE_LOG(LogLyra, Log, TEXT(" ... %d cues loaded on demand"), NumMissingCuesLoaded); UE_LOG(LogLyra, Log, TEXT(" ... %d cues in total"), GCM->AlwaysLoadedCues.Num() + GCM->PreloadedCues.Num() + NumMissingCuesLoaded); } void ULyraGameplayCueManager::OnGameplayTagLoaded(const FGameplayTag& Tag) { FScopeLock ScopeLock(&LoadedGameplayTagsToProcessCS); bool bStartTask = LoadedGameplayTagsToProcess.Num() == 0; FUObjectSerializeContext* LoadContext = FUObjectThreadContext::Get().GetSerializeContext(); UObject* OwningObject = LoadContext ? LoadContext->SerializedObject : nullptr; LoadedGameplayTagsToProcess.Emplace(Tag, OwningObject); if (bStartTask) { TGraphTask::CreateTask().ConstructAndDispatchWhenReady([]() { if (GIsRunning) { if (ULyraGameplayCueManager* StrongThis = Get()) { // If we are garbage collecting we cannot call StaticFindObject (or a few other static uobject functions), so we'll just wait until the GC is over and process the tags then if (IsGarbageCollecting()) { StrongThis->bProcessLoadedTagsAfterGC = true; } else { StrongThis->ProcessLoadedTags(); } } } }); } } void ULyraGameplayCueManager::HandlePostGarbageCollect() { if (bProcessLoadedTagsAfterGC) { ProcessLoadedTags(); } bProcessLoadedTagsAfterGC = false; } void ULyraGameplayCueManager::ProcessLoadedTags() { TArray TaskLoadedGameplayTagsToProcess; { // Lock LoadedGameplayTagsToProcess just long enough to make a copy and clear FScopeLock TaskScopeLock(&LoadedGameplayTagsToProcessCS); TaskLoadedGameplayTagsToProcess = LoadedGameplayTagsToProcess; LoadedGameplayTagsToProcess.Empty(); } // This might return during shutdown, and we don't want to proceed if that is the case if (GIsRunning) { if (RuntimeGameplayCueObjectLibrary.CueSet) { for (const FLoadedGameplayTagToProcessData& LoadedTagData : TaskLoadedGameplayTagsToProcess) { if (RuntimeGameplayCueObjectLibrary.CueSet->GameplayCueDataMap.Contains(LoadedTagData.Tag)) { if (!LoadedTagData.WeakOwner.IsStale()) { ProcessTagToPreload(LoadedTagData.Tag, LoadedTagData.WeakOwner.Get()); } } } } else { UE_LOG(LogLyra, Warning, TEXT("ULyraGameplayCueManager::OnGameplayTagLoaded processed loaded tag(s) but RuntimeGameplayCueObjectLibrary.CueSet was null. Skipping processing.")); } } } void ULyraGameplayCueManager::ProcessTagToPreload(const FGameplayTag& Tag, UObject* OwningObject) { switch (LyraGameplayCueManagerCvars::LoadMode) { case ELyraEditorLoadMode::LoadUpfront: return; case ELyraEditorLoadMode::PreloadAsCuesAreReferenced_GameOnly: #if WITH_EDITOR if (GIsEditor) { return; } #endif break; case ELyraEditorLoadMode::PreloadAsCuesAreReferenced: break; } check(RuntimeGameplayCueObjectLibrary.CueSet); int32* DataIdx = RuntimeGameplayCueObjectLibrary.CueSet->GameplayCueDataMap.Find(Tag); if (DataIdx && RuntimeGameplayCueObjectLibrary.CueSet->GameplayCueData.IsValidIndex(*DataIdx)) { const FGameplayCueNotifyData& CueData = RuntimeGameplayCueObjectLibrary.CueSet->GameplayCueData[*DataIdx]; UClass* LoadedGameplayCueClass = FindObject(nullptr, *CueData.GameplayCueNotifyObj.ToString()); if (LoadedGameplayCueClass) { RegisterPreloadedCue(LoadedGameplayCueClass, OwningObject); } else { bool bAlwaysLoadedCue = OwningObject == nullptr; TWeakObjectPtr WeakOwner = OwningObject; StreamableManager.RequestAsyncLoad(CueData.GameplayCueNotifyObj, FStreamableDelegate::CreateUObject(this, &ThisClass::OnPreloadCueComplete, CueData.GameplayCueNotifyObj, WeakOwner, bAlwaysLoadedCue), FStreamableManager::DefaultAsyncLoadPriority, false, false, TEXT("GameplayCueManager")); } } } void ULyraGameplayCueManager::OnPreloadCueComplete(FSoftObjectPath Path, TWeakObjectPtr OwningObject, bool bAlwaysLoadedCue) { if (bAlwaysLoadedCue || OwningObject.IsValid()) { if (UClass* LoadedGameplayCueClass = Cast(Path.ResolveObject())) { RegisterPreloadedCue(LoadedGameplayCueClass, OwningObject.Get()); } } } void ULyraGameplayCueManager::RegisterPreloadedCue(UClass* LoadedGameplayCueClass, UObject* OwningObject) { check(LoadedGameplayCueClass); const bool bAlwaysLoadedCue = OwningObject == nullptr; if (bAlwaysLoadedCue) { AlwaysLoadedCues.Add(LoadedGameplayCueClass); PreloadedCues.Remove(LoadedGameplayCueClass); PreloadedCueReferencers.Remove(LoadedGameplayCueClass); } else if ((OwningObject != LoadedGameplayCueClass) && (OwningObject != LoadedGameplayCueClass->GetDefaultObject()) && !AlwaysLoadedCues.Contains(LoadedGameplayCueClass)) { PreloadedCues.Add(LoadedGameplayCueClass); TSet& ReferencerSet = PreloadedCueReferencers.FindOrAdd(LoadedGameplayCueClass); ReferencerSet.Add(OwningObject); } } void ULyraGameplayCueManager::HandlePostLoadMap(UWorld* NewWorld) { if (RuntimeGameplayCueObjectLibrary.CueSet) { for (UClass* CueClass : AlwaysLoadedCues) { RuntimeGameplayCueObjectLibrary.CueSet->RemoveLoadedClass(CueClass); } for (UClass* CueClass : PreloadedCues) { RuntimeGameplayCueObjectLibrary.CueSet->RemoveLoadedClass(CueClass); } } for (auto CueIt = PreloadedCues.CreateIterator(); CueIt; ++CueIt) { TSet& ReferencerSet = PreloadedCueReferencers.FindChecked(*CueIt); for (auto RefIt = ReferencerSet.CreateIterator(); RefIt; ++RefIt) { if (!RefIt->ResolveObjectPtr()) { RefIt.RemoveCurrent(); } } if (ReferencerSet.Num() == 0) { PreloadedCueReferencers.Remove(*CueIt); CueIt.RemoveCurrent(); } } } void ULyraGameplayCueManager::UpdateDelayLoadDelegateListeners() { UGameplayTagsManager::Get().OnGameplayTagLoadedDelegate.RemoveAll(this); FCoreUObjectDelegates::GetPostGarbageCollect().RemoveAll(this); FCoreUObjectDelegates::PostLoadMapWithWorld.RemoveAll(this); switch (LyraGameplayCueManagerCvars::LoadMode) { case ELyraEditorLoadMode::LoadUpfront: return; case ELyraEditorLoadMode::PreloadAsCuesAreReferenced_GameOnly: #if WITH_EDITOR if (GIsEditor) { return; } #endif break; case ELyraEditorLoadMode::PreloadAsCuesAreReferenced: break; } UGameplayTagsManager::Get().OnGameplayTagLoadedDelegate.AddUObject(this, &ThisClass::OnGameplayTagLoaded); FCoreUObjectDelegates::GetPostGarbageCollect().AddUObject(this, &ThisClass::HandlePostGarbageCollect); FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &ThisClass::HandlePostLoadMap); } bool ULyraGameplayCueManager::ShouldDelayLoadGameplayCues() const { const bool bClientDelayLoadGameplayCues = true; return !IsRunningDedicatedServer() && bClientDelayLoadGameplayCues; } const FPrimaryAssetType UFortAssetManager_GameplayCueRefsType = TEXT("GameplayCueRefs"); const FName UFortAssetManager_GameplayCueRefsName = TEXT("GameplayCueReferences"); const FName UFortAssetManager_LoadStateClient = FName(TEXT("Client")); void ULyraGameplayCueManager::RefreshGameplayCuePrimaryAsset() { TArray CuePaths; UGameplayCueSet* RuntimeGameplayCueSet = GetRuntimeCueSet(); if (RuntimeGameplayCueSet) { RuntimeGameplayCueSet->GetSoftObjectPaths(CuePaths); } FAssetBundleData BundleData; BundleData.AddBundleAssets(UFortAssetManager_LoadStateClient, CuePaths); FPrimaryAssetId PrimaryAssetId = FPrimaryAssetId(UFortAssetManager_GameplayCueRefsType, UFortAssetManager_GameplayCueRefsName); UAssetManager::Get().AddDynamicAsset(PrimaryAssetId, FSoftObjectPath(), BundleData); }