1837 lines
66 KiB
C++
1837 lines
66 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "GameplayCueManager.h"
|
|
#include "Engine/Blueprint.h"
|
|
#include "Engine/ObjectLibrary.h"
|
|
#include "GameplayCueNotify_Actor.h"
|
|
#include "Misc/MessageDialog.h"
|
|
#include "Stats/StatsMisc.h"
|
|
#include "Misc/ScopedSlowTask.h"
|
|
#include "Modules/ModuleManager.h"
|
|
#include "DrawDebugHelpers.h"
|
|
#include "GameplayTagsManager.h"
|
|
#include "GameplayTagsModule.h"
|
|
#include "AbilitySystemGlobals.h"
|
|
#include "AbilitySystemLog.h"
|
|
#include "AssetRegistry/AssetRegistryModule.h"
|
|
#include "GameplayCueInterface.h"
|
|
#include "GameplayCueSet.h"
|
|
#include "GameplayCueNotify_Static.h"
|
|
#include "AbilitySystemComponent.h"
|
|
#include "Net/DataReplication.h"
|
|
#include "Engine/ActorChannel.h"
|
|
#include "Engine/NetConnection.h"
|
|
#include "Net/UnrealNetwork.h"
|
|
#include "Misc/CoreDelegates.h"
|
|
#include "AbilitySystemReplicationProxyInterface.h"
|
|
#include "UObject/LinkerLoad.h"
|
|
|
|
#include UE_INLINE_GENERATED_CPP_BY_NAME(GameplayCueManager)
|
|
|
|
#if WITH_EDITOR
|
|
#include "Editor.h"
|
|
#include "Engine/Engine.h"
|
|
#include "ISequenceRecorder.h"
|
|
#define LOCTEXT_NAMESPACE "GameplayCueManager"
|
|
#endif
|
|
|
|
int32 LogGameplayCueActorSpawning = 0;
|
|
static FAutoConsoleVariableRef CVarLogGameplayCueActorSpawning(TEXT("AbilitySystem.LogGameplayCueActorSpawning"), LogGameplayCueActorSpawning, TEXT("Log when we create GameplayCueNotify_Actors"), ECVF_Default );
|
|
|
|
int32 DisplayGameplayCues = 0;
|
|
static FAutoConsoleVariableRef CVarDisplayGameplayCues(TEXT("AbilitySystem.DisplayGameplayCues"), DisplayGameplayCues, TEXT("Display GameplayCue events in world as text."), ECVF_Default );
|
|
|
|
int32 DisableGameplayCues = 0;
|
|
static FAutoConsoleVariableRef CVarDisableGameplayCues(TEXT("AbilitySystem.DisableGameplayCues"), DisableGameplayCues, TEXT("Disables all GameplayCue events in the world."), ECVF_Default );
|
|
|
|
float DisplayGameplayCueDuration = 5.f;
|
|
static FAutoConsoleVariableRef CVarDurationeGameplayCues(TEXT("AbilitySystem.GameplayCue.DisplayDuration"), DisplayGameplayCueDuration, TEXT("Disables all GameplayCue events in the world."), ECVF_Default );
|
|
|
|
int32 GameplayCueRunOnDedicatedServer = 0;
|
|
static FAutoConsoleVariableRef CVarDedicatedServerGameplayCues(TEXT("AbilitySystem.GameplayCue.RunOnDedicatedServer"), GameplayCueRunOnDedicatedServer, TEXT("Run gameplay cue events on dedicated server"), ECVF_Default );
|
|
|
|
bool EnableSuppressCuesOnGameplayCueManager = true;
|
|
static FAutoConsoleVariableRef CVarEnableSuppressCuesOnGameplayCueManager(TEXT("AbilitySystem.GameplayCue.EnableSuppressCuesOnGameplayCueManager"), EnableSuppressCuesOnGameplayCueManager, TEXT("Allows the GameplayCueManager to suppress cues when the bSuppressGameplayCues is set on the target AbilitySystemComponent"), ECVF_Default );
|
|
|
|
#if WITH_EDITOR
|
|
USceneComponent* UGameplayCueManager::PreviewComponent = nullptr;
|
|
UWorld* UGameplayCueManager::PreviewWorld = nullptr;
|
|
FGameplayCueProxyTick UGameplayCueManager::PreviewProxyTick;
|
|
#endif
|
|
|
|
UWorld* UGameplayCueManager::CurrentWorld = nullptr;
|
|
|
|
UGameplayCueManager::UGameplayCueManager(const FObjectInitializer& PCIP)
|
|
: Super(PCIP)
|
|
{
|
|
#if WITH_EDITOR
|
|
bAccelerationMapOutdated = true;
|
|
EditorObjectLibraryFullyInitialized = false;
|
|
#endif
|
|
}
|
|
|
|
void UGameplayCueManager::OnCreated()
|
|
{
|
|
FWorldDelegates::OnPostWorldCleanup.AddUObject(this, &UGameplayCueManager::OnPostWorldCleanup);
|
|
FNetworkReplayDelegates::OnPreScrub.AddUObject(this, &UGameplayCueManager::OnPreReplayScrub);
|
|
|
|
#if WITH_EDITOR
|
|
if (GIsRunning)
|
|
{
|
|
// Engine init already completed
|
|
OnEngineInitComplete();
|
|
}
|
|
else
|
|
{
|
|
FCoreDelegates::OnFEngineLoopInitComplete.AddUObject(this, &UGameplayCueManager::OnEngineInitComplete);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void UGameplayCueManager::OnEngineInitComplete()
|
|
{
|
|
#if WITH_EDITOR
|
|
FCoreDelegates::OnFEngineLoopInitComplete.AddUObject(this, &UGameplayCueManager::OnEngineInitComplete);
|
|
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
|
|
AssetRegistryModule.Get().OnInMemoryAssetCreated().AddUObject(this, &UGameplayCueManager::HandleAssetAdded);
|
|
AssetRegistryModule.Get().OnInMemoryAssetDeleted().AddUObject(this, &UGameplayCueManager::HandleAssetDeleted);
|
|
AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UGameplayCueManager::HandleAssetRenamed);
|
|
FWorldDelegates::OnPreWorldInitialization.AddUObject(this, &UGameplayCueManager::ReloadObjectLibrary);
|
|
|
|
if (GIsEditor)
|
|
{
|
|
InitializeEditorObjectLibrary();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
bool IsDedicatedServerForGameplayCue()
|
|
{
|
|
#if WITH_EDITOR
|
|
// This will handle dedicated server PIE case properly
|
|
return GEngine->ShouldAbsorbCosmeticOnlyEvent();
|
|
#else
|
|
// When in standalone non editor, this is the fastest way to check
|
|
return IsRunningDedicatedServer();
|
|
#endif
|
|
}
|
|
|
|
|
|
void UGameplayCueManager::HandleGameplayCues(AActor* TargetActor, const FGameplayTagContainer& GameplayCueTags, EGameplayCueEvent::Type EventType, const FGameplayCueParameters& Parameters, EGameplayCueExecutionOptions Options)
|
|
{
|
|
#if WITH_EDITOR
|
|
if (GIsEditor && TargetActor == nullptr && UGameplayCueManager::PreviewComponent)
|
|
{
|
|
TargetActor = GetMutableDefault<AActor>();
|
|
}
|
|
#endif
|
|
|
|
if (!(Options & EGameplayCueExecutionOptions::IgnoreSuppression) && ShouldSuppressGameplayCues(TargetActor))
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (auto It = GameplayCueTags.CreateConstIterator(); It; ++It)
|
|
{
|
|
HandleGameplayCue(TargetActor, *It, EventType, Parameters, Options);
|
|
}
|
|
}
|
|
|
|
void UGameplayCueManager::HandleGameplayCue(AActor* TargetActor, FGameplayTag GameplayCueTag, EGameplayCueEvent::Type EventType, const FGameplayCueParameters& Parameters, EGameplayCueExecutionOptions Options)
|
|
{
|
|
#if WITH_SERVER_CODE
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_GameplayCueManager_HandleGameplayCue);
|
|
#endif
|
|
|
|
#if WITH_EDITOR
|
|
if (RuntimeGameplayCueObjectLibrary.bHasBeenInitialized == false && ShouldDeferScanningRuntimeLibraries())
|
|
{
|
|
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
|
|
if (AssetRegistryModule.Get().IsGathering())
|
|
{
|
|
ABILITY_LOG(Warning, TEXT("Waiting for AR scan in HandleGameplayCue..."));
|
|
// This will trigger the callback to InitializeRuntimeObjectLibrary via OnKnownGathersComplete
|
|
AssetRegistryModule.Get().WaitForCompletion();
|
|
}
|
|
}
|
|
|
|
if (GIsEditor && TargetActor == nullptr && UGameplayCueManager::PreviewComponent)
|
|
{
|
|
TargetActor = Cast<AActor>(AActor::StaticClass()->GetDefaultObject());
|
|
}
|
|
#endif
|
|
|
|
if (!(Options & EGameplayCueExecutionOptions::IgnoreSuppression) && ShouldSuppressGameplayCues(TargetActor))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!(Options & EGameplayCueExecutionOptions::IgnoreTranslation))
|
|
{
|
|
TranslateGameplayCue(GameplayCueTag, TargetActor, Parameters);
|
|
}
|
|
|
|
RouteGameplayCue(TargetActor, GameplayCueTag, EventType, Parameters, Options);
|
|
}
|
|
|
|
bool UGameplayCueManager::ShouldSuppressGameplayCues(AActor* TargetActor)
|
|
{
|
|
if (DisableGameplayCues ||
|
|
!TargetActor ||
|
|
(GameplayCueRunOnDedicatedServer == 0 && IsDedicatedServerForGameplayCue()))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
void UGameplayCueManager::RouteGameplayCue(AActor* TargetActor, FGameplayTag GameplayCueTag, EGameplayCueEvent::Type EventType, const FGameplayCueParameters& Parameters, EGameplayCueExecutionOptions Options)
|
|
{
|
|
#if WITH_SERVER_CODE
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_GameplayCueManager_RouteGameplayCue);
|
|
#endif
|
|
|
|
if (!Parameters.OriginalTag.IsValid())
|
|
{
|
|
Parameters.OriginalTag = GameplayCueTag;
|
|
}
|
|
|
|
// If we want to ignore interfaces, set the pointer to null
|
|
IGameplayCueInterface* GameplayCueInterface = !(Options & EGameplayCueExecutionOptions::IgnoreInterfaces) ? Cast<IGameplayCueInterface>(TargetActor) : nullptr;
|
|
bool bAcceptsCue = true;
|
|
if (GameplayCueInterface)
|
|
{
|
|
bAcceptsCue = GameplayCueInterface->ShouldAcceptGameplayCue(TargetActor, GameplayCueTag, EventType, Parameters);
|
|
}
|
|
|
|
#if !UE_BUILD_SHIPPING
|
|
if (OnRouteGameplayCue.IsBound())
|
|
{
|
|
OnRouteGameplayCue.Broadcast(TargetActor, GameplayCueTag, EventType, Parameters, Options);
|
|
}
|
|
#endif // !UE_BUILD_SHIPPING
|
|
|
|
#if ENABLE_DRAW_DEBUG
|
|
if (DisplayGameplayCues && !(Options & EGameplayCueExecutionOptions::IgnoreDebug))
|
|
{
|
|
FString DebugStr = FString::Printf(TEXT("[%s] %s - %s"), *GetNameSafe(TargetActor), *GameplayCueTag.ToString(), *EGameplayCueEventToString(EventType) );
|
|
FColor DebugColor = EventType == EGameplayCueEvent::Removed ? FColor::Red : FColor::Green;
|
|
DrawDebugString(TargetActor->GetWorld(), FVector(0.f, 0.f, -10.0f * static_cast<int>(EventType)), DebugStr, TargetActor, DebugColor, DisplayGameplayCueDuration);
|
|
ABILITY_LOG(Display, TEXT("%s"), *DebugStr);
|
|
}
|
|
#endif // ENABLE_DRAW_DEBUG
|
|
|
|
CurrentWorld = TargetActor->GetWorld();
|
|
|
|
// Don't handle gameplay cues when world is tearing down
|
|
if (!GetWorld() || GetWorld()->bIsTearingDown)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Give the global set a chance
|
|
if (bAcceptsCue && !(Options & EGameplayCueExecutionOptions::IgnoreNotifies))
|
|
{
|
|
RuntimeGameplayCueObjectLibrary.CueSet->HandleGameplayCue(TargetActor, GameplayCueTag, EventType, Parameters);
|
|
}
|
|
|
|
// Use the interface even if it's not in the map
|
|
if (GameplayCueInterface && bAcceptsCue)
|
|
{
|
|
GameplayCueInterface->HandleGameplayCue(TargetActor, GameplayCueTag, EventType, Parameters);
|
|
}
|
|
|
|
// This is to force client side replays to record the target of the GC on the next frame. (ForceNetUpdates from the server will not translate into ForceNetUpdates on the client/replays)
|
|
TargetActor->ForceNetUpdate();
|
|
|
|
CurrentWorld = nullptr;
|
|
}
|
|
|
|
void UGameplayCueManager::TranslateGameplayCue(FGameplayTag& Tag, AActor* TargetActor, const FGameplayCueParameters& Parameters)
|
|
{
|
|
#if WITH_SERVER_CODE
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_GameplayCueManager_TranslateGameplayCue);
|
|
#endif
|
|
|
|
TranslationManager.TranslateTag(Tag, TargetActor, Parameters);
|
|
}
|
|
|
|
void UGameplayCueManager::AddGameplayCue_NonReplicated(AActor* Target, const FGameplayTag GameplayCueTag, const FGameplayCueParameters& Parameters)
|
|
{
|
|
if (UAbilitySystemComponent* ASC = UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(Target))
|
|
{
|
|
ASC->AddLooseGameplayTag(GameplayCueTag);
|
|
}
|
|
|
|
if (UGameplayCueManager* GCM = UAbilitySystemGlobals::Get().GetGameplayCueManager())
|
|
{
|
|
GCM->HandleGameplayCue(Target, GameplayCueTag, EGameplayCueEvent::OnActive, Parameters);
|
|
GCM->HandleGameplayCue(Target, GameplayCueTag, EGameplayCueEvent::WhileActive, Parameters);
|
|
}
|
|
}
|
|
|
|
void UGameplayCueManager::RemoveGameplayCue_NonReplicated(AActor* Target, const FGameplayTag GameplayCueTag, const FGameplayCueParameters& Parameters)
|
|
{
|
|
if (UAbilitySystemComponent* ASC = UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(Target))
|
|
{
|
|
ASC->RemoveLooseGameplayTag(GameplayCueTag);
|
|
}
|
|
|
|
if (UGameplayCueManager* GCM = UAbilitySystemGlobals::Get().GetGameplayCueManager())
|
|
{
|
|
GCM->HandleGameplayCue(Target, GameplayCueTag, EGameplayCueEvent::Removed, Parameters);
|
|
}
|
|
}
|
|
|
|
void UGameplayCueManager::ExecuteGameplayCue_NonReplicated(AActor* Target, const FGameplayTag GameplayCueTag, const FGameplayCueParameters& Parameters)
|
|
{
|
|
if (UGameplayCueManager* GCM = UAbilitySystemGlobals::Get().GetGameplayCueManager())
|
|
{
|
|
GCM->HandleGameplayCue(Target, GameplayCueTag, EGameplayCueEvent::Executed, Parameters);
|
|
}
|
|
}
|
|
|
|
void UGameplayCueManager::EndGameplayCuesFor(AActor* TargetActor)
|
|
{
|
|
// Make a copy so that OnOwnerDestroyed can remove itself (if it so chooses)
|
|
TArray<TObjectPtr<AActor>> Children = TargetActor->Children;
|
|
for (AActor* Child : Children)
|
|
{
|
|
AGameplayCueNotify_Actor* NotifyActor = Cast<AGameplayCueNotify_Actor>(Child);
|
|
if (NotifyActor)
|
|
{
|
|
NotifyActor->OnOwnerDestroyed(TargetActor);
|
|
}
|
|
}
|
|
}
|
|
|
|
int32 GameplayCueActorRecycle = 1;
|
|
static FAutoConsoleVariableRef CVarGameplayCueActorRecycle(TEXT("AbilitySystem.GameplayCueActorRecycle"), GameplayCueActorRecycle, TEXT("Allow recycling of GameplayCue Actors"), ECVF_Default );
|
|
|
|
int32 GameplayCueActorRecycleDebug = 0;
|
|
static FAutoConsoleVariableRef CVarGameplayCueActorRecycleDebug(TEXT("AbilitySystem.GameplayCueActorRecycleDebug"), GameplayCueActorRecycleDebug, TEXT("Prints logs for GC actor recycling debugging"), ECVF_Default );
|
|
|
|
bool UGameplayCueManager::IsGameplayCueRecylingEnabled()
|
|
{
|
|
return GameplayCueActorRecycle > 0;
|
|
}
|
|
|
|
bool UGameplayCueManager::ShouldSyncLoadMissingGameplayCues() const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool UGameplayCueManager::ShouldAsyncLoadMissingGameplayCues() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool UGameplayCueManager::IsAsyncLoadingGameplayCueNotifyClass(const FSoftObjectPath& CueNotifyClass) const
|
|
{
|
|
return AsyncLoadPendingGameplayCues.Contains(CueNotifyClass);
|
|
}
|
|
|
|
bool UGameplayCueManager::HandleMissingGameplayCue(UGameplayCueSet* OwningSet, struct FGameplayCueNotifyData& CueData, AActor* TargetActor, EGameplayCueEvent::Type EventType, FGameplayCueParameters& Parameters)
|
|
{
|
|
if (ShouldSyncLoadMissingGameplayCues())
|
|
{
|
|
CueData.LoadedGameplayCueClass = Cast<UClass>(StreamableManager.LoadSynchronous(CueData.GameplayCueNotifyObj, false));
|
|
|
|
if (CueData.LoadedGameplayCueClass)
|
|
{
|
|
ABILITY_LOG(Display, TEXT("GameplayCueNotify %s was not loaded when GameplayCue was invoked, did synchronous load."), *CueData.GameplayCueNotifyObj.ToString());
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
ABILITY_LOG(Warning, TEXT("Late load of GameplayCueNotify %s failed!"), *CueData.GameplayCueNotifyObj.ToString());
|
|
}
|
|
}
|
|
else if (ShouldAsyncLoadMissingGameplayCues())
|
|
{
|
|
const bool bStartAsyncLoad = !IsAsyncLoadingGameplayCueNotifyClass(CueData.GameplayCueNotifyObj);
|
|
const FAsyncLoadPendingGameplayCue PendingCue(TWeakObjectPtr<UGameplayCueSet>(OwningSet), CueData.GameplayCueTag, MakeWeakObjectPtr(TargetActor), EventType, Parameters);
|
|
if (bStartAsyncLoad)
|
|
{
|
|
ABILITY_LOG(Display, TEXT("Deferred gameplay cue with tag '%s' and event type '%d' until GCN class '%s' is loaded (started async loading)."), *CueData.GameplayCueTag.ToString(), int32(EventType), *CueData.GameplayCueNotifyObj.ToString());
|
|
|
|
// Cache the cue parameters to call it when the GCN class finishes async loading
|
|
TArray<FAsyncLoadPendingGameplayCue>& PendingEvents = AsyncLoadPendingGameplayCues.Add(CueData.GameplayCueNotifyObj);
|
|
PendingEvents.Add(PendingCue);
|
|
|
|
// Start async loading the GCN class
|
|
StreamableManager.RequestAsyncLoad(CueData.GameplayCueNotifyObj, FStreamableDelegate::CreateUObject(this, &UGameplayCueManager::OnMissingCueAsyncLoadComplete, CueData.GameplayCueNotifyObj));
|
|
}
|
|
else
|
|
{
|
|
ABILITY_LOG(Display, TEXT("Deferred gameplay cue with tag '%s' and event type '%d' until GCN class '%s' finishes async loading (already loading)."), *CueData.GameplayCueTag.ToString(), int32(EventType), *CueData.GameplayCueNotifyObj.ToString());
|
|
|
|
// Cache the cue parameters to call it when the GCN class finishes async loading
|
|
AsyncLoadPendingGameplayCues[CueData.GameplayCueNotifyObj].Add(PendingCue);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void UGameplayCueManager::OnMissingCueAsyncLoadComplete(FSoftObjectPath LoadedNotifyClass)
|
|
{
|
|
if (!LoadedNotifyClass.ResolveObject())
|
|
{
|
|
// Load failed
|
|
ABILITY_LOG(Warning, TEXT("Late load of GameplayCueNotify %s failed!"), *LoadedNotifyClass.ToString());
|
|
return;
|
|
}
|
|
|
|
if (ensureMsgf(AsyncLoadPendingGameplayCues.Contains(LoadedNotifyClass), TEXT("Expected an entry for '%s' that was created when async loading for it started"), *LoadedNotifyClass.ToString()))
|
|
{
|
|
// Keep the array of the pending cue events and their parameters. Remove the GCN class from the map of pending events
|
|
// because we're no longer async loading the class: the calls to HandleGameplayCue that we're doing now should execute directly.
|
|
TArray<FAsyncLoadPendingGameplayCue> PendingEventsCopy = MoveTemp(AsyncLoadPendingGameplayCues[LoadedNotifyClass]);
|
|
AsyncLoadPendingGameplayCues.Remove(LoadedNotifyClass);
|
|
|
|
ABILITY_LOG(Display, TEXT("Finished async loading GameplayCueNotify class '%s'. Executing %d deferred gameplay cues."), *LoadedNotifyClass.ToString(), PendingEventsCopy.Num());
|
|
|
|
// Call all pending cues for the GCN class that finished loading
|
|
for (const FAsyncLoadPendingGameplayCue& PendingEvent : PendingEventsCopy)
|
|
{
|
|
// Owning set may have been GCed in the meantime
|
|
if (PendingEvent.OwningSet.IsValid())
|
|
{
|
|
CurrentWorld = PendingEvent.TargetActor.IsValid() ? PendingEvent.TargetActor->GetWorld() : nullptr;
|
|
if (!CurrentWorld)
|
|
{
|
|
// TargetActor has since been destroyed. Attempt to get the world from the other actors.
|
|
const AActor* CueInstigator = PendingEvent.Parameters.GetInstigator();
|
|
CurrentWorld = CueInstigator ? CueInstigator->GetWorld() : nullptr;
|
|
if (!CurrentWorld)
|
|
{
|
|
const AActor* EffectCauser = PendingEvent.Parameters.GetEffectCauser();
|
|
CurrentWorld = EffectCauser ? EffectCauser->GetWorld() : nullptr;
|
|
}
|
|
}
|
|
|
|
// Don't handle gameplay cues for worlds that are tearing down
|
|
if (!GetWorld() || GetWorld()->bIsTearingDown)
|
|
{
|
|
ABILITY_LOG(Verbose, TEXT("Skipping deferred gameplay cue due to invalid world. GCN class: %s | GameplayCueTag: %s | EventType: %d"), *LoadedNotifyClass.ToString(), *PendingEvent.GameplayCueTag.ToString(), int32(PendingEvent.EventType));
|
|
continue;
|
|
}
|
|
|
|
// Objects are still valid, re-execute cue
|
|
ABILITY_LOG(Verbose, TEXT("Executing deferred gameplay cue. GCN class: %s | GameplayCueTag: %s | EventType: %d"), *LoadedNotifyClass.ToString(), *PendingEvent.GameplayCueTag.ToString(), int32(PendingEvent.EventType));
|
|
PendingEvent.OwningSet->HandleGameplayCue(PendingEvent.TargetActor.Get(), PendingEvent.GameplayCueTag, PendingEvent.EventType, PendingEvent.Parameters);
|
|
|
|
CurrentWorld = nullptr;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
AGameplayCueNotify_Actor* UGameplayCueManager::FindExistingCueOnActor(const AActor& TargetActor, const TSubclassOf<AGameplayCueNotify_Actor>& CueClass, const FGameplayCueParameters& Parameters) const
|
|
{
|
|
for (AActor* Child : TargetActor.Children)
|
|
{
|
|
if (IsValid(Child) && Child->IsA(CueClass))
|
|
{
|
|
AGameplayCueNotify_Actor* ChildNotify = CastChecked<AGameplayCueNotify_Actor>(Child);
|
|
|
|
// Somehow the LifeSpan can end up being zero, meaning we're about to be destroyed (so don't reuse)
|
|
if (ChildNotify->GameplayCuePendingRemove())
|
|
{
|
|
UE_LOG(LogAbilitySystem, Verbose, TEXT("FindExistingCueActor considered %s, but it was pending remove"), *GetNameSafe(ChildNotify));
|
|
continue;
|
|
}
|
|
|
|
const bool bInstigatorMatches = !ChildNotify->bUniqueInstancePerInstigator || ChildNotify->CueInstigator == Parameters.GetInstigator();
|
|
const bool bSourceMatches = !ChildNotify->bUniqueInstancePerSourceObject || ChildNotify->CueSourceObject == Parameters.GetSourceObject();
|
|
if (bInstigatorMatches && bSourceMatches)
|
|
{
|
|
return ChildNotify;
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
AGameplayCueNotify_Actor* UGameplayCueManager::GetInstancedCueActor(AActor* TargetActor, UClass* GameplayCueNotifyActorClass, const FGameplayCueParameters& Parameters)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_GameplayCueManager_GetInstancedCueActor);
|
|
|
|
const TSubclassOf<AGameplayCueNotify_Actor> CueClass = GameplayCueNotifyActorClass;
|
|
if (!ensure(TargetActor) || !ensure(CueClass))
|
|
{
|
|
UE_LOG(LogAbilitySystem, Error, TEXT("GetInstancedCueActor called with invalid parameters (TargetActor = %s, CueClass = %s)"), *GetNameSafe(TargetActor), *GetNameSafe(GameplayCueNotifyActorClass));
|
|
return nullptr;
|
|
}
|
|
|
|
if (TargetActor && TargetActor->GetActorTransform().ContainsNaN())
|
|
{
|
|
UE_LOG(LogAbilitySystem, Error, TEXT("GetInstancedCueActor called with invalid target actor transform (TargetActor = %s)"), *GetNameSafe(TargetActor));
|
|
return nullptr;
|
|
}
|
|
|
|
// There used to be special code here to handle the case where the TargetActor was a CDO. I'm not sure why that would be (or how that's even possible -- perhaps in the default Blueprint Viewport? But I can't trigger it.)
|
|
// Let's log it in case a user comes across this issue.
|
|
// Animtion preview hack. If we are trying to play the GC on a CDO, then don't use actor recycling and don't set the owner (to the CDO, which would cause problems)
|
|
// And we'll try to manually find (and reuse) the existing instance on the TargetActor.
|
|
UE_CLOG(WITH_EDITOR && TargetActor->HasAnyFlags(RF_ClassDefaultObject), LogAbilitySystem, Warning, TEXT("Adding %s to CDO %s. This used to be explicitly disallowed."), *GetNameSafe(CueClass), *GetNameSafe(TargetActor));
|
|
|
|
UWorld* World = TargetActor->GetWorld();
|
|
if (!World)
|
|
{
|
|
UE_LOG(LogAbilitySystem, Warning, TEXT("GetInstancedCueActor called on TargetActor %s which did not belong to a world (is it a CDO or being destroyed?)"), *GetNameSafe(TargetActor));
|
|
return nullptr;
|
|
}
|
|
UE_CLOG(CurrentWorld != World, LogAbilitySystem, Error, TEXT("GetInstancedCueActor had CurrentWorld set to %s but TargetActor is in World %s"), *GetNameSafe(CurrentWorld), *GetNameSafe(World));
|
|
|
|
// We found the exact Cue we're looking for already on the TargetActor. Use that.
|
|
AGameplayCueNotify_Actor* ExistingCueOnActor = FindExistingCueOnActor(*TargetActor, CueClass, Parameters);
|
|
if (ExistingCueOnActor)
|
|
{
|
|
ExistingCueOnActor->CueInstigator = Parameters.GetInstigator();
|
|
ExistingCueOnActor->CueSourceObject = Parameters.GetSourceObject();
|
|
return ExistingCueOnActor;
|
|
}
|
|
|
|
const bool bUseActorRecycling = (GameplayCueActorRecycle > 0);
|
|
if (bUseActorRecycling)
|
|
{
|
|
if (AGameplayCueNotify_Actor* RecycledCue = FindRecycledCue(CueClass, *World))
|
|
{
|
|
RecycledCue->bInRecycleQueue = false;
|
|
RecycledCue->SetOwner(TargetActor);
|
|
RecycledCue->SetActorLocationAndRotation(TargetActor->GetActorLocation(), TargetActor->GetActorRotation());
|
|
RecycledCue->ReuseAfterRecycle();
|
|
RecycledCue->CueInstigator = Parameters.GetInstigator();
|
|
RecycledCue->CueSourceObject = Parameters.GetSourceObject();
|
|
|
|
UE_CLOG((GameplayCueActorRecycleDebug > 0), LogAbilitySystem, Display, TEXT("GetInstancedCueActor reusing Recycled CueActor: %s"), *GetNameSafe(RecycledCue));
|
|
|
|
#if WITH_EDITOR
|
|
// let things know that we 'spawned'
|
|
ISequenceRecorder& SequenceRecorder = FModuleManager::LoadModuleChecked<ISequenceRecorder>("SequenceRecorder");
|
|
SequenceRecorder.NotifyActorStartRecording(RecycledCue);
|
|
#endif
|
|
|
|
return RecycledCue;
|
|
}
|
|
}
|
|
|
|
// If we can't reuse, then spawn a new one. Since TargetActor is the Owner, a reference to this CueNotify Actor will live in TargetActor::Children.
|
|
FActorSpawnParameters SpawnParams;
|
|
SpawnParams.Owner = TargetActor;
|
|
SpawnParams.OverrideLevel = World->PersistentLevel;
|
|
AGameplayCueNotify_Actor* SpawnedCue = World->SpawnActor<AGameplayCueNotify_Actor>(CueClass, TargetActor->GetActorLocation(), TargetActor->GetActorRotation(), SpawnParams);
|
|
if (ensureMsgf(SpawnedCue != nullptr, TEXT("[%s] - Failed to spawn the cue actor! Check the log for a potential reason spawn actor failed!"), ANSI_TO_TCHAR(__FUNCTION__)))
|
|
{
|
|
SpawnedCue->CueInstigator = Parameters.GetInstigator();
|
|
SpawnedCue->CueSourceObject = Parameters.GetSourceObject();
|
|
}
|
|
|
|
UE_CLOG(LogGameplayCueActorSpawning > 0, LogAbilitySystem, Warning, TEXT("Spawned Gameplay Cue Notify Actor: %s (instance %s)"), *CueClass->GetName(), *GetNameSafe(SpawnedCue));
|
|
return SpawnedCue;
|
|
}
|
|
|
|
AGameplayCueNotify_Actor* UGameplayCueManager::FindRecycledCue(const TSubclassOf<AGameplayCueNotify_Actor>& CueClass, const UWorld& FindInWorld)
|
|
{
|
|
FPreallocationInfo& Info = GetPreallocationInfo(&FindInWorld);
|
|
FGameplayCueNotifyActorArray* PreallocatedList = Info.PreallocatedInstances.Find(CueClass);
|
|
if (!PreallocatedList)
|
|
{
|
|
// No preallocated instances yet
|
|
return nullptr;
|
|
}
|
|
|
|
while (PreallocatedList->Actors.Num() > 0)
|
|
{
|
|
AGameplayCueNotify_Actor* RecycledCue = PreallocatedList->Actors.Pop(EAllowShrinking::No);
|
|
|
|
// Normal check: if cue was destroyed or is pending kill, then don't use it.
|
|
if (IsValid(RecycledCue))
|
|
{
|
|
return RecycledCue;
|
|
}
|
|
|
|
// outside of replays, this should not happen. GC Notifies should not be actually destroyed.
|
|
ensureMsgf(FindInWorld.IsPlayingReplay(), TEXT("RecycledCue is pending kill, garbage or null: %s."), *GetNameSafe(RecycledCue));
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void UGameplayCueManager::NotifyGameplayCueActorFinished(AGameplayCueNotify_Actor* Actor)
|
|
{
|
|
if (!IsValid(Actor))
|
|
{
|
|
ensureMsgf(GetWorld() && GetWorld()->IsPlayingReplay(), TEXT("GameplayCueNotify %s is pending kill or garbage in ::NotifyGameplayCueActorFinished (and not in network demo)"), *GetNameSafe(Actor));
|
|
return;
|
|
}
|
|
|
|
bool bUseActorRecycling = (GameplayCueActorRecycle > 0);
|
|
|
|
#if WITH_EDITOR
|
|
// Don't recycle in preview worlds
|
|
if (Actor->GetWorld()->IsPreviewWorld())
|
|
{
|
|
bUseActorRecycling = false;
|
|
}
|
|
#endif
|
|
|
|
if (bUseActorRecycling)
|
|
{
|
|
if (Actor->bInRecycleQueue)
|
|
{
|
|
// We are already in the recycle queue. This can happen normally
|
|
// (For example the GC is removed and the owner is destroyed in the same frame)
|
|
return;
|
|
}
|
|
|
|
UE_CLOG((GameplayCueActorRecycleDebug > 0), LogAbilitySystem, Warning, TEXT("Recycling CueActor %s"), *GetNameSafe(Actor));
|
|
|
|
if (Actor->Recycle())
|
|
{
|
|
|
|
Actor->bInRecycleQueue = true;
|
|
|
|
FPreallocationInfo& Info = GetPreallocationInfo(Actor->GetWorld());
|
|
FGameplayCueNotifyActorArray& PreAllocatedList = Info.PreallocatedInstances.FindOrAdd(Actor->GetClass());
|
|
|
|
// Put the actor back in the list
|
|
if (ensureMsgf(PreAllocatedList.Actors.Contains(Actor)==false, TEXT("GC Actor PreallocationList already contains Actor %s"), *GetNameSafe(Actor)))
|
|
{
|
|
PreAllocatedList.Actors.Push(Actor);
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
// let things know that we 'de-spawned'
|
|
ISequenceRecorder& SequenceRecorder = FModuleManager::LoadModuleChecked<ISequenceRecorder>("SequenceRecorder");
|
|
SequenceRecorder.NotifyActorStopRecording(Actor);
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
UE_CLOG((GameplayCueActorRecycleDebug > 0), LogAbilitySystem, Error, TEXT("Could not Recycle CueActor %s, Destroying"), *GetNameSafe(Actor));
|
|
}
|
|
|
|
// We didn't recycle, so just destroy
|
|
Actor->Destroy();
|
|
}
|
|
|
|
void UGameplayCueManager::NotifyGameplayCueActorEndPlay(AGameplayCueNotify_Actor* Actor)
|
|
{
|
|
if (Actor && Actor->bInRecycleQueue)
|
|
{
|
|
FPreallocationInfo& Info = GetPreallocationInfo(Actor->GetWorld());
|
|
FGameplayCueNotifyActorArray& PreAllocatedList = Info.PreallocatedInstances.FindOrAdd(Actor->GetClass());
|
|
PreAllocatedList.Actors.Remove(Actor);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
bool UGameplayCueManager::ShouldSyncScanRuntimeObjectLibraries() const
|
|
{
|
|
// Always sync scan the runtime object library
|
|
return true;
|
|
}
|
|
bool UGameplayCueManager::ShouldSyncLoadRuntimeObjectLibraries() const
|
|
{
|
|
// No real need to sync load it anymore
|
|
return false;
|
|
}
|
|
bool UGameplayCueManager::ShouldAsyncLoadRuntimeObjectLibraries() const
|
|
{
|
|
// Async load the run time library at startup
|
|
return true;
|
|
}
|
|
|
|
bool UGameplayCueManager::ShouldDeferScanningRuntimeLibraries() const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
void UGameplayCueManager::InitializeRuntimeObjectLibrary()
|
|
{
|
|
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
|
|
if (AssetRegistryModule.Get().IsGathering() && ShouldDeferScanningRuntimeLibraries())
|
|
{
|
|
ABILITY_LOG(Display, TEXT("Asset registry still gathering. Deferring InitializeRuntimeObjectLibrary."));
|
|
if (!AssetRegistryModule.Get().OnKnownGathersComplete().IsBoundToObject(this))
|
|
{
|
|
AssetRegistryModule.Get().OnKnownGathersComplete().AddUObject(this, &UGameplayCueManager::InitializeRuntimeObjectLibrary);
|
|
}
|
|
return;
|
|
}
|
|
|
|
ABILITY_LOG(Display, TEXT("Processing InitializeRuntimeObjectLibrary."))
|
|
|
|
AssetRegistryModule.Get().OnKnownGathersComplete().RemoveAll(this);
|
|
|
|
UE_SCOPED_ENGINE_ACTIVITY(TEXT("Initializing GameplayCueManager Runtime Object Library"));
|
|
|
|
RuntimeGameplayCueObjectLibrary.Paths = GetAlwaysLoadedGameplayCuePaths();
|
|
if (RuntimeGameplayCueObjectLibrary.CueSet == nullptr)
|
|
{
|
|
RuntimeGameplayCueObjectLibrary.CueSet = NewObject<UGameplayCueSet>(this, TEXT("GlobalGameplayCueSet"));
|
|
}
|
|
|
|
RuntimeGameplayCueObjectLibrary.CueSet->Empty();
|
|
RuntimeGameplayCueObjectLibrary.bHasBeenInitialized = true;
|
|
|
|
RuntimeGameplayCueObjectLibrary.bShouldSyncScan = ShouldSyncScanRuntimeObjectLibraries();
|
|
RuntimeGameplayCueObjectLibrary.bShouldSyncLoad = ShouldSyncLoadRuntimeObjectLibraries();
|
|
RuntimeGameplayCueObjectLibrary.bShouldAsyncLoad = ShouldAsyncLoadRuntimeObjectLibraries();
|
|
|
|
InitObjectLibrary(RuntimeGameplayCueObjectLibrary);
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
void UGameplayCueManager::InitializeEditorObjectLibrary()
|
|
{
|
|
SCOPE_LOG_TIME_IN_SECONDS(*FString::Printf(TEXT("UGameplayCueManager::InitializeEditorObjectLibrary")), nullptr)
|
|
|
|
EditorGameplayCueObjectLibrary.Paths = GetValidGameplayCuePaths();
|
|
if (EditorGameplayCueObjectLibrary.CueSet == nullptr)
|
|
{
|
|
EditorGameplayCueObjectLibrary.CueSet = NewObject<UGameplayCueSet>(this, TEXT("EditorGameplayCueSet"));
|
|
}
|
|
|
|
EditorGameplayCueObjectLibrary.CueSet->Empty();
|
|
EditorGameplayCueObjectLibrary.bHasBeenInitialized = true;
|
|
|
|
// Don't load anything for the editor. Just read whatever the asset registry has.
|
|
EditorGameplayCueObjectLibrary.bShouldSyncScan = IsRunningCommandlet(); // If we are cooking, then sync scan it right away so that we don't miss anything
|
|
EditorGameplayCueObjectLibrary.bShouldAsyncLoad = false;
|
|
EditorGameplayCueObjectLibrary.bShouldSyncLoad = false;
|
|
|
|
InitObjectLibrary(EditorGameplayCueObjectLibrary);
|
|
|
|
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
|
|
if ( AssetRegistryModule.Get().IsLoadingAssets() )
|
|
{
|
|
// Let us know when we are done
|
|
static FDelegateHandle DoOnce =
|
|
AssetRegistryModule.Get().OnFilesLoaded().AddUObject(this, &UGameplayCueManager::InitializeEditorObjectLibrary);
|
|
}
|
|
else
|
|
{
|
|
EditorObjectLibraryFullyInitialized = true;
|
|
if (EditorPeriodicUpdateHandle.IsValid())
|
|
{
|
|
GEditor->GetTimerManager()->ClearTimer(EditorPeriodicUpdateHandle);
|
|
EditorPeriodicUpdateHandle.Invalidate();
|
|
}
|
|
}
|
|
|
|
OnEditorObjectLibraryUpdated.Broadcast();
|
|
}
|
|
|
|
void UGameplayCueManager::RequestPeriodicUpdateOfEditorObjectLibraryWhileWaitingOnAssetRegistry()
|
|
{
|
|
// Asset registry is still loading, so update every 15 seconds until its finished
|
|
if (!EditorObjectLibraryFullyInitialized && !EditorPeriodicUpdateHandle.IsValid())
|
|
{
|
|
GEditor->GetTimerManager()->SetTimer( EditorPeriodicUpdateHandle, FTimerDelegate::CreateUObject(this, &UGameplayCueManager::InitializeEditorObjectLibrary), 15.f, true);
|
|
}
|
|
}
|
|
|
|
void UGameplayCueManager::ReloadObjectLibrary(UWorld* World, const UWorld::InitializationValues IVS)
|
|
{
|
|
if (bAccelerationMapOutdated)
|
|
{
|
|
RefreshObjectLibraries();
|
|
}
|
|
}
|
|
|
|
void UGameplayCueManager::GetEditorObjectLibraryGameplayCueNotifyFilenames(TArray<FString>& Filenames) const
|
|
{
|
|
if (ensure(EditorGameplayCueObjectLibrary.CueSet))
|
|
{
|
|
EditorGameplayCueObjectLibrary.CueSet->GetFilenames(Filenames);
|
|
}
|
|
}
|
|
|
|
void UGameplayCueManager::LoadNotifyForEditorPreview(FGameplayTag GameplayCueTag)
|
|
{
|
|
if (ensure(EditorGameplayCueObjectLibrary.CueSet) && ensure(RuntimeGameplayCueObjectLibrary.CueSet))
|
|
{
|
|
EditorGameplayCueObjectLibrary.CueSet->CopyCueDataToSetForEditorPreview(GameplayCueTag, RuntimeGameplayCueObjectLibrary.CueSet);
|
|
}
|
|
}
|
|
|
|
#endif // WITH_EDITOR
|
|
|
|
TArray<FString> UGameplayCueManager::GetAlwaysLoadedGameplayCuePaths()
|
|
{
|
|
return UAbilitySystemGlobals::Get().GetGameplayCueNotifyPaths();
|
|
}
|
|
|
|
void UGameplayCueManager::AddGameplayCueNotifyPath(const FString& InPath, const bool bShouldRescanCueAssets /* = true */)
|
|
{
|
|
UAbilitySystemGlobals::Get().AddGameplayCueNotifyPath(InPath);
|
|
const int32 NumAdded = RuntimeGameplayCueObjectLibrary.Paths.AddUnique(InPath);
|
|
|
|
if(bShouldRescanCueAssets && NumAdded != INDEX_NONE)
|
|
{
|
|
InitializeRuntimeObjectLibrary();
|
|
}
|
|
}
|
|
|
|
int32 UGameplayCueManager::RemoveGameplayCueNotifyPath(const FString& InPath, const bool bShouldRescanCueAssets /* = true */)
|
|
{
|
|
int32 NumRemovedGlobal = UAbilitySystemGlobals::Get().RemoveGameplayCueNotifyPath(InPath);
|
|
int32 NumRemovedRuntime = RuntimeGameplayCueObjectLibrary.Paths.Remove(InPath);
|
|
|
|
ensureMsgf(NumRemovedGlobal == NumRemovedRuntime, TEXT("Unexpected number of cue paths removed for '%s'"), *InPath);
|
|
|
|
if(bShouldRescanCueAssets && NumRemovedGlobal > 0)
|
|
{
|
|
InitializeRuntimeObjectLibrary();
|
|
}
|
|
|
|
return NumRemovedRuntime;
|
|
}
|
|
|
|
void UGameplayCueManager::RefreshObjectLibraries()
|
|
{
|
|
if (RuntimeGameplayCueObjectLibrary.bHasBeenInitialized)
|
|
{
|
|
check(RuntimeGameplayCueObjectLibrary.CueSet);
|
|
RuntimeGameplayCueObjectLibrary.CueSet->Empty();
|
|
InitObjectLibrary(RuntimeGameplayCueObjectLibrary);
|
|
}
|
|
|
|
if (EditorGameplayCueObjectLibrary.bHasBeenInitialized)
|
|
{
|
|
check(EditorGameplayCueObjectLibrary.CueSet);
|
|
EditorGameplayCueObjectLibrary.CueSet->Empty();
|
|
InitObjectLibrary(EditorGameplayCueObjectLibrary);
|
|
}
|
|
}
|
|
|
|
TSharedPtr<FStreamableHandle> UGameplayCueManager::InitObjectLibrary(FGameplayCueObjectLibrary& Lib)
|
|
{
|
|
TSharedPtr<FStreamableHandle> RetVal;
|
|
|
|
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("Loading Library"), STAT_ObjectLibrary, STATGROUP_LoadTime);
|
|
|
|
// Instantiate the UObjectLibraries if they aren't there already
|
|
if (!Lib.StaticObjectLibrary)
|
|
{
|
|
Lib.StaticObjectLibrary = UObjectLibrary::CreateLibrary(UGameplayCueNotify_Static::StaticClass(), true, GIsEditor && !IsRunningCommandlet());
|
|
if (GIsEditor)
|
|
{
|
|
Lib.StaticObjectLibrary->bIncludeOnlyOnDiskAssets = false;
|
|
}
|
|
}
|
|
if (!Lib.ActorObjectLibrary)
|
|
{
|
|
Lib.ActorObjectLibrary = UObjectLibrary::CreateLibrary(AGameplayCueNotify_Actor::StaticClass(), true, GIsEditor && !IsRunningCommandlet());
|
|
if (GIsEditor)
|
|
{
|
|
Lib.ActorObjectLibrary->bIncludeOnlyOnDiskAssets = false;
|
|
}
|
|
}
|
|
|
|
Lib.bHasBeenInitialized = true;
|
|
|
|
#if WITH_EDITOR
|
|
bAccelerationMapOutdated = false;
|
|
#endif
|
|
|
|
FScopeCycleCounterUObject PreloadScopeActor(Lib.ActorObjectLibrary);
|
|
|
|
// ------------------------------------------------------------------------------------------------------------------
|
|
// Scan asset data. If bShouldSyncScan is false, whatever state the asset registry is in will be what is returned.
|
|
// ------------------------------------------------------------------------------------------------------------------
|
|
|
|
{
|
|
//SCOPE_LOG_TIME_IN_SECONDS(*FString::Printf(TEXT("UGameplayCueManager::InitObjectLibraries Actors. Paths: %s"), *FString::Join(Lib.Paths, TEXT(", "))), nullptr)
|
|
Lib.ActorObjectLibrary->LoadBlueprintAssetDataFromPaths(Lib.Paths, Lib.bShouldSyncScan);
|
|
}
|
|
{
|
|
//SCOPE_LOG_TIME_IN_SECONDS(*FString::Printf(TEXT("UGameplayCueManager::InitObjectLibraries Objects")), nullptr)
|
|
Lib.StaticObjectLibrary->LoadBlueprintAssetDataFromPaths(Lib.Paths, Lib.bShouldSyncScan);
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
// Sync load if told to do so
|
|
// ---------------------------------------------------------
|
|
if (Lib.bShouldSyncLoad)
|
|
{
|
|
#if STATS
|
|
FString PerfMessage = FString::Printf(TEXT("Fully Loaded GameplayCueNotify object library"));
|
|
SCOPE_LOG_TIME_IN_SECONDS(*PerfMessage, nullptr)
|
|
#endif
|
|
Lib.ActorObjectLibrary->LoadAssetsFromAssetData();
|
|
Lib.StaticObjectLibrary->LoadAssetsFromAssetData();
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
// Look for GameplayCueNotifies that handle events
|
|
// ---------------------------------------------------------
|
|
|
|
TArray<FAssetData> ActorAssetDatas;
|
|
Lib.ActorObjectLibrary->GetAssetDataList(ActorAssetDatas);
|
|
|
|
TArray<FAssetData> StaticAssetDatas;
|
|
Lib.StaticObjectLibrary->GetAssetDataList(StaticAssetDatas);
|
|
|
|
TArray<FGameplayCueReferencePair> CuesToAdd;
|
|
TArray<FSoftObjectPath> AssetsToLoad;
|
|
|
|
// ------------------------------------------------------------------------------------------------------------------
|
|
// Build Cue lists for loading. Determines what from the obj library needs to be loaded
|
|
// ------------------------------------------------------------------------------------------------------------------
|
|
BuildCuesToAddToGlobalSet(ActorAssetDatas, GET_MEMBER_NAME_CHECKED(AGameplayCueNotify_Actor, GameplayCueName), CuesToAdd, AssetsToLoad, Lib.ShouldLoad);
|
|
BuildCuesToAddToGlobalSet(StaticAssetDatas, GET_MEMBER_NAME_CHECKED(UGameplayCueNotify_Static, GameplayCueName), CuesToAdd, AssetsToLoad, Lib.ShouldLoad);
|
|
|
|
const FName PropertyName = GET_MEMBER_NAME_CHECKED(AGameplayCueNotify_Actor, GameplayCueName);
|
|
check(PropertyName == GET_MEMBER_NAME_CHECKED(UGameplayCueNotify_Static, GameplayCueName));
|
|
|
|
// ------------------------------------------------------------------------------------------------------------------------------------
|
|
// Add these cues to the set. The UGameplayCueSet is the data structure used in routing the gameplay cue events at runtime.
|
|
// ------------------------------------------------------------------------------------------------------------------------------------
|
|
UGameplayCueSet* SetToAddTo = Lib.CueSet;
|
|
if (!SetToAddTo)
|
|
{
|
|
SetToAddTo = RuntimeGameplayCueObjectLibrary.CueSet;
|
|
}
|
|
check(SetToAddTo);
|
|
SetToAddTo->AddCues(CuesToAdd);
|
|
|
|
// --------------------------------------------
|
|
// Start loading them if necessary
|
|
// --------------------------------------------
|
|
if (Lib.bShouldAsyncLoad)
|
|
{
|
|
auto ForwardLambda = [](TArray<FSoftObjectPath> AssetList, FOnGameplayCueNotifySetLoaded OnLoadedDelegate)
|
|
{
|
|
OnLoadedDelegate.ExecuteIfBound(AssetList);
|
|
};
|
|
|
|
if (AssetsToLoad.Num() > 0)
|
|
{
|
|
FStreamableDelegate Del = FStreamableDelegate::CreateStatic(ForwardLambda, AssetsToLoad, Lib.OnLoaded);
|
|
GameplayCueAssetHandle = StreamableManager.RequestAsyncLoad(MoveTemp(AssetsToLoad), MoveTemp(Del), Lib.AsyncPriority);
|
|
RetVal = GameplayCueAssetHandle;
|
|
}
|
|
else
|
|
{
|
|
// Still fire the delegate even if nothing was found to load
|
|
Lib.OnLoaded.ExecuteIfBound(MoveTemp(AssetsToLoad));
|
|
}
|
|
}
|
|
|
|
// Build Tag Translation table
|
|
TranslationManager.BuildTagTranslationTable();
|
|
return RetVal;
|
|
}
|
|
|
|
static FAutoConsoleVariable CVarGameplyCueAddToGlobalSetDebug(TEXT("GameplayCue.AddToGlobalSet.DebugTag"), TEXT(""), TEXT("Debug Tag adding to global set"), ECVF_Default );
|
|
|
|
void UGameplayCueManager::BuildCuesToAddToGlobalSet(const TArray<FAssetData>& AssetDataList, FName TagPropertyName, TArray<FGameplayCueReferencePair>& OutCuesToAdd, TArray<FSoftObjectPath>& OutAssetsToLoad, FShouldLoadGCNotifyDelegate ShouldLoad)
|
|
{
|
|
UGameplayTagsManager& Manager = UGameplayTagsManager::Get();
|
|
|
|
OutAssetsToLoad.Reserve(OutAssetsToLoad.Num() + AssetDataList.Num());
|
|
|
|
for (const FAssetData& Data: AssetDataList)
|
|
{
|
|
const FName FoundGameplayTag = Data.GetTagValueRef<FName>(TagPropertyName);
|
|
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
|
|
if (CVarGameplyCueAddToGlobalSetDebug->GetString().IsEmpty() == false && FoundGameplayTag.ToString().Contains(CVarGameplyCueAddToGlobalSetDebug->GetString()))
|
|
{
|
|
ABILITY_LOG(Display, TEXT("Adding Tag %s to GlobalSet"), *FoundGameplayTag.ToString());
|
|
}
|
|
#endif
|
|
|
|
// If ShouldLoad delegate is bound and it returns false, don't load this one
|
|
if (ShouldLoad.IsBound() && (ShouldLoad.Execute(Data, FoundGameplayTag) == false))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (ShouldLoadGameplayCueAssetData(Data) == false)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!FoundGameplayTag.IsNone())
|
|
{
|
|
const FString GeneratedClassTag = Data.GetTagValueRef<FString>(FBlueprintTags::GeneratedClassPath);
|
|
if (GeneratedClassTag.IsEmpty())
|
|
{
|
|
ABILITY_LOG(Warning, TEXT("Unable to find GeneratedClass value for AssetData %s"), *Data.GetObjectPathString());
|
|
continue;
|
|
}
|
|
|
|
ABILITY_LOG(Log, TEXT("GameplayCueManager Found: %s / %s"), *FoundGameplayTag.ToString(), *GeneratedClassTag);
|
|
|
|
FGameplayTag GameplayCueTag = Manager.RequestGameplayTag(FoundGameplayTag, false);
|
|
if (GameplayCueTag.IsValid())
|
|
{
|
|
// Add a new NotifyData entry to our flat list for this one
|
|
FSoftObjectPath StringRef;
|
|
StringRef.SetPath(FPackageName::ExportTextPathToObjectPath(GeneratedClassTag));
|
|
bool bFixedUpRef = StringRef.FixupCoreRedirects();
|
|
if (bFixedUpRef)
|
|
{
|
|
ABILITY_LOG(Log, TEXT("GameplayCueManager Redirected: %s -> %s"), *GeneratedClassTag, *StringRef.ToString());
|
|
}
|
|
|
|
OutCuesToAdd.Add(FGameplayCueReferencePair(GameplayCueTag, StringRef));
|
|
|
|
OutAssetsToLoad.Add(StringRef);
|
|
|
|
// Make sure core knows about this ref so it can be properly detected during cook.
|
|
StringRef.PostLoadPath(GetLinker());
|
|
}
|
|
else
|
|
{
|
|
// Warn about this tag but only once to cut down on spam (we may build cue sets multiple times in the editor)
|
|
static TSet<FName> WarnedTags;
|
|
if (WarnedTags.Contains(FoundGameplayTag) == false)
|
|
{
|
|
ABILITY_LOG(Warning, TEXT("Found GameplayCue tag %s in asset %s but there is no corresponding tag in the GameplayTagManager."), *FoundGameplayTag.ToString(), *Data.PackageName.ToString());
|
|
WarnedTags.Add(FoundGameplayTag);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int32 GameplayCueCheckForTooManyRPCs = 1;
|
|
static FAutoConsoleVariableRef CVarGameplayCueCheckForTooManyRPCs(TEXT("AbilitySystem.GameplayCueCheckForTooManyRPCs"), GameplayCueCheckForTooManyRPCs, TEXT("Warns if gameplay cues are being throttled by network code"), ECVF_Default );
|
|
|
|
void UGameplayCueManager::CheckForTooManyRPCs(FName FuncName, const FGameplayCuePendingExecute& PendingCue, const FString& CueID, const FGameplayEffectContext* EffectContext)
|
|
{
|
|
if (GameplayCueCheckForTooManyRPCs)
|
|
{
|
|
static IConsoleVariable* MaxRPCPerNetUpdateCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("net.MaxRPCPerNetUpdate"));
|
|
if (MaxRPCPerNetUpdateCVar)
|
|
{
|
|
AActor* Owner = PendingCue.OwningComponent ? PendingCue.OwningComponent->GetOwner() : nullptr;
|
|
UWorld* World = Owner ? Owner->GetWorld() : nullptr;
|
|
UNetDriver* NetDriver = World ? World->GetNetDriver() : nullptr;
|
|
if (NetDriver)
|
|
{
|
|
const int32 MaxRPCs = MaxRPCPerNetUpdateCVar->GetInt();
|
|
for (UNetConnection* ClientConnection : NetDriver->ClientConnections)
|
|
{
|
|
if (ClientConnection)
|
|
{
|
|
UActorChannel** OwningActorChannelPtr = ClientConnection->FindActorChannel(Owner);
|
|
TSharedRef<FObjectReplicator>* ComponentReplicatorPtr = (OwningActorChannelPtr && *OwningActorChannelPtr) ? (*OwningActorChannelPtr)->ReplicationMap.Find(PendingCue.OwningComponent) : nullptr;
|
|
if (ComponentReplicatorPtr)
|
|
{
|
|
const TArray<FObjectReplicator::FRPCCallInfo>& RemoteFuncInfo = (*ComponentReplicatorPtr)->RemoteFuncInfo;
|
|
for (const FObjectReplicator::FRPCCallInfo& CallInfo : RemoteFuncInfo)
|
|
{
|
|
if (CallInfo.FuncName == FuncName)
|
|
{
|
|
if (CallInfo.Calls > MaxRPCs)
|
|
{
|
|
const FString Instigator = EffectContext ? EffectContext->ToString() : TEXT("None");
|
|
ABILITY_LOG(Warning, TEXT("Attempted to fire %s when no more RPCs are allowed this net update. Max:%d Cue:%s Instigator:%s Component:%s"), *FuncName.ToString(), MaxRPCs, *CueID, *Instigator, *GetPathNameSafe(PendingCue.OwningComponent));
|
|
|
|
// Returning here to only log once per offending RPC.
|
|
return;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UGameplayCueManager::OnGameplayCueNotifyAsyncLoadComplete(TArray<FSoftObjectPath> AssetList)
|
|
{
|
|
for (FSoftObjectPath StringRef : AssetList)
|
|
{
|
|
UClass* GCClass = FindObject<UClass>(nullptr, *StringRef.ToString());
|
|
if (ensure(GCClass))
|
|
{
|
|
LoadedGameplayCueNotifyClasses.Add(GCClass);
|
|
CheckForPreallocation(GCClass);
|
|
}
|
|
}
|
|
}
|
|
|
|
int32 UGameplayCueManager::FinishLoadingGameplayCueNotifies()
|
|
{
|
|
int32 NumLoadeded = 0;
|
|
return NumLoadeded;
|
|
}
|
|
|
|
UGameplayCueSet* UGameplayCueManager::GetRuntimeCueSet()
|
|
{
|
|
return RuntimeGameplayCueObjectLibrary.CueSet;
|
|
}
|
|
|
|
TArray<UGameplayCueSet*> UGameplayCueManager::GetGlobalCueSets()
|
|
{
|
|
TArray<UGameplayCueSet*> Set;
|
|
if (RuntimeGameplayCueObjectLibrary.CueSet)
|
|
{
|
|
Set.Add(RuntimeGameplayCueObjectLibrary.CueSet);
|
|
}
|
|
if (EditorGameplayCueObjectLibrary.CueSet)
|
|
{
|
|
Set.Add(EditorGameplayCueObjectLibrary.CueSet);
|
|
}
|
|
return Set;
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
|
|
UGameplayCueSet* UGameplayCueManager::GetEditorCueSet()
|
|
{
|
|
return EditorGameplayCueObjectLibrary.CueSet;
|
|
}
|
|
|
|
void UGameplayCueManager::HandleAssetAdded(UObject *Object)
|
|
{
|
|
UBlueprint* Blueprint = Cast<UBlueprint>(Object);
|
|
if (Blueprint && Blueprint->GeneratedClass)
|
|
{
|
|
UGameplayCueNotify_Static* StaticCDO = Cast<UGameplayCueNotify_Static>(Blueprint->GeneratedClass->GetDefaultObject(false));
|
|
AGameplayCueNotify_Actor* ActorCDO = Cast<AGameplayCueNotify_Actor>(Blueprint->GeneratedClass->GetDefaultObject(false));
|
|
|
|
if (StaticCDO || ActorCDO)
|
|
{
|
|
if (!Blueprint->GetOutermost()->HasAnyPackageFlags(PKG_ForDiffing) && VerifyNotifyAssetIsInValidPath(Blueprint->GetOuter()->GetPathName()))
|
|
{
|
|
FSoftObjectPath StringRef;
|
|
StringRef.SetPath(Blueprint->GeneratedClass->GetPathName());
|
|
|
|
TArray<FGameplayCueReferencePair> CuesToAdd;
|
|
if (StaticCDO)
|
|
{
|
|
CuesToAdd.Add(FGameplayCueReferencePair(StaticCDO->GameplayCueTag, StringRef));
|
|
}
|
|
else if (ActorCDO)
|
|
{
|
|
CuesToAdd.Add(FGameplayCueReferencePair(ActorCDO->GameplayCueTag, StringRef));
|
|
}
|
|
|
|
// Make sure core knows about this ref so it can be properly detected during cook.
|
|
StringRef.PostLoadPath(Object->GetLinker());
|
|
|
|
for (UGameplayCueSet* Set : GetGlobalCueSets())
|
|
{
|
|
Set->AddCues(CuesToAdd);
|
|
}
|
|
|
|
OnGameplayCueNotifyAddOrRemove.Broadcast();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Handles cleaning up an object library if it matches the passed in object */
|
|
void UGameplayCueManager::HandleAssetDeleted(UObject *Object)
|
|
{
|
|
FSoftObjectPath StringRefToRemove;
|
|
UBlueprint* Blueprint = Cast<UBlueprint>(Object);
|
|
if (Blueprint && Blueprint->GeneratedClass)
|
|
{
|
|
UGameplayCueNotify_Static* StaticCDO = Cast<UGameplayCueNotify_Static>(Blueprint->GeneratedClass->GetDefaultObject(false));
|
|
AGameplayCueNotify_Actor* ActorCDO = Cast<AGameplayCueNotify_Actor>(Blueprint->GeneratedClass->GetDefaultObject(false));
|
|
|
|
if (StaticCDO || ActorCDO)
|
|
{
|
|
StringRefToRemove.SetPath(Blueprint->GeneratedClass->GetPathName());
|
|
}
|
|
}
|
|
|
|
if (StringRefToRemove.IsValid())
|
|
{
|
|
TArray<FSoftObjectPath> StringRefs;
|
|
StringRefs.Add(StringRefToRemove);
|
|
|
|
|
|
for (UGameplayCueSet* Set : GetGlobalCueSets())
|
|
{
|
|
Set->RemoveCuesByStringRefs(StringRefs);
|
|
}
|
|
|
|
OnGameplayCueNotifyAddOrRemove.Broadcast();
|
|
}
|
|
}
|
|
|
|
/** Handles cleaning up an object library if it matches the passed in object */
|
|
void UGameplayCueManager::HandleAssetRenamed(const FAssetData& Data, const FString& String)
|
|
{
|
|
const FString ParentClassName = Data.GetTagValueRef<FString>(FBlueprintTags::ParentClassPath);
|
|
if (!ParentClassName.IsEmpty())
|
|
{
|
|
UClass* DataClass = FindObject<UClass>(nullptr, *ParentClassName);
|
|
if (DataClass)
|
|
{
|
|
UGameplayCueNotify_Static* StaticCDO = Cast<UGameplayCueNotify_Static>(DataClass->GetDefaultObject(false));
|
|
AGameplayCueNotify_Actor* ActorCDO = Cast<AGameplayCueNotify_Actor>(DataClass->GetDefaultObject(false));
|
|
if (StaticCDO || ActorCDO)
|
|
{
|
|
VerifyNotifyAssetIsInValidPath(Data.PackagePath.ToString());
|
|
|
|
for (UGameplayCueSet* Set : GetGlobalCueSets())
|
|
{
|
|
Set->UpdateCueByStringRefs(String + TEXT("_C"), Data.GetObjectPathString() + TEXT("_C"));
|
|
}
|
|
OnGameplayCueNotifyAddOrRemove.Broadcast();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool UGameplayCueManager::VerifyNotifyAssetIsInValidPath(FString Path)
|
|
{
|
|
bool ValidPath = false;
|
|
for (FString& str: GetValidGameplayCuePaths())
|
|
{
|
|
if (Path.Contains(str))
|
|
{
|
|
ValidPath = true;
|
|
}
|
|
}
|
|
|
|
if (!ValidPath)
|
|
{
|
|
FString MessageTry = FString::Printf(TEXT("Warning: Invalid GameplayCue Path %s"), *Path);
|
|
MessageTry += TEXT("\n\nGameplayCue Notifies should only be saved in the following folders:");
|
|
|
|
ABILITY_LOG(Warning, TEXT("Warning: Invalid GameplayCuePath: %s"), *Path);
|
|
ABILITY_LOG(Warning, TEXT("Valid Paths: "));
|
|
for (FString& str: GetValidGameplayCuePaths())
|
|
{
|
|
ABILITY_LOG(Warning, TEXT(" %s"), *str);
|
|
MessageTry += FString::Printf(TEXT("\n %s"), *str);
|
|
}
|
|
|
|
MessageTry += FString::Printf(TEXT("\n\nThis asset must be moved to a valid location to work in game."));
|
|
|
|
const FText MessageText = FText::FromString(MessageTry);
|
|
const FText TitleText = NSLOCTEXT("GameplayCuePathWarning", "GameplayCuePathWarningTitle", "Invalid GameplayCue Path");
|
|
FMessageDialog::Open(EAppMsgType::Ok, MessageText, TitleText);
|
|
}
|
|
|
|
return ValidPath;
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
UWorld* UGameplayCueManager::GetWorld() const
|
|
{
|
|
return GetCachedWorldForGameplayCueNotifies();
|
|
}
|
|
|
|
/* static */ UWorld* UGameplayCueManager::GetCachedWorldForGameplayCueNotifies()
|
|
{
|
|
#if WITH_EDITOR
|
|
if (PreviewWorld)
|
|
return PreviewWorld;
|
|
#endif
|
|
|
|
return CurrentWorld;
|
|
}
|
|
|
|
void UGameplayCueManager::PrintGameplayCueNotifyMap()
|
|
{
|
|
if (ensure(RuntimeGameplayCueObjectLibrary.CueSet))
|
|
{
|
|
RuntimeGameplayCueObjectLibrary.CueSet->PrintCues();
|
|
}
|
|
}
|
|
|
|
void UGameplayCueManager::PrintLoadedGameplayCueNotifyClasses()
|
|
{
|
|
for (UClass* NotifyClass : LoadedGameplayCueNotifyClasses)
|
|
{
|
|
ABILITY_LOG(Display, TEXT("%s"), *GetNameSafe(NotifyClass));
|
|
}
|
|
ABILITY_LOG(Display, TEXT("%d total classes"), LoadedGameplayCueNotifyClasses.Num());
|
|
}
|
|
|
|
static void PrintGameplayCueNotifyMapConsoleCommandFunc(UWorld* InWorld)
|
|
{
|
|
UAbilitySystemGlobals::Get().GetGameplayCueManager()->PrintGameplayCueNotifyMap();
|
|
}
|
|
|
|
FAutoConsoleCommandWithWorld PrintGameplayCueNotifyMapConsoleCommand(
|
|
TEXT("GameplayCue.PrintGameplayCueNotifyMap"),
|
|
TEXT("Displays GameplayCue notify map"),
|
|
FConsoleCommandWithWorldDelegate::CreateStatic(PrintGameplayCueNotifyMapConsoleCommandFunc)
|
|
);
|
|
|
|
static void PrintLoadedGameplayCueNotifyClasses(UWorld* InWorld)
|
|
{
|
|
UAbilitySystemGlobals::Get().GetGameplayCueManager()->PrintLoadedGameplayCueNotifyClasses();
|
|
}
|
|
|
|
FAutoConsoleCommandWithWorld PrintLoadedGameplayCueNotifyClassesCommand(
|
|
TEXT("GameplayCue.PrintLoadedGameplayCueNotifyClasses"),
|
|
TEXT("Displays GameplayCue Notify classes that are loaded"),
|
|
FConsoleCommandWithWorldDelegate::CreateStatic(PrintLoadedGameplayCueNotifyClasses)
|
|
);
|
|
|
|
FScopedGameplayCueSendContext::FScopedGameplayCueSendContext()
|
|
{
|
|
UAbilitySystemGlobals::Get().GetGameplayCueManager()->StartGameplayCueSendContext();
|
|
}
|
|
FScopedGameplayCueSendContext::~FScopedGameplayCueSendContext()
|
|
{
|
|
UAbilitySystemGlobals::Get().GetGameplayCueManager()->EndGameplayCueSendContext();
|
|
}
|
|
|
|
template<class AllocatorType>
|
|
void PullGameplayCueTagsFromSpec(const FGameplayEffectSpec& Spec, TArray<FGameplayTag, AllocatorType>& OutArray)
|
|
{
|
|
// Add all GameplayCue Tags from the GE into the GameplayCueTags PendingCue.list
|
|
for (const FGameplayEffectCue& EffectCue : Spec.Def->GameplayCues)
|
|
{
|
|
for (const FGameplayTag& Tag: EffectCue.GameplayCueTags)
|
|
{
|
|
if (Tag.IsValid())
|
|
{
|
|
OutArray.Add(Tag);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enabling AbilitySystemAlwaysConvertGESpecToGCParams will mean that all calls to gameplay cues with GameplayEffectSpecs will be converted into GameplayCue Parameters server side and then replicated.
|
|
* This potentially saved bandwidth but also has less information, depending on how the GESpec is converted to GC Parameters and what your GC's need to know.
|
|
*/
|
|
|
|
int32 AbilitySystemAlwaysConvertGESpecToGCParams = 0;
|
|
static FAutoConsoleVariableRef CVarAbilitySystemAlwaysConvertGESpecToGCParams(TEXT("AbilitySystem.AlwaysConvertGESpecToGCParams"), AbilitySystemAlwaysConvertGESpecToGCParams, TEXT("Always convert a GameplayCue from GE Spec to GC from GC Parameters on the server"), ECVF_Default );
|
|
|
|
void UGameplayCueManager::InvokeGameplayCueAddedAndWhileActive_FromSpec(UAbilitySystemComponent* OwningComponent, const FGameplayEffectSpec& Spec, FPredictionKey PredictionKey)
|
|
{
|
|
if (Spec.Def->GameplayCues.Num() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (EnableSuppressCuesOnGameplayCueManager && OwningComponent && OwningComponent->bSuppressGameplayCues)
|
|
{
|
|
return;
|
|
}
|
|
|
|
IAbilitySystemReplicationProxyInterface* ReplicationInterface = OwningComponent->GetReplicationInterface();
|
|
if (ReplicationInterface == nullptr)
|
|
{
|
|
// No available Replication Interface, we are going to drop these calls. (By design: someone who wants proxy replication should be ok with GC rpcs being dropped when the proxy is null)
|
|
return;
|
|
}
|
|
|
|
if (AbilitySystemAlwaysConvertGESpecToGCParams)
|
|
{
|
|
// Transform the GE Spec into GameplayCue parmameters here (on the server)
|
|
|
|
FGameplayCueParameters Parameters;
|
|
UAbilitySystemGlobals::Get().InitGameplayCueParameters_GESpec(Parameters, Spec);
|
|
|
|
static TArray<FGameplayTag, TInlineAllocator<4> > Tags;
|
|
Tags.Reset();
|
|
|
|
PullGameplayCueTagsFromSpec(Spec, Tags);
|
|
|
|
if (Tags.Num() == 1)
|
|
{
|
|
ReplicationInterface->Call_InvokeGameplayCueAddedAndWhileActive_WithParams(Tags[0], PredictionKey, Parameters);
|
|
|
|
}
|
|
else if (Tags.Num() > 1)
|
|
{
|
|
ReplicationInterface->Call_InvokeGameplayCuesAddedAndWhileActive_WithParams(FGameplayTagContainer::CreateFromArray(Tags), PredictionKey, Parameters);
|
|
}
|
|
else
|
|
{
|
|
ABILITY_LOG(Warning, TEXT("No actual gameplay cue tags found in GameplayEffect %s (despite it having entries in its gameplay cue list!"), *Spec.Def->GetName());
|
|
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ReplicationInterface->Call_InvokeGameplayCueAddedAndWhileActive_FromSpec(Spec, PredictionKey);
|
|
|
|
}
|
|
}
|
|
|
|
void UGameplayCueManager::InvokeGameplayCueExecuted_FromSpec(UAbilitySystemComponent* OwningComponent, const FGameplayEffectSpec& Spec, FPredictionKey PredictionKey)
|
|
{
|
|
if (Spec.Def->GameplayCues.Num() == 0)
|
|
{
|
|
// This spec doesn't have any GCs, so early out
|
|
ABILITY_LOG(Verbose, TEXT("No GCs in this Spec, so early out: %s"), *Spec.Def->GetName());
|
|
return;
|
|
}
|
|
|
|
if (EnableSuppressCuesOnGameplayCueManager && OwningComponent && OwningComponent->bSuppressGameplayCues)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FGameplayCuePendingExecute PendingCue;
|
|
|
|
if (AbilitySystemAlwaysConvertGESpecToGCParams)
|
|
{
|
|
// Transform the GE Spec into GameplayCue parameters here (on the server)
|
|
PendingCue.PayloadType = EGameplayCuePayloadType::CueParameters;
|
|
PendingCue.OwningComponent = OwningComponent;
|
|
PendingCue.PredictionKey = PredictionKey;
|
|
|
|
PullGameplayCueTagsFromSpec(Spec, PendingCue.GameplayCueTags);
|
|
if (PendingCue.GameplayCueTags.Num() == 0)
|
|
{
|
|
ABILITY_LOG(Warning, TEXT("GE %s has GameplayCues but not valid GameplayCue tag."), *Spec.Def->GetName());
|
|
return;
|
|
}
|
|
|
|
UAbilitySystemGlobals::Get().InitGameplayCueParameters_GESpec(PendingCue.CueParameters, Spec);
|
|
}
|
|
else
|
|
{
|
|
// Transform the GE Spec into a FGameplayEffectSpecForRPC (holds less information than the GE Spec itself, but more information than the FGameplayCueParameter)
|
|
PendingCue.PayloadType = EGameplayCuePayloadType::FromSpec;
|
|
PendingCue.OwningComponent = OwningComponent;
|
|
PendingCue.FromSpec = FGameplayEffectSpecForRPC(Spec);
|
|
PendingCue.PredictionKey = PredictionKey;
|
|
}
|
|
|
|
AddPendingCueExecuteInternal(PendingCue);
|
|
}
|
|
|
|
void UGameplayCueManager::InvokeGameplayCueExecuted(UAbilitySystemComponent* OwningComponent, const FGameplayTag GameplayCueTag, FPredictionKey PredictionKey, FGameplayEffectContextHandle EffectContext)
|
|
{
|
|
if (!GameplayCueTag.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (EnableSuppressCuesOnGameplayCueManager && OwningComponent && OwningComponent->bSuppressGameplayCues)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (OwningComponent)
|
|
{
|
|
FGameplayCuePendingExecute PendingCue;
|
|
PendingCue.PayloadType = EGameplayCuePayloadType::CueParameters;
|
|
PendingCue.GameplayCueTags.Add(GameplayCueTag);
|
|
PendingCue.OwningComponent = OwningComponent;
|
|
UAbilitySystemGlobals::Get().InitGameplayCueParameters(PendingCue.CueParameters, EffectContext);
|
|
PendingCue.PredictionKey = PredictionKey;
|
|
|
|
AddPendingCueExecuteInternal(PendingCue);
|
|
}
|
|
}
|
|
|
|
void UGameplayCueManager::InvokeGameplayCueExecuted_WithParams(UAbilitySystemComponent* OwningComponent, const FGameplayTag GameplayCueTag, FPredictionKey PredictionKey, FGameplayCueParameters GameplayCueParameters)
|
|
{
|
|
if (!GameplayCueTag.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (EnableSuppressCuesOnGameplayCueManager && OwningComponent && OwningComponent->bSuppressGameplayCues)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (OwningComponent)
|
|
{
|
|
FGameplayCuePendingExecute PendingCue;
|
|
PendingCue.PayloadType = EGameplayCuePayloadType::CueParameters;
|
|
PendingCue.GameplayCueTags.Add(GameplayCueTag);
|
|
PendingCue.OwningComponent = OwningComponent;
|
|
PendingCue.CueParameters = GameplayCueParameters;
|
|
PendingCue.PredictionKey = PredictionKey;
|
|
|
|
AddPendingCueExecuteInternal(PendingCue);
|
|
}
|
|
}
|
|
|
|
void UGameplayCueManager::AddPendingCueExecuteInternal(FGameplayCuePendingExecute& PendingCue)
|
|
{
|
|
if (ProcessPendingCueExecute(PendingCue))
|
|
{
|
|
PendingExecuteCues.Add(PendingCue);
|
|
}
|
|
|
|
if (GameplayCueSendContextCount == 0)
|
|
{
|
|
// Not in a context, flush now
|
|
FlushPendingCues();
|
|
}
|
|
}
|
|
|
|
void UGameplayCueManager::StartGameplayCueSendContext()
|
|
{
|
|
GameplayCueSendContextCount++;
|
|
}
|
|
|
|
void UGameplayCueManager::EndGameplayCueSendContext()
|
|
{
|
|
GameplayCueSendContextCount--;
|
|
|
|
if (GameplayCueSendContextCount == 0)
|
|
{
|
|
FlushPendingCues();
|
|
}
|
|
else if (GameplayCueSendContextCount < 0)
|
|
{
|
|
ABILITY_LOG(Warning, TEXT("UGameplayCueManager::EndGameplayCueSendContext called too many times! Negative context count"));
|
|
}
|
|
}
|
|
|
|
void UGameplayCueManager::FlushPendingCues()
|
|
{
|
|
OnFlushPendingCues.Broadcast();
|
|
|
|
TArray<FGameplayCuePendingExecute> LocalPendingExecuteCues = PendingExecuteCues;
|
|
PendingExecuteCues.Empty();
|
|
for (int32 i = 0; i < LocalPendingExecuteCues.Num(); i++)
|
|
{
|
|
FGameplayCuePendingExecute& PendingCue = LocalPendingExecuteCues[i];
|
|
|
|
// Our component may have gone away
|
|
if (PendingCue.OwningComponent)
|
|
{
|
|
bool bHasAuthority = PendingCue.OwningComponent->IsOwnerActorAuthoritative();
|
|
bool bLocalPredictionKey = PendingCue.PredictionKey.IsLocalClientKey();
|
|
|
|
IAbilitySystemReplicationProxyInterface* RepInterface = PendingCue.OwningComponent->GetReplicationInterface();
|
|
if (RepInterface == nullptr)
|
|
{
|
|
// If this returns null, it means "we are replicating througha proxy and have no avatar". Which in this case, we should skip
|
|
continue;
|
|
}
|
|
|
|
// TODO: Could implement non-rpc method for replicating if desired
|
|
if (PendingCue.PayloadType == EGameplayCuePayloadType::CueParameters)
|
|
{
|
|
if (ensure(PendingCue.GameplayCueTags.Num() >= 1))
|
|
{
|
|
if (bHasAuthority)
|
|
{
|
|
RepInterface->ForceReplication();
|
|
if (PendingCue.GameplayCueTags.Num() > 1)
|
|
{
|
|
RepInterface->Call_InvokeGameplayCuesExecuted_WithParams(FGameplayTagContainer::CreateFromArray(PendingCue.GameplayCueTags), PendingCue.PredictionKey, PendingCue.CueParameters);
|
|
}
|
|
else
|
|
{
|
|
RepInterface->Call_InvokeGameplayCueExecuted_WithParams(PendingCue.GameplayCueTags[0], PendingCue.PredictionKey, PendingCue.CueParameters);
|
|
|
|
static FName NetMulticast_InvokeGameplayCueExecuted_WithParamsName = TEXT("NetMulticast_InvokeGameplayCueExecuted_WithParams");
|
|
CheckForTooManyRPCs(NetMulticast_InvokeGameplayCueExecuted_WithParamsName, PendingCue, PendingCue.GameplayCueTags[0].ToString(), nullptr);
|
|
}
|
|
}
|
|
else if (bLocalPredictionKey)
|
|
{
|
|
for (const FGameplayTag& Tag : PendingCue.GameplayCueTags)
|
|
{
|
|
PendingCue.OwningComponent->InvokeGameplayCueEvent(Tag, EGameplayCueEvent::Executed, PendingCue.CueParameters);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (PendingCue.PayloadType == EGameplayCuePayloadType::FromSpec)
|
|
{
|
|
if (bHasAuthority)
|
|
{
|
|
RepInterface->ForceReplication();
|
|
RepInterface->Call_InvokeGameplayCueExecuted_FromSpec(PendingCue.FromSpec, PendingCue.PredictionKey);
|
|
|
|
static FName NetMulticast_InvokeGameplayCueExecuted_FromSpecName = TEXT("NetMulticast_InvokeGameplayCueExecuted_FromSpec");
|
|
CheckForTooManyRPCs(NetMulticast_InvokeGameplayCueExecuted_FromSpecName, PendingCue, PendingCue.FromSpec.Def ? PendingCue.FromSpec.ToSimpleString() : TEXT("FromSpecWithNoDef"), PendingCue.FromSpec.EffectContext.Get());
|
|
}
|
|
else if (bLocalPredictionKey)
|
|
{
|
|
PendingCue.OwningComponent->InvokeGameplayCueEvent(PendingCue.FromSpec, EGameplayCueEvent::Executed);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool UGameplayCueManager::ProcessPendingCueExecute(FGameplayCuePendingExecute& PendingCue)
|
|
{
|
|
// Subclasses can do something here
|
|
return true;
|
|
}
|
|
|
|
bool UGameplayCueManager::DoesPendingCueExecuteMatch(FGameplayCuePendingExecute& PendingCue, FGameplayCuePendingExecute& ExistingCue)
|
|
{
|
|
const FHitResult* PendingHitResult = NULL;
|
|
const FHitResult* ExistingHitResult = NULL;
|
|
|
|
if (PendingCue.PayloadType != ExistingCue.PayloadType)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (PendingCue.OwningComponent != ExistingCue.OwningComponent)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (PendingCue.PredictionKey.GetPredictiveConnectionKey() != ExistingCue.PredictionKey.GetPredictiveConnectionKey())
|
|
{
|
|
// They can both by null, but if they were predicted by different people exclude it
|
|
return false;
|
|
}
|
|
|
|
if (PendingCue.PayloadType == EGameplayCuePayloadType::FromSpec)
|
|
{
|
|
if (PendingCue.FromSpec.Def != ExistingCue.FromSpec.Def)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (PendingCue.FromSpec.Level != ExistingCue.FromSpec.Level)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (PendingCue.GameplayCueTags != ExistingCue.GameplayCueTags)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void UGameplayCueManager::CheckForPreallocation(UClass* GCClass)
|
|
{
|
|
if (AGameplayCueNotify_Actor* InstancedCue = Cast<AGameplayCueNotify_Actor>(GCClass->GetDefaultObject(false)))
|
|
{
|
|
if (InstancedCue->NumPreallocatedInstances > 0 && GameplayCueClassesForPreallocation.Contains(GCClass) == false)
|
|
{
|
|
// Add this to the global list
|
|
GameplayCueClassesForPreallocation.Add(GCClass);
|
|
|
|
// Add it to any world specific lists
|
|
for (FPreallocationInfo& Info : PreallocationInfoList_Internal)
|
|
{
|
|
ensure(Info.ClassesNeedingPreallocation.Contains(GCClass)==false);
|
|
Info.ClassesNeedingPreallocation.Push(GCClass);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
|
|
void UGameplayCueManager::ResetPreallocation(UWorld* World)
|
|
{
|
|
FPreallocationInfo& Info = GetPreallocationInfo(World);
|
|
|
|
Info.PreallocatedInstances.Reset();
|
|
Info.ClassesNeedingPreallocation = GameplayCueClassesForPreallocation;
|
|
}
|
|
|
|
void UGameplayCueManager::UpdatePreallocation(UWorld* World)
|
|
{
|
|
#if WITH_EDITOR
|
|
// Don't preallocate
|
|
if (World->IsPreviewWorld())
|
|
{
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
FPreallocationInfo& Info = GetPreallocationInfo(World);
|
|
|
|
if (Info.ClassesNeedingPreallocation.Num() > 0)
|
|
{
|
|
TSubclassOf<AGameplayCueNotify_Actor> GCClass = Info.ClassesNeedingPreallocation.Last();
|
|
AGameplayCueNotify_Actor* CDO = GCClass->GetDefaultObject<AGameplayCueNotify_Actor>();
|
|
FGameplayCueNotifyActorArray& PreallocatedList = Info.PreallocatedInstances.FindOrAdd(CDO->GetClass());
|
|
|
|
AGameplayCueNotify_Actor* PrespawnedInstance = Cast<AGameplayCueNotify_Actor>(World->SpawnActor(CDO->GetClass()));
|
|
if (ensureMsgf(PrespawnedInstance, TEXT("Failed to prespawn GC notify for: %s"), *GetNameSafe(CDO)))
|
|
{
|
|
ensureMsgf(IsValid(PrespawnedInstance) == true, TEXT("Newly spawned GC is invalid: %s"), *GetNameSafe(CDO));
|
|
|
|
if (LogGameplayCueActorSpawning)
|
|
{
|
|
ABILITY_LOG(Warning, TEXT("Prespawning GC %s"), *GetNameSafe(CDO));
|
|
}
|
|
|
|
PrespawnedInstance->bInRecycleQueue = true;
|
|
PreallocatedList.Actors.Push(PrespawnedInstance);
|
|
PrespawnedInstance->SetActorHiddenInGame(true);
|
|
|
|
if (PreallocatedList.Actors.Num() >= CDO->NumPreallocatedInstances)
|
|
{
|
|
Info.ClassesNeedingPreallocation.Pop(EAllowShrinking::No);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
FPreallocationInfo& UGameplayCueManager::GetPreallocationInfo(const UWorld* World)
|
|
{
|
|
FObjectKey ObjKey(World);
|
|
|
|
for (FPreallocationInfo& Info : PreallocationInfoList_Internal)
|
|
{
|
|
if (ObjKey == Info.OwningWorldKey)
|
|
{
|
|
return Info;
|
|
}
|
|
}
|
|
|
|
FPreallocationInfo NewInfo;
|
|
NewInfo.OwningWorldKey = ObjKey;
|
|
|
|
PreallocationInfoList_Internal.Add(NewInfo);
|
|
return PreallocationInfoList_Internal.Last();
|
|
}
|
|
|
|
void UGameplayCueManager::OnPostWorldCleanup(UWorld* World, bool bSessionEnded, bool bCleanupResources)
|
|
{
|
|
const FObjectKey WorldObjectKey(World);
|
|
|
|
for (int32 idx=0; idx < PreallocationInfoList_Internal.Num(); ++idx)
|
|
{
|
|
FPreallocationInfo& PreallocationInfo = PreallocationInfoList_Internal[idx];
|
|
if (PreallocationInfo.OwningWorldKey != WorldObjectKey)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
ABILITY_LOG(Verbose, TEXT("UGameplayCueManager::OnPostWorldCleanup %s Removing PreallocationInfoList_Internal element %d"), *GetNameSafe(World), idx);
|
|
|
|
// Spit out some debug information to help us track down memory issues
|
|
constexpr bool bWarnOnActiveActors = true;
|
|
DumpPreallocationStats(PreallocationInfo, bWarnOnActiveActors);
|
|
|
|
// Actually remove the entry which can contain hard references
|
|
PreallocationInfoList_Internal.RemoveAtSwap(idx, EAllowShrinking::No);
|
|
idx--;
|
|
}
|
|
|
|
IGameplayCueInterface::ClearTagToFunctionMap();
|
|
}
|
|
|
|
void UGameplayCueManager::DumpPreallocationStats(const FPreallocationInfo& PreallocationInfo, bool bWarnOnActiveActors)
|
|
{
|
|
constexpr bool bAllowLoggingInBuild = !(UE_BUILD_SHIPPING || UE_BUILD_TEST);
|
|
if (!bAllowLoggingInBuild || !UE_LOG_ACTIVE(LogAbilitySystem, Display))
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (auto& It : PreallocationInfo.PreallocatedInstances)
|
|
{
|
|
if (const UClass* ThisClass = It.Key)
|
|
{
|
|
if (const AGameplayCueNotify_Actor* CDO = ThisClass->GetDefaultObject<AGameplayCueNotify_Actor>())
|
|
{
|
|
const TArray<AGameplayCueNotify_Actor*>& List = It.Value.Actors;
|
|
if (List.Num() > CDO->NumPreallocatedInstances)
|
|
{
|
|
ABILITY_LOG(Display, TEXT(" GameplayCueNotify Class '%s' was used simultaneously %d times. The CDO default is only %d preallocated instances."), *ThisClass->GetName(), List.Num(), CDO->NumPreallocatedInstances);
|
|
}
|
|
|
|
if (bWarnOnActiveActors)
|
|
{
|
|
int StillActive = 0;
|
|
for (const AGameplayCueNotify_Actor* Actor : List)
|
|
{
|
|
StillActive += (!Actor->bInRecycleQueue);
|
|
}
|
|
|
|
// NotifyGameplayCueActorFinished should have been called on all of these Actors by the time we're done tearing down the world.
|
|
UE_CLOG(StillActive > 0, LogAbilitySystem, Error, TEXT(" GameplayCueNotify Class '%s' had %d instances still active. This shouldn't happen."), *ThisClass->GetName(), StillActive);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UGameplayCueManager::OnPreReplayScrub(UWorld* World)
|
|
{
|
|
// See if the World's demo net driver is the duplicated collection's driver,
|
|
// and if so, don't reset preallocated instances. Since the preallocations are global
|
|
// among all level collections, this would clear all current preallocated instances from the list,
|
|
// but there's no need to, and the actor instances would still be around, causing a leak.
|
|
const FLevelCollection* const DuplicateLevelCollection = World ? World->FindCollectionByType(ELevelCollectionType::DynamicDuplicatedLevels) : nullptr;
|
|
if (DuplicateLevelCollection && DuplicateLevelCollection->GetDemoNetDriver() == World->GetDemoNetDriver())
|
|
{
|
|
return;
|
|
}
|
|
|
|
FPreallocationInfo& Info = GetPreallocationInfo(World);
|
|
Info.PreallocatedInstances.Reset();
|
|
}
|
|
|
|
#if GAMEPLAYCUE_DEBUG
|
|
FGameplayCueDebugInfo* UGameplayCueManager::GetDebugInfo(int32 Handle, bool Reset)
|
|
{
|
|
static const int32 MaxDebugEntries = 256;
|
|
int32 Index = Handle % MaxDebugEntries;
|
|
|
|
static TArray<FGameplayCueDebugInfo> DebugArray;
|
|
if (DebugArray.Num() == 0)
|
|
{
|
|
DebugArray.AddDefaulted(MaxDebugEntries);
|
|
}
|
|
if (Reset)
|
|
{
|
|
DebugArray[Index] = FGameplayCueDebugInfo();
|
|
}
|
|
|
|
return &DebugArray[Index];
|
|
}
|
|
#endif
|
|
|
|
// ----------------------------------------------------------------
|
|
|
|
static void RunGameplayCueTranslator(UWorld* InWorld)
|
|
{
|
|
UAbilitySystemGlobals::Get().GetGameplayCueManager()->TranslationManager.BuildTagTranslationTable();
|
|
}
|
|
|
|
FAutoConsoleCommandWithWorld RunGameplayCueTranslatorCmd(
|
|
TEXT("GameplayCue.BuildGameplayCueTranslator"),
|
|
TEXT("Displays GameplayCue notify map"),
|
|
FConsoleCommandWithWorldDelegate::CreateStatic(RunGameplayCueTranslator)
|
|
);
|
|
|
|
// -----------------------------------------------------
|
|
|
|
static void PrintGameplayCueTranslator(UWorld* InWorld)
|
|
{
|
|
UAbilitySystemGlobals::Get().GetGameplayCueManager()->TranslationManager.PrintTranslationTable();
|
|
}
|
|
|
|
FAutoConsoleCommandWithWorld PrintGameplayCueTranslatorCmd(
|
|
TEXT("GameplayCue.PrintGameplayCueTranslator"),
|
|
TEXT("Displays GameplayCue notify map"),
|
|
FConsoleCommandWithWorldDelegate::CreateStatic(PrintGameplayCueTranslator)
|
|
);
|
|
|
|
|
|
#if WITH_EDITOR
|
|
#undef LOCTEXT_NAMESPACE
|
|
#endif
|