diff --git a/GMCAbilitySystem.uplugin b/GMCAbilitySystem.uplugin index dfcbf655..c6b7528a 100644 --- a/GMCAbilitySystem.uplugin +++ b/GMCAbilitySystem.uplugin @@ -1,15 +1,15 @@ { "FileVersion": 3, "Version": 1, - "VersionName": "1.0", - "FriendlyName": "GMCAbilitySystem", - "Description": "", - "Category": "GMC", + "VersionName": "1.3", + "FriendlyName": "GMAS - GMC Ability System", + "Description": "An ability system for GMC (General Movement Component)", + "Category": "GMAS", "CreatedBy": "Reznok", "CreatedByURL": "", "DocsURL": "", "MarketplaceURL": "", - "SupportURL": "", + "SupportURL": "https://github.com/reznok/GMCAbilitySystem", "CanContainContent": true, "IsBetaVersion": false, "IsExperimentalVersion": false, diff --git a/Source/GMCAbilitySystem/GMCAbilitySystem.Build.cs b/Source/GMCAbilitySystem/GMCAbilitySystem.Build.cs index 3b94ee9d..6415d6f3 100644 --- a/Source/GMCAbilitySystem/GMCAbilitySystem.Build.cs +++ b/Source/GMCAbilitySystem/GMCAbilitySystem.Build.cs @@ -30,11 +30,18 @@ public GMCAbilitySystem(ReadOnlyTargetRules Target) : base(Target) "CoreUObject", "Engine", "Slate", - "SlateCore", + "SlateCore", "Niagara" // ... add private dependencies that you statically link with here ... } ); + if (Target.Configuration != UnrealTargetConfiguration.Shipping) + { + PrivateDependencyModuleNames.AddRange(new string[] { + "AutomationController", + "AutomationWorker" + }); + } DynamicallyLoadedModuleNames.AddRange( new string[] diff --git a/Source/GMCAbilitySystem/Private/Ability/GMCAbility.cpp b/Source/GMCAbilitySystem/Private/Ability/GMCAbility.cpp index f1ebb91e..80f9e2bf 100644 --- a/Source/GMCAbilitySystem/Private/Ability/GMCAbility.cpp +++ b/Source/GMCAbilitySystem/Private/Ability/GMCAbility.cpp @@ -12,7 +12,7 @@ UWorld* UGMCAbility::GetWorld() const // UObject::ImplementsGetWorld(), which just blithely and blindly calls GetWorld(). return nullptr; } - + #if WITH_EDITOR if (GIsEditor) { @@ -25,14 +25,22 @@ UWorld* UGMCAbility::GetWorld() const if (Contexts.Num() == 0) { UE_LOG(LogGMCAbilitySystem, Error, TEXT("%s: instanciated class with no valid world!"), *GetClass()->GetName()) - return nullptr; + return nullptr; } - + return Contexts[0].World(); } +bool UGMCAbility::IsActive() const +{ + return AbilityState != EAbilityState::PreExecution && AbilityState != EAbilityState::Ended; +} + void UGMCAbility::Tick(float DeltaTime) { + // Don't tick before the ability is initialized or after it has ended + if (AbilityState == EAbilityState::PreExecution || AbilityState == EAbilityState::Ended) return; + if (!OwnerAbilityComponent->HasAuthority()) { if (!bServerConfirmed && ClientStartTime + ServerConfirmTimeout < OwnerAbilityComponent->ActionTimer) @@ -47,35 +55,41 @@ void UGMCAbility::Tick(float DeltaTime) EndAbility(); return; } - + TickTasks(DeltaTime); TickEvent(DeltaTime); } -void UGMCAbility::AncillaryTick(float DeltaTime){ +void UGMCAbility::AncillaryTick(float DeltaTime) { + // Don't tick before the ability is initialized or after it has ended + if (AbilityState == EAbilityState::PreExecution || AbilityState == EAbilityState::Ended) return; + AncillaryTickTasks(DeltaTime); AncillaryTickEvent(DeltaTime); } void UGMCAbility::TickTasks(float DeltaTime) { - for (const TPair& Task : RunningTasks) + for (int i = 0; i < RunningTasks.Num(); i++) { - if (Task.Value == nullptr) {continue;} - Task.Value->Tick(DeltaTime); + UGMCAbilityTaskBase* Task = RunningTasks[i]; + if (Task == nullptr) { continue; } + Task->Tick(DeltaTime); } } -void UGMCAbility::AncillaryTickTasks(float DeltaTime){ - for (const TPair& Task : RunningTasks) +void UGMCAbility::AncillaryTickTasks(float DeltaTime) { + for (int i = 0; i < RunningTasks.Num(); i++) { - if (Task.Value == nullptr) {continue;} - Task.Value->AncillaryTick(DeltaTime); + UGMCAbilityTaskBase* Task = RunningTasks[i]; + if (Task == nullptr) { continue; } + Task->AncillaryTick(DeltaTime); } } void UGMCAbility::Execute(UGMC_AbilitySystemComponent* InAbilityComponent, int InAbilityID, const UInputAction* InputAction) { + // TODO : Add input action tag here to avoid going by the old FGMCAbilityData struct this->AbilityInputAction = InputAction; this->AbilityID = InAbilityID; this->OwnerAbilityComponent = InAbilityComponent; @@ -83,10 +97,10 @@ void UGMCAbility::Execute(UGMC_AbilitySystemComponent* InAbilityComponent, int I PreBeginAbility(); } -bool UGMCAbility::CanAffordAbilityCost() const +bool UGMCAbility::CanAffordAbilityCost(float DeltaTime) const { if (AbilityCost == nullptr || OwnerAbilityComponent == nullptr) return true; - + UGMCAbilityEffect* AbilityEffect = AbilityCost->GetDefaultObject(); for (FGMCAttributeModifier AttributeModifier : AbilityEffect->EffectData.Modifiers) { @@ -94,7 +108,11 @@ bool UGMCAbility::CanAffordAbilityCost() const { if (Attribute->Tag.MatchesTagExact(AttributeModifier.AttributeTag)) { - if (Attribute->Value + AttributeModifier.Value < 0) return false; + AttributeModifier.InitModifier(AbilityEffect, OwnerAbilityComponent->ActionTimer, -1.f, false, DeltaTime); + if (Attribute->Value + AttributeModifier.CalculateModifierValue(*Attribute) < 0.f) + { + return false; + } } } } @@ -117,15 +135,16 @@ void UGMCAbility::CommitAbilityCooldown() void UGMCAbility::CommitAbilityCost() { if (AbilityCost == nullptr || OwnerAbilityComponent == nullptr) return; - + const UGMCAbilityEffect* EffectCDO = DuplicateObject(AbilityCost->GetDefaultObject(), this); FGMCAbilityEffectData EffectData = EffectCDO->EffectData; EffectData.OwnerAbilityComponent = OwnerAbilityComponent; + EffectData.SourceAbilityComponent = OwnerAbilityComponent; AbilityCostInstance = OwnerAbilityComponent->ApplyAbilityEffect(DuplicateObject(EffectCDO, this), EffectData); } -void UGMCAbility::RemoveAbilityCost(){ - if(AbilityCostInstance){ +void UGMCAbility::RemoveAbilityCost() { + if (AbilityCostInstance) { OwnerAbilityComponent->RemoveActiveAbilityEffect(AbilityCostInstance); } } @@ -167,6 +186,36 @@ void UGMCAbility::HandleTaskHeartbeat(int TaskID) } } +void UGMCAbility::CancelConflictingAbilities() +{ + for (const auto& AbilityToCancelTag : CancelAbilitiesWithTag) { + if (AbilityTag == AbilityToCancelTag) { + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Ability (tag) %s is trying to cancel itself, if you attempt to reset the ability, please use //TODO instead"), *AbilityTag.ToString()); + continue; + } + + if (OwnerAbilityComponent->EndAbilitiesByTag(AbilityToCancelTag)) { + UE_LOG(LogGMCAbilitySystem, Verbose, TEXT("Ability (tag) %s has been cancelled by (tag) %s"), *AbilityTag.ToString(), *AbilityToCancelTag.ToString()); + } + } + + if (!EndOtherAbilitiesQuery.IsEmpty()) + { + for (const auto& ActiveAbility : OwnerAbilityComponent->GetActiveAbilities()) + { + if (ActiveAbility.Value == this) continue; + + if (EndOtherAbilitiesQuery.Matches(ActiveAbility.Value->AbilityDefinition)) + { + ActiveAbility.Value->SetPendingEnd(); + UE_LOG(LogGMCAbilitySystem, Verbose, TEXT("Ability %s cancelled ability %s (matching definition query)"), + *AbilityTag.ToString(), *ActiveAbility.Value->AbilityTag.ToString()); + } + } + } +} + + void UGMCAbility::ServerConfirm() { bServerConfirmed = true; @@ -202,12 +251,12 @@ void UGMCAbility::OnGameplayTaskInitialized(UGameplayTask& Task) UGMCAbilityTaskBase* AbilityTask = Cast(&Task); if (!AbilityTask) { - UE_LOG(LogGMCAbilitySystem, Error, TEXT("UGMCAbility::OnGameplayTaskInitialized called with non-UGMCAbilityTaskBase task")); + // UE_LOG(LogGMCAbilitySystem, Error, TEXT("UGMCAbility::OnGameplayTaskInitialized called with non-UGMCAbilityTaskBase task")); return; } AbilityTask->SetAbilitySystemComponent(OwnerAbilityComponent); AbilityTask->Ability = this; - + } void UGMCAbility::OnGameplayTaskActivated(UGameplayTask& Task) @@ -222,12 +271,37 @@ void UGMCAbility::OnGameplayTaskDeactivated(UGameplayTask& Task) void UGMCAbility::FinishEndAbility() { + for (const TPair& Task : RunningTasks) { if (Task.Value == nullptr) continue; Task.Value->EndTaskGMAS(); } - + + // End handled effect + for (const auto& EfData : DeclaredEffect) + { + // Skip Auth effect removal on client + if (EfData.Value == EGMCAbilityEffectQueueType::ServerAuth && !OwnerAbilityComponent->HasAuthority()) { continue;} + + if (UGMCAbilityEffect* Effect = OwnerAbilityComponent->GetEffectById(EfData.Key)) + { + // Don't try to close effects that are already ended + if (Effect->CurrentState == EGMASEffectState::Started) + { + OwnerAbilityComponent->RemoveActiveAbilityEffectSafe(Effect, EfData.Value); + } + else + { + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Effect Handle %d already ended for ability %s"), EfData.Key, *AbilityTag.ToString()); + } + } + else + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Effect Handle %d not found for ability %s"), EfData.Key, *AbilityTag.ToString()); + } + } + AbilityState = EAbilityState::Ended; } @@ -243,7 +317,18 @@ bool UGMCAbility::PreExecuteCheckEvent_Implementation() { } -bool UGMCAbility::PreBeginAbility() { +void UGMCAbility::DeclareEffect(int OutEffectHandle, EGMCAbilityEffectQueueType EffectType) +{ + if (DeclaredEffect.Contains(OutEffectHandle)) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Effect Handle %d already declared for ability %s"), OutEffectHandle, *AbilityTag.ToString()); + return; + } + DeclaredEffect.Add(OutEffectHandle, EffectType); +} + +bool UGMCAbility::PreBeginAbility() +{ if (IsOnCooldown()) { UE_LOG(LogGMCAbilitySystem, Verbose, TEXT("Ability Activation for %s Stopped By Cooldown"), *AbilityTag.ToString()); @@ -259,6 +344,32 @@ bool UGMCAbility::PreBeginAbility() { return false; } + + TArray ActiveAbilities; + OwnerAbilityComponent->GetActiveAbilities().GenerateValueArray(ActiveAbilities); + + for (auto& OtherAbilityTag : BlockedByOtherAbility) + { + if (ActiveAbilities.FindByPredicate([&OtherAbilityTag](const UGMCAbility* ActiveAbility) { + return ActiveAbility + && ActiveAbility->IsActive() + && ActiveAbility->AbilityTag.MatchesTag(OtherAbilityTag); + })) + { + UE_LOG(LogGMCAbilitySystem, Verbose, TEXT("Ability Activation for %s Stopped because Blocked By Other Ability (%s)"), *AbilityTag.ToString(), *OtherAbilityTag.ToString()); + CancelAbility(); + return false; + } + } + + + if (OwnerAbilityComponent->IsAbilityTagBlocked(AbilityTag)) { + UE_LOG(LogGMCAbilitySystem, Verbose, TEXT("Ability Activation for %s Stopped because Blocked By Other Ability"), *AbilityTag.ToString()); + CancelAbility(); + return false; + } + + BeginAbility(); return true; @@ -267,32 +378,36 @@ bool UGMCAbility::PreBeginAbility() { void UGMCAbility::BeginAbility() { - - if (OwnerAbilityComponent->IsAbilityTagBlocked(AbilityTag)) { - CancelAbility(); - return; + + + OwnerAbilityComponent->OnAbilityActivated.Broadcast(this, AbilityTag); + + if (!BlockOtherAbilitiesQuery.IsEmpty()) + { + FGameplayTagQuery BlockQuery = BlockOtherAbilitiesQuery; + for (auto& ActiveAbility : OwnerAbilityComponent->GetActiveAbilities()) + { + const FGameplayTagContainer& ActiveAbilityTags = ActiveAbility.Value->AbilityDefinition; + + if (BlockQuery.Matches(ActiveAbilityTags)) + { + ActiveAbility.Value->SetPendingEnd(); + UE_LOG(LogGMCAbilitySystem, Verbose, TEXT("Ability %s blocked ability %s (matching query)"), + *AbilityTag.ToString(), *ActiveAbility.Value->AbilityTag.ToString()); + } + } } - if (bApplyCooldownAtAbilityBegin) { CommitAbilityCooldown(); } - + // Initialize Ability AbilityState = EAbilityState::Initialized; // Cancel Abilities in CancelAbilitiesWithTag container - for (const auto& AbilityToCancelTag : CancelAbilitiesWithTag) { - if (AbilityTag == AbilityToCancelTag) { - UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Ability (tag) %s is trying to cancel itself, if you attempt to reset the ability, please use //TODO instead"), *AbilityTag.ToString()); - continue; - } - - if (OwnerAbilityComponent->EndAbilitiesByTag(AbilityToCancelTag)) { - UE_LOG(LogGMCAbilitySystem, Verbose, TEXT("Ability (tag) %s has been cancelled by (tag) %s"), *AbilityTag.ToString(), *AbilityToCancelTag.ToString()); - } - } + CancelConflictingAbilities(); // Execute BP Event BeginAbilityEvent(); @@ -303,6 +418,7 @@ void UGMCAbility::EndAbility() if (AbilityState != EAbilityState::Ended) { FinishEndAbility(); EndAbilityEvent(); + OwnerAbilityComponent->OnAbilityEnded.Broadcast(this); } } @@ -319,16 +435,16 @@ AActor* UGMCAbility::GetOwnerActor() const return OwnerAbilityComponent->GetOwner(); } -AGMC_Pawn* UGMCAbility::GetOwnerPawn() const{ - if (AGMC_Pawn* OwningPawn = Cast(GetOwnerActor())){ +AGMC_Pawn* UGMCAbility::GetOwnerPawn() const { + if (AGMC_Pawn* OwningPawn = Cast(GetOwnerActor())) { return OwningPawn; } return nullptr; } -AGMC_PlayerController* UGMCAbility::GetOwningPlayerController() const{ - if (const AGMC_Pawn* OwningPawn = GetOwnerPawn()){ - if(AGMC_PlayerController* OwningPC = Cast(OwningPawn->GetController())){ +AGMC_PlayerController* UGMCAbility::GetOwningPlayerController() const { + if (const AGMC_Pawn* OwningPawn = GetOwnerPawn()) { + if (AGMC_PlayerController* OwningPC = Cast(OwningPawn->GetController())) { return OwningPC; } } @@ -345,3 +461,9 @@ void UGMCAbility::SetOwnerJustTeleported(bool bValue) { OwnerAbilityComponent->bJustTeleported = bValue; } + +void UGMCAbility::ModifyBlockOtherAbilitiesViaDefinitionQuery(const FGameplayTagQuery& NewQuery) +{ + BlockOtherAbilitiesQuery = NewQuery; + UE_LOG(LogGMCAbilitySystem, Verbose, TEXT("BlockOtherAbilityByDefinitionQuery modified: %s"), *NewQuery.GetDescription()); +} diff --git a/Source/GMCAbilitySystem/Private/Ability/Tasks/GMCAbilityTaskBase.cpp b/Source/GMCAbilitySystem/Private/Ability/Tasks/GMCAbilityTaskBase.cpp index 798eb8c1..fda35bf0 100644 --- a/Source/GMCAbilitySystem/Private/Ability/Tasks/GMCAbilityTaskBase.cpp +++ b/Source/GMCAbilitySystem/Private/Ability/Tasks/GMCAbilityTaskBase.cpp @@ -30,11 +30,16 @@ void UGMCAbilityTaskBase::RegisterTask(UGMCAbilityTaskBase* Task) void UGMCAbilityTaskBase::Tick(float DeltaTime) { + +} + +void UGMCAbilityTaskBase::AncillaryTick(float DeltaTime){ // Locally controlled server pawns don't need to send heartbeats if (AbilitySystemComponent->GMCMovementComponent->IsLocallyControlledServerPawn()) return; // If not the server version of the component, send heartbeats - if (AbilitySystemComponent->GetNetMode() != NM_DedicatedServer) + if (AbilitySystemComponent->GetNetMode() != NM_DedicatedServer && + AbilitySystemComponent->GetNetMode() != NM_ListenServer) { if (ClientLastHeartbeatSentTime + HeartbeatInterval < AbilitySystemComponent->ActionTimer) { @@ -45,14 +50,10 @@ void UGMCAbilityTaskBase::Tick(float DeltaTime) else if (LastHeartbeatReceivedTime + HeartbeatMaxInterval < AbilitySystemComponent->ActionTimer) { UE_LOG(LogGMCReplication, Error, TEXT("Server Task Heartbeat Timeout, Cancelling Ability: %s"), *Ability->GetName()); + AbilitySystemComponent->OnTaskTimeout.Broadcast(Ability->AbilityTag); Ability->EndAbility(); EndTask(); } - -} - -void UGMCAbilityTaskBase::AncillaryTick(float DeltaTime){ - } void UGMCAbilityTaskBase::ClientProgressTask() @@ -64,3 +65,17 @@ void UGMCAbilityTaskBase::ClientProgressTask() const FInstancedStruct TaskDataInstance = FInstancedStruct::Make(TaskData); Ability->OwnerAbilityComponent->QueueTaskData(TaskDataInstance); } + +void UGMCAbilityTaskBase::Heartbeat() +{ + AbilitySystemComponent->GMCMovementComponent->SV_SwapServerState(); + LastHeartbeatReceivedTime = AbilitySystemComponent->ActionTimer; + AbilitySystemComponent->GMCMovementComponent->SV_SwapServerState(); +} + +bool UGMCAbilityTaskBase::IsClientOrRemoteListenServerPawn() const +{ + return (AbilitySystemComponent->GetNetMode() != NM_DedicatedServer && + AbilitySystemComponent->GetNetMode() != NM_ListenServer) || + AbilitySystemComponent->GMCMovementComponent->IsLocallyControlledServerPawn(); +} diff --git a/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataByte.cpp b/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataByte.cpp index 79883141..a7cf416f 100644 --- a/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataByte.cpp +++ b/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataByte.cpp @@ -16,7 +16,7 @@ void UGMCAbilityTask_SetTargetDataByte::Activate() { Super::Activate(); - if (AbilitySystemComponent->GetNetMode() != NM_DedicatedServer) + if (IsClientOrRemoteListenServerPawn()) { ClientProgressTask(); } diff --git a/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataFloat.cpp b/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataFloat.cpp index dd7df7f0..f75bc9f2 100644 --- a/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataFloat.cpp +++ b/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataFloat.cpp @@ -16,7 +16,7 @@ void UGMCAbilityTask_SetTargetDataFloat::Activate() { Super::Activate(); - if (AbilitySystemComponent->GetNetMode() != NM_DedicatedServer) + if (IsClientOrRemoteListenServerPawn()) { ClientProgressTask(); } diff --git a/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataGameplayTag.cpp b/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataGameplayTag.cpp index 6847bc08..9a122df0 100644 --- a/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataGameplayTag.cpp +++ b/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataGameplayTag.cpp @@ -11,7 +11,7 @@ UGMCAbilityTask_SetTargetDataGameplayTag* UGMCAbilityTask_SetTargetDataGameplayT void UGMCAbilityTask_SetTargetDataGameplayTag::Activate(){ Super::Activate(); - if (AbilitySystemComponent->GetNetMode() != NM_DedicatedServer) + if (IsClientOrRemoteListenServerPawn()) { ClientProgressTask(); } diff --git a/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataHit.cpp b/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataHit.cpp new file mode 100644 index 00000000..29bca21e --- /dev/null +++ b/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataHit.cpp @@ -0,0 +1,44 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "Ability/Tasks/SetTargetDataHit.h" +#include "GMCAbilityComponent.h" + +UGMCAbilityTask_SetTargetDataHit* UGMCAbilityTask_SetTargetDataHit::SetTargetDataHit(UGMCAbility* OwningAbility, FHitResult InHit) +{ + UGMCAbilityTask_SetTargetDataHit* Task = NewAbilityTask(OwningAbility); + Task->Ability = OwningAbility; + Task->Target = InHit; + return Task; +} + +void UGMCAbilityTask_SetTargetDataHit::Activate() +{ + Super::Activate(); + + if (IsClientOrRemoteListenServerPawn()) + { + ClientProgressTask(); + } +} + +void UGMCAbilityTask_SetTargetDataHit::ProgressTask(FInstancedStruct& TaskData) +{ + Super::ProgressTask(TaskData); + const FGMCAbilityTaskTargetDataHit Data = TaskData.Get(); + + Completed.Broadcast(Data.Target); + EndTask(); +} + +void UGMCAbilityTask_SetTargetDataHit::ClientProgressTask() +{ + FGMCAbilityTaskTargetDataHit TaskData; + TaskData.TaskType = EGMCAbilityTaskDataType::Progress; + TaskData.AbilityID = Ability->GetAbilityID(); + TaskData.TaskID = TaskID; + TaskData.Target = Target; + const FInstancedStruct TaskDataInstance = FInstancedStruct::Make(TaskData); + + Ability->OwnerAbilityComponent->QueueTaskData(TaskDataInstance); +} \ No newline at end of file diff --git a/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataInstancedStruct.cpp b/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataInstancedStruct.cpp new file mode 100644 index 00000000..0cc784b3 --- /dev/null +++ b/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataInstancedStruct.cpp @@ -0,0 +1,39 @@ +#include "Ability/Tasks/SetTargetDataInstancedStruct.h" +#include "GMCAbilityComponent.h" + +UGMCAbilityTask_SetTargetDataInstancedStruct* UGMCAbilityTask_SetTargetDataInstancedStruct::SetTargetDataInstancedStruct(UGMCAbility* OwningAbility, FInstancedStruct InstancedStruct){ + UGMCAbilityTask_SetTargetDataInstancedStruct* Task = NewAbilityTask(OwningAbility); + Task->Ability = OwningAbility; + Task->Target = InstancedStruct; + return Task; +} + +void UGMCAbilityTask_SetTargetDataInstancedStruct::Activate(){ + Super::Activate(); + + if (IsClientOrRemoteListenServerPawn()) + { + ClientProgressTask(); + } +} + +void UGMCAbilityTask_SetTargetDataInstancedStruct::ProgressTask(FInstancedStruct& TaskData){ + Super::ProgressTask(TaskData); + const FGMCAbilityTaskTargetDataInstancedStruct Data = TaskData.Get(); + + Completed.Broadcast(Data.Target); + EndTask(); +} + +void UGMCAbilityTask_SetTargetDataInstancedStruct::ClientProgressTask(){ + FGMCAbilityTaskTargetDataInstancedStruct TaskData; + TaskData.TaskType = EGMCAbilityTaskDataType::Progress; + TaskData.AbilityID = Ability->GetAbilityID(); + TaskData.TaskID = TaskID; + TaskData.Target = Target; + const FInstancedStruct TaskDataInstance = FInstancedStruct::Make(TaskData); + + Ability->OwnerAbilityComponent->QueueTaskData(TaskDataInstance); +} + + diff --git a/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataInt.cpp b/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataInt.cpp index 11d3353c..d29774e8 100644 --- a/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataInt.cpp +++ b/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataInt.cpp @@ -16,7 +16,7 @@ void UGMCAbilityTask_SetTargetDataInt::Activate() { Super::Activate(); - if (AbilitySystemComponent->GetNetMode() != NM_DedicatedServer) + if (IsClientOrRemoteListenServerPawn()) { ClientProgressTask(); } diff --git a/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataObject.cpp b/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataObject.cpp index ca7852c9..c848c8df 100644 --- a/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataObject.cpp +++ b/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataObject.cpp @@ -16,7 +16,7 @@ void UGMCAbilityTask_SetTargetDataObject::Activate() { Super::Activate(); - if (AbilitySystemComponent->GetNetMode() != NM_DedicatedServer) + if (IsClientOrRemoteListenServerPawn()) { ClientProgressTask(); } diff --git a/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataTransform.cpp b/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataTransform.cpp new file mode 100644 index 00000000..076bf3a6 --- /dev/null +++ b/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataTransform.cpp @@ -0,0 +1,39 @@ +#include "Ability/Tasks/SetTargetDataTransform.h" +#include "GMCAbilityComponent.h" + +UGMCAbilityTask_SetTargetDataTransform* UGMCAbilityTask_SetTargetDataTransform::SetTargetDataTransform(UGMCAbility* OwningAbility, FTransform Transform){ + UGMCAbilityTask_SetTargetDataTransform* Task = NewAbilityTask(OwningAbility); + Task->Ability = OwningAbility; + Task->Target = Transform; + return Task; +} + +void UGMCAbilityTask_SetTargetDataTransform::Activate(){ + Super::Activate(); + + if (IsClientOrRemoteListenServerPawn()) + { + ClientProgressTask(); + } +} + +void UGMCAbilityTask_SetTargetDataTransform::ProgressTask(FInstancedStruct& TaskData){ + Super::ProgressTask(TaskData); + const FGMCAbilityTaskTargetDataTransform Data = TaskData.Get(); + + Completed.Broadcast(Data.Target); + EndTask(); +} + +void UGMCAbilityTask_SetTargetDataTransform::ClientProgressTask(){ + FGMCAbilityTaskTargetDataTransform TaskData; + TaskData.TaskType = EGMCAbilityTaskDataType::Progress; + TaskData.AbilityID = Ability->GetAbilityID(); + TaskData.TaskID = TaskID; + TaskData.Target = Target; + const FInstancedStruct TaskDataInstance = FInstancedStruct::Make(TaskData); + + Ability->OwnerAbilityComponent->QueueTaskData(TaskDataInstance); +} + + diff --git a/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataVector3.cpp b/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataVector3.cpp index c540e05f..c5f55b47 100644 --- a/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataVector3.cpp +++ b/Source/GMCAbilitySystem/Private/Ability/Tasks/SetTargetDataVector3.cpp @@ -16,7 +16,7 @@ void UGMCAbilityTask_SetTargetDataVector3::Activate() { Super::Activate(); - if (AbilitySystemComponent->GetNetMode() != NM_DedicatedServer) + if (IsClientOrRemoteListenServerPawn()) { ClientProgressTask(); } @@ -25,6 +25,13 @@ void UGMCAbilityTask_SetTargetDataVector3::Activate() void UGMCAbilityTask_SetTargetDataVector3::ProgressTask(FInstancedStruct& TaskData) { Super::ProgressTask(TaskData); + if (TaskData.GetScriptStruct() != FGMCAbilityTaskTargetDataVector3::StaticStruct()) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("UGMCAbilityTask_SetTargetDataVector3::ProgressTask: Invalid TaskData")); + EndTask(); + return; + } + const FGMCAbilityTaskTargetDataVector3 Data = TaskData.Get(); Completed.Broadcast(Data.Target); diff --git a/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForGMCMontageChange.cpp b/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForGMCMontageChange.cpp index 17911953..e374f93b 100644 --- a/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForGMCMontageChange.cpp +++ b/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForGMCMontageChange.cpp @@ -43,7 +43,7 @@ void UGMCAbilityTask_WaitForGMCMontageChange::Tick(float DeltaTime) { Super::Tick(DeltaTime); - const UAnimMontage* RunningMontage = OrganicMovementCmp->GetActiveMontage(OrganicMovementCmp->MontageTracker); + RunningMontage = OrganicMovementCmp->GetActiveMontage(OrganicMovementCmp->MontageTracker); // If the montage has changed, finish the task if (StartingMontage != RunningMontage) @@ -56,7 +56,7 @@ void UGMCAbilityTask_WaitForGMCMontageChange::OnFinish() { if (GetState() != EGameplayTaskState::Finished) { - Completed.Broadcast(); + Completed.Broadcast(RunningMontage); EndTask(); } } diff --git a/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForGameplayTagChange.cpp b/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForGameplayTagChange.cpp index 58e4fe41..998c3fb0 100644 --- a/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForGameplayTagChange.cpp +++ b/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForGameplayTagChange.cpp @@ -31,7 +31,7 @@ void UGMCAbilityTask_WaitForGameplayTagChange::OnGameplayTagChanged(const FGamep break; case Unset: - MatchedTags = AddedTags.Filter(Tags); + MatchedTags = RemovedTags.Filter(Tags); break; case Changed: diff --git a/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForInputKeyPress.cpp b/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForInputKeyPress.cpp index 0492f27d..0884b0ca 100644 --- a/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForInputKeyPress.cpp +++ b/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForInputKeyPress.cpp @@ -1,34 +1,58 @@ #include "Ability/Tasks/WaitForInputKeyPress.h" #include "EnhancedInputComponent.h" +#include "EnhancedInputSubsystems.h" #include "Components/GMCAbilityComponent.h" +#include "Kismet/KismetSystemLibrary.h" -UGMCAbilityTask_WaitForInputKeyPress* UGMCAbilityTask_WaitForInputKeyPress::WaitForKeyPress(UGMCAbility* OwningAbility) +UGMCAbilityTask_WaitForInputKeyPress* UGMCAbilityTask_WaitForInputKeyPress::WaitForKeyPress(UGMCAbility* OwningAbility, bool bCheckForPressDuringActivation, float MaxDuration) { UGMCAbilityTask_WaitForInputKeyPress* Task = NewAbilityTask(OwningAbility); Task->Ability = OwningAbility; + Task->bShouldCheckForPressDuringActivation = bCheckForPressDuringActivation; + Task->MaxDuration = MaxDuration; return Task; } void UGMCAbilityTask_WaitForInputKeyPress::Activate() { Super::Activate(); - + + StartTime = AbilitySystemComponent->ActionTimer; + if (Ability->bAllowMultipleInstances) { UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Ability %s is set to allow multiple instances and this should not be used with WaitForInputKeyPress AbilityTask !"), *Ability->GetName()); ClientProgressTask(); return; } - if (Ability->AbilityInputAction != nullptr) + UEnhancedInputComponent* EnhancedInputComponent = GetEnhancedInputComponent(); + + if (Ability->AbilityInputAction != nullptr && InputComponent != nullptr) { - UEnhancedInputComponent* const InputComponent = GetEnhancedInputComponent(); - - const FEnhancedInputActionEventBinding& Binding = InputComponent->BindAction( + const FEnhancedInputActionEventBinding& Binding = EnhancedInputComponent->BindAction( Ability->AbilityInputAction, ETriggerEvent::Started, this, &UGMCAbilityTask_WaitForInputKeyPress::OnKeyPressed); + + InputBindingHandle = Binding.GetHandle(); + + // Check if button was held when entering the task + if (bShouldCheckForPressDuringActivation) + { + FInputActionValue ActionValue = FInputActionValue(); + APlayerController* PC = AbilitySystemComponent->GetOwner()->GetInstigatorController(); + if (UEnhancedInputLocalPlayerSubsystem* InputSubSystem = ULocalPlayer::GetSubsystem(PC->GetLocalPlayer())) { + ActionValue = InputSubSystem->GetPlayerInput() ? InputSubSystem->GetPlayerInput()->GetActionValue(Ability->AbilityInputAction) : FInputActionValue(); + } + + if (!ActionValue.GetMagnitude()) + { + InputBindingHandle = -1; + ClientProgressTask(); + } + } } else { @@ -36,21 +60,37 @@ void UGMCAbilityTask_WaitForInputKeyPress::Activate() } } +void UGMCAbilityTask_WaitForInputKeyPress::AncillaryTick(float DeltaTime) +{ + Super::AncillaryTick(DeltaTime); + if (bTaskCompleted) return; + + Duration = AbilitySystemComponent->ActionTimer - StartTime; + OnTick.Broadcast(Duration); + + if (MaxDuration > 0 && Duration >= MaxDuration) + { + ClientProgressTask(); + bTimedOut = true; + } +} + void UGMCAbilityTask_WaitForInputKeyPress::OnKeyPressed(const FInputActionValue& InputActionValue) { + // Unbind from the input component so we don't fire multiple times. - if (UEnhancedInputComponent* InputComponent = GetValid(GetEnhancedInputComponent())) + if (InputComponent) { InputComponent->RemoveActionBindingForHandle(InputBindingHandle); InputBindingHandle = -1; } - + ClientProgressTask(); } -UEnhancedInputComponent* UGMCAbilityTask_WaitForInputKeyPress::GetEnhancedInputComponent() const +UEnhancedInputComponent* UGMCAbilityTask_WaitForInputKeyPress::GetEnhancedInputComponent() { - UInputComponent* InputComponent = Ability->OwnerAbilityComponent->GetOwner()->GetComponentByClass(); + InputComponent = Ability->OwnerAbilityComponent->GetOwner()->GetComponentByClass(); if (InputComponent) { if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked(InputComponent)) @@ -64,7 +104,15 @@ UEnhancedInputComponent* UGMCAbilityTask_WaitForInputKeyPress::GetEnhancedInputC void UGMCAbilityTask_WaitForInputKeyPress::OnTaskCompleted() { EndTask(); - Completed.Broadcast(); + Duration = AbilitySystemComponent->ActionTimer - StartTime; + if (!bTimedOut) + { + Completed.Broadcast(Duration); + } + else + { + TimedOut.Broadcast(Duration); + } bTaskCompleted = true; } @@ -75,7 +123,7 @@ void UGMCAbilityTask_WaitForInputKeyPress::OnDestroy(bool bInOwnerFinished) // If we're still bound to the input component for some reason, we'll want to unbind. if (InputBindingHandle != -1) { - if (UEnhancedInputComponent* InputComponent = GetValid(GetEnhancedInputComponent())) + if (InputComponent) { InputComponent->RemoveActionBindingForHandle(InputBindingHandle); InputBindingHandle = -1; diff --git a/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForInputKeyPressParameterized.cpp b/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForInputKeyPressParameterized.cpp new file mode 100644 index 00000000..60bd63d5 --- /dev/null +++ b/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForInputKeyPressParameterized.cpp @@ -0,0 +1,150 @@ +#include "Ability/Tasks/WaitForInputKeyPressParameterized.h" + +#include "EnhancedInputComponent.h" +#include "EnhancedInputSubsystems.h" +#include "Components/GMCAbilityComponent.h" + +UGMCAbilityTask_WaitForInputKeyPressParameterized* UGMCAbilityTask_WaitForInputKeyPressParameterized::WaitForKeyPress(UGMCAbility* OwningAbility, UInputAction* InputAction, bool bCheckForPressDuringActivation, float MaxDuration) +{ + UGMCAbilityTask_WaitForInputKeyPressParameterized* Task = NewAbilityTask(OwningAbility); + Task->Ability = OwningAbility; + Task->InputActionToWaitFor = InputAction; + Task->MaxDuration = MaxDuration; + Task->bShouldCheckForPressDuringActivation = bCheckForPressDuringActivation; + return Task; +} + +void UGMCAbilityTask_WaitForInputKeyPressParameterized::Activate() +{ + Super::Activate(); + + StartTime = AbilitySystemComponent->ActionTimer; + + if (Ability->bAllowMultipleInstances) { + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Ability %s is set to allow multiple instances and this should not be used with WaitForInputKeyPress AbilityTask !"), *Ability->GetName()); + ClientProgressTask(); + return; + } + + if (Ability->AbilityInputAction != nullptr) + { + UEnhancedInputComponent* const EnhInputComponent = GetEnhancedInputComponent(); + + if (InputComponent) + { + const FEnhancedInputActionEventBinding& Binding = EnhInputComponent->BindAction( + InputActionToWaitFor, ETriggerEvent::Started, this, + &UGMCAbilityTask_WaitForInputKeyPressParameterized::OnKeyPressed); + + InputBindingHandle = Binding.GetHandle(); + + // Check if button was held when entering the task + if (bShouldCheckForPressDuringActivation) + { + FInputActionValue ActionValue = FInputActionValue(); + APlayerController* PC = AbilitySystemComponent->GetOwner()->GetInstigatorController(); + if (UEnhancedInputLocalPlayerSubsystem* InputSubSystem = ULocalPlayer::GetSubsystem(PC->GetLocalPlayer())) { + ActionValue = InputSubSystem->GetPlayerInput() ? InputSubSystem->GetPlayerInput()->GetActionValue(Ability->AbilityInputAction) : FInputActionValue(); + } + if (ActionValue.GetMagnitude() == 1) + { + InputBindingHandle = -1; + ClientProgressTask(); + } + } + } + } + else + { + ClientProgressTask(); + } +} + +void UGMCAbilityTask_WaitForInputKeyPressParameterized::AncillaryTick(float DeltaTime) +{ + Super::AncillaryTick(DeltaTime); + + if (bTaskCompleted) return; + + Duration = AbilitySystemComponent->ActionTimer - StartTime; + OnTick.Broadcast(Duration); + + if (MaxDuration > 0 && Duration >= MaxDuration) + { + ClientProgressTask(); + bTimedOut = true; + } + +} + +void UGMCAbilityTask_WaitForInputKeyPressParameterized::OnKeyPressed(const FInputActionValue& InputActionValue) +{ + // Unbind from the input component so we don't fire multiple times. + if (UEnhancedInputComponent* EnhInputComponent = GetValid(GetEnhancedInputComponent())) + { + EnhInputComponent->RemoveActionBindingForHandle(InputBindingHandle); + InputBindingHandle = -1; + } + + ClientProgressTask(); +} + +UEnhancedInputComponent* UGMCAbilityTask_WaitForInputKeyPressParameterized::GetEnhancedInputComponent() +{ + InputComponent = Ability->OwnerAbilityComponent->GetOwner()->GetComponentByClass(); + if (InputComponent) + { + if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked(InputComponent)) + { + return EnhancedInputComponent; + } + } + return nullptr; +} + +void UGMCAbilityTask_WaitForInputKeyPressParameterized::OnTaskCompleted() +{ + EndTask(); + Duration = AbilitySystemComponent->ActionTimer - StartTime; + if (!bTimedOut) + { + Completed.Broadcast(Duration); + } + else + { + TimedOut.Broadcast(Duration); + } + bTaskCompleted = true; +} + +void UGMCAbilityTask_WaitForInputKeyPressParameterized::OnDestroy(bool bInOwnerFinished) +{ + Super::OnDestroy(bInOwnerFinished); + + // If we're still bound to the input component for some reason, we'll want to unbind. + if (InputBindingHandle != -1) + { + if (InputComponent) + { + InputComponent->RemoveActionBindingForHandle(InputBindingHandle); + InputBindingHandle = -1; + } + } +} + +void UGMCAbilityTask_WaitForInputKeyPressParameterized::ProgressTask(FInstancedStruct& TaskData) +{ + Super::ProgressTask(TaskData); + OnTaskCompleted(); +} + +void UGMCAbilityTask_WaitForInputKeyPressParameterized::ClientProgressTask() +{ + FGMCAbilityTaskData TaskData; + TaskData.TaskType = EGMCAbilityTaskDataType::Progress; + TaskData.AbilityID = Ability->GetAbilityID(); + TaskData.TaskID = TaskID; + const FInstancedStruct TaskDataInstance = FInstancedStruct::Make(TaskData); + + Ability->OwnerAbilityComponent->QueueTaskData(TaskDataInstance); +} diff --git a/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForInputKeyRelease.cpp b/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForInputKeyRelease.cpp index d2184dc7..4919177e 100644 --- a/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForInputKeyRelease.cpp +++ b/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForInputKeyRelease.cpp @@ -5,21 +5,24 @@ #include "Components/GMCAbilityComponent.h" #include "Kismet/KismetSystemLibrary.h" -UGMCAbilityTask_WaitForInputKeyRelease* UGMCAbilityTask_WaitForInputKeyRelease::WaitForKeyRelease(UGMCAbility* OwningAbility, bool bCheckForReleaseDuringActivation) +UGMCAbilityTask_WaitForInputKeyRelease* UGMCAbilityTask_WaitForInputKeyRelease::WaitForKeyRelease(UGMCAbility* OwningAbility, bool bCheckForReleaseDuringActivation, float MaxDuration) { UGMCAbilityTask_WaitForInputKeyRelease* Task = NewAbilityTask(OwningAbility); Task->Ability = OwningAbility; Task->bShouldCheckForReleaseDuringActivation = bCheckForReleaseDuringActivation; + Task->MaxDuration = MaxDuration; return Task; } void UGMCAbilityTask_WaitForInputKeyRelease::Activate() { Super::Activate(); + + StartTime = AbilitySystemComponent->ActionTimer; UEnhancedInputComponent* const InputComponent = GetEnhancedInputComponent(); - if (Ability->AbilityInputAction != nullptr) + if (Ability->AbilityInputAction != nullptr && InputComponent != nullptr) { FEnhancedInputActionEventBinding& Binding = InputComponent->BindAction( Ability->AbilityInputAction, ETriggerEvent::Completed, this, @@ -52,15 +55,25 @@ void UGMCAbilityTask_WaitForInputKeyRelease::Activate() } } -void UGMCAbilityTask_WaitForInputKeyRelease::OnKeyReleased(const FInputActionValue& InputActionValue) +void UGMCAbilityTask_WaitForInputKeyRelease::AncillaryTick(float DeltaTime) { - // Unbind since we're done now. - ClientProgressTask(); - if (UInputComponent* const InputComponent = GetValid(GetEnhancedInputComponent())) + Super::AncillaryTick(DeltaTime); + if (bTaskCompleted) return; + + Duration = AbilitySystemComponent->ActionTimer - StartTime; + OnTick.Broadcast(Duration); + + if (MaxDuration > 0 && Duration >= MaxDuration) { - InputComponent->RemoveActionBindingForHandle(InputBindingHandle); + ClientProgressTask(); + bTimedOut = true; } +} +void UGMCAbilityTask_WaitForInputKeyRelease::OnKeyReleased(const FInputActionValue& InputActionValue) +{ + // Unbind since we're done now. + ClientProgressTask(); InputBindingHandle = -1; } @@ -80,7 +93,15 @@ UEnhancedInputComponent* UGMCAbilityTask_WaitForInputKeyRelease::GetEnhancedInpu void UGMCAbilityTask_WaitForInputKeyRelease::OnTaskCompleted() { EndTask(); - Completed.Broadcast(); + Duration = AbilitySystemComponent->ActionTimer - StartTime; + if (!bTimedOut) + { + Completed.Broadcast(Duration); + } + else + { + TimedOut.Broadcast(Duration); + } bTaskCompleted = true; } @@ -108,6 +129,11 @@ void UGMCAbilityTask_WaitForInputKeyRelease::ProgressTask(FInstancedStruct& Task void UGMCAbilityTask_WaitForInputKeyRelease::ClientProgressTask() { + if (UInputComponent* const InputComponent = GetValid(GetEnhancedInputComponent())) + { + InputComponent->RemoveActionBindingForHandle(InputBindingHandle); + } + FGMCAbilityTaskData TaskData; TaskData.TaskType = EGMCAbilityTaskDataType::Progress; TaskData.AbilityID = Ability->GetAbilityID(); diff --git a/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForRotateYawTowardsDirection.cpp b/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForRotateYawTowardsDirection.cpp new file mode 100644 index 00000000..c269bfc3 --- /dev/null +++ b/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForRotateYawTowardsDirection.cpp @@ -0,0 +1,72 @@ +#include "Ability/Tasks/WaitForRotateYawTowardsDirection.h" + +#include "GMCMovementUtilityComponent.h" +#include "Components/GMCAbilityComponent.h" + + +UGMCAbilityTask_RotateYawTowardsDirection::UGMCAbilityTask_RotateYawTowardsDirection(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +UGMCAbilityTask_RotateYawTowardsDirection* UGMCAbilityTask_RotateYawTowardsDirection::WaitForRotateYawTowardsDirection( + UGMCAbility* OwningAbility, FVector TargetDirection, float RotationSpeed) +{ + UGMCAbilityTask_RotateYawTowardsDirection* Task = NewAbilityTask(OwningAbility); + Task->DesiredDirection = TargetDirection.GetSafeNormal(); + Task->RotationSpeed = RotationSpeed; + return Task; +} + +void UGMCAbilityTask_RotateYawTowardsDirection::Activate() +{ + Super::Activate(); + bTickingTask = true; + + MovementComponent = AbilitySystemComponent->GMCMovementComponent; + + if (!MovementComponent) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("RotateYawTowardsDirection called without a valid Movement Component")); + OnFinish(); + return; + } + + StartTime = GetWorld()->GetTimeSeconds(); + + UE_LOG(LogGMCAbilitySystem, Verbose, TEXT("RotateYawTowardsDirection activated with direction: %s, speed: %f"), + *DesiredDirection.ToString(), RotationSpeed); +} + +void UGMCAbilityTask_RotateYawTowardsDirection::Tick(float DeltaTime) +{ + Super::Tick(DeltaTime); + + if (!MovementComponent || DesiredDirection.IsNearlyZero()) + { + OnFinish(); + return; + } + + MovementComponent->RotateYawTowardsDirection(DesiredDirection, RotationSpeed, DeltaTime); + + // check if rotation is complete + const FRotator CurrentRotation = MovementComponent->GetOwner()->GetActorRotation(); + const FRotator TargetRotation = DesiredDirection.Rotation(); + const float AngleDifference = FMath::Abs(FRotator::NormalizeAxis(CurrentRotation.Yaw - TargetRotation.Yaw)); + + if (AngleDifference < 0.01f) + { + OnFinish(); + } +} + +void UGMCAbilityTask_RotateYawTowardsDirection::OnFinish() +{ + if (GetState() != EGameplayTaskState::Finished) + { + float Duration = GetWorld()->GetTimeSeconds() - StartTime; + Completed.Broadcast(Duration); + EndTask(); + } +} diff --git a/Source/GMCAbilitySystem/Private/Attributes/GMCAttributeModifier.cpp b/Source/GMCAbilitySystem/Private/Attributes/GMCAttributeModifier.cpp new file mode 100644 index 00000000..086ba877 --- /dev/null +++ b/Source/GMCAbilitySystem/Private/Attributes/GMCAttributeModifier.cpp @@ -0,0 +1,129 @@ +#include "Attributes/GMCAttributeModifier.h" + +#include "GMCAbilityComponent.h" + +float FGMCAttributeModifier::GetValue() const +{ + // Get The Value Type + switch (ValueType) + { + case EGMCAttributeModifierType::AMT_Value: + return ModifierValue; + case EGMCAttributeModifierType::AMT_Attribute: + { + if (SourceAbilityEffect.IsValid() && SourceAbilityEffect->GetOwnerAbilityComponent()) + { + return SourceAbilityEffect->GetOwnerAbilityComponent()->GetAttributeValueByTag(ValueAsAttribute); + } + UE_LOG(LogGMCAbilitySystem, Error, TEXT("SourceAbilityEffect is null in FAttribute::AddModifier")); + return 0.f; + } + case EGMCAttributeModifierType::AMT_Custom: + if (CustomModifierClass && SourceAbilityEffect.IsValid() && SourceAbilityEffect->GetOwnerAbilityComponent()) + { + if (UGMCAttributeModifierCustom_Base* CustomModifier = CustomModifierClass->GetDefaultObject()) + { + return CustomModifier->Calculate(SourceAbilityEffect.Get(), SourceAbilityEffect->GetOwnerAbilityComponent()->GetAttributeByTag(AttributeTag)); + } + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Custom Modifier Class is null in FAttribute::AddModifier")); + } + else + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("CustomModifierClass is null or SourceAbilityEffect/SourceAbilitySystemComponent is invalid in FAttribute::AddModifier")); + } + break; + } + + checkNoEntry() + return 0.f; +} + +float FGMCAttributeModifier::CalculateModifierValue(const FAttribute& Attribute) const +{ + float TargetValue = GetValue(); + + // First set Percentage values to a fraction + switch (Op) + { + case EModifierType::AddPercentageAttribute: + case EModifierType::AddPercentageInitialValue: + case EModifierType::AddPercentageAttributeSum: + case EModifierType::AddPercentageMissing: + case EModifierType::AddPercentageMinClamp: + case EModifierType::AddPercentageMaxClamp: + case EModifierType::AddPercentageOfAttributeRawValue: + TargetValue /= 100.f; + break; + } + + switch (Op) + { + case EModifierType::Add: + return TargetValue * DeltaTime; + case EModifierType::AddPercentageInitialValue: + return Attribute.InitialValue * TargetValue * DeltaTime; + case EModifierType::AddPercentageAttribute: + return SourceAbilityEffect->GetOwnerAbilityComponent()->GetAttributeValueByTag(ValueAsAttribute) * TargetValue * DeltaTime; + case EModifierType::AddPercentageMaxClamp: + { + const float MaxValue = Attribute.Clamp.MaxAttributeTag.IsValid() ? SourceAbilityEffect->GetOwnerAbilityComponent()->GetAttributeValueByTag(Attribute.Clamp.MaxAttributeTag) : Attribute.Clamp.Max; + return MaxValue * TargetValue * DeltaTime; + } + case EModifierType::AddPercentageMinClamp: + { + const float MinValue = Attribute.Clamp.MinAttributeTag.IsValid() ? SourceAbilityEffect->GetOwnerAbilityComponent()->GetAttributeValueByTag(Attribute.Clamp.MinAttributeTag) : Attribute.Clamp.Min; + return MinValue * TargetValue * DeltaTime; + } + case EModifierType::AddPercentageAttributeSum: + { + float Sum = 0.f; + for (auto& AttTag : Attributes) + { + Sum += SourceAbilityEffect->GetOwnerAbilityComponent()->GetAttributeValueByTag(AttTag); + } + return TargetValue * Sum * DeltaTime; + } + case EModifierType::AddScaledBetween: + { + const float XBound = XAsAttribute ? SourceAbilityEffect->GetOwnerAbilityComponent()->GetAttributeValueByTag(XAttribute) : X; + const float YBound = YAsAttribute ? SourceAbilityEffect->GetOwnerAbilityComponent()->GetAttributeValueByTag(YAttribute) : Y; + return FMath::Clamp(FMath::Lerp(XBound, YBound, TargetValue), X, Y) * DeltaTime; + } + case EModifierType::AddClampedBetween: + { + const float XBound = XAsAttribute ? SourceAbilityEffect->GetOwnerAbilityComponent()->GetAttributeValueByTag(XAttribute) : X; + const float YBound = YAsAttribute ? SourceAbilityEffect->GetOwnerAbilityComponent()->GetAttributeValueByTag(YAttribute) : Y; + return FMath::Clamp(TargetValue, XBound, YBound) * DeltaTime; + } + case EModifierType::AddPercentageMissing: + { + const float MissingValue = Attribute.InitialValue - Attribute.Value; + return TargetValue * MissingValue * DeltaTime; + } + case EModifierType::AddPercentageOfAttributeRawValue: + { + const float RawValue = SourceAbilityEffect->GetOwnerAbilityComponent()->GetAttributeRawValue(Attribute.Tag); + return TargetValue * RawValue * DeltaTime; + } + } + + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Unknown Modifier Type in FAttribute::AddModifier for Attribute %s, operator %d"), *Attribute.Tag.ToString(), static_cast(Op)); + checkNoEntry(); + return 0.f; +} + +void FGMCAttributeModifier::InitModifier(UGMCAbilityEffect* Effect, double InActionTimer, int InApplicationIdx, bool bInRegisterInHistory, float InDeltaTime) +{ + if (!Effect) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Effect or AbilitySystemComponent is null in FGMCAttributeModifier::InitModifier")); + return; + } + + SourceAbilityEffect = Effect; + bRegisterInHistory = bInRegisterInHistory; + DeltaTime = InDeltaTime; + ApplicationIndex = InApplicationIdx; + ActionTimer = InActionTimer; + +} diff --git a/Source/GMCAbilitySystem/Private/Attributes/GMCAttributeModifierCustom_Base.cpp b/Source/GMCAbilitySystem/Private/Attributes/GMCAttributeModifierCustom_Base.cpp new file mode 100644 index 00000000..ae7ec195 --- /dev/null +++ b/Source/GMCAbilitySystem/Private/Attributes/GMCAttributeModifierCustom_Base.cpp @@ -0,0 +1,32 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "GMCAttributeModifierCustom_Base.h" +#include "GMCAbilitySystem/Public/Attributes/GMCAttributes.h" + +float UGMCAttributeModifierCustom_Base::Calculate(UGMCAbilityEffect* SourceEffect, const FAttribute* Attribute) +{ + if (!CheckValidity(SourceEffect, Attribute)) + { + return 0.f; // Invalid state, return 0 + } + + return K2_Calculate(SourceEffect, Attribute->Tag); +} + +bool UGMCAttributeModifierCustom_Base::CheckValidity(const UGMCAbilityEffect* SourceEffect, const FAttribute* Attribute) const +{ + if (SourceEffect == nullptr) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("SourceEffect is null in UGMCAttributeModifierCustom_Base::CheckValidity")); + return false; + } + + if (Attribute == nullptr) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Attribute is null in UGMCAttributeModifierCustom_Base::CheckValidity")); + return false; + } + + return true; +} diff --git a/Source/GMCAbilitySystem/Private/Attributes/GMCAttributes.cpp b/Source/GMCAbilitySystem/Private/Attributes/GMCAttributes.cpp new file mode 100644 index 00000000..f9ad0d85 --- /dev/null +++ b/Source/GMCAbilitySystem/Private/Attributes/GMCAttributes.cpp @@ -0,0 +1,97 @@ +#include "Attributes/GMCAttributes.h" + +#include "GMCAbilityComponent.h" + +void FAttribute::AddModifier(const FGMCAttributeModifier& PendingModifier) const +{ + + const float ModifierValue = PendingModifier.CalculateModifierValue(*this); + + if (PendingModifier.bRegisterInHistory) + { + ValueTemporalModifiers.Add(FAttributeTemporaryModifier(PendingModifier.ApplicationIndex, ModifierValue, PendingModifier.ActionTimer, PendingModifier.SourceAbilityEffect)); + } + else + { + RawValue = Clamp.ClampValue(RawValue + ModifierValue); + } + + bIsDirty = true; +} + +void FAttribute::CalculateValue() const +{ + + Value = Clamp.ClampValue(RawValue); + + for (auto& Mod : ValueTemporalModifiers) + { + if (Mod.InstigatorEffect.IsValid()) + { + Value += Mod.Value; + Value = Clamp.ClampValue(Value); + } + else + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Orphelin Attribute Modifier found in FAttribute::CalculateValue")); + checkNoEntry(); + } + } + + bIsDirty = false; +} + +void FAttribute::RemoveTemporalModifier(int ApplicationIndex, const UGMCAbilityEffect* InstigatorEffect) const +{ + for (int i = ValueTemporalModifiers.Num() - 1; i >= 0; i--) + { + if (ValueTemporalModifiers[i].ApplicationIndex == ApplicationIndex && ValueTemporalModifiers[i].InstigatorEffect == InstigatorEffect) + { + ValueTemporalModifiers.RemoveAt(i); + bIsDirty = true; + } + } +} + +void FAttribute::PurgeTemporalModifier(double CurrentActionTimer) +{ + + if (!bIsGMCBound) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("PurgeTemporalModifier called on an unbound attribute %s"), *Tag.ToString()); + checkNoEntry(); + return; + } + + bool bMustReprocessModifiers = false; + for (int i = ValueTemporalModifiers.Num() - 1; i >= 0; i--) + { + if (ValueTemporalModifiers[i].ActionTimer > CurrentActionTimer) + { + ValueTemporalModifiers.RemoveAt(i); + bMustReprocessModifiers = true; + } + } + + if (bMustReprocessModifiers) + { + bIsDirty = true; + } +} + +FString FAttribute::ToString() const +{ + if (bIsGMCBound) + { + return FString::Printf(TEXT("%s : %0.3f Bound[n%i/%0.2fmb]"), *Tag.ToString(), Value, ValueTemporalModifiers.Num(), ValueTemporalModifiers.GetAllocatedSize() / 1048576.0f); + } + else + { + return FString::Printf(TEXT("%s : %0.3f"), *Tag.ToString(), Value); + } +} + +bool FAttribute::operator<(const FAttribute& Other) const +{ + return Tag.ToString() < Other.Tag.ToString(); +} diff --git a/Source/GMCAbilitySystem/Private/Attributes/PreDefinedCustomCalculator/GMCModifierCustom_Exponent.cpp b/Source/GMCAbilitySystem/Private/Attributes/PreDefinedCustomCalculator/GMCModifierCustom_Exponent.cpp new file mode 100644 index 00000000..192f9f99 --- /dev/null +++ b/Source/GMCAbilitySystem/Private/Attributes/PreDefinedCustomCalculator/GMCModifierCustom_Exponent.cpp @@ -0,0 +1,48 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "GMCModifierCustom_Exponent.h" + +#include "GMCAttributes.h" + +float UGMCModifierCustom_Exponent::Calculate(UGMCAbilityEffect* SourceEffect, const FAttribute* Attribute) +{ + + if (!CheckValidity(SourceEffect, Attribute)) + { + return 0.f; // Invalid state, return 0 + } + + switch (ExponentType) { + case EGMCMC_ExponentType::Mapping: + { + const float x = Attribute->Value * 3; + const float rawExp = FMath::Exp(x); + constexpr float minExp = 1.f; + constexpr float MaxExp = 0x1.42096ff2afc4p+4; // exp(3) + + return Min + ((rawExp - minExp) / (MaxExp - minExp)) * (Max - Min); + break; + } + case EGMCMC_ExponentType::Easing: + { + const float easedT = Attribute->Value == 0 ? 0 : FMath::Pow(2, 10 *(Attribute->Value - 1.f)); + return Min + easedT * (Max - Min); + break; + } + case EGMCMC_ExponentType::CustomPower: + { + const float poweredT = FMath::Pow(Attribute->Value, k); + return Min + poweredT * (Max - Min); + break; + } + case EGMCMC_ExponentType::Saturated: + { + const float expValue = 1 - FMath::Exp(-k * Attribute->Value); + return Min + expValue * (Max - Min); + break; + } + } + + return Attribute->Value; // Fallback, should not happen +} diff --git a/Source/GMCAbilitySystem/Private/Attributes/PreDefinedCustomCalculator/GMCModifierCustom_Exponent.h b/Source/GMCAbilitySystem/Private/Attributes/PreDefinedCustomCalculator/GMCModifierCustom_Exponent.h new file mode 100644 index 00000000..f446338e --- /dev/null +++ b/Source/GMCAbilitySystem/Private/Attributes/PreDefinedCustomCalculator/GMCModifierCustom_Exponent.h @@ -0,0 +1,41 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "GMCAttributeModifierCustom_Base.h" +#include "GMCModifierCustom_Exponent.generated.h" + +UENUM() +enum class EGMCMC_ExponentType +{ + Mapping UMETA(DisplayName = "Mapping", ToolTip="Classique Exponent scale"), + Easing UMETA(DisplayName = "Easing", ToolTip="Easing scale"), + CustomPower UMETA(DisplayName = "Custom Power", ToolTip="Custom Power"), + Saturated UMETA(DisplayName = "Saturated", ToolTip="Saturated scale") +}; + +/** + * + */ +UCLASS() +class GMCABILITYSYSTEM_API UGMCModifierCustom_Exponent : public UGMCAttributeModifierCustom_Base +{ + GENERATED_BODY() + + public: + + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = Exponent) + EGMCMC_ExponentType ExponentType; + + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = Exponent) + float Min = 0.0f; + + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = Exponent) + float Max = 0.f; + + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = Exponent, meta=(EditCondition = "ExponentType == EGMCMC_ExponentType::Saturated || ExponentType == EGMCMC_ExponentType::CustomPower", EditConditionHides)); + float k = 0.f; + + virtual float Calculate(UGMCAbilityEffect* SourceEffect, const FAttribute* Attribute) override; +}; diff --git a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp index 0f768271..c05a7e47 100644 --- a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp +++ b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp @@ -1,4 +1,4 @@ -// Fill out your copyright notice in the Description page of Project Settings. +// Fill out your copyright notice in the Description page of Project Settings. #include "Components/GMCAbilityComponent.h" @@ -6,13 +6,16 @@ #include "GMCAbilitySystem.h" #include "GMCOrganicMovementComponent.h" #include "GMCPlayerController.h" +#include "NiagaraFunctionLibrary.h" #include "Ability/GMCAbility.h" #include "Ability/GMCAbilityMapData.h" #include "Attributes/GMCAttributesData.h" #include "Effects/GMCAbilityEffect.h" +#include "Kismet/GameplayStatics.h" #include "Kismet/KismetSystemLibrary.h" #include "Net/UnrealNetwork.h" + // Sets default values for this component's properties UGMC_AbilitySystemComponent::UGMC_AbilitySystemComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { @@ -46,22 +49,12 @@ FDelegateHandle UGMC_AbilitySystemComponent::AddFilteredTagChangeDelegate(const void UGMC_AbilitySystemComponent::RemoveFilteredTagChangeDelegate(const FGameplayTagContainer& Tags, FDelegateHandle Handle) { - if (!Handle.IsValid()) - { - UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Passed an invalid delegate to unbind for tag changes on %s"), *Tags.ToString()) - return; - } - for (int32 Index = FilteredTagDelegates.Num() - 1; Index >= 0; --Index) { TPair& SearchPair = FilteredTagDelegates[Index]; if (SearchPair.Key == Tags) { - if (!SearchPair.Value.Remove(Handle)) - { - UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Unable to unbind a tag change delegate for %s"), *Tags.ToString()) - } - + SearchPair.Value.Remove(Handle); if (!SearchPair.Value.IsBound()) { FilteredTagDelegates.RemoveAt(Index); @@ -91,44 +84,13 @@ void UGMC_AbilitySystemComponent::BindReplicationData() // We sort our attributes alphabetically by tag so that it's deterministic. for (auto& AttributeForBind : BoundAttributes.Attributes) { - GMCMovementComponent->BindSinglePrecisionFloat(AttributeForBind.BaseValue, - EGMC_PredictionMode::ServerAuth_Output_ClientValidated, - EGMC_CombineMode::CombineIfUnchanged, - EGMC_SimulationMode::Periodic_Output, - EGMC_InterpolationFunction::TargetValue); - - GMCMovementComponent->BindSinglePrecisionFloat(AttributeForBind.Value, - EGMC_PredictionMode::ServerAuth_Output_ClientValidated, - EGMC_CombineMode::CombineIfUnchanged, - EGMC_SimulationMode::Periodic_Output, - EGMC_InterpolationFunction::TargetValue); - - GMCMovementComponent->BindSinglePrecisionFloat(AttributeForBind.AdditiveModifier, - EGMC_PredictionMode::ServerAuth_Output_ClientValidated, - EGMC_CombineMode::CombineIfUnchanged, - EGMC_SimulationMode::Periodic_Output, - EGMC_InterpolationFunction::TargetValue); - - GMCMovementComponent->BindSinglePrecisionFloat(AttributeForBind.MultiplyModifier, - EGMC_PredictionMode::ServerAuth_Output_ClientValidated, - EGMC_CombineMode::CombineIfUnchanged, - EGMC_SimulationMode::Periodic_Output, - EGMC_InterpolationFunction::TargetValue); - - GMCMovementComponent->BindSinglePrecisionFloat(AttributeForBind.DivisionModifier, + GMCMovementComponent->BindSinglePrecisionFloat(AttributeForBind.RawValue, EGMC_PredictionMode::ServerAuth_Output_ClientValidated, EGMC_CombineMode::CombineIfUnchanged, EGMC_SimulationMode::Periodic_Output, EGMC_InterpolationFunction::TargetValue); } - // Sync'd Action Timer - GMCMovementComponent->BindDoublePrecisionFloat(ActionTimer, - EGMC_PredictionMode::ServerAuth_Output_ClientValidated, - EGMC_CombineMode::CombineIfUnchanged, - EGMC_SimulationMode::None, - EGMC_InterpolationFunction::TargetValue); - // Granted Abilities GMCMovementComponent->BindGameplayTagContainer(GrantedAbilityTags, EGMC_PredictionMode::ServerAuth_Output_ClientValidated, @@ -142,20 +104,6 @@ void UGMC_AbilitySystemComponent::BindReplicationData() EGMC_CombineMode::CombineIfUnchanged, EGMC_SimulationMode::Periodic_Output, EGMC_InterpolationFunction::TargetValue); - - // AbilityData Binds - // These are mostly client-inputs made to the server as Ability Requests - GMCMovementComponent->BindInt(AbilityData.AbilityActivationID, - EGMC_PredictionMode::ClientAuth_Input, - EGMC_CombineMode::CombineIfUnchanged, - EGMC_SimulationMode::None, - EGMC_InterpolationFunction::TargetValue); - - GMCMovementComponent->BindGameplayTag(AbilityData.InputTag, - EGMC_PredictionMode::ClientAuth_Input, - EGMC_CombineMode::CombineIfUnchanged, - EGMC_SimulationMode::None, - EGMC_InterpolationFunction::TargetValue); // TaskData Bind GMCMovementComponent->BindInstancedStruct(TaskData, @@ -170,49 +118,62 @@ void UGMC_AbilitySystemComponent::BindReplicationData() EGMC_SimulationMode::PeriodicAndOnChange_Output, EGMC_InterpolationFunction::TargetValue); - GMCMovementComponent->BindInstancedStruct(AcknowledgeId, - EGMC_PredictionMode::ClientAuth_Input, - EGMC_CombineMode::CombineIfUnchanged, - EGMC_SimulationMode::None, - EGMC_InterpolationFunction::TargetValue); + // Bind our operation queues. + QueuedAbilityOperations.BindToGMC(GMCMovementComponent); + QueuedEffectOperations.BindToGMC(GMCMovementComponent); + QueuedEffectOperations_ClientAuth.BindToGMC(GMCMovementComponent); + QueuedEventOperations.BindToGMC(GMCMovementComponent); } void UGMC_AbilitySystemComponent::GenAncillaryTick(float DeltaTime, bool bIsCombinedClientMove) { - + // Caution if you override Ancillarytick, this value should wrap up the override. + bInAncillaryTick = true; + OnAncillaryTick.Broadcast(DeltaTime); - ClientHandlePendingEffect(); + ClientHandlePendingOperation(QueuedEffectOperations); ServerHandlePendingEffect(DeltaTime); + + ClientHandlePendingOperation(QueuedEventOperations); CheckActiveTagsChanged(); + + ProcessAttributes(false); + CheckAttributeChanged(); - - TickActiveEffects(DeltaTime); - TickActiveCooldowns(DeltaTime); - TickAncillaryActiveAbilities(DeltaTime); + CheckUnBoundAttributeChanged(); - // Activate abilities from ancillary tick if they have bActivateOnMovementTick set to false - if (AbilityData.InputTag != FGameplayTag::EmptyTag) - { - TryActivateAbilitiesByInputTag(AbilityData.InputTag, AbilityData.ActionInput, false); - } + TickActiveCooldowns(DeltaTime); SendTaskDataToActiveAbility(false); + TickAncillaryActiveAbilities(DeltaTime); + + // Check if we have a valid operation + TGMASBoundQueueOperation Operation; + if (QueuedAbilityOperations.GetCurrentBoundOperation(Operation, true)) + { + ProcessAbilityOperation(Operation, false); + } ClearAbilityAndTaskData(); - bInGMCTime = false; + QueuedEffectOperations_ClientAuth.ClearCurrentOperation(); + + bInAncillaryTick = false; } +UGMCAbilityEffect* UGMC_AbilitySystemComponent::GetActiveEffectByHandle(int EffectID) const +{ + return ActiveEffects.Contains(EffectID) ? ActiveEffects[EffectID] : nullptr; +} -TArray UGMC_AbilitySystemComponent::GetActivesEffectByTag(FGameplayTag GameplayTag) const { +TArray UGMC_AbilitySystemComponent::GetActiveEffectsByTag(FGameplayTag GameplayTag, bool bMatchExact) const +{ TArray ActiveEffectsFound; - UE_LOG(LogGMCAbilitySystem, Error, TEXT("Searching for Active Effects with Tag: %s"), *GameplayTag.ToString()); - for (const TTuple& EffectFound : ActiveEffects) { - if (IsValid(EffectFound.Value) && EffectFound.Value->EffectData.EffectTag.MatchesTag(GameplayTag)) { + if (IsValid(EffectFound.Value) && bMatchExact ? EffectFound.Value->EffectData.EffectTag.MatchesTagExact(GameplayTag) : EffectFound.Value->EffectData.EffectTag.MatchesTag(GameplayTag)) { ActiveEffectsFound.Add(EffectFound.Value); } } @@ -221,7 +182,8 @@ TArray UGMC_AbilitySystemComponent::GetActivesEffectByTag(FG } -UGMCAbilityEffect* UGMC_AbilitySystemComponent::GetFirstActiveEffectByTag(FGameplayTag GameplayTag) const { +UGMCAbilityEffect* UGMC_AbilitySystemComponent::GetFirstActiveEffectByTag(FGameplayTag GameplayTag) const +{ for (auto& EffectFound : ActiveEffects) { if (EffectFound.Value && EffectFound.Value->EffectData.EffectTag.MatchesTag(GameplayTag)) { return EffectFound.Value; @@ -250,7 +212,7 @@ void UGMC_AbilitySystemComponent::RemoveAbilityMapData(UGMCAbilityMapData* Abili void UGMC_AbilitySystemComponent::AddStartingEffects(TArray> EffectsToAdd) { - for (const TSubclassOf Effect : EffectsToAdd) + for (const TSubclassOf& Effect : EffectsToAdd) { StartingEffects.AddUnique(Effect); } @@ -258,7 +220,7 @@ void UGMC_AbilitySystemComponent::AddStartingEffects(TArray> EffectsToRemove) { - for (const TSubclassOf Effect : EffectsToRemove) + for (const TSubclassOf& Effect : EffectsToRemove) { StartingEffects.Remove(Effect); } @@ -341,18 +303,17 @@ TArray UGMC_AbilitySystemComponent::GetActiveTagsByParentTag(const void UGMC_AbilitySystemComponent::TryActivateAbilitiesByInputTag(const FGameplayTag& InputTag, const UInputAction* InputAction, bool bFromMovementTick) { - for (const TSubclassOf& ActivatedAbility : GetGrantedAbilitiesByTag(InputTag)) { const UGMCAbility* AbilityCDO = ActivatedAbility->GetDefaultObject(); if(AbilityCDO && bFromMovementTick == AbilityCDO->bActivateOnMovementTick){ UE_LOG(LogGMCAbilitySystem, VeryVerbose, TEXT("Trying to Activate Ability: %s from %s"), *GetNameSafe(ActivatedAbility), bFromMovementTick ? TEXT("Movement") : TEXT("Ancillary")); - TryActivateAbility(ActivatedAbility, InputAction); + TryActivateAbility(ActivatedAbility, InputAction, InputTag); } } } -bool UGMC_AbilitySystemComponent::TryActivateAbility(const TSubclassOf ActivatedAbility, const UInputAction* InputAction) +bool UGMC_AbilitySystemComponent::TryActivateAbility(const TSubclassOf ActivatedAbility, const UInputAction* InputAction, const FGameplayTag ActivationTag) { if (ActivatedAbility == nullptr) return false; @@ -378,12 +339,6 @@ bool UGMC_AbilitySystemComponent::TryActivateAbility(const TSubclassOf(this, ActivatedAbility); Ability->AbilityData = AbilityData; + Ability->AbilityData.InputTag = ActivationTag; Ability->Execute(this, AbilityID, InputAction); ActiveAbilities.Add(AbilityID, Ability); if (HasAuthority()) {RPCConfirmAbilityActivation(AbilityID);} - + return true; } -void UGMC_AbilitySystemComponent::QueueAbility(FGameplayTag InputTag, const UInputAction* InputAction) +void UGMC_AbilitySystemComponent::QueueAbility(FGameplayTag InputTag, const UInputAction* InputAction, bool bPreventConcurrentActivation) { if (GetOwnerRole() != ROLE_AutonomousProxy && GetOwnerRole() != ROLE_Authority) return; FGMCAbilityData Data; Data.InputTag = InputTag; Data.ActionInput = InputAction; - QueuedAbilities.Push(Data); + + + // Early local check for ability + FAbilityMapData* MapEntry = AbilityMap.Find(InputTag); + if (MapEntry == nullptr || MapEntry->Abilities.IsEmpty()) return; + + // This local check will prevent concurrent activation of the same ability if the ability is contain in the input map + if (bPreventConcurrentActivation) { + for (const TPair& ActiveAbility : ActiveAbilities) + { + if (ActiveAbility.Value && MapEntry->Abilities.ContainsByPredicate([&ActiveAbility](const TSubclassOf& Ability) { + return ActiveAbility.Value->IsA(Ability); + })) return; + } + } + + + + TGMASBoundQueueOperation Operation; + QueuedAbilityOperations.QueueOperation(Operation, EGMASBoundQueueOperationType::Activate, InputTag, Data); } int32 UGMC_AbilitySystemComponent::GetQueuedAbilityCount(FGameplayTag AbilityTag) { - int32 Result = 0; - - for (const auto& QueuedData : QueuedAbilities) - { - if (QueuedData.InputTag == AbilityTag) Result++; - } - return Result; + return QueuedAbilityOperations.NumMatching(AbilityTag, EGMASBoundQueueOperationType::Activate); } int32 UGMC_AbilitySystemComponent::GetActiveAbilityCount(TSubclassOf AbilityClass) @@ -443,7 +412,7 @@ bool UGMC_AbilitySystemComponent::IsAbilityTagBlocked(const FGameplayTag Ability if (IsValid(ActiveAbility.Value) && ActiveAbility.Value->AbilityState != EAbilityState::Ended) { for (auto& Tag : ActiveAbility.Value->BlockOtherAbility) { - if (Tag.MatchesTag(AbilityTag)) { + if (AbilityTag.MatchesTag(Tag)) { UE_LOG(LogGMCAbilitySystem, Verbose, TEXT("Ability can't activate, blocked by Ability: %s"), *ActiveAbility.Value->GetName()); return true; } @@ -461,7 +430,7 @@ int UGMC_AbilitySystemComponent::EndAbilitiesByTag(FGameplayTag AbilityTag) { { if (ActiveAbilityData.Value->AbilityTag.MatchesTag(AbilityTag)) { - ActiveAbilityData.Value->SetPendingEnd(); + ActiveAbilityData.Value->EndAbility(); AbilitiesEnded++; } } @@ -483,6 +452,26 @@ int UGMC_AbilitySystemComponent::EndAbilitiesByClass(TSubclassOf Ab } +int UGMC_AbilitySystemComponent::EndAbilitiesByQuery(const FGameplayTagQuery& Query) +{ + int AbilitiesEnded = 0; + + for (const auto& ActiveAbilityData : ActiveAbilities) + { + if (UGMCAbility* Ability = ActiveAbilityData.Value) + { + if (Query.Matches(Ability->AbilityDefinition)) + { + Ability->SetPendingEnd(); + AbilitiesEnded++; + UE_LOG(LogGMCAbilitySystem, Verbose, TEXT("cancelled ability %s by query"), *Ability->AbilityTag.ToString()); + } + } + } + return AbilitiesEnded; +} + + int32 UGMC_AbilitySystemComponent::GetActiveAbilityCountByTag(FGameplayTag AbilityTag) { int32 Result = 0; @@ -564,30 +553,56 @@ bool UGMC_AbilitySystemComponent::IsServerOnly() const void UGMC_AbilitySystemComponent::GenPredictionTick(float DeltaTime) { bJustTeleported = false; - ActionTimer += DeltaTime; + // ActionTimer += DeltaTime; + ActionTimer = GMCMovementComponent->GetMoveTimestamp(); ApplyStartingEffects(); + + // Purge "future" temporary modifiers on replay + if (GMCMovementComponent->CL_IsReplaying()) + { + for (FAttribute& Attribute : BoundAttributes.Attributes) + { + Attribute.PurgeTemporalModifier(ActionTimer); + } + } + SendTaskDataToActiveAbility(true); TickActiveAbilities(DeltaTime); + TickActiveEffects(DeltaTime); + + ProcessAttributes(true); + // Abilities CleanupStaleAbilities(); + + // Advance our queue action timers. + QueuedAbilityOperations.GenPredictionTick(DeltaTime); + QueuedEffectOperations.GenPredictionTick(DeltaTime); + QueuedEventOperations.GenPredictionTick(DeltaTime); // Was an ability used? - if (AbilityData.InputTag != FGameplayTag::EmptyTag) + if (TGMASBoundQueueOperation Operation; + QueuedAbilityOperations.GetCurrentBoundOperation(Operation, true)) { - TryActivateAbilitiesByInputTag(AbilityData.InputTag, AbilityData.ActionInput, true); + ProcessAbilityOperation(Operation, true); } - SendTaskDataToActiveAbility(true); - + ServerHandlePredictedPendingEffect(DeltaTime); + } void UGMC_AbilitySystemComponent::GenSimulationTick(float DeltaTime) { - CheckActiveTagsChanged(); - CheckAttributeChanged(); + + if (!GMCMovementComponent->IsSmoothedListenServerPawn()) + { + CheckActiveTagsChanged(); + CheckAttributeChanged(); + CheckUnBoundAttributeChanged(); + } if (GMCMovementComponent->GetSmoothingTargetIdx() == -1) return; const FVector TargetLocation = GMCMovementComponent->MoveHistory[GMCMovementComponent->GetSmoothingTargetIdx()].OutputState.ActorLocation.Read(); @@ -601,21 +616,34 @@ void UGMC_AbilitySystemComponent::GenSimulationTick(float DeltaTime) void UGMC_AbilitySystemComponent::PreLocalMoveExecution() { - if (QueuedAbilities.Num() > 0) - { - AbilityData = QueuedAbilities.Pop(); - } if (QueuedTaskData.Num() > 0) { TaskData = QueuedTaskData.Pop(); } - + // Advance our client-auth queues. + QueuedAbilityOperations.PreLocalMovement(); + QueuedEffectOperations_ClientAuth.PreLocalMovement(); + + if (GetNetMode() == NM_Standalone || GMCMovementComponent->IsLocallyControlledServerPawn()) + { + // We'll never get the pre remote movement trigger in this case, soooo... + QueuedEffectOperations.PreRemoteMovement(); + QueuedEventOperations.PreRemoteMovement(); + } +} + +void UGMC_AbilitySystemComponent::PreRemoteMoveExecution() +{ + // Advance our server-auth queues. + QueuedEffectOperations.PreRemoteMovement(); + QueuedEventOperations.PreRemoteMovement(); } void UGMC_AbilitySystemComponent::BeginPlay() { Super::BeginPlay(); + InitializeStartingAbilities(); InitializeAbilityMap(); SetStartingTags(); @@ -625,6 +653,7 @@ void UGMC_AbilitySystemComponent::InstantiateAttributes() { BoundAttributes = FGMCAttributeSet(); UnBoundAttributes = FGMCUnboundAttributeSet(); + OldUnBoundAttributes = FGMCUnboundAttributeSet(); if(AttributeDataAssets.IsEmpty()) return; // Loop through each of the data assets inputted into the component to create new attributes. @@ -636,7 +665,8 @@ void UGMC_AbilitySystemComponent::InstantiateAttributes() for(const FAttributeData AttributeData : AttributeDataAsset->AttributeData){ FAttribute NewAttribute; NewAttribute.Tag = AttributeData.AttributeTag; - NewAttribute.BaseValue = AttributeData.DefaultValue; + NewAttribute.InitialValue = AttributeData.DefaultValue; + SetAttributeInitialValue(NewAttribute.Tag, NewAttribute.InitialValue); NewAttribute.Clamp = AttributeData.Clamp; NewAttribute.Clamp.AbilityComponent = this; NewAttribute.bIsGMCBound = AttributeData.bGMCBound; @@ -645,15 +675,9 @@ void UGMC_AbilitySystemComponent::InstantiateAttributes() if(AttributeData.bGMCBound){ BoundAttributes.AddAttribute(NewAttribute); } - else if (GetOwnerRole() == ROLE_Authority || GetNetMode() == NM_Standalone) { - // FFastArraySerializer will duplicate all attributes on first replication if we - // add the attributes on the clients as well. - UnBoundAttributes.AddAttribute(NewAttribute); - - } - - if (!AttributeData.bGMCBound) { + else { // Initiate old unbound attributes + UnBoundAttributes.AddAttribute(NewAttribute); OldUnBoundAttributes.AddAttribute(NewAttribute); } } @@ -663,21 +687,20 @@ void UGMC_AbilitySystemComponent::InstantiateAttributes() for (const FAttribute& Attribute : BoundAttributes.Attributes) { - Attribute.CalculateValue(); + Attribute.Init(); } - // We need to be non-const to ensure we can mark the item dirty. for (FAttribute& Attribute : UnBoundAttributes.Items) { - Attribute.CalculateValue(); + Attribute.Init(); UnBoundAttributes.MarkItemDirty(Attribute); } - + for (const FAttribute& Attribute : OldUnBoundAttributes.Items) { - Attribute.CalculateValue(); + Attribute.Init(); } - + OldBoundAttributes = BoundAttributes; } @@ -743,6 +766,7 @@ void UGMC_AbilitySystemComponent::CheckAttributeChanged() { FAttribute& OldAttribute = OldBoundAttributes.Attributes[i]; if (Attribute.Value != OldAttribute.Value) { + NativeAttributeChangeDelegate.Broadcast(Attribute.Tag, OldAttribute.Value, Attribute.Value); OnAttributeChanged.Broadcast(Attribute.Tag, OldAttribute.Value, Attribute.Value); OldAttribute.Value = Attribute.Value; } @@ -777,20 +801,26 @@ void UGMC_AbilitySystemComponent::TickActiveEffects(float DeltaTime) for (const TPair& Effect : ActiveEffects) { - if (!IsValid(Effect.Value)) { + if (!Effect.Value->IsValidLowLevel()) { UE_LOG(LogGMCAbilitySystem, Error, TEXT("Active Effect id %d is null or pending kill, removing from the list."), Effect.Key); CompletedActiveEffects.Push(Effect.Key); continue; } Effect.Value->Tick(DeltaTime); - if (Effect.Value->bCompleted) {CompletedActiveEffects.Push(Effect.Key);} + if (Effect.Value->bCompleted) + { + CompletedActiveEffects.Push(Effect.Key); + } // Check for predicted effects that have not been server confirmed if (!HasAuthority() && - ProcessedEffectIDs.Contains(Effect.Key) && - !ProcessedEffectIDs[Effect.Key] && Effect.Value->ClientEffectApplicationTime + ClientEffectApplicationTimeout < ActionTimer) + !Effect.Value->EffectData.bServerAuth + && ProcessedEffectIDs.Contains(Effect.Key) + && ProcessedEffectIDs[Effect.Key] == EGMCEffectAnswerState::Pending + && Effect.Value->ClientEffectApplicationTime + ClientEffectApplicationTimeout < ActionTimer) { + ProcessedEffectIDs[Effect.Key] = EGMCEffectAnswerState::Timeout; UE_LOG(LogGMCAbilitySystem, Error, TEXT("Effect `%s` Not Confirmed By Server (ID: `%d`), Removing..."), *GetNameSafe(Effect.Value), Effect.Key); Effect.Value->EndEffect(); CompletedActiveEffects.Push(Effect.Key); @@ -806,8 +836,40 @@ void UGMC_AbilitySystemComponent::TickActiveEffects(float DeltaTime) ActiveEffects.Remove(EffectID); ActiveEffectsData.RemoveAll([EffectID](const FGMCAbilityEffectData& EffectData) {return EffectData.EffectID == EffectID;}); } + + // Clean effect handles + TArray CurrentHandles; + EffectHandles.GetKeys(CurrentHandles); + for (const int Handle : CurrentHandles) + { + const auto& Data = EffectHandles[Handle]; + if (Data.NetworkId > 0 && !ActiveEffects.Contains(Data.NetworkId)) + { + EffectHandles.Remove(Handle); + } + } + +} + +void UGMC_AbilitySystemComponent::ProcessAttributes(bool bInGenPredictionTick) +{ + for (const FAttribute* Attribute : GetAllAttributes()) + { + if (Attribute->IsDirty() && Attribute->bIsGMCBound == bInGenPredictionTick) + { + + Attribute->CalculateValue(); + // Broadcast dirty change if unbound + if (!Attribute->bIsGMCBound) + { + UnBoundAttributes.MarkAttributeDirty(*Attribute); + } + } + } + } + void UGMC_AbilitySystemComponent::TickActiveAbilities(float DeltaTime) { for (const TPair& Ability : ActiveAbilities) @@ -841,16 +903,17 @@ void UGMC_AbilitySystemComponent::OnRep_ActiveEffectsData() { if (ActiveEffectData.EffectID == 0) continue; - if (!ProcessedEffectIDs.Contains(ActiveEffectData.EffectID)) + if (!ProcessedEffectIDs.Contains(ActiveEffectData.EffectID) || ProcessedEffectIDs[ActiveEffectData.EffectID] == EGMCEffectAnswerState::Timeout) { + // The client never predicted this effect, so we process it as a new effect. UGMCAbilityEffect* EffectCDO = DuplicateObject(UGMCAbilityEffect::StaticClass()->GetDefaultObject(), this); - FGMCAbilityEffectData EffectData = ActiveEffectData; - ApplyAbilityEffect(EffectCDO, EffectData); - ProcessedEffectIDs.Add(EffectData.EffectID, true); - UE_LOG(LogGMCAbilitySystem, VeryVerbose, TEXT("Replicated Effect: %d"), ActiveEffectData.EffectID); + + ApplyAbilityEffect(EffectCDO, ActiveEffectData); + ProcessedEffectIDs.Add(ActiveEffectData.EffectID, EGMCEffectAnswerState::Validated); + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("[Client] Effect [%d] %s has been force apply by the server"), ActiveEffectData.EffectID, *ActiveEffectData.EffectTag.ToString()); } - ProcessedEffectIDs[ActiveEffectData.EffectID] = true; + ProcessedEffectIDs[ActiveEffectData.EffectID] = EGMCEffectAnswerState::Validated; } } @@ -863,7 +926,7 @@ void UGMC_AbilitySystemComponent::CheckRemovedEffects() // Ensure this effect has already been confirmed by the server so that if it's now missing, // it means the server removed it - if (!ProcessedEffectIDs[Effect.Key]){return;} + if (ProcessedEffectIDs[Effect.Key] == EGMCEffectAnswerState::Pending){return;} if (!ActiveEffectsData.ContainsByPredicate([Effect](const FGMCAbilityEffectData& EffectData) {return EffectData.EffectID == Effect.Key;})) { @@ -872,125 +935,102 @@ void UGMC_AbilitySystemComponent::CheckRemovedEffects() } } -void UGMC_AbilitySystemComponent::AddPendingEffectApplications(FGMCOuterApplicationWrapper& Wrapper) { - check(HasAuthority()) - - Wrapper.ClientGraceTimeRemaining = 1.f; - Wrapper.LateApplicationID = GenerateLateApplicationID(); - - PendingApplicationServer.Add(Wrapper); - RPCClientAddPendingEffectApplication(Wrapper); -} - - -void UGMC_AbilitySystemComponent::RPCClientAddPendingEffectApplication_Implementation( - FGMCOuterApplicationWrapper Wrapper) { - PendingApplicationClient.Add(Wrapper); -} - - - void UGMC_AbilitySystemComponent::ServerHandlePendingEffect(float DeltaTime) { if (!HasAuthority()) { return; } - FGMCAcknowledgeId& AckId = AcknowledgeId.GetMutable(); - - - for (int i = PendingApplicationServer.Num() - 1; i >= 0; i--) { - FGMCOuterApplicationWrapper& Wrapper = PendingApplicationServer[i]; + // Handle our GMC-replicated effect operation, if any. We can't actually replicate + // the message server-to-client via GMC, but we *can* preserve it in the move history in + // case it is relevant to replay. + TGMASBoundQueueOperation BoundOperation; + if (QueuedEffectOperations.GetCurrentBoundOperation(BoundOperation, true)) + { + // Move this into our RPC queue to wait on acknowledgment. + QueuedEffectOperations.QueuePreparedOperation(BoundOperation, false); - if (Wrapper.ClientGraceTimeRemaining <= 0.f || AckId.Id.Contains(Wrapper.LateApplicationID)) { + // And send it via RPC, so that the client gets it. + ClientQueueOperation(BoundOperation); + }; - switch (Wrapper.Type) { - case EGMC_AddEffect: { - const FGMCOuterEffectAdd& Data = Wrapper.OuterApplicationData.Get(); - UGMCAbilityEffect* AbilityEffect = DuplicateObject(Data.EffectClass->GetDefaultObject(), this); - AbilityEffect->EffectData.EffectID = Wrapper.LateApplicationID; - FGMCAbilityEffectData EffectData = Data.InitializationData.IsValid() ? Data.InitializationData : AbilityEffect->EffectData; - UGMCAbilityEffect* FX = ApplyAbilityEffect(AbilityEffect, EffectData); - if (Wrapper.ClientGraceTimeRemaining <= 0.f) { - UE_LOG(LogGMCAbilitySystem, Log, TEXT("Client Add Effect `%s ` Missed Grace time, Force application : id: %d"), *GetNameSafe(Data.EffectClass), FX->EffectData.EffectID); - } - } break; - case EGMC_RemoveEffect: { - const FGMCOuterEffectRemove& Data = Wrapper.OuterApplicationData.Get(); - RemoveEffectById(Data.Ids); - if (Wrapper.ClientGraceTimeRemaining <= 0.f) { - UE_LOG(LogGMCAbilitySystem, Log, TEXT("Client Remove Effect Missed Grace time, Force remove")); - } - } break; + // Handle our 'outer' RPC effect operations. + QueuedEffectOperations.DeductGracePeriod(DeltaTime); + auto Operations = QueuedEffectOperations.GetQueuedRPCOperations(); + for (auto& Operation : Operations) { + if (ShouldProcessOperation(Operation, QueuedEffectOperations, true)) + { + if (Operation.GracePeriodExpired()) + { + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Client effect operation missed grace period, forcing on server.")) } - PendingApplicationServer.RemoveAt(i); + ProcessOperation(Operation); + QueuedEffectOperations.RemoveOperationById(Operation.GetOperationId()); } - else { - Wrapper.ClientGraceTimeRemaining -= DeltaTime; - } - - } - } - -void UGMC_AbilitySystemComponent::ClientHandlePendingEffect() { - - - for (int i = PendingApplicationClient.Num() - 1; i >= 0; i--) +void UGMC_AbilitySystemComponent::ServerHandlePredictedPendingEffect(float DeltaTime) +{ + if (!HasAuthority()) return; + + // Check for any client-auth effects. + TGMASBoundQueueOperation BoundOperation; + if (QueuedEffectOperations_ClientAuth.GetCurrentBoundOperation(BoundOperation, true)) { - FGMCOuterApplicationWrapper& LateApplicationData = PendingApplicationClient[i]; + ProcessOperation(BoundOperation); + } - switch (LateApplicationData.Type) { - case EGMC_AddEffect: { - const FGMCOuterEffectAdd& Data = LateApplicationData.OuterApplicationData.Get(); + // Check for any queued-for-move predicted effects. + // We use the client auth effect queue's (otherwise-unused) RPC operations queue to avoid creating an entire new one. + while (QueuedEffectOperations_ClientAuth.PopNextRPCOperation(BoundOperation)) + { + ProcessOperation(BoundOperation); + } + +} - if (Data.EffectClass == nullptr) { - UE_LOG(LogGMCAbilitySystem, Error, TEXT("ClientHandlePendingEffect: EffectClass is null")); - break; - } - - UGMCAbilityEffect* CDO = Data.EffectClass->GetDefaultObject(); +template +void UGMC_AbilitySystemComponent::ClientHandlePendingOperation(TGMASBoundQueue& QueuedOperations) { - if (CDO == nullptr) { - UE_LOG(LogGMCAbilitySystem, Error, TEXT("ClientHandlePendingEffect: CDO is null")); - break; - } - - UGMCAbilityEffect* AbilityEffect = DuplicateObject(CDO, this); - AbilityEffect->EffectData.EffectID = LateApplicationData.LateApplicationID; - FGMCAbilityEffectData EffectData = Data.InitializationData.IsValid() ? Data.InitializationData : AbilityEffect->EffectData; - ApplyAbilityEffect(AbilityEffect, EffectData); - } break; - case EGMC_RemoveEffect: { - const FGMCOuterEffectRemove& Data = LateApplicationData.OuterApplicationData.Get(); - RemoveEffectById(Data.Ids); - } break; - } - - PendingApplicationClient.RemoveAt(i); - AcknowledgeId.GetMutable().Id.Add(LateApplicationData.LateApplicationID); - } +// Handle our RPC effect operations. MoveCycle operations will be sent via RPC + // just like the Outer ones, but will be preserved in the movement history. + auto RPCOperations = QueuedOperations.GetQueuedRPCOperations(); + for (auto& Operation : RPCOperations) { + if (QueuedOperations.IsAcknowledged(Operation.GetOperationId())) + { + ProcessOperation(Operation); + QueuedOperations.RemoveOperationById(Operation.GetOperationId()); + } + if (ShouldProcessOperation(Operation, QueuedOperations, false)) + { + QueuedOperations.Acknowledge(Operation.GetOperationId()); + } + } } - -int UGMC_AbilitySystemComponent::GenerateLateApplicationID() { - int NewEffectID = static_cast(ActionTimer * 100); - while (ActiveEffects.Contains(NewEffectID)) +void UGMC_AbilitySystemComponent::ClientHandlePredictedPendingEffect() +{ + TGMASBoundQueueOperation BoundOperation; + if (QueuedEffectOperations_ClientAuth.GetCurrentBoundOperation(BoundOperation)) { - NewEffectID++; + ProcessOperation(BoundOperation); + } + + // We use the client auth effect queue's (otherwise-unused) RPC operations queue to avoid creating an entire new one. + while (QueuedEffectOperations_ClientAuth.PopNextRPCOperation(BoundOperation)) + { + ProcessOperation(BoundOperation); } - - return NewEffectID; } - -void UGMC_AbilitySystemComponent::RPCTaskHeartbeat_Implementation(int AbilityID, int TaskID) +bool UGMC_AbilitySystemComponent::IsLocallyControlledPawnASC() const { - if (ActiveAbilities.Contains(AbilityID) && ActiveAbilities[AbilityID] != nullptr) + if (const APawn* Pawn = Cast(GetOwner())) { - ActiveAbilities[AbilityID]->HandleTaskHeartbeat(TaskID); + return Pawn->IsLocallyControlled(); } + + return false; } void UGMC_AbilitySystemComponent::RPCClientEndEffect_Implementation(int EffectID) @@ -1002,6 +1042,14 @@ void UGMC_AbilitySystemComponent::RPCClientEndEffect_Implementation(int EffectID } } +void UGMC_AbilitySystemComponent::RPCTaskHeartbeat_Implementation(int AbilityID, int TaskID) +{ + if (ActiveAbilities.Contains(AbilityID) && ActiveAbilities[AbilityID] != nullptr) + { + ActiveAbilities[AbilityID]->HandleTaskHeartbeat(TaskID); + } +} + void UGMC_AbilitySystemComponent::RPCClientEndAbility_Implementation(int AbilityID) { if (ActiveAbilities.Contains(AbilityID)) @@ -1030,7 +1078,7 @@ void UGMC_AbilitySystemComponent::ApplyStartingEffects(bool bForce) { if (!Algo::FindByPredicate(ActiveEffects, [Effect](const TPair& ActiveEffect) { return IsValid(ActiveEffect.Value) && ActiveEffect.Value->GetClass() == Effect; })) { - ApplyAbilityEffect(Effect, FGMCAbilityEffectData{}); + ApplyAbilityEffectShort(Effect, EGMCAbilityEffectQueueType::ServerAuth); } } bStartingEffectsApplied = true; @@ -1058,13 +1106,14 @@ TArray> UGMC_AbilitySystemComponent::GetGrantedAbilitie void UGMC_AbilitySystemComponent::ClearAbilityAndTaskData() { AbilityData = FGMCAbilityData{}; + QueuedAbilityOperations.ClearCurrentOperation(); TaskData = FInstancedStruct::Make(FGMCAbilityTaskData{}); } void UGMC_AbilitySystemComponent::SendTaskDataToActiveAbility(bool bFromMovement) { - const FGMCAbilityTaskData TaskDataFromInstance = TaskData.Get(); + const FGMCAbilityTaskData TaskDataFromInstance = TaskData.IsValid() ? TaskData.Get() : FGMCAbilityTaskData{}; if (TaskDataFromInstance != FGMCAbilityTaskData{} && /*safety check*/ TaskDataFromInstance.TaskID >= 0) { if (ActiveAbilities.Contains(TaskDataFromInstance.AbilityID) && ActiveAbilities[TaskDataFromInstance.AbilityID]->bActivateOnMovementTick == bFromMovement) @@ -1089,6 +1138,7 @@ bool UGMC_AbilitySystemComponent::CheckActivationTags(const UGMCAbility* Ability } } + // Blocking Tags for (const FGameplayTag Tag : Ability->ActivationBlockedTags) { @@ -1099,10 +1149,36 @@ bool UGMC_AbilitySystemComponent::CheckActivationTags(const UGMCAbility* Ability } } + // single activation query + if (!Ability->ActivationQuery.IsEmpty() && !Ability->ActivationQuery.Matches(ActiveTags)) + { + UE_LOG(LogGMCAbilitySystem, Verbose, TEXT("Ability can't activate, blocked by query: %s"), + *Ability->ActivationQuery.GetDescription()); + return false; + } + return true; +} + + +void UGMC_AbilitySystemComponent::ClearAbilityMap() +{ + // For each AbilityMap in the map AbilityMap: + for (auto& AbilityMapData : AbilityMap) + { + if (GrantedAbilityTags.HasTag(AbilityMapData.Value.InputTag)) + { + GrantedAbilityTags.RemoveTag(AbilityMapData.Value.InputTag); + } + } + AbilityMap.Empty(); } +void UGMC_AbilitySystemComponent::SetAttributeInitialValue(const FGameplayTag& AttributeTag, float& BaseValue) +{ + OnInitializeAttributeInitialValue(AttributeTag, BaseValue); +} void UGMC_AbilitySystemComponent::InitializeAbilityMap(){ for (UGMCAbilityMapData* StartingAbilityMap : AbilityMaps) @@ -1141,12 +1217,12 @@ void UGMC_AbilitySystemComponent::RemoveAbilityMapData(const FAbilityMapData& Ab { AbilityMap.Remove(AbilityMapData.InputTag); } + + if (GrantedAbilityTags.HasTag(AbilityMapData.InputTag)) { - if (GrantedAbilityTags.HasTag(AbilityMapData.InputTag)) - { - GrantedAbilityTags.RemoveTag(AbilityMapData.InputTag); - } + GrantedAbilityTags.RemoveTag(AbilityMapData.InputTag); } + } void UGMC_AbilitySystemComponent::InitializeStartingAbilities() @@ -1157,40 +1233,374 @@ void UGMC_AbilitySystemComponent::InitializeStartingAbilities() } } -void UGMC_AbilitySystemComponent::OnRep_UnBoundAttributes() +bool UGMC_AbilitySystemComponent::ProcessAbilityOperation( + const TGMASBoundQueueOperation& Operation, bool bFromMovementTick) { - - if (OldUnBoundAttributes.Items.Num() != UnBoundAttributes.Items.Num()) + EGMASBoundQueueOperationType OperationType = Operation.GetOperationType(); + if (OperationType == EGMASBoundQueueOperationType::Activate) { - UE_LOG(LogGMCAbilitySystem, Error, TEXT("OnRep_UnBoundAttributes: Mismatched Attribute Old != New Value !")); + TryActivateAbilitiesByInputTag(Operation.GetTag(), Operation.Payload.ActionInput, bFromMovementTick); + return true; } - TArray& OldAttributes = OldUnBoundAttributes.Items; - const TArray& CurrentAttributes = UnBoundAttributes.Items; + if (OperationType == EGMASBoundQueueOperationType::Cancel) + { + EndAbilitiesByTag(Operation.GetTag()); + if (Operation.ItemClass) + { + EndAbilitiesByClass(Operation.ItemClass); + } + return true; + } + + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Received ability operation with invalid operation type %s for %s!"), + *UEnum::GetValueAsString(OperationType), *Operation.GetTag().ToString()) + return false; +} + +UGMCAbilityEffect* UGMC_AbilitySystemComponent::ProcessOperation( + const TGMASBoundQueueOperation& Operation) +{ + EGMASBoundQueueOperationType OperationType = Operation.GetOperationType(); + + if (OperationType == EGMASBoundQueueOperationType::Add) + { + if (Operation.ItemClass == nullptr) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted to process an add effect operation with no set class!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName()) + } + + UGMCAbilityEffect* Effect = DuplicateObject(Operation.ItemClass->GetDefaultObject(), this); + FGMCAbilityEffectData EffectData = Operation.Payload; + + if (!EffectData.IsValid()) + { + EffectData = Effect->EffectData; + } + + if (Operation.Header.PayloadIds.Ids.Num() > 0) + { + EffectData.EffectID = Operation.Header.PayloadIds.Ids[0]; + } + + if (EffectData.EffectID > 0 && ActiveEffects.Contains(EffectData.EffectID)) + { + const auto& ExistingEffect = ActiveEffects[EffectData.EffectID]; + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("[%20s] %s attempted to process an explicit ID add effect operation for %s with existing effect %d [%s]"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), *Effect->GetClass()->GetName(), EffectData.EffectID, *ExistingEffect->GetClass()->GetName()) + return nullptr; + } + + ApplyAbilityEffect(Effect, EffectData); + + for (auto& [EffectHandle, EffectHandleData] : EffectHandles) + { + // If we don't already have a known effect ID, attach it to our handle now. + if (EffectHandleData.NetworkId <= 0 && EffectHandleData.OperationId == Operation.Header.OperationId) + { + EffectHandleData.NetworkId = Effect->EffectData.EffectID; + } + } + + return Effect; + } + + if (OperationType == EGMASBoundQueueOperationType::Remove) + { + for (auto& [Id, Effect]: ActiveEffects) + { + if (Operation.GetPayloadIds().Contains(Id)) + { + RemoveActiveAbilityEffect(Effect); + } + } + return nullptr; + } + + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Received ability operation with invalid operation type %s for %s!"), + *UEnum::GetValueAsString(OperationType), *Operation.ItemClass->GetName()) + return nullptr; +} + +void UGMC_AbilitySystemComponent::ExecuteSyncedEvent(FGMASSyncedEventContainer EventData) +{ + if (!HasAuthority()) + { + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Client attempted to create a SyncedEvent")); + return; + } + + if (EventData.EventTag == FGameplayTag::EmptyTag) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Attempted to create a SyncedEvent with an empty tag")); + return; + } + + EventData.EventType = EGMASSyncedEventType::BlueprintImplemented; + + TGMASBoundQueueOperation Operation; + if (CreateSyncedEventOperation(Operation, EventData) == -1 ) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Failed to create SyncedEvent")); + return; + } + + QueuedEventOperations.QueuePreparedOperation(Operation, false); + ClientQueueOperation(Operation); +} + +void UGMC_AbilitySystemComponent::ProcessOperation(const TGMASBoundQueueOperation& Operation) +{ + FGMASSyncedEventContainer EventDataContainer = Operation.Payload; + + switch (EventDataContainer.EventType) + { + case EGMASSyncedEventType::BlueprintImplemented: + { + OnSyncedEvent.Broadcast(EventDataContainer); + break; + } + case EGMASSyncedEventType::AddImpulse: + { + AddImpulseEvent(EventDataContainer); + break; + } + case EGMASSyncedEventType::PlayMontage: + { + break; + } + default: + break; + } + +} + +template +bool UGMC_AbilitySystemComponent::IsOperationValid(const TGMASBoundQueueOperation& Operation) const +{ + if (!Operation.IsValid()) + { + if (Operation.Header.OperationId != -1) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%s] %s received invalid queued operation %d!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), Operation.Header.OperationId) + return false; + } + return false; + } + return true; +} + +template +bool UGMC_AbilitySystemComponent::ShouldProcessOperation( + const TGMASBoundQueueOperation& Operation, TGMASBoundQueue& QueuedOperations, bool bIsServer) const +{ + if (!IsOperationValid(Operation)) + { + return false; + } + + if (bIsServer) + { + return HasAuthority() && (QueuedOperations.IsAcknowledged(Operation.GetOperationId()) || + Operation.GracePeriodExpired() || GetNetMode() == NM_Standalone); + } + else + { + return !QueuedOperations.IsAcknowledged(Operation.GetOperationId()) && (GMCMovementComponent->IsLocallyControlledServerPawn() || GMCMovementComponent->IsAutonomousProxy()); + } +} + +void UGMC_AbilitySystemComponent::AddImpulse(FVector Impulse, bool bVelChange) +{ + if (!HasAuthority()) + { + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Client attempted to apply knock back effect")); + return; + } + + TGMASBoundQueueOperation Operation; + FGMASSyncedEventContainer EventData; + + FGMASSyncedEventData_AddImpulse ImpulseData; + ImpulseData.Impulse = Impulse; + ImpulseData.bVelocityChange = bVelChange; + + EventData.EventType = EGMASSyncedEventType::AddImpulse; + EventData.InstancedPayload = FInstancedStruct::Make(ImpulseData); + + if (CreateSyncedEventOperation(Operation, EventData) == -1 ) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Failed to create knock back effect")); + return; + } + + QueuedEventOperations.QueuePreparedOperation(Operation, false); + ClientQueueOperation(Operation); + +} + +void UGMC_AbilitySystemComponent::AddImpulseEvent(const FGMASSyncedEventContainer& EventData) const +{ + const FGMASSyncedEventData_AddImpulse KBData = EventData.InstancedPayload.Get(); + GMCMovementComponent->AddImpulse(KBData.Impulse, KBData.bVelocityChange); +} + +void UGMC_AbilitySystemComponent::ClientQueueOperation( + const TGMASBoundQueueOperation& Operation) +{ + RPCClientQueueEffectOperation(Operation.Header); +} + +void UGMC_AbilitySystemComponent::ClientQueueOperation( + const TGMASBoundQueueOperation& Operation) +{ + RPCClientQueueEventOperation(Operation.Header); +} + +void UGMC_AbilitySystemComponent::RPCClientQueueEventOperation_Implementation(const FGMASBoundQueueRPCHeader& Header) +{ + QueuedEventOperations.QueueOperationFromHeader(Header, false); +} + +void UGMC_AbilitySystemComponent::RPCClientQueueEffectOperation_Implementation(const FGMASBoundQueueRPCHeader& Header) +{ + QueuedEffectOperations.QueueOperationFromHeader(Header, false); +} + +void UGMC_AbilitySystemComponent::OnRep_UnBoundAttributes() +{ + if (OldUnBoundAttributes.Items.Num() != UnBoundAttributes.Items.Num()) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("OnRep_UnBoundAttributes: Mismatched Attribute Old != New Value !")); + } +} + +void UGMC_AbilitySystemComponent::CheckUnBoundAttributeChanged() +{ + TArray& OldAttributes = OldUnBoundAttributes.Items; + const TArray& CurrentAttributes = UnBoundAttributes.Items; TMap OldValues; // If this mitchmatch, that mean we need to reset the number of attributes - for (FAttribute& Attribute : OldAttributes){ OldValues.Add(Attribute.Tag, &Attribute.Value); } - - for (const FAttribute& Attribute : CurrentAttributes){ if (OldValues.Contains(Attribute.Tag) && *OldValues[Attribute.Tag] != Attribute.Value){ NativeAttributeChangeDelegate.Broadcast(Attribute.Tag, *OldValues[Attribute.Tag], Attribute.Value); OnAttributeChanged.Broadcast(Attribute.Tag, *OldValues[Attribute.Tag], Attribute.Value); - UnBoundAttributes.MarkAttributeDirty(Attribute); // Update Old Value *OldValues[Attribute.Tag] = Attribute.Value; } } +} + +int UGMC_AbilitySystemComponent::GetNextAvailableEffectID() const +{ + if (ActionTimer == 0) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[ApplyAbilityEffect] Action Timer is 0, cannot generate Effect ID. Is it a listen server smoothed pawn?")); + return -1; + } + + int NewEffectID = static_cast(ActionTimer * 100); + while (ActiveEffects.Contains(NewEffectID) || CheckIfEffectIDQueued(NewEffectID)) + { + NewEffectID++; + } + UE_LOG(LogGMCAbilitySystem, VeryVerbose, TEXT("[Server: %hhd] Generated Effect ID: %d"), HasAuthority(), NewEffectID); + + return NewEffectID; +} + +bool UGMC_AbilitySystemComponent::CheckIfEffectIDQueued(int EffectID) const +{ + for (const auto& Operation : QueuedEffectOperations.GetQueuedRPCOperations()) + { + if (Operation.Payload.EffectID == EffectID) + { + return true; + } + } + + for (const auto& Operation : QueuedEffectOperations_ClientAuth.GetQueuedRPCOperations()) + { + if (Operation.Payload.EffectID == EffectID) + { + return true; + } + } + + return false; +} + +int UGMC_AbilitySystemComponent::CreateEffectOperation( + TGMASBoundQueueOperation& OutOperation, + const TSubclassOf& EffectClass, + const FGMCAbilityEffectData& EffectData, + bool bForcedEffectId, + EGMCAbilityEffectQueueType QueueType) +{ + TArray PayloadIds {}; + + FGMCAbilityEffectData PayloadData; + if (EffectData.IsValid()) + { + PayloadData = EffectData; + } + else + { + PayloadData = EffectClass->GetDefaultObject()->EffectData; + } + + if (QueueType == EGMCAbilityEffectQueueType::ServerAuth) + { + PayloadData.bServerAuth = true; + } + + + if (bForcedEffectId) + { + if (PayloadData.EffectID == 0) + { + PayloadData.EffectID = GetNextAvailableEffectID(); + } + PayloadIds.Add(PayloadData.EffectID); + } + + if (QueueType == EGMCAbilityEffectQueueType::PredictedQueued || QueueType == EGMCAbilityEffectQueueType::ClientAuth) + { + QueuedEffectOperations_ClientAuth.MakeOperation(OutOperation, EGMASBoundQueueOperationType::Add, PayloadData.EffectTag, PayloadData, PayloadIds, EffectClass, 1.f, static_cast(QueueType)); + } + else + { + QueuedEffectOperations.MakeOperation(OutOperation, EGMASBoundQueueOperationType::Add, PayloadData.EffectTag, PayloadData, PayloadIds, EffectClass, 1.f, static_cast(QueueType)); + } + return PayloadData.EffectID; +} +int UGMC_AbilitySystemComponent::CreateSyncedEventOperation( + TGMASBoundQueueOperation& OutOperation, + const FGMASSyncedEventContainer& EventData) +{ + if (!HasAuthority()) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Client attempted to create a synced event operation")); + return -1; + } + TArray PayloadIds {}; + int ID = GetNextAvailableEffectID(); + PayloadIds.Add(ID); + QueuedEventOperations.MakeOperation(OutOperation, EGMASBoundQueueOperationType::Add, FGameplayTag::EmptyTag, EventData, PayloadIds, nullptr, 1.f, static_cast(EGMCAbilityEffectQueueType::ServerAuth)); + + return ID; } //BP Version @@ -1202,34 +1612,261 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf Operation; + if (CreateEffectOperation(Operation, Effect, InitializationData, bOuterActivation, bOuterActivation ? EGMCAbilityEffectQueueType::ServerAuth : EGMCAbilityEffectQueueType::Predicted) == -1) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s could not create an effect of type %s!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), *Effect->GetName()) + return nullptr; + } + // We are trying to apply an effect from an outside source, so we will need to go trough a different routing to apply it if (bOuterActivation) { if (HasAuthority()) { - FGMCOuterApplicationWrapper Wrapper = FGMCOuterApplicationWrapper::Make(Effect, InitializationData); - AddPendingEffectApplications(Wrapper); + QueuedEffectOperations.QueuePreparedOperation(Operation, false); + ClientQueueOperation(Operation); } return nullptr; } + + if (!GMCMovementComponent->IsExecutingMove() && GetNetMode() != NM_Standalone) + { + // For backwards compatibility, we do not reject this if we're outside a movement cycle. However, we will at least + // log it. + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("[%20s] %s tried to apply a predicted effect of type %s outside a movement cycle!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), *Effect->GetName()) + } + return ProcessOperation(Operation); +} + +int32 UGMC_AbilitySystemComponent::GetNextAvailableEffectHandle() const +{ + if (ActionTimer == 0) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[ApplyAbilityEffect] Action Timer is 0, cannot generate Effect ID. Is it a listen server smoothed pawn?")); + return -1; + } + + int NewEffectHandle = static_cast(ActionTimer * 100); + while (EffectHandles.Contains(NewEffectHandle)) + { + NewEffectHandle++; + } + + return NewEffectHandle; +} + +void UGMC_AbilitySystemComponent::GetEffectFromHandle_BP(int EffectHandle, bool& bOutSuccess, int32& OutEffectNetworkId, + UGMCAbilityEffect*& OutEffect) +{ + bOutSuccess = GetEffectFromHandle(EffectHandle, OutEffectNetworkId, OutEffect); +} + +bool UGMC_AbilitySystemComponent::GetEffectFromHandle(int EffectHandle, int32& OutEffectNetworkId, + UGMCAbilityEffect*& OutEffect) const +{ + FGMASQueueOperationHandle HandleData; + + if (!GetEffectHandle(EffectHandle, HandleData)) return false; + + OutEffectNetworkId = HandleData.NetworkId; + if (HandleData.NetworkId > 0) + { + OutEffect = ActiveEffects[HandleData.NetworkId]; + } + return true; +} + +bool UGMC_AbilitySystemComponent::GetEffectHandle(int EffectHandle, FGMASQueueOperationHandle& HandleData) const +{ + for (auto& [ID, Handle] : EffectHandles) + { + if (Handle.Handle == EffectHandle) + { + HandleData = Handle; + return true; + } + } + return false; +} + +void UGMC_AbilitySystemComponent::RemoveEffectHandle(int EffectHandle) +{ + EffectHandles.Remove(EffectHandle); +} + +void UGMC_AbilitySystemComponent::ApplyAbilityEffectSafe(TSubclassOf EffectClass, + FGMCAbilityEffectData InitializationData, EGMCAbilityEffectQueueType QueueType, bool& OutSuccess, int& OutEffectHandle, int& OutEffectId, + UGMCAbilityEffect*& OutEffect, UGMCAbility* HandlingAbility) +{ + OutSuccess = ApplyAbilityEffect(EffectClass, InitializationData, QueueType, OutEffectHandle, OutEffectId, OutEffect); + + + + if (OutSuccess && HandlingAbility) + { + HandlingAbility->DeclareEffect(OutEffectId, QueueType); + } +} + +UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffectShort(TSubclassOf EffectClass, + EGMCAbilityEffectQueueType QueueType, UGMCAbility* HandlingAbility) +{ - UGMCAbilityEffect* AbilityEffect = DuplicateObject(Effect->GetDefaultObject(), this); + bool bOutSuccess; + int OutEffectHandle; + int OutEffectId; + UGMCAbilityEffect* OutEffect = nullptr; - FGMCAbilityEffectData EffectData; - if (InitializationData.IsValid()) + ApplyAbilityEffectSafe(EffectClass, FGMCAbilityEffectData{}, QueueType, bOutSuccess, OutEffectHandle, OutEffectId, OutEffect, HandlingAbility); + return bOutSuccess ? OutEffect : nullptr; +} + + +bool UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf EffectClass, + FGMCAbilityEffectData InitializationData, EGMCAbilityEffectQueueType QueueType, int& OutEffectHandle, int& OutEffectId, UGMCAbilityEffect*& OutEffect) +{ + OutEffect = nullptr; + OutEffectId = -1; + OutEffectHandle = -1; + if (EffectClass == nullptr) { - EffectData = InitializationData; + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Trying to apply Effect, but effect is null!")); + return false; } - else + + const bool bPregenerateEffectId = QueueType != EGMCAbilityEffectQueueType::Predicted && QueueType != EGMCAbilityEffectQueueType::PredictedQueued; + + + TGMASBoundQueueOperation Operation; + const int EffectID = CreateEffectOperation(Operation, EffectClass, InitializationData, bPregenerateEffectId, QueueType); + if (bPregenerateEffectId && EffectID == -1) { - EffectData = AbilityEffect->EffectData; + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s could not create an effect of type %s!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), *EffectClass->GetName()) + return false; } + + FGMASQueueOperationHandle HandleData; + HandleData.Handle = GetNextAvailableEffectHandle(); + HandleData.NetworkId = EffectID; + HandleData.OperationId = Operation.Header.OperationId; + + EffectHandles.Add(HandleData.Handle, HandleData); - ApplyAbilityEffect(AbilityEffect, EffectData); - return AbilityEffect; + switch(QueueType) + { + case EGMCAbilityEffectQueueType::Predicted: + { + if (!GMCMovementComponent->IsExecutingMove() && GetNetMode() != NM_Standalone && !bInAncillaryTick) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted to apply predicted effect %d of type %s outside of a GMC move!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), EffectID, *EffectClass->GetName()) + return false; + } + + // Apply effect immediately. + OutEffect = ProcessOperation(Operation); + OutEffectId = OutEffect->EffectData.EffectID; + OutEffectHandle = HandleData.Handle; + return true; + } + case EGMCAbilityEffectQueueType::PredictedQueued: + { + if (GMCMovementComponent->IsExecutingMove() || bInAncillaryTick) + { + // We're in a move context, just add it directly rather than queuing. + OutEffect = ProcessOperation(Operation); + OutEffectId = OutEffect->EffectData.EffectID; + } + else + { + // We utilize the ClientAuth queue's RPC queue for the sake of convenience. + QueuedEffectOperations_ClientAuth.QueuePreparedOperation(Operation, false); + } + OutEffectHandle = HandleData.Handle; + return true; + } + case EGMCAbilityEffectQueueType::ServerAuthMove: + case EGMCAbilityEffectQueueType::ServerAuth: + { + if (!HasAuthority()) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted to apply server-queued effect %d of type %s on a client!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), EffectID, *EffectClass->GetName()) + return false; + } + + if (QueueType == EGMCAbilityEffectQueueType::ServerAuthMove) Operation.Header.RPCGracePeriodSeconds = Operation.Payload.ClientGraceTime;; + + QueuedEffectOperations.QueuePreparedOperation(Operation, QueueType == EGMCAbilityEffectQueueType::ServerAuthMove); + + if (QueueType == EGMCAbilityEffectQueueType::ServerAuth) + { + // Queue for RPC and throw this to our client. + ClientQueueOperation(Operation); + } + + OutEffectId = EffectID; + OutEffectHandle = HandleData.Handle; + return true; + } + + case EGMCAbilityEffectQueueType::ClientAuth: + { + if (GetNetMode() != NM_Standalone && !GMCMovementComponent->IsAutonomousProxy() && !GMCMovementComponent->IsLocallyControlledServerPawn()) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted to apply client-auth effect %d of type %s on a server!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), EffectID, *EffectClass->GetName()) + return false; + } + + QueuedEffectOperations_ClientAuth.QueuePreparedOperation(Operation, true); + OutEffectId = EffectID; + return true; + } + } + + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted to apply effect of type %s but something has gone BADLY wrong!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), *EffectClass->GetName()) + return false; +} + +UGMCAbilityEffect* UGMC_AbilitySystemComponent::GetEffectById(const int EffectId) const +{ + if (!ActiveEffects.Contains(EffectId)) return nullptr; + + return ActiveEffects[EffectId]; +} + + +TArray UGMC_AbilitySystemComponent::GetEffectsByIds(const TArray EffectIds) const { + TArray EffectsFound; + + for (int EffectId : EffectIds) + { + if (ActiveEffects.Contains(EffectId)) + { + EffectsFound.Add(ActiveEffects[EffectId]); + } + } + + return EffectsFound; } + +FString UGMC_AbilitySystemComponent::GetEffectsNameAsString(const TArray& EffectList) const { + FString EffectNames; + for (UGMCAbilityEffect* Effect : EffectList) + { + EffectNames += (Effect ? GetNameSafe(Effect->GetClass()) : "NULLPTR EFFECT") + ", "; + } + return EffectNames; +} + + UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(UGMCAbilityEffect* Effect, FGMCAbilityEffectData InitializationData) { if (Effect == nullptr) { @@ -1240,24 +1877,13 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(UGMCAbilityEf // Force the component this is being applied to to be the owner InitializationData.OwnerAbilityComponent = this; + InitializationData.SourceAbilityComponent = this; Effect->InitializeEffect(InitializationData); if (Effect->EffectData.EffectID == 0) { - if (ActionTimer == 0) - { - UE_LOG(LogGMCAbilitySystem, Error, TEXT("[ApplyAbilityEffect] Action Timer is 0, cannot generate Effect ID. Is it a listen server smoothed pawn?")); - return nullptr; - } - - int NewEffectID = static_cast(ActionTimer * 100); - while (ActiveEffects.Contains(NewEffectID)) - { - NewEffectID++; - } - Effect->EffectData.EffectID = NewEffectID; - UE_LOG(LogGMCAbilitySystem, VeryVerbose, TEXT("[Server: %hhd] Generated Effect ID: %d"), HasAuthority(), Effect->EffectData.EffectID); + Effect->EffectData.EffectID = GetNextAvailableEffectID(); } // This is Replicated, so only server needs to manage it @@ -1267,10 +1893,11 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(UGMCAbilityEf } else { - ProcessedEffectIDs.Add(Effect->EffectData.EffectID, false); + ProcessedEffectIDs.Add(Effect->EffectData.EffectID, EGMCEffectAnswerState::Pending); } - + ActiveEffects.Add(Effect->EffectData.EffectID, Effect); + return Effect; } @@ -1282,17 +1909,63 @@ void UGMC_AbilitySystemComponent::RemoveActiveAbilityEffect(UGMCAbilityEffect* E } if (!ActiveEffects.Contains(Effect->EffectData.EffectID)) return; + Effect->EndEffect(); } -int32 UGMC_AbilitySystemComponent::RemoveEffectByTag(FGameplayTag InEffectTag, int32 NumToRemove, bool bOuterActivation) { +void UGMC_AbilitySystemComponent::RemoveActiveAbilityEffectByHandle(int EffectHandle, EGMCAbilityEffectQueueType QueueType) +{ + UGMCAbilityEffect* Effect = GetEffectById(EffectHandle); + if (Effect == nullptr) + { + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("[%20s] %s tried to remove effect with handle %d, but it doesn't exist!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), EffectHandle); + return; + } + + RemoveActiveAbilityEffectSafe(Effect, QueueType); +} + +void UGMC_AbilitySystemComponent::RemoveActiveAbilityEffectSafe(UGMCAbilityEffect* Effect, + EGMCAbilityEffectQueueType QueueType) +{ + if (Effect == nullptr) + { + ensureAlwaysMsgf(false, TEXT("[%20s] %s tried to remove a null effect!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName()); + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("[%20s] %s tried to remove a null effect!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName()); + return; + } + - if (NumToRemove < -1 || !InEffectTag.IsValid()) { - return 0; + RemoveEffectByIdSafe({ Effect->EffectData.EffectID }, QueueType); +} + + +void UGMC_AbilitySystemComponent::RemoveActiveAbilityEffectByTag(FGameplayTag Tag, EGMCAbilityEffectQueueType QueueType, bool bAllInstance) { + + for (auto& [EffectId, Effect] : ActiveEffects) + { + if (IsValid(Effect) && Tag.MatchesTag(Effect->EffectData.EffectTag)) + { + RemoveEffectByIdSafe({ EffectId }, QueueType); + if (!bAllInstance) { + return; + } + } + } + +} + +TArray UGMC_AbilitySystemComponent::EffectsMatchingTag(const FGameplayTag& Tag, int32 NumToRemove) const +{ + if (NumToRemove < -1 || !Tag.IsValid()) { + return {}; } - TMap EffectsToRemove; - int32 NumRemoved = 0; + TArray EffectsToRemove; + int NumRemoved = 0; for(const TTuple Effect : ActiveEffects) { @@ -1300,37 +1973,70 @@ int32 UGMC_AbilitySystemComponent::RemoveEffectByTag(FGameplayTag InEffectTag, i break; } - if(Effect.Value->EffectData.EffectTag.IsValid() && Effect.Value->EffectData.EffectTag.MatchesTagExact(InEffectTag)){ - EffectsToRemove.Add(Effect.Key, Effect.Value); + if(Effect.Value->EffectData.EffectTag.IsValid() && Effect.Value->EffectData.EffectTag.MatchesTagExact(Tag)){ + EffectsToRemove.Add(Effect.Value->EffectData.EffectID); NumRemoved++; } } - - if (bOuterActivation) { - if (HasAuthority() && EffectsToRemove.Num() > 0) { + return EffectsToRemove; +} - TArray EffectIDsToRemove; - for (const auto& ToRemove : EffectsToRemove) { - EffectIDsToRemove.Add(ToRemove.Key); - } - - FGMCOuterApplicationWrapper Wrapper = FGMCOuterApplicationWrapper::Make(EffectIDsToRemove); - AddPendingEffectApplications(Wrapper); - } +int32 UGMC_AbilitySystemComponent::RemoveEffectByTag(FGameplayTag InEffectTag, int32 NumToRemove, bool bOuterActivation) { + + if (NumToRemove < -1 || !InEffectTag.IsValid()) { return 0; } - for (auto& ToRemove : EffectsToRemove) { - ToRemove.Value->EndEffect(); + TArray EffectsToRemove = EffectsMatchingTag(InEffectTag, NumToRemove); + + if (EffectsToRemove.Num() > 0) + { + RemoveEffectById(EffectsToRemove, bOuterActivation); + } + + return EffectsToRemove.Num(); +} + +int32 UGMC_AbilitySystemComponent::RemoveEffectByTagSafe(FGameplayTag InEffectTag, int32 NumToRemove, + EGMCAbilityEffectQueueType QueueType) +{ + if (NumToRemove < -1 || !InEffectTag.IsValid()) { + return 0; } + + TArray EffectsToRemove = EffectsMatchingTag(InEffectTag, NumToRemove); - return NumRemoved; + + if (EffectsToRemove.Num() > 0) + { + RemoveEffectByIdSafe(EffectsToRemove, QueueType); + } + + return EffectsToRemove.Num(); } +int UGMC_AbilitySystemComponent::RemoveEffectsByQuery(const FGameplayTagQuery& Query, EGMCAbilityEffectQueueType QueueType) +{ + int EffectsRemoved = 0; -bool UGMC_AbilitySystemComponent::RemoveEffectById(TArray Ids, bool bOuterActivation) { + for (const auto& EffectEntry : ActiveEffects) + { + if (UGMCAbilityEffect* Effect = EffectEntry.Value) + { + if (Query.Matches(Effect->EffectData.EffectDefinition)) + { + RemoveActiveAbilityEffectSafe(Effect, QueueType); + EffectsRemoved++; + UE_LOG(LogGMCAbilitySystem, Verbose, TEXT("Removed effect %s by query"), *Effect->EffectData.EffectTag.ToString()); + } + } + } + return EffectsRemoved; +} +bool UGMC_AbilitySystemComponent::RemoveEffectByIdSafe(TArray Ids, EGMCAbilityEffectQueueType QueueType) +{ if (!Ids.Num()) { return true; } @@ -1338,26 +2044,134 @@ bool UGMC_AbilitySystemComponent::RemoveEffectById(TArray Ids, bool bOuterA // check all IDs exists for (int Id : Ids) { if (!ActiveEffects.Contains(Id)) { - UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Trying to remove effect with ID %d, but it doesn't exist!"), Id); + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("[%20s] %s tried to remove effect with ID %d, but it doesn't exist!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), Id); return false; } } - if (bOuterActivation) { - if (HasAuthority()) { - FGMCOuterApplicationWrapper Wrapper = FGMCOuterApplicationWrapper::Make(Ids); - AddPendingEffectApplications(Wrapper); - } - return true; + switch(QueueType) { + case EGMCAbilityEffectQueueType::Predicted: + { + if (!GMCMovementComponent->IsExecutingMove() && GetNetMode() != NM_Standalone && !bInAncillaryTick) + { + + ensureMsgf(false, TEXT("[%20s] %s attempted a predicted removal of effects outside of a movement cycle! (%s)"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), *GetEffectsNameAsString(GetEffectsByIds(Ids))); + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted a predicted removal of effects outside of a movement cycle! (%s)"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), *GetEffectsNameAsString(GetEffectsByIds(Ids))); + return false; + } + + TArray EffectsToRemove; + for (int Id : Ids) { + if (ActiveEffects.Contains(Id)) { + EffectsToRemove.Add(ActiveEffects[Id]); + } + } + + for (auto Effect : EffectsToRemove) { + RemoveActiveAbilityEffect(Effect); + } + + return true; + } + case EGMCAbilityEffectQueueType::PredictedQueued: + { + // If in move, silenttly remove the effect as predicted + if (GMCMovementComponent->IsExecutingMove() || bInAncillaryTick) + { + TArray EffectsToRemove; + for (int Id : Ids) { + if (ActiveEffects.Contains(Id)) { + EffectsToRemove.Add(ActiveEffects[Id]); + } + } + + for (auto Effect : EffectsToRemove) { + RemoveActiveAbilityEffect(Effect); + } + + } + else { + TGMASBoundQueueOperation Operation; + FGMCAbilityEffectData Data; + QueuedEffectOperations_ClientAuth.MakeOperation(Operation, EGMASBoundQueueOperationType::Remove, FGameplayTag::EmptyTag, Data, Ids); + QueuedEffectOperations_ClientAuth.QueuePreparedOperation(Operation, false); + } + return true; + } + case EGMCAbilityEffectQueueType::ClientAuth: + { + if (QueueType == EGMCAbilityEffectQueueType::ClientAuth) + { + if (GetNetMode() != NM_Standalone && (HasAuthority() && !GMCMovementComponent->IsLocallyControlledServerPawn())) + { + ensureMsgf(false, TEXT("[%20s] %s attempted a client-auth removal of %d effects on a server! (%s)"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), Ids.Num(), *GetEffectsNameAsString(GetEffectsByIds(Ids))); + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted a client-auth removal of %d effects on a server! (%s)"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), Ids.Num(), *GetEffectsNameAsString(GetEffectsByIds(Ids))) + return false; + } + } + + TGMASBoundQueueOperation Operation; + FGMCAbilityEffectData Data; + QueuedEffectOperations_ClientAuth.MakeOperation(Operation, EGMASBoundQueueOperationType::Remove, FGameplayTag::EmptyTag, Data, Ids); + QueuedEffectOperations_ClientAuth.QueuePreparedOperation(Operation, QueueType == EGMCAbilityEffectQueueType::ClientAuth); + return true; + } + case EGMCAbilityEffectQueueType::ServerAuthMove: + case EGMCAbilityEffectQueueType::ServerAuth: + { + if (!HasAuthority()) + { + ensureMsgf(false, TEXT("[%20s] %s attempted a server-auth removal of %d effects on a client! (%s)"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), Ids.Num(), *GetEffectsNameAsString(GetEffectsByIds(Ids))); + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted a server-auth removal of %d effects on a client! (%s)"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), Ids.Num(), *GetEffectsNameAsString(GetEffectsByIds(Ids))) + return false; + } + + TGMASBoundQueueOperation Operation; + FGMCAbilityEffectData Data; + QueuedEffectOperations.MakeOperation(Operation, EGMASBoundQueueOperationType::Remove, FGameplayTag::EmptyTag, Data, Ids); + QueuedEffectOperations.QueuePreparedOperation(Operation, QueueType == EGMCAbilityEffectQueueType::ServerAuthMove); + + if (QueueType == EGMCAbilityEffectQueueType::ServerAuth) + { + // Send the operation over to our client via standard RPC. + ClientQueueOperation(Operation); + } + return true; + } } - for (auto& Effect : ActiveEffects) { - if (Ids.Contains(Effect.Key)) { - Effect.Value->EndEffect(); - } + ensureMsgf(false, TEXT("[%20s] %s attempted a removal of effects but something went horribly wrong! (%s)"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), *GetEffectsNameAsString(GetEffectsByIds(Ids))); + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted a removal of effects but something went horribly wrong! (%s)"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(),*GetEffectsNameAsString(GetEffectsByIds(Ids))) + return false; +} + +bool UGMC_AbilitySystemComponent::RemoveEffectByHandle(int EffectHandle, EGMCAbilityEffectQueueType QueueType) +{ + int32 EffectID; + UGMCAbilityEffect* Effect; + if (GetEffectFromHandle(EffectHandle, EffectID, Effect) && EffectID > 0) + { + return RemoveEffectByIdSafe({ EffectID }, QueueType); } + + return false; +} + + +bool UGMC_AbilitySystemComponent::RemoveEffectById(TArray Ids, bool bOuterActivation) { + + // Just hit up the newer version. + return RemoveEffectByIdSafe(Ids, bOuterActivation ? EGMCAbilityEffectQueueType::ServerAuth : EGMCAbilityEffectQueueType::Predicted); - return true; } @@ -1375,13 +2189,15 @@ int32 UGMC_AbilitySystemComponent::GetNumEffectByTag(FGameplayTag InEffectTag){ TArray UGMC_AbilitySystemComponent::GetAllAttributes() const{ TArray AllAttributes; - for (const FAttribute& Attribute : UnBoundAttributes.Items){ - AllAttributes.Add(&Attribute); + + for (int32 i = 0; i < UnBoundAttributes.Items.Num(); i++){ + AllAttributes.Add(&UnBoundAttributes.Items[i]); } - for (const FAttribute& Attribute : BoundAttributes.Attributes){ - AllAttributes.Add(&Attribute); + for (int32 i = 0; i < BoundAttributes.Attributes.Num(); i++){ + AllAttributes.Add(&BoundAttributes.Attributes[i]); } + return AllAttributes; } @@ -1410,7 +2226,16 @@ float UGMC_AbilitySystemComponent::GetAttributeValueByTag(const FGameplayTag Att { return Att->Value; } - return 0; + return 0.f; +} + +float UGMC_AbilitySystemComponent::GetAttributeRawValue(FGameplayTag AttributeTag) const +{ + if (const FAttribute* Att = GetAttributeByTag(AttributeTag)) + { + return Att->RawValue; + } + return 0.f; } @@ -1427,20 +2252,21 @@ bool UGMC_AbilitySystemComponent::SetAttributeValueByTag(FGameplayTag AttributeT { if (const FAttribute* Att = GetAttributeByTag(AttributeTag)) { - Att->SetBaseValue(NewValue); + /*Att->SetBaseValue(NewValue); if (bResetModifiers) { Att->ResetModifiers(); } - Att->CalculateValue(); + Att->CalculateValue();*/ + UnBoundAttributes.MarkAttributeDirty(*Att); return true; } return false; } -float UGMC_AbilitySystemComponent::GetBaseAttributeValueByTag(FGameplayTag AttributeTag) const{ +float UGMC_AbilitySystemComponent::GetAttributeInitialValueByTag(FGameplayTag AttributeTag) const{ if(!AttributeTag.IsValid()){return -1.0f;} for(UGMCAttributesData* DataAsset : AttributeDataAssets){ for(FAttributeData DefaultAttribute : DataAsset->AttributeData){ @@ -1463,7 +2289,7 @@ FString UGMC_AbilitySystemComponent::GetAllAttributesString() const{ } FString UGMC_AbilitySystemComponent::GetActiveEffectsDataString() const{ - FString FinalString = TEXT("\n"); + FString FinalString = FString::Printf(TEXT("%d total\n"), ActiveEffectsData.Num()); for(const FGMCAbilityEffectData& ActiveEffectData : ActiveEffectsData){ FinalString += ActiveEffectData.ToString() + TEXT("\n"); } @@ -1471,7 +2297,7 @@ FString UGMC_AbilitySystemComponent::GetActiveEffectsDataString() const{ } FString UGMC_AbilitySystemComponent::GetActiveEffectsString() const{ - FString FinalString = TEXT("\n"); + FString FinalString = FString::Printf(TEXT("%d total\n"), ActiveEffects.Num()); for(const TTuple ActiveEffect : ActiveEffects){ FinalString += ActiveEffect.Value->ToString() + TEXT("\n"); } @@ -1488,45 +2314,148 @@ FString UGMC_AbilitySystemComponent::GetActiveAbilitiesString() const{ #pragma endregion ToStringHelpers -void UGMC_AbilitySystemComponent::ApplyAbilityEffectModifier(FGMCAttributeModifier AttributeModifier, bool bModifyBaseValue, bool bNegateValue, UGMC_AbilitySystemComponent* SourceAbilityComponent) -{ - // Provide an opportunity to modify the attribute modifier before applying it - UGMCAttributeModifierContainer* AttributeModifierContainer = NewObject(this); - AttributeModifierContainer->AttributeModifier = AttributeModifier; +void UGMC_AbilitySystemComponent::ApplyAbilityAttributeModifier(const FGMCAttributeModifier& AttributeModifier) +{ + // Todo : re-add later // Broadcast the event to allow modifications to happen before application - OnPreAttributeChanged.Broadcast(AttributeModifierContainer, SourceAbilityComponent); - - // Apply the modified attribute modifier. If no changes were made, it's just the same as the original - // Extra copying going on here? Can this be done with a reference? BPs are weird. - AttributeModifier = AttributeModifierContainer->AttributeModifier; + //OnPreAttributeChanged.Broadcast(AttributeModifierContainer, SourceAbilityComponent); if (const FAttribute* AffectedAttribute = GetAttributeByTag(AttributeModifier.AttributeTag)) { - // If we are unbound that means we shouldn't predict. - if(!AffectedAttribute->bIsGMCBound && !HasAuthority()) return; - float OldValue = AffectedAttribute->Value; - FGMCUnboundAttributeSet OldUnboundAttributes = UnBoundAttributes; + // If attribute is unbound and this is the client that means we shouldn't predict. + if(!AffectedAttribute->bIsGMCBound && !HasAuthority()) { + return; + } - if (bNegateValue) + AffectedAttribute->AddModifier(AttributeModifier); + } +} + +//////////////// FX + +UNiagaraComponent* UGMC_AbilitySystemComponent::SpawnParticleSystemAttached(FFXSystemSpawnParameters SpawnParams, + bool bIsClientPredicted, bool bDelayByGMCSmoothing) +{ + if (SpawnParams.SystemTemplate == nullptr) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Trying to spawn FX, but FX is null!")); + return nullptr; + } + + if (SpawnParams.WorldContextObject == nullptr) + { + SpawnParams.WorldContextObject = GetWorld(); + } + + if (HasAuthority()) + { + MC_SpawnParticleSystemAttached(SpawnParams, bIsClientPredicted, bDelayByGMCSmoothing); + } + + // Sim Proxies can delay FX by the smoothing delay to better line up + if (bDelayByGMCSmoothing && !HasAuthority() && !IsLocallyControlledPawnASC()) + { + float Delay = GMCMovementComponent->GetTime() - GMCMovementComponent->GetSmoothingTime(); + FTimerHandle DelayHandle; + GetWorld()->GetTimerManager().SetTimer(DelayHandle, [this, SpawnParams]() { - AttributeModifier.Value = -AttributeModifier.Value; - } - AffectedAttribute->ApplyModifier(AttributeModifier, bModifyBaseValue); + UNiagaraFunctionLibrary::SpawnSystemAttachedWithParams(SpawnParams); + }, Delay, false); + return nullptr; + } + + UNiagaraComponent* SpawnedComponent = UNiagaraFunctionLibrary::SpawnSystemAttachedWithParams(SpawnParams); + return SpawnedComponent; +} + +void UGMC_AbilitySystemComponent::MC_SpawnParticleSystemAttached_Implementation(const FFXSystemSpawnParameters& SpawnParams, bool bIsClientPredicted, bool bDelayByGMCSmoothing) +{ + // Server already spawned + if (HasAuthority()) return; + + // Owning client already spawned + if (IsLocallyControlledPawnASC() && bIsClientPredicted) return; + + SpawnParticleSystemAttached(SpawnParams, bIsClientPredicted, bDelayByGMCSmoothing); +} - // Only broadcast a change if we've genuinely changed. - if (OldValue != AffectedAttribute->Value) +UNiagaraComponent* UGMC_AbilitySystemComponent::SpawnParticleSystemAtLocation(FFXSystemSpawnParameters SpawnParams, bool bIsClientPredicted, + bool bDelayByGMCSmoothing) +{ + if (SpawnParams.SystemTemplate == nullptr) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Trying to spawn FX, but FX is null!")); + return nullptr; + } + + if (SpawnParams.WorldContextObject == nullptr) + { + SpawnParams.WorldContextObject = GetWorld(); + } + + if (HasAuthority()) + { + MC_SpawnParticleSystemAtLocation(SpawnParams, bIsClientPredicted, bDelayByGMCSmoothing); + } + + // Sim Proxies can delay FX by the smoothing delay to better line up + if (bDelayByGMCSmoothing && !HasAuthority() && !IsLocallyControlledPawnASC()) + { + float Delay = GMCMovementComponent->GetTime() - GMCMovementComponent->GetSmoothingTime(); + FTimerHandle DelayHandle; + GetWorld()->GetTimerManager().SetTimer(DelayHandle, [this, SpawnParams]() { - OnAttributeChanged.Broadcast(AffectedAttribute->Tag, OldValue, AffectedAttribute->Value); - NativeAttributeChangeDelegate.Broadcast(AffectedAttribute->Tag, OldValue, AffectedAttribute->Value); - } + UNiagaraFunctionLibrary::SpawnSystemAtLocationWithParams(SpawnParams); + }, Delay, false); - BoundAttributes.MarkAttributeDirty(*AffectedAttribute); - UnBoundAttributes.MarkAttributeDirty(*AffectedAttribute); - if (!AffectedAttribute->bIsGMCBound) { - OnRep_UnBoundAttributes(); - } + UE_LOG(LogTemp, Warning, TEXT("Delay: %f"), Delay); + + return nullptr; } + + UNiagaraComponent* SpawnedComponent = UNiagaraFunctionLibrary::SpawnSystemAtLocationWithParams(SpawnParams); + return SpawnedComponent; +} + +void UGMC_AbilitySystemComponent::MC_SpawnParticleSystemAtLocation_Implementation(const FFXSystemSpawnParameters& SpawnParams, + bool bIsClientPredicted, bool bDelayByGMCSmoothing) +{ + // Server already spawned + if (HasAuthority()) return; + + // Owning client already spawned + if (IsLocallyControlledPawnASC() && bIsClientPredicted) return; + + SpawnParticleSystemAtLocation(SpawnParams, bIsClientPredicted, bDelayByGMCSmoothing); +} + +void UGMC_AbilitySystemComponent::SpawnSound(USoundBase* Sound, const FVector Location, const float VolumeMultiplier, const float PitchMultiplier, const bool bIsClientPredicted) +{ + // Spawn sound + if (Sound == nullptr) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Trying to spawn sound, but sound is null!")); + return; + } + + if (HasAuthority()) + { + MC_SpawnSound(Sound, Location, VolumeMultiplier, PitchMultiplier, bIsClientPredicted); + } + + // Spawn Sound At Location + UGameplayStatics::PlaySoundAtLocation(GetWorld(), Sound, Location, VolumeMultiplier, PitchMultiplier); +} + +void UGMC_AbilitySystemComponent::MC_SpawnSound_Implementation(USoundBase* Sound, const FVector Location, const float VolumeMultiplier, const float PitchMultiplier, + bool bIsClientPredicted) +{ + // Server already spawned + if (HasAuthority()) return; + + if (IsLocallyControlledPawnASC() && bIsClientPredicted) return; + SpawnSound(Sound, Location, VolumeMultiplier, PitchMultiplier, bIsClientPredicted); } // ReplicatedProps diff --git a/Source/GMCAbilitySystem/Private/Debug/GameplayDebuggerCategory_GMCAbilitySystem.cpp b/Source/GMCAbilitySystem/Private/Debug/GameplayDebuggerCategory_GMCAbilitySystem.cpp index 63fb11dd..aae0ddf2 100644 --- a/Source/GMCAbilitySystem/Private/Debug/GameplayDebuggerCategory_GMCAbilitySystem.cpp +++ b/Source/GMCAbilitySystem/Private/Debug/GameplayDebuggerCategory_GMCAbilitySystem.cpp @@ -15,25 +15,27 @@ FGameplayDebuggerCategory_GMCAbilitySystem::FGameplayDebuggerCategory_GMCAbility void FGameplayDebuggerCategory_GMCAbilitySystem::CollectData(APlayerController* OwnerPC, AActor* DebugActor) { - if (OwnerPC) + if (DebugActor) { - if (OwnerPC->GetPawn()) - { - DataPack.ActorName = OwnerPC->GetPawn()->GetName(); - - if (const UGMC_AbilitySystemComponent* AbilityComponent = OwnerPC->GetPawn()->FindComponentByClass()) - { - DataPack.GrantedAbilities = AbilityComponent->GetGrantedAbilities().ToStringSimple(); - DataPack.ActiveTags = AbilityComponent->GetActiveTags().ToStringSimple(); - DataPack.Attributes = AbilityComponent->GetAllAttributesString(); - DataPack.ActiveEffects = AbilityComponent->GetActiveEffectsString(); - DataPack.ActiveEffectData = AbilityComponent->GetActiveEffectsDataString(); - DataPack.ActiveAbilities = AbilityComponent->GetActiveAbilitiesString(); - } - } - else + DataPack.ActorName = DebugActor->GetName(); + + if (const UGMC_AbilitySystemComponent* AbilityComponent = DebugActor->FindComponentByClass()) { - DataPack.ActorName = TEXT("Spectator or missing pawn"); + AbilityComponent->GMCMovementComponent->SV_SwapServerState(); + DataPack.GrantedAbilities = AbilityComponent->GetGrantedAbilities().ToStringSimple(); + DataPack.NBGrantedAbilities = AbilityComponent->GetGrantedAbilities().Num(); + DataPack.ActiveTags = AbilityComponent->GetActiveTags().ToStringSimple(); + DataPack.NBActiveTags = AbilityComponent->GetActiveTags().Num(); + DataPack.Attributes = AbilityComponent->GetAllAttributesString(); + DataPack.NBAttributes = AbilityComponent->GetAllAttributes().Num(); + DataPack.ActiveEffects = AbilityComponent->GetActiveEffectsString(); + DataPack.NBActiveEffects = AbilityComponent->GetActiveEffects().Num(); + DataPack.ActiveEffectData = AbilityComponent->GetActiveEffectsDataString(); + DataPack.NBActiveEffectData = AbilityComponent->ActiveEffectsData.Num(); + DataPack.ActiveAbilities = AbilityComponent->GetActiveAbilitiesString(); + DataPack.NBActiveAbilities = AbilityComponent->GetActiveAbilities().Num(); + + AbilityComponent->GMCMovementComponent->SV_SwapServerState(); } } } @@ -41,26 +43,46 @@ void FGameplayDebuggerCategory_GMCAbilitySystem::CollectData(APlayerController* void FGameplayDebuggerCategory_GMCAbilitySystem::DrawData(APlayerController* OwnerPC, FGameplayDebuggerCanvasContext& CanvasContext) { + const AActor* LocalDebugActor = FindLocalDebugActor(); + const UGMC_AbilitySystemComponent* AbilityComponent = LocalDebugActor ? LocalDebugActor->FindComponentByClass() : nullptr; + if (AbilityComponent == nullptr) return; + if (!DataPack.ActorName.IsEmpty()) { CanvasContext.Printf(TEXT("{yellow}Actor name: {white}%s"), *DataPack.ActorName); - const UGMC_AbilitySystemComponent* AbilityComponent = OwnerPC->GetPawn() ? OwnerPC->GetPawn()->FindComponentByClass() : nullptr; - if (AbilityComponent == nullptr) return; + constexpr int MaxCharDisplayAbilities = 100; // Abilities - CanvasContext.Printf(TEXT("{blue}[server] {yellow}Granted Abilities: {white}%s"), *DataPack.GrantedAbilities); + CanvasContext.Printf(TEXT("{blue}[server] {yellow}Granted Abilities (%d): {white}%s%s"), DataPack.NBGrantedAbilities, *DataPack.GrantedAbilities.Left(MaxCharDisplayAbilities), DataPack.GrantedAbilities.Len() > MaxCharDisplayAbilities ? TEXT("...") : TEXT("")); // Show client-side data if (AbilityComponent) { - CanvasContext.Printf(TEXT("{green}[client] {yellow}Granted Abilities: {white}%s"), *AbilityComponent->GetGrantedAbilities().ToStringSimple()); + if (DataPack.NBGrantedAbilities != AbilityComponent->GetGrantedAbilities().Num()) + { + CanvasContext.Printf( + TEXT("{green}[client] {yellow}Granted Abilities (%d): {red} [INCOHERENCY] {white}%s%s"), + AbilityComponent->GetGrantedAbilities().Num(), + *AbilityComponent->GetGrantedAbilities().ToStringSimple().Left(MaxCharDisplayAbilities), + AbilityComponent->GetGrantedAbilities().ToStringSimple().Len() > MaxCharDisplayAbilities ? TEXT("...") : TEXT("")); + } + else + { + CanvasContext.Printf( + TEXT("{green}[client] {yellow}Granted Abilities (%d): {white}%s%s"), AbilityComponent->GetGrantedAbilities().Num(), + *AbilityComponent->GetGrantedAbilities().ToStringSimple().Left(MaxCharDisplayAbilities), + AbilityComponent->GetGrantedAbilities().ToStringSimple().Len() > MaxCharDisplayAbilities ? TEXT("...") : TEXT("")); + } } // Active Abilities - CanvasContext.Printf(TEXT("{blue}[server] {yellow}Active Abilities: {white}%s"), *DataPack.ActiveAbilities); + CanvasContext.Printf(TEXT("\n{blue}[server] {yellow}Active Abilities: {white}%s"), *DataPack.ActiveAbilities); // Show client-side data if (AbilityComponent) { - CanvasContext.Printf(TEXT("{green}[client] {yellow}Active Abilities: {white}%s"), *AbilityComponent->GetActiveAbilitiesString()); + if (DataPack.NBActiveAbilities != AbilityComponent->GetActiveAbilities().Num()) + CanvasContext.Printf(TEXT("{green}[client] {yellow}Active Abilities: {red} [INCOHERENCY] {white}%s"), *AbilityComponent->GetActiveAbilitiesString()); + else + CanvasContext.Printf(TEXT("{green}[client] {yellow}Active Abilities: {white}%s"), *AbilityComponent->GetActiveAbilitiesString()); } // Tags @@ -68,7 +90,10 @@ void FGameplayDebuggerCategory_GMCAbilitySystem::DrawData(APlayerController* Own // Show client-side data if (AbilityComponent) { - CanvasContext.Printf(TEXT("{green}[client] {yellow}Active Tags: {white}%s"), *AbilityComponent->GetActiveTags().ToStringSimple()); + if (DataPack.NBActiveTags != AbilityComponent->GetActiveTags().Num()) + CanvasContext.Printf(TEXT("{green}[client] {yellow}Active Tags: {red} [INCOHERENCY] {white}%s"), *AbilityComponent->GetActiveTags().ToStringSimple()); + else + CanvasContext.Printf(TEXT("{green}[client] {yellow}Active Tags: {white}%s"), *AbilityComponent->GetActiveTags().ToStringSimple()); } // Attributes @@ -76,7 +101,10 @@ void FGameplayDebuggerCategory_GMCAbilitySystem::DrawData(APlayerController* Own // Show client-side data if (AbilityComponent) { - CanvasContext.Printf(TEXT("{green}[client] {yellow}Attributes: {white}%s"), *AbilityComponent->GetAllAttributesString()); + if (DataPack.NBAttributes != AbilityComponent->GetAllAttributes().Num()) + CanvasContext.Printf(TEXT("{green}[client] {yellow}Attributes: {red} [INCOHERENCY] {white}%s"), *AbilityComponent->GetAllAttributesString()); + else + CanvasContext.Printf(TEXT("{green}[client] {yellow}Attributes: {white}%s"), *AbilityComponent->GetAllAttributesString()); } // Active Effects @@ -84,7 +112,10 @@ void FGameplayDebuggerCategory_GMCAbilitySystem::DrawData(APlayerController* Own // Show client-side data if (AbilityComponent) { - CanvasContext.Printf(TEXT("{green}[client] {yellow}Active Effects: {white}%s"), *AbilityComponent->GetActiveEffectsString()); + if (DataPack.NBActiveEffects != AbilityComponent->GetActiveEffects().Num()) + CanvasContext.Printf(TEXT("{green}[client] {yellow}Active Effects: {red} [INCOHERENCY] {white}%s"), *AbilityComponent->GetActiveEffectsString()); + else + CanvasContext.Printf(TEXT("{green}[client] {yellow}Active Effects: {white}%s"), *AbilityComponent->GetActiveEffectsString()); } // Active Effects Data @@ -92,6 +123,9 @@ void FGameplayDebuggerCategory_GMCAbilitySystem::DrawData(APlayerController* Own // Show client-side data if (AbilityComponent) { + if (DataPack.NBActiveEffectData != AbilityComponent->ActiveEffectsData.Num()) + CanvasContext.Printf(TEXT("{green}[client] {yellow}Active Effects Data: {red} [INCOHERENCY] {white}%s"), *AbilityComponent->GetActiveEffectsDataString()); + else CanvasContext.Printf(TEXT("{green}[client] {yellow}Active Effects Data: {white}%s"), *AbilityComponent->GetActiveEffectsDataString()); } @@ -112,6 +146,12 @@ void FGameplayDebuggerCategory_GMCAbilitySystem::FRepData::Serialize(FArchive& A Ar << ActiveEffects; Ar << ActiveEffectData; Ar << ActiveAbilities; + Ar << NBGrantedAbilities; + Ar << NBActiveTags; + Ar << NBAttributes; + Ar << NBActiveEffects; + Ar << NBActiveEffectData; + Ar << NBActiveAbilities; } #endif \ No newline at end of file diff --git a/Source/GMCAbilitySystem/Private/Effects/GMCAbilityEffect.cpp b/Source/GMCAbilitySystem/Private/Effects/GMCAbilityEffect.cpp index f2d0f280..b1ba79fd 100644 --- a/Source/GMCAbilitySystem/Private/Effects/GMCAbilityEffect.cpp +++ b/Source/GMCAbilitySystem/Private/Effects/GMCAbilityEffect.cpp @@ -5,16 +5,24 @@ #include "GMCAbilitySystem.h" #include "Components/GMCAbilityComponent.h" +#include "Interfaces/IPluginManager.h" #include "Kismet/KismetSystemLibrary.h" +#if WITH_EDITOR +void UGMCAbilityEffect::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) +{ + UObject::PostEditChangeProperty(PropertyChangedEvent); + + +} +#endif void UGMCAbilityEffect::InitializeEffect(FGMCAbilityEffectData InitializationData) { EffectData = InitializationData; OwnerAbilityComponent = EffectData.OwnerAbilityComponent; - SourceAbilityComponent = EffectData.SourceAbilityComponent; - + if (OwnerAbilityComponent == nullptr) { UE_LOG(LogGMCAbilitySystem, Error, TEXT("OwnerAbilityComponent is null in UGMCAbilityEffect::InitializeEffect")); @@ -42,7 +50,7 @@ void UGMCAbilityEffect::InitializeEffect(FGMCAbilityEffectData InitializationDat { EffectData.EndTime = EffectData.StartTime + EffectData.Duration; } - + // Start Immediately if (EffectData.Delay == 0) { @@ -53,6 +61,8 @@ void UGMCAbilityEffect::InitializeEffect(FGMCAbilityEffectData InitializationDat void UGMCAbilityEffect::StartEffect() { + bHasStarted = true; + // Ensure tag requirements are met before applying the effect if( ( EffectData.ApplicationMustHaveTags.Num() > 0 && !DoesOwnerHaveTagFromContainer(EffectData.ApplicationMustHaveTags) ) || DoesOwnerHaveTagFromContainer(EffectData.ApplicationMustNotHaveTags) || @@ -62,45 +72,43 @@ void UGMCAbilityEffect::StartEffect() EndEffect(); return; } - - bHasStarted = true; + // Effect Query + if (!EffectData.ActivationQuery.IsEmpty() && !EffectData.ActivationQuery.Matches(OwnerAbilityComponent->GetActiveTags())) + { + EndEffect(); + return; + } + AddTagsToOwner(); AddAbilitiesToOwner(); - EndActiveAbilitiesFromOwner(); + EndActiveAbilitiesFromOwner(EffectData.CancelAbilityOnActivation); + + EndActiveAbilitiesByDefinitionQuery(EffectData.EndAbilityOnActivationQuery); + + bHasAppliedEffect = true; + + OwnerAbilityComponent->OnEffectApplied.Broadcast(this); // Instant effects modify base value and end instantly - if (EffectData.bIsInstant) + if (EffectData.EffectType == EGMASEffectType::Instant + || EffectData.EffectType == EGMASEffectType::Persistent + || (EffectData.EffectType == EGMASEffectType::Periodic && EffectData.bPeriodicFirstTick)) { - for (const FGMCAttributeModifier& Modifier : EffectData.Modifiers) + for (int i = 0; i < EffectData.Modifiers.Num(); i++) { - OwnerAbilityComponent->ApplyAbilityEffectModifier(Modifier, true); + FGMCAttributeModifier ModCpy = EffectData.Modifiers[i]; + ModCpy.InitModifier(this, OwnerAbilityComponent->ActionTimer, i, IsEffectModifiersRegisterInHistory(), 1.f); + OwnerAbilityComponent->ApplyAbilityAttributeModifier(ModCpy); } - EndEffect(); - return; - } - // Duration Effects that aren't periodic alter modifiers, not base - if (!EffectData.bIsInstant && EffectData.Period == 0) - { - EffectData.bNegateEffectAtEnd = true; - for (const FGMCAttributeModifier& Modifier : EffectData.Modifiers) + if (EffectData.EffectType == EGMASEffectType::Instant) { - OwnerAbilityComponent->ApplyAbilityEffectModifier(Modifier, false); + EndEffect(); } } - - // Tick period at start - if (EffectData.bPeriodTickAtStart && EffectData.Period > 0) - { - PeriodTick(); - } - - // Instant effects instantly end - if (EffectData.bIsInstant) - { - EndEffect(); - } + + StartEffectEvent(); UpdateState(EGMASEffectState::Started, true); } @@ -108,28 +116,41 @@ void UGMCAbilityEffect::StartEffect() void UGMCAbilityEffect::EndEffect() { + // Prevent EndEffect from being called multiple times if (bCompleted) return; + bCompleted = true; if (CurrentState != EGMASEffectState::Ended) { UpdateState(EGMASEffectState::Ended, true); } + - // Only remove tags and abilities if the effect has started - if (!bHasStarted) return; - - if (EffectData.bNegateEffectAtEnd) + // Only remove tags and abilities if the effect has started and applied + if (!bHasStarted || !bHasAppliedEffect) return; + // If the effect is not an instant effect, we need to negate the modifiers + if (IsEffectModifiersRegisterInHistory()) { - for (const FGMCAttributeModifier& Modifier : EffectData.Modifiers) + for (int i = 0; i < EffectData.Modifiers.Num(); i++) { - OwnerAbilityComponent->ApplyAbilityEffectModifier(Modifier, false, true); + if (const FAttribute* Attribute = OwnerAbilityComponent->GetAttributeByTag(EffectData.Modifiers[i].AttributeTag)) + { + Attribute->RemoveTemporalModifier(i, this); + } } } - RemoveTagsFromOwner(); + EndActiveAbilitiesByDefinitionQuery(EffectData.EndAbilityOnEndQuery); + + EndActiveAbilitiesFromOwner(EffectData.CancelAbilityOnEnd); + RemoveTagsFromOwner(EffectData.bPreserveGrantedTagsIfMultiple); RemoveAbilitiesFromOwner(); + + OwnerAbilityComponent->OnEffectRemoved.Broadcast(this); + + EndEffectEvent(); } @@ -160,32 +181,95 @@ void UGMCAbilityEffect::BeginDestroy() { void UGMCAbilityEffect::Tick(float DeltaTime) { - if (bCompleted) return; - EffectData.CurrentDuration += DeltaTime; + // Aherys : I'm not sure if this is correct. Sometime this is GC. We need to catch why, and when. + if (bCompleted || IsUnreachable()) { + if (IsUnreachable()) { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Effect is unreachable : %s"), *EffectData.EffectTag.ToString()); + ensureMsgf(false, TEXT("Effect is being ticked after being completed or GC : %s"), *EffectData.EffectTag.ToString()); + } + return; + } + + EffectData.CurrentDuration = OwnerAbilityComponent->ActionTimer - EffectData.StartTime; TickEvent(DeltaTime); // Ensure tag requirements are met before applying the effect - if( ( EffectData.MustHaveTags.Num() > 0 && !DoesOwnerHaveTagFromContainer(EffectData.MustHaveTags) ) || + if( (EffectData.MustHaveTags.Num() > 0 && !DoesOwnerHaveTagFromContainer(EffectData.MustHaveTags) ) || DoesOwnerHaveTagFromContainer(EffectData.MustNotHaveTags) ) { EndEffect(); } + // query to maintain effect + if ( !EffectData.MustMaintainQuery.IsEmpty() && EffectData.MustMaintainQuery.Matches(OwnerAbilityComponent->GetActiveTags())) + { + EndEffect(); + } - // If there's a period, check to see if it's time to tick - if (!IsPeriodPaused() && EffectData.Period > 0 && CurrentState == EGMASEffectState::Started) + + if (!IsPaused() && CurrentState == EGMASEffectState::Started && AttributeDynamicCondition()) { - const float Mod = FMath::Fmod(OwnerAbilityComponent->ActionTimer, EffectData.Period); - if (Mod < PrevPeriodMod) + if (EffectData.EffectType == EGMASEffectType::Ticking) { + // If there's a period, check to see if it's time to tick + + for (int i = 0; i < EffectData.Modifiers.Num(); i++) { + FGMCAttributeModifier Modifier = EffectData.Modifiers[i]; + Modifier.InitModifier(this, OwnerAbilityComponent->ActionTimer, i, IsEffectModifiersRegisterInHistory(), DeltaTime); + OwnerAbilityComponent->ApplyAbilityAttributeModifier(Modifier); + } // End for each modifier + + + } // End Ticking + else if (EffectData.EffectType == EGMASEffectType::Periodic) { - PeriodTick(); + + + const float CurrentElapsedTime = OwnerAbilityComponent->ActionTimer - EffectData.StartTime; + float PreviousElapsedTime = CurrentElapsedTime - OwnerAbilityComponent->GMCMovementComponent->GetMoveDeltaTime(); + PreviousElapsedTime = FMath::Max(PreviousElapsedTime, 0.f); // Ensure we don't go negative + + int32 PreviousPeriod = FMath::TruncToInt(PreviousElapsedTime / EffectData.PeriodicInterval); + int32 CurrentPeriod = FMath::TruncToInt(CurrentElapsedTime / EffectData.PeriodicInterval); + + if (CurrentPeriod > PreviousPeriod) { + int32 NumTickToApply = CurrentPeriod - PreviousPeriod; + + for (int i = 0; i < NumTickToApply; i++) { + for (int y = 0; y < EffectData.Modifiers.Num(); y++) { + FGMCAttributeModifier Modifier = EffectData.Modifiers[y]; + Modifier.InitModifier(this, OwnerAbilityComponent->ActionTimer, y, IsEffectModifiersRegisterInHistory(), 1.f); + OwnerAbilityComponent->ApplyAbilityAttributeModifier(Modifier); + } + } + + if (NumTickToApply > 0) + { + PeriodTick(); + } + } + } - PrevPeriodMod = Mod; } + + + CheckState(); } +int32 UGMCAbilityEffect::CalculatePeriodicTicksBetween(float Period, float StartActionTimer, float EndActionTimer) +{ + if (Period <= 0.0f || EndActionTimer <= StartActionTimer) { return 0; } + + float FirstTick = FMath::CeilToFloat(StartActionTimer / Period) * Period; + if (FirstTick > EndActionTimer) { return 0; } + + + float LastTick = FMath::FloorToFloat(EndActionTimer / Period) * Period; + + return FMath::RoundToInt((LastTick - FirstTick) / Period) + 1; +} + void UGMCAbilityEffect::TickEvent_Implementation(float DeltaTime) { } @@ -198,12 +282,11 @@ bool UGMCAbilityEffect::AttributeDynamicCondition_Implementation() const { void UGMCAbilityEffect::PeriodTick() { - if (AttributeDynamicCondition()) { - for (const FGMCAttributeModifier& AttributeModifier : EffectData.Modifiers) - { - OwnerAbilityComponent->ApplyAbilityEffectModifier(AttributeModifier, true); - } - } + PeriodTickEvent(); +} + +void UGMCAbilityEffect::PeriodTickEvent_Implementation() +{ } void UGMCAbilityEffect::UpdateState(EGMASEffectState State, bool Force) @@ -216,9 +299,43 @@ void UGMCAbilityEffect::UpdateState(EGMASEffectState State, bool Force) CurrentState = State; } -bool UGMCAbilityEffect::IsPeriodPaused() +bool UGMCAbilityEffect::IsPaused() +{ + return DoesOwnerHaveTagFromContainer(EffectData.PauseEffect); +} + +bool UGMCAbilityEffect::IsEffectModifiersRegisterInHistory() const +{ + return EffectData.EffectType != EGMASEffectType::Instant && EffectData.bNegateEffectAtEnd; +} + +float UGMCAbilityEffect::ProcessCustomModifier(const TSubclassOf& MCClass, const FAttribute* Attribute) +{ + UGMCAttributeModifierCustom_Base** MCI = CustomModifiersInstances.Find(MCClass); + if (MCI == nullptr) + { + MCI = &CustomModifiersInstances.Add(MCClass, NewObject(this, MCClass)); + } + + if (*MCI == nullptr) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Custom Modifier Instance is null for class %s in UGMCAbilityEffect::ProcessCustomModifier"), *MCClass->GetName()); + return 0.f; + } + + return (*MCI)->Calculate(this, Attribute); +} + +void UGMCAbilityEffect::GetOwnerActor(AActor*& OutOwnerActor) const { - return DoesOwnerHaveTagFromContainer(EffectData.PausePeriodicEffect); + if (OwnerAbilityComponent) + { + OutOwnerActor = OwnerAbilityComponent->GetOwner(); + } + else + { + OutOwnerActor = nullptr; + } } void UGMCAbilityEffect::AddTagsToOwner() @@ -231,14 +348,22 @@ void UGMCAbilityEffect::AddTagsToOwner() void UGMCAbilityEffect::RemoveTagsFromOwner(bool bPreserveOnMultipleInstances) { - - if (bPreserveOnMultipleInstances && EffectData.EffectTag.IsValid()) { - TArray ActiveEffect = OwnerAbilityComponent->GetActivesEffectByTag(EffectData.EffectTag); - - if (ActiveEffect.Num() > 1) { - return; + if (bPreserveOnMultipleInstances) + { + if (EffectData.EffectTag.IsValid()) { + TArray ActiveEffect = OwnerAbilityComponent->GetActiveEffectsByTag(EffectData.EffectTag); + + if (ActiveEffect.Num() > 1) { + return; + } + } + else + { + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Effect Tag is not valid with PreserveMultipleInstances in UGMCAbilityEffect::RemoveTagsFromOwner")); } } + + for (const FGameplayTag Tag : EffectData.GrantedTags) { @@ -263,15 +388,14 @@ void UGMCAbilityEffect::RemoveAbilitiesFromOwner() } -void UGMCAbilityEffect::EndActiveAbilitiesFromOwner() { +void UGMCAbilityEffect::EndActiveAbilitiesFromOwner(const FGameplayTagContainer& TagContainer) { - for (const FGameplayTag Tag : EffectData.CancelAbilityOnActivation) + for (const FGameplayTag Tag : TagContainer) { OwnerAbilityComponent->EndAbilitiesByTag(Tag); } } - bool UGMCAbilityEffect::DoesOwnerHaveTagFromContainer(FGameplayTagContainer& TagContainer) const { for (const FGameplayTag Tag : TagContainer) @@ -324,3 +448,26 @@ void UGMCAbilityEffect::CheckState() default: break; } } + +void UGMCAbilityEffect::EndActiveAbilitiesByDefinitionQuery(FGameplayTagQuery EndAbilityOnActivationViaDefinitionQuery) +{ + + if (EndAbilityOnActivationViaDefinitionQuery.IsEmpty()) return; + + int NumCancelled = OwnerAbilityComponent->EndAbilitiesByQuery(EndAbilityOnActivationViaDefinitionQuery); + + UE_LOG(LogGMCAbilitySystem, Verbose, TEXT("Effect %s cancelled %d ability(ies) via EffectDefinition query."), + *EffectData.EffectTag.ToString(), NumCancelled); +} + +void UGMCAbilityEffect::ModifyMustMaintainQuery(const FGameplayTagQuery& NewQuery) +{ + EffectData.MustMaintainQuery = NewQuery; + UE_LOG(LogGMCAbilitySystem, Verbose, TEXT("MustMainQuery modified: %s"), *NewQuery.GetDescription()); +} + +void UGMCAbilityEffect::ModifyEndAbilitiesOnEndQuery(const FGameplayTagQuery& NewQuery) +{ + EffectData.EndAbilityOnEndQuery = NewQuery; + UE_LOG(LogGMCAbilitySystem, Verbose, TEXT("EndAbilityOnEndViaDefinitionQuery modified: %s"), *NewQuery.GetDescription()); +} diff --git a/Source/GMCAbilitySystem/Private/Utility/GMASBoundQueue.cpp b/Source/GMCAbilitySystem/Private/Utility/GMASBoundQueue.cpp new file mode 100644 index 00000000..1a3be8b2 --- /dev/null +++ b/Source/GMCAbilitySystem/Private/Utility/GMASBoundQueue.cpp @@ -0,0 +1 @@ +#include "Utility/GMASBoundQueue.h" diff --git a/Source/GMCAbilitySystem/Private/Utility/GameplayElementMapping.cpp b/Source/GMCAbilitySystem/Private/Utility/GameplayElementMapping.cpp index eb72749c..a05ac1e4 100644 --- a/Source/GMCAbilitySystem/Private/Utility/GameplayElementMapping.cpp +++ b/Source/GMCAbilitySystem/Private/Utility/GameplayElementMapping.cpp @@ -148,7 +148,11 @@ void FGMCGameplayElementTagPropertyMap::Initialize(UObject* Owner, UGMC_AbilityS // We're invalid, so remove. UE_LOG(LogGMCAbilitySystem, Error, TEXT("FGMCGameplayElementTagPropertyMap: Removing invalid mapping [index %d, tags %s, property %s] for %s"), Index, *Mapping.TagsToMap.ToString(), *Mapping.PropertyName.ToString(), *GetNameSafe(Owner)); - PropertyMappings.RemoveAtSwap(Index, 1, false); +#if ENGINE_MAJOR_VERSION >=5 && ENGINE_MINOR_VERSION >= 4 + PropertyMappings.RemoveAtSwap(Index, 1, EAllowShrinking::No); +#else + PropertyMappings.RemoveAtSwap(Index, 1, false); +#endif } ApplyCurrentTags(); diff --git a/Source/GMCAbilitySystem/Public/Ability/GMCAbility.h b/Source/GMCAbilitySystem/Public/Ability/GMCAbility.h index ce52bd2e..9809d8f8 100644 --- a/Source/GMCAbilitySystem/Public/Ability/GMCAbility.h +++ b/Source/GMCAbilitySystem/Public/Ability/GMCAbility.h @@ -11,6 +11,7 @@ UENUM(BlueprintType) enum class EAbilityState : uint8 { + PreExecution, Initialized, Running, Waiting, @@ -32,11 +33,12 @@ class GMCABILITYSYSTEM_API UGMCAbility : public UObject, public IGameplayTaskOwn UFUNCTION(BlueprintCallable, Category = "GMCAbilitySystem") virtual UWorld* GetWorld() const override; - + + //// Ability State // EAbilityState. Use Getters/Setters UPROPERTY(BlueprintReadOnly, Category = "GMCAbilitySystem") - EAbilityState AbilityState; + EAbilityState AbilityState = EAbilityState::PreExecution; // Data used to execute this ability UPROPERTY(BlueprintReadOnly, Category = "GMCAbilitySystem") @@ -46,7 +48,8 @@ class GMCABILITYSYSTEM_API UGMCAbility : public UObject, public IGameplayTaskOwn UFUNCTION() int GetNextTaskID(){TaskIDCounter += 1; return TaskIDCounter;} - + + bool IsActive() const; int GetAbilityID() const {return AbilityID;};; @@ -74,6 +77,9 @@ class GMCABILITYSYSTEM_API UGMCAbility : public UObject, public IGameplayTaskOwn UFUNCTION(BlueprintNativeEvent, meta=(DisplayName="Ability PreExecution Check"), Category="GMCAbilitySystem|Ability") bool PreExecuteCheckEvent(); + // Declare an effect, that will allow to automatically end it when the ability ends + void DeclareEffect(int OutEffectHandle, EGMCAbilityEffectQueueType EffectType); + UFUNCTION() virtual bool PreBeginAbility(); @@ -137,8 +143,9 @@ class GMCABILITYSYSTEM_API UGMCAbility : public UObject, public IGameplayTaskOwn bool bAllowMultipleInstances {false}; // Check to see if affected attributes in the AbilityCost would still be >= 0 after committing the cost + // Delta time can be required if the cost is time based. UFUNCTION(BlueprintPure, Category = "GMCAbilitySystem") - virtual bool CanAffordAbilityCost() const; + virtual bool CanAffordAbilityCost(float DeltaTime = 1.f) const; // Apply the effects in AbilityCost and (Re-)apply the CooldownTime of this ability // Warning : Will apply CooldownTime regardless of already being on cooldown @@ -205,12 +212,32 @@ class GMCABILITYSYSTEM_API UGMCAbility : public UObject, public IGameplayTaskOwn // Prevent Abilities with these tags from activating when this ability is activated FGameplayTagContainer BlockOtherAbility; + UPROPERTY(EditDefaultsOnly, Category = "GMCAbilitySystem", meta=(Categories="Ability")) + // If those ability are active, they will prevent this ability from activating + FGameplayTagContainer BlockedByOtherAbility; + + /** + * Cancels active abilities based on specific conditions. + * + * This method performs the cancellation of abilities on the owner ability component depending on the tags specified in + * `CancelAbilitiesWithTag` and `EndOtherAbilitiesQuery`. It also prevents an ability from unintentionally canceling itself. + * + * Abilities are checked and canceled as follows: + * - Each tag in `CancelAbilitiesWithTag` is evaluated against the current ability tag (`AbilityTag`). If the tag matches, + * the current ability is skipped. + * - Calls `EndAbilitiesByTag` on the owner ability component for abilities matching a tag in `CancelAbilitiesWithTag`. + * - Iterates through the active abilities in the owner ability component and checks if they match the query in + * `EndOtherAbilitiesQuery`. If a match is found, those abilities are set to pending end. + * + */ + virtual void CancelConflictingAbilities(); + /** * If true, activate on movement tick, if false, activate on ancillary tick. Defaults to true. * Should be set to false for actions that should not be replayed on mispredictions. i.e. firing a weapon */ UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "GMCAbilitySystem") - bool bActivateOnMovementTick = true; + bool bActivateOnMovementTick = false; UFUNCTION() void ServerConfirm(); @@ -255,8 +282,31 @@ class GMCABILITYSYSTEM_API UGMCAbility : public UObject, public IGameplayTaskOwn public: FString ToString() const{ - return FString::Printf(TEXT("[name: ] %s (State %s) [Tag %s] | NumTasks %d"), *GetName(), *EnumToString(AbilityState), *AbilityTag.ToString(), RunningTasks.Num()); + return FString::Printf(TEXT("[name: %s] [Tag %s] (%s) | NumTasks %d"), *GetName().Left(30), *AbilityTag.ToString(), *EnumToString(AbilityState), RunningTasks.Num()); } + UPROPERTY(EditDefaultsOnly, Category = "GMCAbilitySystem") + // Container for a more generalized definition of abilities + FGameplayTagContainer AbilityDefinition; + + + TMap DeclaredEffect; + + // Queries + UPROPERTY(EditDefaultsOnly, Category = "GMCAbilitySystem", meta=(DisplayName="Activation Tags Query")) + // query must match at activation + FGameplayTagQuery ActivationQuery; + + UPROPERTY(EditDefaultsOnly, Category = "GMCAbilitySystem", meta=(DisplayName="Cancel Ability via Definition Query")) + // End Abilities via Definition + FGameplayTagQuery EndOtherAbilitiesQuery; + + UPROPERTY(EditDefaultsOnly, Category = "GMCAbilitySystem", meta=(DisplayName="Block Other Ability via Definition Query")) + // Block Abilities via Definition + FGameplayTagQuery BlockOtherAbilitiesQuery; + + UFUNCTION(BlueprintCallable, Category = "GMAS|Abilities|Queries") + void ModifyBlockOtherAbilitiesViaDefinitionQuery(const FGameplayTagQuery& NewQuery); + }; diff --git a/Source/GMCAbilitySystem/Public/Ability/GMCAbilityMapData.h b/Source/GMCAbilitySystem/Public/Ability/GMCAbilityMapData.h index 6dd93d84..361de722 100644 --- a/Source/GMCAbilitySystem/Public/Ability/GMCAbilityMapData.h +++ b/Source/GMCAbilitySystem/Public/Ability/GMCAbilityMapData.h @@ -33,7 +33,7 @@ UCLASS() class GMCABILITYSYSTEM_API UGMCAbilityMapData : public UPrimaryDataAsset{ GENERATED_BODY() - UPROPERTY(EditDefaultsOnly, Category = "GMCAbilitySystem") + UPROPERTY(EditDefaultsOnly, Category = "GMCAbilitySystem", meta=(TitleProperty="{InputTag}")) TArray AbilityMapData; public: diff --git a/Source/GMCAbilitySystem/Public/Ability/Tasks/GMCAbilityTaskBase.h b/Source/GMCAbilitySystem/Public/Ability/Tasks/GMCAbilityTaskBase.h index 7deb59bc..4ba885dc 100644 --- a/Source/GMCAbilitySystem/Public/Ability/Tasks/GMCAbilityTaskBase.h +++ b/Source/GMCAbilitySystem/Public/Ability/Tasks/GMCAbilityTaskBase.h @@ -7,6 +7,8 @@ class UGMC_AbilitySystemComponent; class UGameplayTasksComponent; + + UCLASS(Abstract, BlueprintType, meta = (ExposedAsyncProxy=AsyncTask), config = Game) class GMCABILITYSYSTEM_API UGMCAbilityTaskBase : public UGameplayTask { @@ -64,10 +66,7 @@ class GMCABILITYSYSTEM_API UGMCAbilityTaskBase : public UGameplayTask // Task must make sure this is handled properly virtual void ClientProgressTask(); - void Heartbeat() - { - LastHeartbeatReceivedTime = AbilitySystemComponent->ActionTimer; - }; + virtual void Heartbeat(); protected: bool bTaskCompleted; @@ -75,13 +74,19 @@ class GMCABILITYSYSTEM_API UGMCAbilityTaskBase : public UGameplayTask /** Task Owner that created us */ TWeakObjectPtr TaskOwner; + // Whether this task is running on a client or a client on a listen server + bool IsClientOrRemoteListenServerPawn() const; + private: // How often client sends heartbeats to server - float HeartbeatInterval = .5f; + float HeartbeatInterval = .2f; // Max time between heartbeats before server cancels task - float HeartbeatMaxInterval = 1.f; + // Aherys: previous value was 0.3f it's maybe a bit too low for harsh network conditions + float HeartbeatMaxInterval = 3.f; - float ClientLastHeartbeatSentTime; - float LastHeartbeatReceivedTime; + float ClientLastHeartbeatSentTime = 0.f; + float LastHeartbeatReceivedTime = 0.f; + + }; diff --git a/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataByte.h b/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataByte.h index 9a004b9e..5559228d 100644 --- a/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataByte.h +++ b/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataByte.h @@ -32,7 +32,7 @@ class GMCABILITYSYSTEM_API UGMCAbilityTask_SetTargetDataByte : public UGMCAbilit virtual void ProgressTask(FInstancedStruct& TaskData) override; virtual void ClientProgressTask() override; - UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"), DisplayName="Set Target Data (Byte)",Category = "GMCAbilitySystem/Tasks") + UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"), DisplayName="Set Target Data (Byte)", Category = "GMAS|Abilities|Tasks") static UGMCAbilityTask_SetTargetDataByte* SetTargetDataByte(UGMCAbility* OwningAbility, uint8 Byte); //Overriding BP async action base diff --git a/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataFloat.h b/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataFloat.h index d8a6f909..7de9575f 100644 --- a/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataFloat.h +++ b/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataFloat.h @@ -32,7 +32,7 @@ class GMCABILITYSYSTEM_API UGMCAbilityTask_SetTargetDataFloat : public UGMCAbili virtual void ProgressTask(FInstancedStruct& TaskData) override; virtual void ClientProgressTask() override; - UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"), DisplayName="Set Target Data (Float)",Category = "GMCAbilitySystem/Tasks") + UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"), DisplayName="Set Target Data (Float)", Category = "GMAS|Abilities|Tasks") static UGMCAbilityTask_SetTargetDataFloat* SetTargetDataFloat(UGMCAbility* OwningAbility, float Float); //Overriding BP async action base diff --git a/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataGameplayTag.h b/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataGameplayTag.h index a3bfc5d2..9050aa78 100644 --- a/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataGameplayTag.h +++ b/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataGameplayTag.h @@ -39,7 +39,7 @@ class GMCABILITYSYSTEM_API UGMCAbilityTask_SetTargetDataGameplayTag : public UGM virtual void ProgressTask(FInstancedStruct& TaskData) override; virtual void ClientProgressTask() override; - UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"), DisplayName="Set Target Data (Gameplay Tag)",Category = "GMCAbilitySystem/Tasks") + UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"), DisplayName="Set Target Data (Gameplay Tag)", Category = "GMAS|Abilities|Tasks") static UGMCAbilityTask_SetTargetDataGameplayTag* SetTargetDataGameplayTag(UGMCAbility* OwningAbility, FGameplayTag InTag); //Overriding BP async action base diff --git a/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataHit.h b/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataHit.h new file mode 100644 index 00000000..af56ba8f --- /dev/null +++ b/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataHit.h @@ -0,0 +1,48 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "GMCAbilityTaskBase.h" +#include "GMCAbilityTaskData.h" +#include "Ability/GMCAbility.h" +#include "LatentActions.h" +#include "SetTargetDataHit.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FUGMCAbilityTaskTargetDataHitAsyncActionPin, FHitResult, target); + +USTRUCT(BlueprintType) +struct FGMCAbilityTaskTargetDataHit : public FGMCAbilityTaskData +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadWrite, Category = "GMCAbilitySystem") + FHitResult Target{FHitResult()}; +}; + + +/** + * + */ +UCLASS() +class GMCABILITYSYSTEM_API UGMCAbilityTask_SetTargetDataHit : public UGMCAbilityTaskBase +{ + GENERATED_BODY() + +public: + + UPROPERTY(BlueprintAssignable) + FUGMCAbilityTaskTargetDataHitAsyncActionPin Completed; + + UPROPERTY(BlueprintReadWrite, Category = "GMCAbilitySystem") + FHitResult Target; + + virtual void ProgressTask(FInstancedStruct& TaskData) override; + virtual void ClientProgressTask() override; + + UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"), DisplayName="Set Target Data (Hit Result)", Category = "GMAS|Abilities|Tasks") + static UGMCAbilityTask_SetTargetDataHit* SetTargetDataHit(UGMCAbility* OwningAbility, FHitResult InHit); + + //Overriding BP async action base + virtual void Activate() override; +}; diff --git a/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataInstancedStruct.h b/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataInstancedStruct.h new file mode 100644 index 00000000..3f501183 --- /dev/null +++ b/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataInstancedStruct.h @@ -0,0 +1,47 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "GMCAbilityTaskBase.h" +#include "GMCAbilityTaskData.h" +#include "Ability/GMCAbility.h" +#include "LatentActions.h" +#include "SetTargetDataInstancedStruct.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FUGMCAbilityTaskTargetDataInstancedStructAsyncActionPin, FInstancedStruct, Target); + + +USTRUCT(BlueprintType) +struct FGMCAbilityTaskTargetDataInstancedStruct : public FGMCAbilityTaskData +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadWrite, Category = "GMCAbilitySystem") + FInstancedStruct Target{FInstancedStruct()}; +}; + +/** + * + */ +UCLASS() +class GMCABILITYSYSTEM_API UGMCAbilityTask_SetTargetDataInstancedStruct : public UGMCAbilityTaskBase +{ + GENERATED_BODY() + +public: + UPROPERTY(BlueprintAssignable) + FUGMCAbilityTaskTargetDataInstancedStructAsyncActionPin Completed; + + UPROPERTY(BlueprintReadWrite, Category = "GMCAbilitySystem") + FInstancedStruct Target; + + virtual void ProgressTask(FInstancedStruct& TaskData) override; + virtual void ClientProgressTask() override; + + UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"), DisplayName="Set Target Data (Instanced Struct)", Category = "GMAS|Abilities|Tasks") + static UGMCAbilityTask_SetTargetDataInstancedStruct* SetTargetDataInstancedStruct(UGMCAbility* OwningAbility, FInstancedStruct InstancedStruct); + + //Overriding BP async action base + virtual void Activate() override; +}; diff --git a/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataInt.h b/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataInt.h index 5616714b..37789aa0 100644 --- a/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataInt.h +++ b/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataInt.h @@ -32,7 +32,7 @@ class GMCABILITYSYSTEM_API UGMCAbilityTask_SetTargetDataInt : public UGMCAbility virtual void ProgressTask(FInstancedStruct& TaskData) override; virtual void ClientProgressTask() override; - UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"), DisplayName="Set Target Data (Int)",Category = "GMCAbilitySystem/Tasks") + UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"), DisplayName="Set Target Data (Int)", Category = "GMAS|Abilities|Tasks") static UGMCAbilityTask_SetTargetDataInt* SetTargetDataInt(UGMCAbility* OwningAbility, int Int); //Overriding BP async action base diff --git a/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataObject.h b/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataObject.h index ab1fa52d..5bc5429c 100644 --- a/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataObject.h +++ b/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataObject.h @@ -33,7 +33,7 @@ class GMCABILITYSYSTEM_API UGMCAbilityTask_SetTargetDataObject : public UGMCAbil virtual void ProgressTask(FInstancedStruct& TaskData) override; virtual void ClientProgressTask() override; - UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"), DisplayName="Set Target Data (Object)",Category = "GMCAbilitySystem/Tasks") + UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"), DisplayName="Set Target Data (Object)", Category = "GMAS|Abilities|Tasks") static UGMCAbilityTask_SetTargetDataObject* SetTargetDataObject(UGMCAbility* OwningAbility, UObject* Object); //Overriding BP async action base diff --git a/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataTransform.h b/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataTransform.h new file mode 100644 index 00000000..14854425 --- /dev/null +++ b/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataTransform.h @@ -0,0 +1,47 @@ +// + +#pragma once + +#include "CoreMinimal.h" +#include "GMCAbilityTaskBase.h" +#include "GMCAbilityTaskData.h" +#include "Ability/GMCAbility.h" +#include "LatentActions.h" +#include "SetTargetDataTransform.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FUGMCAbilityTaskTargetDataTransformAsyncActionPin, FTransform, Target); + + +USTRUCT(BlueprintType) +struct FGMCAbilityTaskTargetDataTransform : public FGMCAbilityTaskData +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadWrite, Category = "GMCAbilitySystem") + FTransform Target{FTransform()}; +}; + +/** + * + */ +UCLASS() +class GMCABILITYSYSTEM_API UGMCAbilityTask_SetTargetDataTransform : public UGMCAbilityTaskBase +{ + GENERATED_BODY() + +public: + UPROPERTY(BlueprintAssignable) + FUGMCAbilityTaskTargetDataTransformAsyncActionPin Completed; + + UPROPERTY(BlueprintReadWrite, Category = "GMCAbilitySystem") + FTransform Target; + + virtual void ProgressTask(FInstancedStruct& TaskData) override; + virtual void ClientProgressTask() override; + + UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"), DisplayName="Set Target Data (Transform)", Category = "GMAS|Abilities|Tasks") + static UGMCAbilityTask_SetTargetDataTransform* SetTargetDataTransform(UGMCAbility* OwningAbility, FTransform Transform); + + //Overriding BP async action base + virtual void Activate() override; +}; diff --git a/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataVector3.h b/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataVector3.h index 2f629314..89e889c9 100644 --- a/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataVector3.h +++ b/Source/GMCAbilitySystem/Public/Ability/Tasks/SetTargetDataVector3.h @@ -32,7 +32,7 @@ class GMCABILITYSYSTEM_API UGMCAbilityTask_SetTargetDataVector3 : public UGMCAbi virtual void ProgressTask(FInstancedStruct& TaskData) override; virtual void ClientProgressTask() override; - UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"), DisplayName="Set Target Data (Vector3)",Category = "GMCAbilitySystem/Tasks") + UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"), DisplayName="Set Target Data (Vector3)", Category = "GMAS|Abilities|Tasks") static UGMCAbilityTask_SetTargetDataVector3* SetTargetDataVector3(UGMCAbility* OwningAbility, FVector Vector3); //Overriding BP async action base diff --git a/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitDelay.h b/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitDelay.h index 6d861bf8..858ef7dd 100644 --- a/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitDelay.h +++ b/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitDelay.h @@ -21,7 +21,7 @@ class GMCABILITYSYSTEM_API UGMCAbilityTask_WaitDelay : public UGMCAbilityTaskBas // virtual FString GetDebugString() const override; /** Wait specified time. This is functionally the same as a standard Delay node. */ - UFUNCTION(BlueprintCallable, Category="GMCAbility|Tasks", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE")) + UFUNCTION(BlueprintCallable, Category = "GMAS|Abilities|Tasks", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE")) static UGMCAbilityTask_WaitDelay* WaitDelay(UGMCAbility* OwningAbility, float Time); private: diff --git a/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForGMCMontageChange.h b/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForGMCMontageChange.h index b934c765..4902efb7 100644 --- a/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForGMCMontageChange.h +++ b/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForGMCMontageChange.h @@ -3,7 +3,7 @@ #include "GMCOrganicMovementComponent.h" #include "WaitForGMCMontageChange.generated.h" -DECLARE_DYNAMIC_MULTICAST_DELEGATE(FGMCAbilityTaskWaitForGMCMontageChangeDelayOutputPin); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGMCAbilityTaskWaitForGMCMontageChangeDelayOutputPin, UAnimMontage*, CurrentMontage); UCLASS() class GMCABILITYSYSTEM_API UGMCAbilityTask_WaitForGMCMontageChange : public UGMCAbilityTaskBase @@ -20,7 +20,7 @@ class GMCABILITYSYSTEM_API UGMCAbilityTask_WaitForGMCMontageChange : public UGMC // virtual FString GetDebugString() const override; /** Triggers if the montage changes (Allows for Networked Interrupt). *ONLY WORKS FOR ORGANIC MOVEMENT COMPONENT* */ - UFUNCTION(BlueprintCallable, Category="GMCAbility|Tasks", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE")) + UFUNCTION(BlueprintCallable, Category = "GMAS|Abilities|Tasks", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE")) static UGMCAbilityTask_WaitForGMCMontageChange* WaitForGMCMontageChange(UGMCAbility* OwningAbility); UPROPERTY() @@ -31,4 +31,7 @@ class GMCABILITYSYSTEM_API UGMCAbilityTask_WaitForGMCMontageChange : public UGMC private: void OnFinish(); + + UPROPERTY() + UAnimMontage* RunningMontage; }; \ No newline at end of file diff --git a/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForGameplayTagChange.h b/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForGameplayTagChange.h index 4e858de6..6c945d78 100644 --- a/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForGameplayTagChange.h +++ b/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForGameplayTagChange.h @@ -29,7 +29,7 @@ class GMCABILITYSYSTEM_API UGMCAbilityTask_WaitForGameplayTagChange : public UGM FGameplayTagContainer Tags; EGMCWaitForGameplayTagChangeType ChangeType; - UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly="true", HidePin="OwningAbility", DefaultToSelf="OwningAbility", DisplayName="Wait for Gameplay Tag Change"), Category="GMCAbilitySystem|Tasks") + UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly="true", HidePin="OwningAbility", DefaultToSelf="OwningAbility", DisplayName="Wait for Gameplay Tag Change"), Category = "GMAS|Abilities|Tasks") static UGMCAbilityTask_WaitForGameplayTagChange* WaitForGameplayTagChange(UGMCAbility* OwningAbility, const FGameplayTagContainer& WatchedTags, EGMCWaitForGameplayTagChangeType ChangeType = Changed); virtual void Activate() override; diff --git a/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForInputKeyPress.h b/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForInputKeyPress.h index 0847de03..3867c9fc 100644 --- a/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForInputKeyPress.h +++ b/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForInputKeyPress.h @@ -8,7 +8,7 @@ #include "LatentActions.h" #include "WaitForInputKeyPress.generated.h" -DECLARE_DYNAMIC_MULTICAST_DELEGATE(FAbilityTaskWaitForInputKeyPress); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAbilityTaskWaitForInputKeyPress, float, Duration); /** * @@ -25,22 +25,43 @@ class GMCABILITYSYSTEM_API UGMCAbilityTask_WaitForInputKeyPress : public UGMCAbi virtual void ProgressTask(FInstancedStruct& TaskData) override; virtual void ClientProgressTask() override; - UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"), DisplayName="Wait For Input Key Press",Category = "GMCAbilitySystem/Tasks") - static UGMCAbilityTask_WaitForInputKeyPress* WaitForKeyPress(UGMCAbility* OwningAbility); + UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"), DisplayName="Wait For Input Key Press", Category = "GMAS|Abilities|Tasks") + static UGMCAbilityTask_WaitForInputKeyPress* WaitForKeyPress(UGMCAbility* OwningAbility, bool bCheckForPressDuringActivation = true, float MaxDuration = 0.0f); //Overriding BP async action base virtual void Activate() override; + + virtual void AncillaryTick(float DeltaTime) override; UPROPERTY(BlueprintAssignable) FAbilityTaskWaitForInputKeyPress Completed; + UPROPERTY(BlueprintAssignable) + FAbilityTaskWaitForInputKeyPress OnTick; + + // Called when duration goes over MaxDuration + UPROPERTY(BlueprintAssignable) + FAbilityTaskWaitForInputKeyPress TimedOut; + protected: void OnKeyPressed(const FInputActionValue& InputActionValue); + /** If true, we may complete this task during activation if the ability's input action key is already released. */ + UPROPERTY(Transient) + bool bShouldCheckForPressDuringActivation = false; + private: - UEnhancedInputComponent* GetEnhancedInputComponent() const; + UEnhancedInputComponent* GetEnhancedInputComponent(); int64 InputBindingHandle = -1; + + float MaxDuration; + float StartTime; + float Duration; + bool bTimedOut; + + UPROPERTY() + UInputComponent* InputComponent; }; diff --git a/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForInputKeyPressParameterized.h b/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForInputKeyPressParameterized.h new file mode 100644 index 00000000..a7b8a416 --- /dev/null +++ b/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForInputKeyPressParameterized.h @@ -0,0 +1,70 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Ability/Tasks/GMCAbilityTaskBase.h" +#include "EnhancedInputComponent.h" +#include "Ability/GMCAbility.h" +#include "InstancedStruct.h" +#include "LatentActions.h" +#include "WaitForInputKeyPressParameterized.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAbilityTaskWaitForInputKeyPressParameterized, float, Duration); + +/** + * + */ +UCLASS() +class GMCABILITYSYSTEM_API UGMCAbilityTask_WaitForInputKeyPressParameterized : public UGMCAbilityTaskBase { + GENERATED_BODY() + +public: + + void OnTaskCompleted(); + virtual void OnDestroy(bool bInOwnerFinished) override; + + virtual void ProgressTask(FInstancedStruct& TaskData) override; + virtual void ClientProgressTask() override; + + UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility"), DisplayName = "Wait For Input Key Press Parameterized", Category = "GMAS|Abilities|Tasks") + static UGMCAbilityTask_WaitForInputKeyPressParameterized* WaitForKeyPress(UGMCAbility* OwningAbility, UInputAction* InputAction, bool bCheckForPressDuringActivation = true, float MaxDuration = 0.0f); + + //Overriding BP async action base + virtual void Activate() override; + + virtual void AncillaryTick(float DeltaTime) override; + + UPROPERTY(BlueprintAssignable) + FAbilityTaskWaitForInputKeyPressParameterized Completed; + + UPROPERTY(BlueprintAssignable) + FAbilityTaskWaitForInputKeyPressParameterized OnTick; + + // Called when duration goes over MaxDuration + UPROPERTY(BlueprintAssignable) + FAbilityTaskWaitForInputKeyPressParameterized TimedOut; + + UPROPERTY() + UInputAction* InputActionToWaitFor = nullptr; + +protected: + + void OnKeyPressed(const FInputActionValue& InputActionValue); + + /** If true, we may complete this task during activation if the ability's input action key is already released. */ + UPROPERTY(Transient) + bool bShouldCheckForPressDuringActivation = false; + +private: + + UEnhancedInputComponent* GetEnhancedInputComponent(); + + int64 InputBindingHandle = -1; + + float MaxDuration; + float StartTime; + float Duration; + bool bTimedOut; + + UPROPERTY() + UInputComponent* InputComponent; +}; diff --git a/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForInputKeyRelease.h b/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForInputKeyRelease.h index 450ce726..d2e9a945 100644 --- a/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForInputKeyRelease.h +++ b/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForInputKeyRelease.h @@ -6,7 +6,7 @@ #include "WaitForInputKeyRelease.generated.h" // DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FUWaitForInputKeyReleaseAsyncActionPin, float, DurationHeld); -DECLARE_DYNAMIC_MULTICAST_DELEGATE(FGMCAbilityTaskWaitForInputKeyRelease); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGMCAbilityTaskWaitForInputKeyRelease, float, Duration); UCLASS() @@ -28,15 +28,24 @@ class GMCABILITYSYSTEM_API UGMCAbilityTask_WaitForInputKeyRelease : public UGMCA * @param bCheckForReleaseDuringActivation If true, we may complete this task during activation if the ability's input action key is already released. * @return */ - UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"), DisplayName="Wait For Input Key Release",Category = "GMCAbilitySystem/Tasks") - static UGMCAbilityTask_WaitForInputKeyRelease* WaitForKeyRelease(UGMCAbility* OwningAbility, bool bCheckForReleaseDuringActivation = true); + UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"), DisplayName="Wait For Input Key Release", Category = "GMAS|Abilities|Tasks") + static UGMCAbilityTask_WaitForInputKeyRelease* WaitForKeyRelease(UGMCAbility* OwningAbility, bool bCheckForReleaseDuringActivation = true, float MaxDuration = 0.0f); //Overriding BP async action base virtual void Activate() override; + virtual void AncillaryTick(float DeltaTime) override; + UPROPERTY(BlueprintAssignable) FGMCAbilityTaskWaitForInputKeyRelease Completed; + UPROPERTY(BlueprintAssignable) + FGMCAbilityTaskWaitForInputKeyRelease OnTick; + + // Called when duration goes over MaxDuration + UPROPERTY(BlueprintAssignable) + FGMCAbilityTaskWaitForInputKeyRelease TimedOut; + protected: void OnKeyReleased(const FInputActionValue& InputActionValue); @@ -50,10 +59,13 @@ class GMCABILITYSYSTEM_API UGMCAbilityTask_WaitForInputKeyRelease : public UGMCA UEnhancedInputComponent* GetEnhancedInputComponent() const; int64 InputBindingHandle = -1; + + float MaxDuration; - // Todo: Add duration back in float StartTime; float Duration; double OldTime; + + bool bTimedOut = false; }; \ No newline at end of file diff --git a/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForRotateYawTowardsDirection.h b/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForRotateYawTowardsDirection.h new file mode 100644 index 00000000..7acd362c --- /dev/null +++ b/Source/GMCAbilitySystem/Public/Ability/Tasks/WaitForRotateYawTowardsDirection.h @@ -0,0 +1,40 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Ability/Tasks/GMCAbilityTaskBase.h" +#include "GMCMovementUtilityComponent.h" +#include "WaitForRotateYawTowardsDirection.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGMCAbilityTaskWaitForRotateYawTowardsDirectionOutputPin, float, Duration); + +class UGMCMovementComponent; + +UCLASS() +class GMCABILITYSYSTEM_API UGMCAbilityTask_RotateYawTowardsDirection : public UGMCAbilityTaskBase +{ + GENERATED_UCLASS_BODY() + + virtual void Activate() override; + virtual void Tick(float DeltaTime) override; + + UPROPERTY(BlueprintAssignable) + FGMCAbilityTaskWaitForRotateYawTowardsDirectionOutputPin Completed; + + UFUNCTION(BlueprintCallable, Category = "GMAS|Abilities|Tasks", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE")) + static UGMCAbilityTask_RotateYawTowardsDirection* WaitForRotateYawTowardsDirection(UGMCAbility* OwningAbility, FVector TargetDirection, float RotationSpeed); + +private: + + UPROPERTY() + UGMC_MovementUtilityCmp* MovementComponent; + + UPROPERTY() + FVector DesiredDirection; + + UPROPERTY() + float RotationSpeed; + + float StartTime = 0.0f; + + void OnFinish(); +}; diff --git a/Source/GMCAbilitySystem/Public/Attributes/FAttributeUnitTest.cpp b/Source/GMCAbilitySystem/Public/Attributes/FAttributeUnitTest.cpp new file mode 100644 index 00000000..9b91c419 --- /dev/null +++ b/Source/GMCAbilitySystem/Public/Attributes/FAttributeUnitTest.cpp @@ -0,0 +1,7 @@ +// ReSharper disable CppExpressionWithoutSideEffects +#include "GMCAttributeModifier.h" +#include "GMCAttributes.h" +#include "Misc/AutomationTest.h" + +//IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAttributeNewOperatorsTest, "UE5.Marketplace.DeepWorlds_GMCAbilitySystem.Source.GMCAbilitySystem.Public.Attributes.NewOperatorsTest", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) + diff --git a/Source/GMCAbilitySystem/Public/Attributes/GMCAttributeModifier.h b/Source/GMCAbilitySystem/Public/Attributes/GMCAttributeModifier.h index d3fef80f..bd0d3055 100644 --- a/Source/GMCAbilitySystem/Public/Attributes/GMCAttributeModifier.h +++ b/Source/GMCAbilitySystem/Public/Attributes/GMCAttributeModifier.h @@ -1,36 +1,130 @@ #pragma once #include "GameplayTags.h" +#include "GMCAttributeModifierCustom_Base.h" #include "GMCAttributeModifier.generated.h" + +class UGMCAbilityEffect; +class UGMC_AbilitySystemComponent; + UENUM(BlueprintType) enum class EModifierType : uint8 { - // Adds to value - Add, - // Adds to value multiplier. Base Multiplier is 1. A modifier value of 1 will double the value. - Multiply, - // Adds to value divisor. Base Divisor is 1. A modifier value of 1 will halve the value. - Divide + Add UMETA(DisplayName = "+ [Add]"), + // Add To Attribute a Percentage of the Base Value + AddPercentageInitialValue UMETA(DisplayName = "% [Add Percentage Of Initial Value]"), + // Add to Attribute the Percentage of an selected Attribute (Value is the Percentage, Attribute as Value is the Attribute to use) + AddPercentageAttribute UMETA(DisplayName = "% [Add Percentage Of Attribute]"), + // Add to Attribute the Percentage of the Max Clamp Value + AddPercentageMaxClamp UMETA(DisplayName = "% [Add Percentage Of Max Clamp]"), + // Add to Attribute the Percentage of the Min Clamp Value + AddPercentageMinClamp UMETA(DisplayName = "% [Add Percentage Of Min Clamp]"), + // Add to Attribute the Percentage of the sum of selecteds Attributes (+20% of the Intelligence + Strength) + AddPercentageAttributeSum UMETA(DisplayName = "% [Add Percentage Of Attribute Sum]"), + // Add to Attribute a Value Scaled between X and Y depending of Value + AddScaledBetween UMETA(DisplayName = "X-y [Scaled Between]"), + // Add to Attribute a value, but this is Clamped between X and Y + AddClampedBetween UMETA(DisplayName = "+ [Add Clamped]"), + // Add to Attribute the Percentage of the Missing Value compare to the Base Value + AddPercentageMissing UMETA(DisplayName = "% [Add Percentage Of Missing Value]"), + // Add to Attribute the Percentage of an Attribute Raw Value (Raw Value is the attribute value without any temporal modifiers) + AddPercentageOfAttributeRawValue UMETA(DisplayName = "% [Add Percentage Of Attribute Raw Value"), +}; + +UENUM(BlueprintType) +enum class EGMCAttributeModifierType : uint8 +{ + AMT_Value UMETA(DisplayName = "Value", ToolTip = "Raw Value"), + AMT_Attribute UMETA(DisplayName = "Attribute", ToolTip = "Attribute that will be used to calculate the value"), + AMT_Custom UMETA(DisplayName = "Custom", ToolTip = "Custom modifier class that will be used to calculate the value"), }; USTRUCT(BlueprintType) struct FGMCAttributeModifier { GENERATED_BODY() + + public: + + // Return the value (Auto apply Custom Modifier if set, or return target attribute, or raw value) + float GetValue() const; + + // Return the value to apply to an attribute on calculation. + float CalculateModifierValue(const FAttribute& Attribute) const; + + // If isn't ticking, set DeltaTime to 1.f ! + void InitModifier(UGMCAbilityEffect* Effect, double InActionTimer, int InApplicationIdx, bool bInRegisterInHistory = false, float + InDeltaTime = 1.f); - UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Attribute", meta = (Categories="Attribute")) - FGameplayTag AttributeTag; + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Attribute", meta = (Categories="Attribute")) + FGameplayTag AttributeTag; + + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Attribute", meta=(DisplayAfter = "AttributeTag")) + EGMCAttributeModifierType ValueType {EGMCAttributeModifierType::AMT_Value}; + + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Attribute", meta = (Categories="Attribute", EditConditionHides, + EditCondition = "ValueType == EGMCAttributeModifierType::AMT_Attribute || Op == EModifierType::AddPercentageAttribute || Op == EModifierType::AddPercentageOfAttributeRawValue", + DisplayAfter = "ValueType")) + FGameplayTag ValueAsAttribute; + + UPROPERTY(Transient) + TWeakObjectPtr SourceAbilityEffect{nullptr}; + + UPROPERTY(Transient) + bool bRegisterInHistory{false}; + + float DeltaTime {1.f}; + + double ActionTimer {0.0}; + + int ApplicationIndex{0}; + + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GMCAbilitySystem") + EModifierType Op{EModifierType::Add}; + + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GMCAbilitySystem", + meta=(DisplayAfter = "ValueType", EditConditionHides, EditCondition = "ValueType == EGMCAttributeModifierType::AMT_Custom")) + TSubclassOf CustomModifierClass{nullptr}; + + // Metadata tags to be passed with the attribute + // Ie: DamageType (Element.Fire, Element.Electric), DamageSource (Source.Player, Source.Boss), etc + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GMCAbilitySystem") + FGameplayTagContainer MetaTags; + + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GMCAbilitySystem", + meta=(EditCondition = "ValueType == EGMCAttributeModifierType::AMT_Value", EditConditionHides, DisplayAfter = "ValueType")) + float ModifierValue{0}; + + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GMCAbilitySystem", + meta=(EditCondition = "(Op == EModifierType::AddScaledBetween || Op == EModifierType::AddClampedBetween) && !XAsAttribute", EditConditionHides + , DisplayAfter = "XAsAttribute")) + float X {0.f}; + + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GMCAbilitySystem", + meta=(EditCondition = "(Op == EModifierType::AddScaledBetween || Op == EModifierType::AddClampedBetween) && !YAsAttribute", EditConditionHides + , DisplayAfter = "YAsAttribute")) + float Y {0.f}; + + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GMCAbilitySystem", DisplayName="X As Attribute", + meta=(EditCondition = "Op == EModifierType::AddScaledBetween || Op == EModifierType::AddClampedBetween", EditConditionHides)); + bool XAsAttribute{false}; + + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GMCAbilitySystem", DisplayName="Y As Attribute", + meta=(EditCondition = "Op == EModifierType::AddScaledBetween || Op == EModifierType::AddClampedBetween", EditConditionHides)); + bool YAsAttribute{false}; - // Value to modify the attribute by - UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GMCAbilitySystem") - float Value{0}; + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GMCAbilitySystem", + meta=(EditCondition = "(Op == EModifierType::AddScaledBetween || Op == EModifierType::AddClampedBetween) && XAsAttribute", EditConditionHides, + DisplayAfter = "XAsAttribute")) + FGameplayTag XAttribute; - UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GMCAbilitySystem") - EModifierType ModifierType{EModifierType::Add}; + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GMCAbilitySystem", + meta=(EditCondition = "(Op == EModifierType::AddScaledBetween || Op == EModifierType::AddClampedBetween) && YAsAttribute", EditConditionHides, + DisplayAfter = "YAsAttribute")) + FGameplayTag YAttribute; - // Metadata tags to be passed with the attribute - // Ie: DamageType (Element.Fire, Element.Electric), DamageSource (Source.Player, Source.Boss), etc - UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GMCAbilitySystem") - FGameplayTagContainer MetaTags; + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GMCAbilitySystem", + meta=(EditCondition = "Op == EModifierType::AddPercentageAttributeSum", EditConditionHides, DisplayAfter = "ValueType")) + FGameplayTagContainer Attributes; }; \ No newline at end of file diff --git a/Source/GMCAbilitySystem/Public/Attributes/GMCAttributeModifierCustom_Base.h b/Source/GMCAbilitySystem/Public/Attributes/GMCAttributeModifierCustom_Base.h new file mode 100644 index 00000000..3e368005 --- /dev/null +++ b/Source/GMCAbilitySystem/Public/Attributes/GMCAttributeModifierCustom_Base.h @@ -0,0 +1,50 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "GMCAbilitySystem.h" +#include "UObject/Object.h" +#include "GMCAttributeModifierCustom_Base.generated.h" + +struct FAttribute; +class UGMCAbilityEffect; +class UGMC_AbilitySystemComponent; +/** + * @class UGMCAttributeModifierCustom_Base + * + * @brief Defines a custom calculator for applying modifiers to attributes in a system. + * + * The UGMCAttributeModifierCustomCalculator class provides the capability to calculate + * and apply custom attribute modifications based on user-defined logic. This is particularly + * useful in situations where attributes need to be adjusted dynamically based on specific + * conditions or custom input. + * + * It supports plug-and-play functionality for entities that utilize modifiable attributes, + * allowing users to define and override custom calculation logic if required. + * + * This class is expected to serve as a base class or utility for systems where attribute + * modifiers do not follow a predefined rule set and need bespoke calculations. + * + * Features include: + * - Extension support for custom logic and behaviors. + * - Compatibility with general modifier systems. + */ +UCLASS(Blueprintable) +class GMCABILITYSYSTEM_API UGMCAttributeModifierCustom_Base : public UObject +{ + GENERATED_BODY() + + public: + + UFUNCTION(BlueprintImplementableEvent) + float K2_Calculate(UGMCAbilityEffect* SourceEffect, FGameplayTag AttributeTag) const; + + // Designed to be overridden in C++ or Blueprint, this function will be called to calculate the final value of the attribute modifier. + // If override in C++, don't call super to avoid calling the Blueprint event and pay the performance cost of the Blueprint call. + virtual float Calculate(UGMCAbilityEffect* SourceEffect, const FAttribute* Attribute); + +protected: + bool CheckValidity(const UGMCAbilityEffect* SourceEffect, const FAttribute* Attribute) const; +}; diff --git a/Source/GMCAbilitySystem/Public/Attributes/GMCAttributes.h b/Source/GMCAbilitySystem/Public/Attributes/GMCAttributes.h index bf79f365..0a275e56 100644 --- a/Source/GMCAbilitySystem/Public/Attributes/GMCAttributes.h +++ b/Source/GMCAbilitySystem/Public/Attributes/GMCAttributes.h @@ -7,103 +7,80 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FAttributeChanged, float, OldValue, float, NewValue); -USTRUCT(BlueprintType) -struct GMCABILITYSYSTEM_API FAttribute : public FFastArraySerializerItem + + +USTRUCT() +struct FModifierHistoryEntry { GENERATED_BODY() - FAttribute(){}; - void Init() const - { - CalculateValue(false); - } + TWeakObjectPtr InstigatorEffect; + + float ActionTimer = 0.f; - UPROPERTY(BlueprintAssignable) - FAttributeChanged OnAttributeChanged; + float Value = 0.f; +}; + +USTRUCT(Blueprintable) +struct FAttributeTemporaryModifier +{ + GENERATED_BODY() UPROPERTY() - mutable float AdditiveModifier{0}; - + // Index used to identify the application of this modifier + int ApplicationIndex = 0; + + UPROPERTY() + // The value that we would like to apply + float Value = 0.f; + UPROPERTY() - mutable float MultiplyModifier{1}; + double ActionTimer = 0.0; + // The effect that applied this modifier UPROPERTY() - mutable float DivisionModifier{1}; + TWeakObjectPtr InstigatorEffect = nullptr; +}; + +USTRUCT(BlueprintType) +struct GMCABILITYSYSTEM_API FAttribute : public FFastArraySerializerItem +{ + GENERATED_BODY() + FAttribute(){}; - void ApplyModifier(const FGMCAttributeModifier& Modifier, bool bModifyBaseValue) const + void Init() const { - switch(Modifier.ModifierType) - { - case EModifierType::Add: - if (bModifyBaseValue) - { - BaseValue += Modifier.Value; - BaseValue = Clamp.ClampValue(BaseValue); - } - else - { - AdditiveModifier += Modifier.Value; - } - break; - case EModifierType::Multiply: - MultiplyModifier += Modifier.Value; - break; - case EModifierType::Divide: - DivisionModifier += Modifier.Value; - break; - default: - break; - } - + RawValue = Clamp.ClampValue(InitialValue); CalculateValue(); - } - void CalculateValue(bool bClamp = true) const - { - // Prevent divide by 0 and negative divisors - float LocalDivisionModifier = DivisionModifier; - if (LocalDivisionModifier <= 0){ - LocalDivisionModifier = 1; - } + + void AddModifier(const FGMCAttributeModifier& PendingModifier) const; - // Prevent negative multipliers - float LocalMultiplyModifier = MultiplyModifier; - if (LocalMultiplyModifier < 0){ - LocalMultiplyModifier = 0; - } - - Value =((BaseValue + AdditiveModifier) * LocalMultiplyModifier) / LocalDivisionModifier; - if (bClamp) - { - Value = Clamp.ClampValue(Value); - } - } + // Return true if the attribute has been modified + void CalculateValue() const; - // Reset the modifiers to the base value. May cause jank if there's effects going on. - void ResetModifiers() const - { - MultiplyModifier = 1; - DivisionModifier = 1; - } + void RemoveTemporalModifier(int ApplicationIndex, const UGMCAbilityEffect* InstigatorEffect) const; - // Allow for externally directly setting the BaseValue - // Usually preferred to go through Effects/Modifiers instead of this - void SetBaseValue(const float NewValue) const - { - BaseValue = Clamp.ClampValue(NewValue); - } + // Used to purge "future modifiers" during replay + void PurgeTemporalModifier(double CurrentActionTimer); + + UPROPERTY(BlueprintAssignable) + FAttributeChanged OnAttributeChanged; + + // Temporal Modifier + Accumulated Value UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem") mutable float Value{0}; + // Value when the attribute has been initialized UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem") - mutable float BaseValue{0}; + mutable float InitialValue{0}; // Attribute.* UPROPERTY(EditDefaultsOnly, Category="Attribute", meta = (Categories="Attribute")) FGameplayTag Tag{FGameplayTag::EmptyTag}; - + // Whether this should be bound over GMC or not. // NOTE: If you don't bind it, you can't use it for any kind of prediction. UPROPERTY(EditDefaultsOnly, Category = "GMCAbilitySystem") @@ -111,19 +88,33 @@ struct GMCABILITYSYSTEM_API FAttribute : public FFastArraySerializerItem // Clamp the attribute to a certain range // Clamping will only happen if this is modified - UPROPERTY(EditDefaultsOnly, Category = "GMCAbilitySystem") + UPROPERTY(EditDefaultsOnly, Category = "GMCAbilitySystem", meta=(TitleProperty="({min}, {max} {MinAttributeTag}, {MaxAttributeTag})")) FAttributeClamp Clamp{}; - FString ToString() const{ - return FString::Printf(TEXT("%s : %f (Bound: %d)"), *Tag.ToString(), Value, bIsGMCBound); - } + FString ToString() const; - bool operator < (const FAttribute& Other) const + bool IsDirty() const { - return Tag.ToString() < Other.Tag.ToString(); + return bIsDirty; } + + bool operator< (const FAttribute& Other) const; + + // This is the sum of permanent modification applied to this attribute. + UPROPERTY() + mutable float RawValue = 0.f; + +protected: + + UPROPERTY() + mutable TArray ValueTemporalModifiers; + + mutable bool bIsDirty = false; + }; + + USTRUCT(BlueprintType) struct GMCABILITYSYSTEM_API FGMCAttributeSet{ GENERATED_BODY() @@ -155,8 +146,7 @@ struct FGMCUnboundAttributeSet : public FFastArraySerializer void AddAttribute(const FAttribute& NewAttribute) { - Items.Add(NewAttribute); - MarkArrayDirty(); + MarkItemDirty(Items.Add_GetRef(NewAttribute)); } TArray GetAttributes() const @@ -176,10 +166,11 @@ struct FGMCUnboundAttributeSet : public FFastArraySerializer } } + + bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParams) { - return FFastArraySerializer::FastArrayDeltaSerialize(Items, DeltaParams, - *this); + return FFastArraySerializer::FastArrayDeltaSerialize(Items, DeltaParams,*this); } }; @@ -188,6 +179,6 @@ struct TStructOpsTypeTraits : public TStructOpsTypeTrai { enum { - WithNetDeltaSerializer = true + WithNetDeltaSerializer = true, }; }; \ No newline at end of file diff --git a/Source/GMCAbilitySystem/Public/Attributes/GMCAttributesData.h b/Source/GMCAbilitySystem/Public/Attributes/GMCAttributesData.h index 3c29ba52..27c18b2e 100644 --- a/Source/GMCAbilitySystem/Public/Attributes/GMCAttributesData.h +++ b/Source/GMCAbilitySystem/Public/Attributes/GMCAttributesData.h @@ -37,6 +37,6 @@ class GMCABILITYSYSTEM_API UGMCAttributesData : public UPrimaryDataAsset{ GENERATED_BODY() public: - UPROPERTY(EditDefaultsOnly, Category="AttributeData") + UPROPERTY(EditDefaultsOnly, Category="AttributeData", meta=(TitleProperty="{AttributeTag} ({DefaultValue})")) TArray AttributeData; }; diff --git a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h index cad75c50..d4b7c600 100644 --- a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h +++ b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h @@ -1,4 +1,3 @@ -// Fill out your copyright notice in the Description page of Project Settings. #pragma once @@ -11,10 +10,15 @@ #include "Ability/Tasks/GMCAbilityTaskData.h" #include "Effects/GMCAbilityEffect.h" #include "Components/ActorComponent.h" -#include "GMCAbilityOuterApplication.h" +#include "Containers/Deque.h" +#include "Utility/GMASBoundQueue.h" +#include "Utility/GMASSyncedEvent.h" #include "GMCAbilityComponent.generated.h" +class UNiagaraComponent; +struct FFXSystemSpawnParameters; +class UNiagaraSystem; class UGMCAbilityAnimInstance; class UGMCAbilityMapData; class UGMCAttributesData; @@ -25,10 +29,18 @@ DECLARE_MULTICAST_DELEGATE_ThreeParams(FGameplayAttributeChangedNative, const FG DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAncillaryTick, float, DeltaTime); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSyncedEvent, const FGMASSyncedEventContainer&, EventData); + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnAbilityActivated, UGMCAbility*, Ability, FGameplayTag, AbilityTag); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAbilityEnded, UGMCAbility*, Ability); + DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnActiveTagsChanged, FGameplayTagContainer, AddedTags, FGameplayTagContainer, RemovedTags); DECLARE_MULTICAST_DELEGATE_TwoParams(FGameplayTagFilteredMulticastDelegate, const FGameplayTagContainer&, const FGameplayTagContainer&); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnEffectApplied, UGMCAbilityEffect*, AppliedEffect); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnEffectRemoved, UGMCAbilityEffect*, RemovedEffect); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnTaskTimeout, FGameplayTag, TaskTag); USTRUCT() struct FEffectStatePrediction @@ -44,6 +56,56 @@ struct FEffectStatePrediction uint8 State; }; +USTRUCT() +struct FGMASQueueOperationHandle +{ + GENERATED_BODY() + + UPROPERTY() + int32 Handle { -1 }; + + UPROPERTY() + int32 OperationId { -1 }; + + UPROPERTY() + int32 NetworkId { -1 }; +}; + +UENUM(BlueprintType) +enum class EGMCAbilityEffectQueueType : uint8 +{ + /// Immediately applied, only valid within the GMC movement cycle. Should be applied on both client and server. + Predicted UMETA(DisplayName="Predicted"), + + /// Predicted effect, not replicated but will be queued for addition in the next GMC movement cycle. Valid even + /// outside of the GMC movement cycle. Should be applied on both client and server. If used during the GMC + /// movement cycle, this is silently turned into Predicted. + PredictedQueued UMETA(DisplayName="Predicted [Queued]"), + + /// Only valid on server; queued from server and sent to client via RPC. Valid even outside of the GMC movement cycle. + ServerAuth UMETA(DisplayName="Server Auth"), + + /// Only valid on client; queued from client and sent to the server via GMC bindings. Valid even outside of the + /// GMC movement cycle. You almost certainly don't want to use this, but it's here for the sake of completeness. + ClientAuth UMETA(Hidden, DisplayName="Client Auth"), + + /// Only valid on server; queued from server and recorded in the GMC move history. Valid even outside of the GMC + /// movement cycle. Slower than ServerAuth, only use this if you really need to preserve the effect application in + /// the movement history. you almost certainly don't want to use this, but it's here for the sake of completeness. + ServerAuthMove UMETA(Hidden, DisplayName="ADVANCED: Server Auth [Movement Cycle]") +}; + +UENUM(BlueprintType) +enum class EGMCEffectAnswerState : uint8 +{ + // Effect is not answered yet + Pending UMETA(DisplayName="Pending"), + // Effect reach out of prediction windows, was cancelled but can be re-applied + Timeout UMETA(DisplayName="Timeout"), + // Effect was answered and applied + Validated UMETA(DisplayName="Accepted") +}; + class UGMCAbility; UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent, DisplayName="GMC Ability System Component"), meta=(Categories="GMAS")) @@ -71,19 +133,26 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo FGameplayTagContainer GetGrantedAbilities() const { return GrantedAbilityTags; } // Gameplay tags that the controller has + UFUNCTION(BlueprintCallable, Category="GMAS|Abilities") FGameplayTagContainer GetActiveTags() const { return ActiveTags; } // Return the active ability effects TMap GetActiveEffects() const { return ActiveEffects; } + UGMCAbilityEffect* GetActiveEffectByHandle(int EffectID) const; + // Return active Effect with tag + // Match exact doesn't look for depth in the tag, it will only match the exact tag UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMAS|Abilities") - TArray GetActivesEffectByTag(FGameplayTag GameplayTag) const; + TArray GetActiveEffectsByTag(FGameplayTag GameplayTag, bool bMatchExact = true) const; // Get the first active effect with the Effecttag UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMAS|Abilities") UGMCAbilityEffect* GetFirstActiveEffectByTag(FGameplayTag GameplayTag) const; + // Return ability map that contains mapping of ability input tags to ability classes + TMap GetAbilityMap() { return AbilityMap; } + UFUNCTION(BlueprintCallable, Category="GMAS|Abilities") void AddAbilityMapData(UGMCAbilityMapData* AbilityMapData); @@ -147,11 +216,20 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo void TryActivateAbilitiesByInputTag(const FGameplayTag& InputTag, const UInputAction* InputAction = nullptr, bool bFromMovementTick=true); // Do not call directly on client, go through QueueAbility. Can be used to call server-side abilities (like AI). - bool TryActivateAbility(TSubclassOf ActivatedAbility, const UInputAction* InputAction = nullptr); - - // Queue an ability to be executed + bool TryActivateAbility(TSubclassOf ActivatedAbility, const UInputAction* InputAction = nullptr, const FGameplayTag ActivationTag = FGameplayTag::EmptyTag); + + + /** + * Queue an ability for activation based on the provided input tag and action. + * + * @param InputTag The gameplay tag associated with the ability to queue. + * @param InputAction The input action triggering the ability. + * @param bPreventConcurrentActivation If true, prevents the concurrent activation of abilities already in progress. The check is made locally + * reducing server charge, but also required in case of activation key wait inside the ability. + */ UFUNCTION(BlueprintCallable, DisplayName="Activate Ability", Category="GMAS|Abilities") - void QueueAbility(UPARAM(meta=(Categories="Input"))FGameplayTag InputTag, const UInputAction* InputAction = nullptr); + void QueueAbility(UPARAM(meta=(Categories="Input")) + FGameplayTag InputTag, const UInputAction* InputAction = nullptr, bool bPreventConcurrentActivation = false); UFUNCTION(BlueprintCallable, DisplayName="Count Queued Ability Instances", Category="GMAS|Abilities") int32 GetQueuedAbilityCount(FGameplayTag AbilityTag); @@ -160,7 +238,7 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo int32 GetActiveAbilityCount(TSubclassOf AbilityClass); // Perform a check in every active ability against BlockOtherAbility and check if the tag provided is present - bool IsAbilityTagBlocked(const FGameplayTag AbilityTag) const; + virtual bool IsAbilityTagBlocked(const FGameplayTag AbilityTag) const; UFUNCTION(BlueprintCallable, DisplayName="End Abilities (By Tag)", Category="GMAS|Abilities") // End all abilities with the corresponding tag, returns the number of abilities ended @@ -170,6 +248,10 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo // End all abilities with the corresponding tag, returns the number of abilities ended int EndAbilitiesByClass(TSubclassOf AbilityClass); + UFUNCTION(BlueprintCallable, DisplayName = "End Abilities (By Definition Query)", Category="GMAS|Abilities") + // End all abilities matching query + int EndAbilitiesByQuery(const FGameplayTagQuery& Query); + UFUNCTION(BlueprintCallable, DisplayName="Count Activated Ability Instances (by tag)", Category="GMAS|Abilities") int32 GetActiveAbilityCountByTag(FGameplayTag AbilityTag); @@ -219,38 +301,127 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo UFUNCTION() void OnRep_UnBoundAttributes(); + void CheckUnBoundAttributeChanged(); + + int GetNextAvailableEffectID() const; + bool CheckIfEffectIDQueued(int EffectID) const; + int CreateEffectOperation(TGMASBoundQueueOperation& OutOperation, const TSubclassOf& Effect, const FGMCAbilityEffectData& EffectData, bool bForcedEffectId = true, EGMCAbilityEffectQueueType QueueType = EGMCAbilityEffectQueueType::Predicted); + int CreateSyncedEventOperation(TGMASBoundQueueOperation& OutOperation, const FGMASSyncedEventContainer& EventData); + + /** - * Applies an effect to the Ability Component + * Applies an effect to the Ability Component. If bOuterActivation is false, the effect will be immediately + * applied; if either is true, the operation will be queued but no valid effect will be returned. If + * Outer Activation is true, the effect *must* be applied on the server. * * @param Effect Effect to apply - * @param AdditionalModifiers Additional Modifiers to apply with this effect application - * @param SourceAbilityComponent Ability Component from which this effect originated - * @param bOverwriteExistingModifiers Whether or not to replace existing modifiers that have the same name as additional modifiers. If false, will add them. - * @param bAppliedByServer Is this Effect only applied by server? Used to help client predict the unpredictable. + * @param InitializationData Effect initialization data. + * @param bOuterActivation Whether this effect should be replicated outside of GMC, via normal Unreal RPC */ - UFUNCTION(BlueprintCallable, Category="GMAS|Effects", meta = (AutoCreateRefTerm = "AdditionalModifiers")) + UFUNCTION(BlueprintCallable, Category="GMAS|Effects", DisplayName="Apply Ability Effect (Legacy)", meta=(DeprecatedFunction, DeprecationMessage="Please use the more modern ApplyAbilityEffect which takes a queue type.")) UGMCAbilityEffect* ApplyAbilityEffect(TSubclassOf Effect, FGMCAbilityEffectData InitializationData, bool bOuterActivation = false); + + // BP-specific version of + /** + * Applies an effect to the ability component. If the Queue Type is Predicted, the effect will be immediately added + * on both client and server; this must happen within the GMC movement lifecycle for it to be valid. If the + * Queue Type is anything else, the effect must be queued on the server and will be replicated to the client. + * @param HandlingAbility : Optional ability handling, if provided, the end ability will trigger also automatically the end of the effect. + */ + UFUNCTION(BlueprintCallable, Category="GMAS|Effects", DisplayName="Apply Ability Effect") + void ApplyAbilityEffectSafe(TSubclassOf EffectClass, FGMCAbilityEffectData InitializationData, EGMCAbilityEffectQueueType QueueType, + UPARAM(DisplayName="Success") + bool& OutSuccess, + UPARAM(DisplayName="Effect Handle") int& OutEffectHandle, + UPARAM(DisplayName="Effect Network ID") int& OutEffectId, + UPARAM(DisplayName="Effect Instance") UGMCAbilityEffect*& OutEffect, + UPARAM(DisplayName="(Opt) Ability Handling") UGMCAbility* HandlingAbility = nullptr); + + /** Short version of ApplyAbilityEffect (Fire and Forget, return nullptr if fail, or the effect instance if success) + * Don't suggest it for BP user to avoid confusion. + * @param HandlingAbility : Optional ability handling, if provided, the end ability will trigger al + */ + UGMCAbilityEffect* ApplyAbilityEffectShort(TSubclassOf EffectClass, EGMCAbilityEffectQueueType QueueType, + UPARAM(DisplayName="(Opt) Ability Handling") UGMCAbility* HandlingAbility = nullptr); + + /** + * Applies an effect to the ability component. If the Queue Type is Predicted, the effect will be immediately added + * on both client and server; this must happen within the GMC movement lifecycle for it to be valid. If the + * Queue Type is anything else, the effect must be queued on the server and will be replicated to the client. + * + * @param EffectClass The class of ability effect to add. + * @param InitializationData The initialization data for the ability effect. + * @param QueueType How to queue the effect. + * @param OutEffectHandle A local handle to this effect, only valid locally. + * @param OutEffectId The newly-created effect's network ID, if one is available. Valid across server/client. + * @param OutEffect The newly-created effect instance, if available. + * @return true if the effect was applied, false otherwise. + */ + bool ApplyAbilityEffect(TSubclassOf EffectClass, FGMCAbilityEffectData InitializationData, EGMCAbilityEffectQueueType QueueType, int& OutEffectHandle, int& OutEffectId, UGMCAbilityEffect*& OutEffect); + + // Do not call this directly unless you know what you are doing. Otherwise, always go through the above ApplyAbilityEffect variant! UGMCAbilityEffect* ApplyAbilityEffect(UGMCAbilityEffect* Effect, FGMCAbilityEffectData InitializationData); - + UFUNCTION(BlueprintCallable, Category="GMAS|Effects") + UGMCAbilityEffect* GetEffectById(const int EffectId) const; + + UFUNCTION(BlueprintCallable, Category="GMAS|Effects") + TArray GetEffectsByIds(const TArray EffectIds) const; + + UFUNCTION(BlueprintCallable, Category="GMAS|Effects") + FString GetEffectsNameAsString(const TArray& EffectList) const; + + TArray EffectsMatchingTag(const FGameplayTag& Tag, int32 NumToRemove = -1) const; + + // Do not call this directly unless you know what you are doing; go through the RemoveActiveAbilityEffectSafe if + // doing this from outside of the component, to allow queuing and sanity-check. UFUNCTION(BlueprintCallable, Category="GMAS|Effects") void RemoveActiveAbilityEffect(UGMCAbilityEffect* Effect); + UFUNCTION(BlueprintCallable, Category="GMAS|Effects") + void RemoveActiveAbilityEffectByHandle(int EffectHandle, EGMCAbilityEffectQueueType QueueType = EGMCAbilityEffectQueueType::Predicted); + + UFUNCTION(BlueprintCallable, Category="GMAS|Effects", DisplayName="Remove Active Ability Effect (Safe)") + void RemoveActiveAbilityEffectSafe(UGMCAbilityEffect* Effect, EGMCAbilityEffectQueueType QueueType = EGMCAbilityEffectQueueType::Predicted); + + UFUNCTION(BlueprintCallable, Category="GMAS|Effects", DisplayName="Remove Active Ability Effect by Tag (Safe)") + void RemoveActiveAbilityEffectByTag(FGameplayTag Tag, EGMCAbilityEffectQueueType QueueType = EGMCAbilityEffectQueueType::Predicted, bool bAllInstance = false); + /** * Removes an instanced effect if it exists. If NumToRemove == -1, remove all. Returns the number of removed instances. * If the inputted count is higher than the number of active corresponding effects, remove all we can. */ - UFUNCTION(BlueprintCallable, Category="GMAS|Effects") + UFUNCTION(BlueprintCallable, Category="GMAS|Effects", DisplayName="Remove Effect by Tag (Legacy)", meta=(DeprecatedFunction, DeprecationMessage="Please use the more modern RemoveEffectByTagSafe which takes a queue type.")) int32 RemoveEffectByTag(FGameplayTag InEffectTag, int32 NumToRemove=-1, bool bOuterActivation = false); + /** + * Removes an instanced effect if it exists. If NumToRemove == -1, remove all. Returns the number of removed instances. + * If the inputted count is higher than the number of active corresponding effects, remove all we can. + */ + UFUNCTION(BlueprintCallable, Category="GMAS|Effects", DisplayName="Remove Effects by Tag (Safe)") + int32 RemoveEffectByTagSafe(FGameplayTag InEffectTag, int32 NumToRemove=-1, EGMCAbilityEffectQueueType QueueType = EGMCAbilityEffectQueueType::Predicted); + /** * Removes an instanced effect by ids. * return false if any of the ids are invalid. */ - UFUNCTION(BlueprintCallable, Category="GMAS|Effects") + UFUNCTION(BlueprintCallable, Category="GMAS|Effects", DisplayName="Remove Effects by Id (Legacy)", meta=(DeprecatedFunction, DeprecationMessage="Please use the more modern RemoveEffectByIdSafe which takes a queue type.")) bool RemoveEffectById(TArray Ids, bool bOuterActivation = false); + /** + * Removes an instanced effect by ids. + * return false if any of the ids are invalid. + */ + UFUNCTION(BlueprintCallable, Category="GMAS|Effects", DisplayName="Remove Effects by Id (Safe)") + bool RemoveEffectByIdSafe(TArray Ids, EGMCAbilityEffectQueueType QueueType = EGMCAbilityEffectQueueType::Predicted); + + UFUNCTION(BlueprintCallable, Category="GMAS|Effects", DisplayName="Remove Effect by Handle") + bool RemoveEffectByHandle(int EffectHandle, EGMCAbilityEffectQueueType QueueType); + + UFUNCTION(BlueprintCallable, Category="GMAS|Effects", DisplayName="Remove Effects by Definition Query") + int32 RemoveEffectsByQuery(const FGameplayTagQuery& Query, EGMCAbilityEffectQueueType QueueType); + /** * Gets the number of active effects with the inputted tag. * Returns -1 if tag is invalid. @@ -270,24 +441,52 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo // Called during the Ancillary Tick UPROPERTY(BlueprintAssignable) FOnAncillaryTick OnAncillaryTick; + + // Called when a task times out + UPROPERTY(BlueprintAssignable) + FOnTaskTimeout OnTaskTimeout; + //// // Called when the set of active tags changes. UPROPERTY(BlueprintAssignable) FOnActiveTagsChanged OnActiveTagsChanged; + // Called when an ability is successfully activated + UPROPERTY(BlueprintAssignable) + FOnAbilityActivated OnAbilityActivated; + + UPROPERTY(BlueprintAssignable) + FOnAbilityEnded OnAbilityEnded; + + // Called when a synced event is executed + UPROPERTY(BlueprintAssignable) + FOnSyncedEvent OnSyncedEvent; + FGameplayTagContainer PreviousActiveTags; + UPROPERTY(BlueprintAssignable) + FOnEffectApplied OnEffectApplied; + + UPROPERTY(BlueprintAssignable) + FOnEffectRemoved OnEffectRemoved; + /** Returns an array of pointers to all attributes */ TArray GetAllAttributes() const; /** Get an Attribute using its Tag */ - const FAttribute* GetAttributeByTag(FGameplayTag AttributeTag) const; + const FAttribute* GetAttributeByTag(UPARAM(meta=(Categories="Attribute")) FGameplayTag AttributeTag) const; - // Get Attribute value by Tag + TMap GetActiveAbilities() const { return ActiveAbilities; } + + // Get Attribute value (RawValue + Temporal Modifiers) by Tag UFUNCTION(BlueprintPure, Category="GMAS|Attributes") float GetAttributeValueByTag(UPARAM(meta=(Categories="Attribute"))FGameplayTag AttributeTag) const; + // Get Attribute Value without Temporal Modifiers + UFUNCTION(BlueprintPure, Category="GMAS|Attributes") + float GetAttributeRawValue(UPARAM(meta=(Categories="Attribute"))FGameplayTag AttributeTag) const; + // Get Attribute value by Tag UFUNCTION(BlueprintPure, Category="GMAS|Attributes") FAttributeClamp GetAttributeClampByTag(UPARAM(meta=(Categories="Attribute"))FGameplayTag AttributeTag) const; @@ -295,16 +494,16 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo // Set Attribute value by Tag // Will NOT trigger an "OnAttributeChanged" Event // bResetModifiers: Will reset all modifiers on the attribute to the base value. DO NOT USE if you have any active effects that modify this attribute. - UFUNCTION(BlueprintCallable, Category="GMAS|Attributes") + UFUNCTION(BlueprintCallable, Category="GMAS|Attributes", meta=(DeprecatedFunction, DeprecationMessage="Please use ApplyAbilityAttributeModifier instead.")) bool SetAttributeValueByTag(UPARAM(meta=(Categories="Attribute"))FGameplayTag AttributeTag, float NewValue, bool bResetModifiers = false); /** Get the default value of an attribute from the data assets. */ UFUNCTION(BlueprintCallable, Category="GMAS|Attributes") - float GetBaseAttributeValueByTag(UPARAM(meta=(Categories="Attribute"))FGameplayTag AttributeTag) const; + float GetAttributeInitialValueByTag(UPARAM(meta=(Categories="Attribute"))FGameplayTag AttributeTag) const; // Apply modifiers that affect attributes UFUNCTION(BlueprintCallable, Category="GMAS|Attributes") - void ApplyAbilityEffectModifier(FGMCAttributeModifier AttributeModifier,bool bModifyBaseValue, bool bNegateValue = false, UGMC_AbilitySystemComponent* SourceAbilityComponent = nullptr); + void ApplyAbilityAttributeModifier(const FGMCAttributeModifier& AttributeModifier); UPROPERTY(BlueprintReadWrite, Category = "GMCAbilitySystem") bool bJustTeleported; @@ -347,6 +546,7 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo */ void RemoveAttributeChangeDelegate(FDelegateHandle Handle); + #pragma region GMC // GMC UFUNCTION(BlueprintCallable, Category="GMAS") @@ -364,6 +564,9 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo UFUNCTION(BlueprintCallable, Category="GMAS") virtual void PreLocalMoveExecution(); + UFUNCTION(BlueprintCallable, Category="GMAS") + virtual void PreRemoteMoveExecution(); + #pragma endregion GMC #pragma region ToStringHelpers @@ -425,10 +628,26 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo // Map of Ability Tags to Ability Classes TMap AbilityMap; +public: + // Empty the AbilityMap and remove all granted abilities from existing maps + UFUNCTION(BlueprintCallable, Category="GMAS|Abilities") + void ClearAbilityMap(); + + virtual void SetAttributeInitialValue(const FGameplayTag& AttributeTag, float& BaseValue); + + UFUNCTION(BlueprintImplementableEvent, Category="GMAS|Abilities") + void OnInitializeAttributeInitialValue(const FGameplayTag& AttributeTag, float& BaseValue); + +private: // List of filtered tag delegates to call when tags change. TArray> FilteredTagDelegates; FGameplayAttributeChangedNative NativeAttributeChangeDelegate; + + // Will calculate and process stack of attributes + void ProcessAttributes(bool bInGenPredictionTick); + + TArray ModifierHistory; // Get the map from the data asset and apply that to the component's map void InitializeAbilityMap(); @@ -438,35 +657,71 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo // Add the starting ability tags to GrantedAbilities at start void InitializeStartingAbilities(); - TArray QueuedAbilities; TArray QueuedTaskData; - // Current Ability Data being processed - // Members of this struct are bound over GMC - // FGMCAbilityData AbilityData; + // Queued ability operations (activate, cancel, etc.) + TGMASBoundQueue QueuedAbilityOperations; + bool ProcessAbilityOperation(const TGMASBoundQueueOperation& Operation, bool bFromMovementTick); + + TGMASBoundQueue QueuedEffectOperations; + TGMASBoundQueue QueuedEffectOperations_ClientAuth; + + TGMASBoundQueue QueuedEventOperations; + + + template + bool IsOperationValid(const TGMASBoundQueueOperation& Operation) const; + + template + bool ShouldProcessOperation(const TGMASBoundQueueOperation& Operation, TGMASBoundQueue& QueuedOperations, bool bIsServer = true) const; + + // Events + virtual void ProcessOperation(const TGMASBoundQueueOperation& Operation); + + // Event Implementations + + // Execute an event that is created by the server where execution is synced between server and client + UFUNCTION(BlueprintCallable, Category = "GMASSyncedEvent") + void ExecuteSyncedEvent(FGMASSyncedEventContainer EventData); + + + + UFUNCTION(BlueprintCallable, DisplayName="Add Impulse (Synced Event)", Category = "Impulse") + void AddImpulse(FVector Impulse, bool bVelChange = false); + void AddImpulseEvent(const FGMASSyncedEventContainer& EventData) const; + + // Effects + virtual UGMCAbilityEffect* ProcessOperation(const TGMASBoundQueueOperation& Operation); + + + void ClientQueueOperation(const TGMASBoundQueueOperation& Operation); + void ClientQueueOperation(const TGMASBoundQueueOperation& Operation); + + UFUNCTION(Client, Reliable) + void RPCClientQueueEffectOperation(const FGMASBoundQueueRPCHeader& Header); + + UFUNCTION(Client, Reliable) + void RPCClientQueueEventOperation(const FGMASBoundQueueRPCHeader& Header); // Predictions of Effect state changes FEffectStatePrediction EffectStatePrediction{}; TArray QueuedEffectStates; - - UPROPERTY() TMap ActiveAbilities; - - UPROPERTY() TMap ActiveCooldowns; - - + int GenerateAbilityID() const {return ActionTimer * 100;} // Set Attributes to either a default object or a provided TSubClassOf in BP defaults // This must run before variable binding void InstantiateAttributes(); + + void SetStartingTags(); // Check if ActiveTags has changed and call delegates @@ -483,6 +738,7 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo // Tick Predicted and Active Effects void TickActiveEffects(float DeltaTime); + // Tick active abilities, primarily the Tasks inside them void TickActiveAbilities(float DeltaTime); @@ -511,32 +767,29 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo UPROPERTY() TMap ActiveEffects; - // Effect applied externally, pending activation, used by server and client. Not replicated. - //TODO: Later we will need to encapsulate this with Instanced struct to have a more generic way to handle this, and have cohabitation server <-> client - UPROPERTY() - TArray PendingApplicationServer; - UPROPERTY() - TArray PendingApplicationClient; + TMap EffectHandles; - // doesn't work ATM. - UPROPERTY(BlueprintReadOnly, Category = "GMCAbilitySystem", meta=(AllowPrivateAccess="true")) - bool bInGMCTime = false; + int GetNextAvailableEffectHandle() const; - // TODO: Need to be pushed later on a int64 32 index + 32 bitfield - // Binded Used for acknowledge server initiated ability/effect - FInstancedStruct AcknowledgeId = FInstancedStruct::Make(FGMCAcknowledgeId{}); + UFUNCTION(BlueprintCallable, Category="GMAS|Effects") + void GetEffectFromHandle_BP(int EffectHandle, bool& bOutSuccess, int32& OutEffectNetworkId, UGMCAbilityEffect*& OutEffect); + + bool GetEffectFromHandle(int EffectHandle, int32& OutEffectNetworkId, UGMCAbilityEffect*& OutEffect) const; + bool GetEffectHandle(int EffectHandle, FGMASQueueOperationHandle& HandleData) const; - void AddPendingEffectApplications(FGMCOuterApplicationWrapper& Wrapper); - // Let the client know that the server ask for an external effect application - UFUNCTION(Client, Reliable) - void RPCClientAddPendingEffectApplication(FGMCOuterApplicationWrapper Wrapper); + void RemoveEffectHandle(int EffectHandle); - void ServerHandlePendingEffect(float DeltaTime); + UPROPERTY(BlueprintReadOnly, Category = "GMCAbilitySystem", meta=(AllowPrivateAccess="true")) + bool bInAncillaryTick = false; - void ClientHandlePendingEffect(); + void ServerHandlePendingEffect(float DeltaTime); + void ServerHandlePredictedPendingEffect(float DeltaTime); - int GenerateLateApplicationID(); + template + void ClientHandlePendingOperation(TGMASBoundQueue& QueuedOperations); + + void ClientHandlePredictedPendingEffect(); int LateApplicationIDCounter = 0; @@ -544,7 +797,7 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo // This need to be persisted for a while // This never empties out so it'll infinitely grow, probably a better way to accomplish this UPROPERTY() - TMap ProcessedEffectIDs; + TMap ProcessedEffectIDs; // Let the client know that the server has activated this ability as well // Needed for the client to cancel mis-predicted abilities @@ -564,5 +817,38 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo void RPCClientEndEffect(int EffectID); friend UGMCAbilityAnimInstance; + +public: + // Networked FX + // Is this ASC locally controlled? + bool IsLocallyControlledPawnASC() const; + + // Spawn a Niagara system attached to a component + // IsClientPredicted: If true, the system will be spawned on the client immediately. False, the local client will spawn it when the multicast is received + // bDelayByGMCSmoothing: If true, the system will be spawned with a delay for SimProxies to match the smoothing delay + UFUNCTION(BlueprintCallable, Category="GMAS|FX") + UNiagaraComponent* SpawnParticleSystemAttached(FFXSystemSpawnParameters SpawnParams, bool bIsClientPredicted = false, bool bDelayByGMCSmoothing = false); + + UFUNCTION(NetMulticast, Unreliable) + void MC_SpawnParticleSystemAttached(const FFXSystemSpawnParameters& SpawnParams, bool bIsClientPredicted = false, bool bDelayByGMCSmoothing = false); + + // Spawn a Niagara system at a world location + // IsClientPredicted: If true, the system will be spawned on the client immediately. False, the local client will spawn it when the multicast is received + // bDelayByGMCSmoothing: If true, the system will be spawned with a delay for SimProxies to match the smoothing delay + UFUNCTION(BlueprintCallable, Category="GMAS|FX") + UNiagaraComponent* SpawnParticleSystemAtLocation(FFXSystemSpawnParameters SpawnParams, bool bIsClientPredicted = false, bool bDelayByGMCSmoothing = false); + + UFUNCTION(NetMulticast, Unreliable) + void MC_SpawnParticleSystemAtLocation(const FFXSystemSpawnParameters& SpawnParams, bool bIsClientPredicted = false, bool bDelayByGMCSmoothing = false); + + + // Spawn a Sound at the given location + UFUNCTION(BlueprintCallable, Category="GMAS|FX") + void SpawnSound(USoundBase* Sound, FVector Location, float VolumeMultiplier = 1.f, float PitchMultiplier = 1.f, bool bIsClientPredicted = false); + + UFUNCTION(NetMulticast, Unreliable) + void MC_SpawnSound(USoundBase* Sound, FVector Location, float VolumeMultiplier = 1.f, float PitchMultiplier = 1.f, bool bIsClientPredicted = false); + + friend class FGameplayDebuggerCategory_GMCAbilitySystem; }; diff --git a/Source/GMCAbilitySystem/Public/Components/GMCAbilityOuterApplication.h b/Source/GMCAbilitySystem/Public/Components/GMCAbilityOuterApplication.h deleted file mode 100644 index 85bffac5..00000000 --- a/Source/GMCAbilitySystem/Public/Components/GMCAbilityOuterApplication.h +++ /dev/null @@ -1,86 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "InstancedStruct.h" -#include "GMCAbilityOuterApplication.generated.h" - -USTRUCT() -struct FGMCAcknowledgeId { - GENERATED_BODY() - - UPROPERTY() - TArray Id = {}; -}; - - -UENUM() -enum EGMCOuterApplicationType { - EGMC_AddEffect, - EGMC_RemoveEffect, -}; - -USTRUCT(BlueprintType) -struct FGMCOuterEffectAdd { - GENERATED_BODY() - - UPROPERTY() - TSubclassOf EffectClass; - - UPROPERTY() - FGMCAbilityEffectData InitializationData; -}; - -USTRUCT(BlueprintType) -struct FGMCOuterEffectRemove { - GENERATED_BODY() - - UPROPERTY() - TArray Ids = {}; -}; - -USTRUCT(BlueprintType) -struct FGMCOuterApplicationWrapper { - GENERATED_BODY() - - UPROPERTY() - TEnumAsByte Type = EGMC_AddEffect; - - UPROPERTY() - FInstancedStruct OuterApplicationData; - - UPROPERTY() - int LateApplicationID = 0; - - float ClientGraceTimeRemaining = 0.f; - - template - static FGMCOuterApplicationWrapper Make(Args... args) - { - FGMCOuterApplicationWrapper Wrapper; - Wrapper.OuterApplicationData = FInstancedStruct::Make(args...); - return Wrapper; - } - - -}; - -template<> inline FGMCOuterApplicationWrapper FGMCOuterApplicationWrapper::Make(TSubclassOf Effect, FGMCAbilityEffectData InitializationData) -{ - FGMCOuterApplicationWrapper Wrapper; - Wrapper.Type = EGMC_AddEffect; - Wrapper.OuterApplicationData = FInstancedStruct::Make(); - FGMCOuterEffectAdd& Data = Wrapper.OuterApplicationData.GetMutable(); - Data.EffectClass = Effect; - Data.InitializationData = InitializationData; - return Wrapper; -} - -template<> inline FGMCOuterApplicationWrapper FGMCOuterApplicationWrapper::Make(TArray Ids) -{ - FGMCOuterApplicationWrapper Wrapper; - Wrapper.Type = EGMC_RemoveEffect; - Wrapper.OuterApplicationData = FInstancedStruct::Make(); - FGMCOuterEffectRemove& Data = Wrapper.OuterApplicationData.GetMutable(); - Data.Ids = Ids; - return Wrapper; -} diff --git a/Source/GMCAbilitySystem/Public/Debug/GameplayDebuggerCategory_GMCAbilitySystem.h b/Source/GMCAbilitySystem/Public/Debug/GameplayDebuggerCategory_GMCAbilitySystem.h index 2252dcfe..ae373ce0 100644 --- a/Source/GMCAbilitySystem/Public/Debug/GameplayDebuggerCategory_GMCAbilitySystem.h +++ b/Source/GMCAbilitySystem/Public/Debug/GameplayDebuggerCategory_GMCAbilitySystem.h @@ -25,11 +25,17 @@ class GMCABILITYSYSTEM_API FGameplayDebuggerCategory_GMCAbilitySystem : public F // Put all data you want to display here FString ActorName; FString GrantedAbilities; + int NBGrantedAbilities; FString ActiveTags; + int NBActiveTags; FString Attributes; + int NBAttributes; FString ActiveEffects; + int NBActiveEffects; FString ActiveEffectData; + int NBActiveEffectData; FString ActiveAbilities; + int NBActiveAbilities; void Serialize(FArchive& Ar); }; diff --git a/Source/GMCAbilitySystem/Public/Effects/GMCAbilityEffect.h b/Source/GMCAbilitySystem/Public/Effects/GMCAbilityEffect.h index d631ac30..7ea1552a 100644 --- a/Source/GMCAbilitySystem/Public/Effects/GMCAbilityEffect.h +++ b/Source/GMCAbilitySystem/Public/Effects/GMCAbilityEffect.h @@ -6,7 +6,7 @@ #include "GameplayTagContainer.h" #include "UObject/Object.h" #include "GMCAbilitySystem.h" -#include "Attributes/GMCAttributeModifier.h" +#include "GMCAttributeModifier.h" #include "GMCAbilityEffect.generated.h" class UGMC_AbilitySystemComponent; @@ -14,9 +14,10 @@ class UGMC_AbilitySystemComponent; UENUM(BlueprintType) enum class EGMASEffectType : uint8 { - Instant, // Applies Instantly - Duration, // Lasts for X time - Infinite // Lasts forever + Instant UMETA(DisplayName = "Instant", ToolTip = "Effect applies instantly, Apply fully and immediatly die, no tick"), + Ticking UMETA(DisplayName = "Ticking", ToolTip = "Effect applies periodically, Apply each tick multiplied by DeltaTime (Modifiers is Amount per second), and die after Duration or Removal"), + Persistent UMETA(DisplayName = "Persistent", ToolTip = "Effect applies persistently, Apply fully immediatly, and die after Duration or Removal"), + Periodic UMETA(DisplayName = "Periodic", ToolTip = "Effect applies periodically, Each interval applu fully the modifier value. Ticking. Die after duration of Removal"), }; UENUM(BlueprintType) @@ -27,6 +28,8 @@ enum class EGMASEffectState : uint8 Ended // Lasts forever }; + + // Container for exposing the attribute modifier to blueprints UCLASS() class GMCABILITYSYSTEM_API UGMCAttributeModifierContainer : public UObject @@ -60,6 +63,9 @@ struct FGMCAbilityEffectData UPROPERTY() int EffectID; + UPROPERTY() + uint8 bServerAuth : 1 {false}; // The server will never be acknowledge/predicted + UPROPERTY(BlueprintReadOnly, Category = "GMCAbilitySystem") double StartTime; @@ -69,37 +75,32 @@ struct FGMCAbilityEffectData UPROPERTY(BlueprintReadOnly, Category = "GMCAbilitySystem") double CurrentDuration{0.f}; - // Instantly applies effect then exits. Will not tick. - UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GMCAbilitySystem") - bool bIsInstant = true; - + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem") + EGMASEffectType EffectType = EGMASEffectType::Instant; + // Apply an inversed version of the modifiers at effect end - UPROPERTY() + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GMCAbilitySystem", meta=(EditCondition = "EffectType == EGMASEffectType::Ticking || EffectType == EGMASEffectType::Persistent || EffectType == EGMASEffectType::Periodic", EditConditionHides)) bool bNegateEffectAtEnd = false; // Delay before the effect starts UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem") double Delay = 0; - // How long the effect lasts - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem") - double Duration = 0; + // Periodic will tick immediatly if true + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem" , meta=(EditCondition = "EffectType == EGMASEffectType::Periodic", EditConditionHides)) + bool bPeriodicFirstTick = true; - // How often the periodic effect ticks - // Suggest keeping this above .01 - UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GMCAbilitySystem") - double Period = 0; - - // For Period effects, whether first tick should happen immediately - UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GMCAbilitySystem") - bool bPeriodTickAtStart = false; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem" , meta=(EditCondition = "EffectType == EGMASEffectType::Periodic", EditConditionHides, ClampMin = "0.1", UIMin = "0.1")) + float PeriodicInterval = 1.f; + // How long the effect lasts, 0 for infinite + // Does nothing if effect is instant + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem", meta=(EditCondition = "EffectType == EGMASEffectType::Ticking || EffectType == EGMASEffectType::Persistent || EffectType == EGMASEffectType::Periodic", EditConditionHides)) + double Duration = 0; + // Time in seconds that the client has to apply itself an external effect before the server will force it. If this time is reach, a rollback is likely to happen. UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GMCAbilitySystem", AdvancedDisplay) float ClientGraceTime = 1.f; - - UPROPERTY() - int LateApplicationID = -1; // Tag to identify this effect UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem") @@ -108,6 +109,11 @@ struct FGMCAbilityEffectData UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem") FGameplayTagContainer GrantedTags; + // Whether to preserve the granted tags if multiple instances of the same effect are applied + // If false, will remove all stacks of the tag + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GMCAbilitySystem") + bool bPreserveGrantedTagsIfMultiple = false; + // Tags that the owner must have to apply this effect UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem") FGameplayTagContainer ApplicationMustHaveTags; @@ -127,14 +133,18 @@ struct FGMCAbilityEffectData UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem") FGameplayTagContainer GrantedAbilities; - // If tag is present, periodic effect will not tick. Duration is not affected. + // If tag is present, this effect will not tick. Duration is not affected. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem") - FGameplayTagContainer PausePeriodicEffect; + FGameplayTagContainer PauseEffect; // On activation, will end ability present in this container UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem") FGameplayTagContainer CancelAbilityOnActivation; + // When this effect end, it will end ability present in this container + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem") + FGameplayTagContainer CancelAbilityOnEnd; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem") TArray Modifiers; @@ -151,8 +161,29 @@ struct FGMCAbilityEffectData } FString ToString() const{ - return FString::Printf(TEXT("[id: %d] [Tag: %s] (Duration: %.3lf) (CurrentDuration: %.3lf)"), EffectID, *EffectTag.ToString(), Duration, CurrentDuration); + return FString::Printf(TEXT("[id: %d] [Tag: %s] (Dur: %.3lf) (CurDur: %.3lf)"), EffectID, *EffectTag.ToString(), Duration, CurrentDuration); } + + // query stuff + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem") + // Container for a more generalized definition of effects + FGameplayTagContainer EffectDefinition; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem") + // query must match on effect activation + FGameplayTagQuery ActivationQuery; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem") + // query must be maintained throughout effect + FGameplayTagQuery MustMaintainQuery; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem", meta = (DisplayName = "End Ability On Activation Via Definition Query")) + // end ability on effect activation if definition matches query + FGameplayTagQuery EndAbilityOnActivationQuery; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem", meta = (DisplayName = "End Ability On End Via Definition Query")) + // end ability on effect end if definition matches query + FGameplayTagQuery EndAbilityOnEndQuery; }; /** @@ -165,6 +196,11 @@ class GMCABILITYSYSTEM_API UGMCAbilityEffect : public UObject { GENERATED_BODY() +#if WITH_EDITOR + virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override; +#endif + + public: EGMASEffectState CurrentState; @@ -172,26 +208,33 @@ class GMCABILITYSYSTEM_API UGMCAbilityEffect : public UObject FGMCAbilityEffectData EffectData; UFUNCTION(BlueprintCallable, Category = "GMCAbilitySystem") - virtual void InitializeEffect(FGMCAbilityEffectData InitializationData); - - virtual void EndEffect(); + void InitializeEffect(FGMCAbilityEffectData InitializationData); + + UFUNCTION(BlueprintCallable, Category = "GMCAbilitySystem") + void EndEffect(); virtual void BeginDestroy() override; virtual void Tick(float DeltaTime); + int32 CalculatePeriodicTicksBetween(float Period, float StartActionTimer, float EndActionTimer); + // Return the current duration of the effect - UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMAS|Abilities") + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMAS|Effects") float GetCurrentDuration() const { return EffectData.CurrentDuration; } - // Return the current duration of the effect - UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMAS|Abilities") + // Return the effect data struct of targeted effect + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMAS|Effects") FGMCAbilityEffectData GetEffectData() const { return EffectData; } - // Return the current duration of the effect - UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMAS|Abilities") + // Return the total duration of the effect + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMAS|Effects") float GetEffectTotalDuration() const { return EffectData.Duration; } + // Return the current remaining duration of the effect + UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMAS|Effects") + float GetEffectRemainingDuration() const { return EffectData.Duration - EffectData.CurrentDuration; } + UFUNCTION(BlueprintNativeEvent, meta=(DisplayName="Effect Tick"), Category="GMCAbilitySystem") void TickEvent(float DeltaTime); @@ -200,24 +243,47 @@ class GMCABILITYSYSTEM_API UGMCAbilityEffect : public UObject UFUNCTION(BlueprintNativeEvent, meta=(DisplayName="Dynamic Condition"), Category="GMCAbilitySystem") bool AttributeDynamicCondition() const; - virtual void PeriodTick(); + void PeriodTick(); + + UFUNCTION(BlueprintNativeEvent, meta=(DisplayName="Period Tick"), Category="GMCAbilitySystem") + void PeriodTickEvent(); void UpdateState(EGMASEffectState State, bool Force=false); - virtual bool IsPeriodPaused(); + virtual bool IsPaused(); + + bool IsEffectModifiersRegisterInHistory() const; + float ProcessCustomModifier(const TSubclassOf& MCClass, const FAttribute* attribute); + bool bCompleted; // Time that the client applied this Effect. Used for when a client predicts an effect, if the server has not // confirmed this effect within a time range, the effect will be cancelled. float ClientEffectApplicationTime; + UFUNCTION(BlueprintPure, Category = "GMCAbilitySystem") + void GetOwnerActor(AActor*& OwnerActor) const; + + UFUNCTION(BlueprintPure, Category = "GMCAbilitySystem") + UGMC_AbilitySystemComponent* GetOwnerAbilityComponent() const { return OwnerAbilityComponent; } + protected: - UPROPERTY(BlueprintReadOnly, Category = "GMCAbilitySystem") - UGMC_AbilitySystemComponent* SourceAbilityComponent; + + UPROPERTY(Transient) + TMap, UGMCAttributeModifierCustom_Base*> CustomModifiersInstances; UPROPERTY(BlueprintReadOnly, Category = "GMCAbilitySystem") - UGMC_AbilitySystemComponent* OwnerAbilityComponent; + UGMC_AbilitySystemComponent* OwnerAbilityComponent = nullptr; + + // Apply the things that should happen as soon as an effect starts. Tags, instant effects, etc. + virtual void StartEffect(); + +private: + bool bHasStarted; + bool bHasAppliedEffect; + + void CheckState(); // Tags void AddTagsToOwner(); @@ -227,27 +293,34 @@ class GMCABILITYSYSTEM_API UGMCAbilityEffect : public UObject void AddAbilitiesToOwner(); void RemoveAbilitiesFromOwner(); - void EndActiveAbilitiesFromOwner(); + void EndActiveAbilitiesFromOwner(const FGameplayTagContainer& TagContainer); // Does the owner have any of the tags from the container? bool DoesOwnerHaveTagFromContainer(FGameplayTagContainer& TagContainer) const; bool DuplicateEffectAlreadyApplied(); - // Apply the things that should happen as soon as an effect starts. Tags, instant effects, etc. - virtual void StartEffect(); - - bool bHasStarted; + void EndActiveAbilitiesByDefinitionQuery(FGameplayTagQuery); -private: - // Used for calculating when to tick Period effects - float PrevPeriodMod = 0; - void CheckState(); - public: + + // Blueprint Event for when the effect starts + UFUNCTION(BlueprintImplementableEvent) + void StartEffectEvent(); + + UFUNCTION(BlueprintImplementableEvent) + void EndEffectEvent(); + + FString ToString() { - return FString::Printf(TEXT("[name: %s] (State %s) | Started: %d | Period Paused: %d | Data: %s"), *GetName(), *EnumToString(CurrentState), bHasStarted, IsPeriodPaused(), *EffectData.ToString()); + return FString::Printf(TEXT("[name: %s] (%s) | %s | %s | Data: %s"), *GetName().Right(30), *EnumToString(CurrentState), bHasStarted ? TEXT("Started") : TEXT("Not Started"), IsPaused() ? TEXT("Paused") : TEXT("Running"), *EffectData.ToString()); } + + UFUNCTION(BlueprintCallable, Category = "GMCAbilitySystem|Effects|Queries") + void ModifyMustMaintainQuery(const FGameplayTagQuery& NewQuery); + + UFUNCTION(BlueprintCallable, Category = "GMCAbilitySystem|Effects|Queries") + void ModifyEndAbilitiesOnEndQuery(const FGameplayTagQuery& NewQuery); }; diff --git a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h new file mode 100644 index 00000000..0a1869a3 --- /dev/null +++ b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h @@ -0,0 +1,594 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "GMCMovementUtilityComponent.h" +#include "InstancedStruct.h" +#include "UObject/Object.h" +#include "UObject/ConstructorHelpers.h" +#include "GMASBoundQueue.generated.h" + +UENUM(BlueprintType) +enum class EGMASBoundQueueOperationType : uint8 +{ + None, + Add, + Remove, + Activate, + Cancel +}; + +USTRUCT(BlueprintType) +struct FGMASBoundQueueOperationIdSet +{ + GENERATED_BODY() + + UPROPERTY() + TArray Ids = {}; +}; + +USTRUCT(BlueprintType) +struct GMCABILITYSYSTEM_API FGMASBoundQueueRPCHeader +{ + GENERATED_BODY() + + UPROPERTY() + int32 OperationId { -1 }; + + UPROPERTY() + uint8 OperationTypeRaw { 0 }; + + EGMASBoundQueueOperationType GetOperationType() const + { + return static_cast(OperationTypeRaw); + } + + UPROPERTY() + FGameplayTag Tag { FGameplayTag::EmptyTag }; + + UPROPERTY() + FName ItemClassName { NAME_None }; + + UPROPERTY() + FGMASBoundQueueOperationIdSet PayloadIds {}; + + UPROPERTY() + FInstancedStruct InstancedPayload {}; + + UPROPERTY() + float RPCGracePeriodSeconds { 1.f }; + + UPROPERTY() + uint8 ExtraFlags { 0 }; +}; + +template +struct GMCABILITYSYSTEM_API TGMASBoundQueueOperation +{ + FGMASBoundQueueRPCHeader Header {}; + + // An actual class to be utilized with this, in case we need to instance it. + TSubclassOf ItemClass { nullptr }; + + FInstancedStruct InstancedPayloadIds; + + // The struct payload for this item. + T Payload; + + // The instanced struct representation of this payload, used to actually + // bind for replication. + FInstancedStruct InstancedPayload; + + EGMASBoundQueueOperationType GetOperationType() const + { + return Header.GetOperationType(); + } + + void SetOperationType(EGMASBoundQueueOperationType OperationType) + { + Header.OperationTypeRaw = static_cast(OperationType); + } + + int32 GetOperationId() const { return Header.OperationId; } + + TArray GetPayloadIds() const { return Header.PayloadIds.Ids; } + + FGameplayTag GetTag() const { return Header.Tag; } + + bool GracePeriodExpired() const + { + return Header.RPCGracePeriodSeconds <= 0.f; + } + + bool IsValid() const + { + return Header.OperationTypeRaw != 0 && (Header.ItemClassName != NAME_None || Header.Tag != FGameplayTag::EmptyTag || Header.PayloadIds.Ids.Num() > 0); + } + + void Refresh(bool bDecodePayload = false) + { + if (bDecodePayload) + { + // Incoming from remote. + Payload = Header.InstancedPayload.Get(); + if (Header.PayloadIds.Ids.Num() == 0 && InstancedPayloadIds.IsValid()) + { + Header.PayloadIds = InstancedPayloadIds.Get(); + } + } + else + { + // Outgoing + Header.InstancedPayload = FInstancedStruct::Make(Payload); + InstancedPayloadIds = FInstancedStruct::Make(Header.PayloadIds); + } + + RefreshClass(); + } + + void RefreshClass() + { + if (Header.ItemClassName != NAME_None && !ItemClass) + { + // Get a handle to our class, for instancing purposes. + TSoftClassPtr ClassPtr = TSoftClassPtr(FSoftObjectPath(Header.ItemClassName.ToString())); + ItemClass = ClassPtr.LoadSynchronous(); + } + + if (ItemClass && Header.ItemClassName == NAME_None) + { + Header.ItemClassName = FName(ItemClass->GetPathName()); + } + } + +}; + +USTRUCT(BlueprintType) +struct GMCABILITYSYSTEM_API FGMASBoundQueueAcknowledgement +{ + GENERATED_BODY() + + UPROPERTY() + int32 Id { -1 }; + + UPROPERTY() + float Lifetime { 5.f }; +}; + +USTRUCT(BlueprintType) +struct GMCABILITYSYSTEM_API FGMASBoundQueueAcknowledgements +{ + GENERATED_BODY() + + UPROPERTY() + TArray AckSet; +}; + +USTRUCT(BlueprintType) +struct GMCABILITYSYSTEM_API FGMASBoundQueueEmptyData +{ + GENERATED_BODY() +}; + +template +class GMCABILITYSYSTEM_API TGMASBoundQueue +{ + +public: + + TGMASBoundQueueOperation CurrentOperation; + + int BI_ActionTimer { -1 }; + int BI_Acknowledgements { -1 }; + int BI_OperationId { -1 }; + int BI_OperationType { -1 }; + int BI_OperationTag { -1 }; + int BI_OperationClass { -1 }; + int BI_OperationExtraFlags { -1 }; + int BI_OperationPayload { -1 }; + int BI_OperationPayloadIds { -1 }; + + TArray> QueuedBoundOperations; + TArray> QueuedRPCOperations; + + FInstancedStruct Acknowledgments; + + double ActionTimer { 0 }; + + void BindToGMC(UGMC_MovementUtilityCmp* MovementComponent) + { + const EGMC_PredictionMode Prediction = ClientAuth ? EGMC_PredictionMode::ClientAuth_Input : EGMC_PredictionMode::ServerAuth_Input_ClientValidated; + const EGMC_PredictionMode AckPrediction = ClientAuth ? EGMC_PredictionMode::ServerAuth_Output_ClientValidated : EGMC_PredictionMode::ClientAuth_Input; + + Acknowledgments = FInstancedStruct::Make(FGMASBoundQueueAcknowledgements()); + + // Our queue's action timer is always server-auth. + BI_ActionTimer = MovementComponent->BindDoublePrecisionFloat( + ActionTimer, + EGMC_PredictionMode::ServerAuth_Output_ClientValidated, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::None, + EGMC_InterpolationFunction::TargetValue + ); + + if (!ClientAuth) + { + // Acknowledgements are bound client-auth for server-auth effects. + BI_Acknowledgements = MovementComponent->BindInstancedStruct( + Acknowledgments, + AckPrediction, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::None, + EGMC_InterpolationFunction::TargetValue); + } + else + { + // For client-auth stuff, we bind the individual pieces of the queue. + // We will probably not use this often (if ever), but it exists just-in-case. + + BI_OperationId = MovementComponent->BindInt( + CurrentOperation.Header.OperationId, + Prediction, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::Periodic_Output, + EGMC_InterpolationFunction::TargetValue); + + BI_OperationType = MovementComponent->BindByte( + CurrentOperation.Header.OperationTypeRaw, + Prediction, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::Periodic_Output, + EGMC_InterpolationFunction::TargetValue); + + BI_OperationTag = MovementComponent->BindGameplayTag( + CurrentOperation.Header.Tag, + Prediction, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::Periodic_Output, + EGMC_InterpolationFunction::TargetValue); + + BI_OperationClass = MovementComponent->BindName( + CurrentOperation.Header.ItemClassName, + Prediction, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::Periodic_Output, + EGMC_InterpolationFunction::TargetValue); + + BI_OperationPayload = MovementComponent->BindInstancedStruct( + CurrentOperation.Header.InstancedPayload, + Prediction, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::Periodic_Output, + EGMC_InterpolationFunction::TargetValue); + + BI_OperationExtraFlags = MovementComponent->BindByte( + CurrentOperation.Header.ExtraFlags, + Prediction, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::Periodic_Output, + EGMC_InterpolationFunction::TargetValue); + + BI_OperationPayloadIds = MovementComponent->BindInstancedStruct( + CurrentOperation.InstancedPayloadIds, + Prediction, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::Periodic_Output, + EGMC_InterpolationFunction::TargetValue); + } + + CurrentOperation = TGMASBoundQueueOperation(); + CurrentOperation.Header = FGMASBoundQueueRPCHeader(); + CurrentOperation.Refresh(false); + } + + void PreLocalMovement() + { + if (QueuedBoundOperations.Num() > 0 && ClientAuth) + { + CurrentOperation = QueuedBoundOperations.Pop(); + CurrentOperation.Refresh(false); + } + } + + void PreRemoteMovement() + { + ClearCurrentOperation(); + if (QueuedBoundOperations.Num() > 0 && !ClientAuth) + { + CurrentOperation = QueuedBoundOperations.Pop(); + CurrentOperation.Refresh(false); + } + } + + int32 GenerateOperationId() const + { + int32 NewOperationId = ActionTimer * 100; + + TGMASBoundQueueOperation Operation; + + while (GetOperationById(NewOperationId, Operation)) + { + NewOperationId++; + } + return NewOperationId; + } + + void ClearCurrentOperation() + { + CurrentOperation.Header = FGMASBoundQueueRPCHeader(); + + // CurrentOperation.Header.OperationId = -1; + // CurrentOperation.Header.Tag = FGameplayTag::EmptyTag; + // CurrentOperation.Header.ItemClassName = NAME_None; + // CurrentOperation.SetOperationType(EGMASBoundQueueOperationType::None); + // CurrentOperation.Header.PayloadIds.Ids.Empty(); + + CurrentOperation.Payload = T(); + CurrentOperation.Refresh(false); + } + + void GenPredictionTick(float DeltaTime) + { + ActionTimer += DeltaTime; + ExpireStaleAcks(DeltaTime); + } + + int32 MakeOperation(TGMASBoundQueueOperation& NewOperation, EGMASBoundQueueOperationType Type, FGameplayTag Tag, const T& Payload, TArray PayloadIds = {}, TSubclassOf ItemClass = nullptr, float RPCGracePeriod = 1.f, uint8 ExtraFlags = 0) + { + NewOperation.Header.OperationId = GenerateOperationId(); + NewOperation.SetOperationType(Type); + NewOperation.Header.Tag = Tag; + NewOperation.Payload = Payload; + NewOperation.ItemClass = ItemClass; + NewOperation.Header.RPCGracePeriodSeconds = RPCGracePeriod; + NewOperation.Header.PayloadIds.Ids = PayloadIds; + NewOperation.Header.ExtraFlags = ExtraFlags; + + NewOperation.Refresh(false); + + return NewOperation.GetOperationId(); + } + + int32 MakeOperation(TGMASBoundQueueOperation& NewOperation, const FGMASBoundQueueRPCHeader& Header, const T& Payload) + { + NewOperation.Header = Header; + NewOperation.Payload = Payload; + NewOperation.Refresh(); + NewOperation.InstancedPayload = FInstancedStruct::Make(Payload); + NewOperation.InstancedPayloadIds = FInstancedStruct::Make(NewOperation.Header.PayloadIds); + + return NewOperation.GetOperationId(); + } + + void QueuePreparedOperation(TGMASBoundQueueOperation& NewOperation, bool bMovementSynced = true) + { + TGMASBoundQueueOperation TestOperation = TGMASBoundQueueOperation(); + + // Don't bother queueing it if it already exists. + if (GetOperationById(NewOperation.GetOperationId(), TestOperation)) return; + + if (bMovementSynced) + { + // This needs to be handled via GMC, so add it to our queue. + QueuedBoundOperations.Push(NewOperation); + } + else + { + QueuedRPCOperations.Push(NewOperation); + } + } + + int32 QueueOperation(TGMASBoundQueueOperation& NewOperation, EGMASBoundQueueOperationType Type, FGameplayTag Tag, const T& Payload, TArray PayloadIds = {}, TSubclassOf ItemClass = nullptr, bool bMovementSynced = true, float RPCGracePeriod = 1.f) + { + MakeOperation(NewOperation, Type, Tag, Payload, PayloadIds, ItemClass, RPCGracePeriod); + QueuePreparedOperation(NewOperation, bMovementSynced); + return NewOperation.GetOperationId(); + } + + void QueueOperationFromHeader(const FGMASBoundQueueRPCHeader& Header, bool bMovementSynced) + { + TGMASBoundQueueOperation NewOperation; + + NewOperation.Header = Header; + NewOperation.Refresh(true); + QueuePreparedOperation(NewOperation, bMovementSynced); + } + + int Num() const + { + return QueuedBoundOperations.Num(); + } + + int NumMatching(FGameplayTag Tag, EGMASBoundQueueOperationType Type = EGMASBoundQueueOperationType::None) const + { + int Result = 0; + for (const auto& Operation : QueuedBoundOperations) + { + if (Operation.GetTag() == Tag) + { + if (Type == EGMASBoundQueueOperationType::None || Operation.GetOperationType() == Type) Result++; + } + } + for (const auto& Operation : QueuedRPCOperations) + { + if (Operation.GetTag() == Tag) + { + if (Type == EGMASBoundQueueOperationType::None || Operation.GetOperationType() == Type) Result++; + } + } + return Result; + } + + const TArray>& GetQueuedOperations() const { return QueuedBoundOperations; } + + const TArray>& GetQueuedRPCOperations() const { return QueuedRPCOperations; } + + bool GetOperationById(int32 OperationId, TGMASBoundQueueOperation& OutOperation) const + { + for (const auto& Operation : GetQueuedOperations()) + { + if (Operation.GetOperationId() == OperationId) + { + OutOperation = Operation; + return true; + } + } + + for (const auto& Operation : GetQueuedRPCOperations()) + { + if (Operation.GetOperationId() == OperationId) + { + OutOperation = Operation; + return true; + } + } + + return false; + } + + bool HasOperationWithPayloadId(int32 PayloadId) const + { + for (const auto& Operation : QueuedBoundOperations) + { + if (Operation.GetPayloadIds().Contains(PayloadId)) + { + return true; + } + } + + for (const auto& Operation : QueuedRPCOperations) + { + if (Operation.GetPayloadIds().Contains(PayloadId)) + { + return true; + } + } + + return false; + } + + bool RemoveOperationById(int32 OperationId) + { + int TargetIdx = -1; + + for (int Idx = 0; Idx < QueuedRPCOperations.Num() && TargetIdx == -1; Idx++) + { + if(QueuedRPCOperations[Idx].GetOperationId() == OperationId) + { + TargetIdx = Idx; + } + } + + if (TargetIdx != -1) + { +#if ENGINE_MAJOR_VERSION >=5 && ENGINE_MINOR_VERSION >= 4 + QueuedRPCOperations.RemoveAtSwap(TargetIdx, 1, EAllowShrinking::No); +#else + QueuedRPCOperations.RemoveAtSwap(TargetIdx, 1, false); +#endif + return true; + } + + TargetIdx = -1; + for (int Idx = 0; Idx < QueuedBoundOperations.Num() && TargetIdx == -1; Idx++) + { + if(QueuedBoundOperations[Idx].GetOperationId() == OperationId) + { + TargetIdx = Idx; + } + } + + if (TargetIdx != -1) + { +#if ENGINE_MAJOR_VERSION >=5 && ENGINE_MINOR_VERSION >= 4 + QueuedBoundOperations.RemoveAtSwap(TargetIdx, 1, EAllowShrinking::No); +#else + QueuedBoundOperations.RemoveAtSwap(TargetIdx, 1, false); +#endif + return true; + } + + return false; + } + + bool GetCurrentBoundOperation(TGMASBoundQueueOperation& Operation, bool bRefresh = false) + { + Operation = CurrentOperation; + if (Operation.GetOperationType() != EGMASBoundQueueOperationType::None) + { + if (bRefresh) + { + Operation.Refresh(true); + } + else + { + Operation.RefreshClass(); + } + return true; + } + + return false; + } + + bool PopNextRPCOperation(TGMASBoundQueueOperation& Operation) + { + if (QueuedRPCOperations.Num() == 0) return false; + + Operation = QueuedRPCOperations.Pop(); + return true; + } + + void DeductGracePeriod(float DeltaTime) + { + for (auto& Operation : QueuedRPCOperations) + { + Operation.Header.RPCGracePeriodSeconds -= DeltaTime; + } + } + + void Acknowledge(int32 OperationId, float AckLifetime = 5.f) + { + if (!IsAcknowledged(OperationId)) + { + FGMASBoundQueueAcknowledgement NewAck; + NewAck.Id = OperationId; + NewAck.Lifetime = AckLifetime; + + auto& Acks = Acknowledgments.GetMutable(); + Acks.AckSet.Add(NewAck); + } + } + + bool IsAcknowledged(int32 OperationId) const + { + const auto& Acks = Acknowledgments.Get(); + for (const auto& Ack : Acks.AckSet) + { + if (Ack.Id == OperationId) return true; + } + return false; + } + + void ExpireStaleAcks(float DeltaTime) + { + // Deduct from our ack lifetime; if we've gone stale, remove the stale acks to avoid it just growing forever. + TArray FreshAcks; + if (!Acknowledgments.IsValid()) { // that fix the crash, but doesn't fix the issue. @packetdancer + return; + } + + auto& Acks = Acknowledgments.GetMutable(); + for (auto& Ack : Acks.AckSet) + { + Ack.Lifetime -= DeltaTime; + if (Ack.Lifetime > 0.f) + { + FreshAcks.Add(Ack); + } + } + Acks.AckSet = FreshAcks; + } + +}; diff --git a/Source/GMCAbilitySystem/Public/Utility/GMASSyncedEvent.h b/Source/GMCAbilitySystem/Public/Utility/GMASSyncedEvent.h new file mode 100644 index 00000000..f34d1155 --- /dev/null +++ b/Source/GMCAbilitySystem/Public/Utility/GMASSyncedEvent.h @@ -0,0 +1,55 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTags.h" + +// Fix for instanced struct on previous 5.5 version +#if ENGINE_MAJOR_VERSION >=5 && ENGINE_MINOR_VERSION >= 5 + #include "StructUtils/InstancedStruct.h" +#else + #include "InstancedStruct.h" +#endif + +#include "GMASSyncedEvent.generated.h" + +UCLASS() +class GMCABILITYSYSTEM_API UGMASSyncedEvent : public UObject +{ + GENERATED_BODY() +}; + +UENUM() +enum EGMASSyncedEventType +{ + BlueprintImplemented, + Custom, + AddImpulse, + PlayMontage +}; + +USTRUCT(BlueprintType) +struct GMCABILITYSYSTEM_API FGMASSyncedEventContainer +{ + GENERATED_BODY() + + UPROPERTY() + TEnumAsByte EventType{BlueprintImplemented}; + + UPROPERTY(BlueprintReadWrite, Category = "GMASSyncedEvent") + FGameplayTag EventTag; + + UPROPERTY(BlueprintReadWrite, Category = "GMASSyncedEvent") + FInstancedStruct InstancedPayload; +}; + +USTRUCT(BlueprintType) +struct GMCABILITYSYSTEM_API FGMASSyncedEventData_AddImpulse +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadWrite, Category = "Impulse") + FVector Impulse {FVector::Zero()}; + + UPROPERTY(BlueprintReadWrite, Category = "Impulse") + bool bVelocityChange {false}; +}; \ No newline at end of file