From 2b4053cedd90a0b8e812db8e9df7b815620c4048 Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Fri, 27 Sep 2024 14:32:48 -0700 Subject: [PATCH 01/18] Beginnings of general queue implementation --- .../Public/Components/GMCAbilityComponent.h | 4 +- .../Public/Utility/GMASBoundQueue.h | 161 ++++++++++++++++++ 2 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h diff --git a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h index cad75c50..e5c46edd 100644 --- a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h +++ b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h @@ -12,6 +12,7 @@ #include "Effects/GMCAbilityEffect.h" #include "Components/ActorComponent.h" #include "GMCAbilityOuterApplication.h" +#include "Utility/GMASBoundQueue.h" #include "GMCAbilityComponent.generated.h" @@ -459,7 +460,8 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo UPROPERTY() TMap ActiveCooldowns; - + + TGMASBoundQueueOperation QueuedEffects; int GenerateAbilityID() const {return ActionTimer * 100;} diff --git a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h new file mode 100644 index 00000000..34876a16 --- /dev/null +++ b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h @@ -0,0 +1,161 @@ +// 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" + +enum class EGMASBoundQueueOperationType : uint8 +{ + None, + Add, + Remove, + Cancel +}; + +template +struct GMCABILITYSYSTEM_API TGMASBoundQueueOperation +{ + // A unique ID for this operation. + int32 OperationId { -1 }; + + // What this operation actually *is* (add, remove, etc.) + EGMASBoundQueueOperationType OperationType { EGMASBoundQueueOperationType::None }; + + // The tag associated with this operation (if any) + FGameplayTag Tag { FGameplayTag::EmptyTag }; + + // An actual class to be utilized with this, in case we need to instance it. + TSubclassOf ItemClass { nullptr }; + + // An FName representation of this item, used for GMC replication. + FName ItemClassName { NAME_None }; + + // The struct payload for this item. + T Payload; + + // The instanced struct representation of this payload, used to actually + // bind for replication. + FInstancedStruct InstancedPayload; + + // If true, this item must be replicated via GMC and thus preserved in the + // movement history. If false, this item can be sent via standard Unreal RPC + // separate from GMC. + bool bMovementSync { true }; + +}; + +template +class GMCABILITYSYSTEM_API TGMASBoundQueue +{ + +public: + + TGMASBoundQueueOperation CurrentOperation; + + int BI_OperationId { -1 }; + int BI_OperationType { -1 }; + int BI_OperationTag { -1 }; + int BI_OperationClass { -1 }; + int BI_OperationPayload { -1 }; + + TArray> QueuedOperations; + + double ActionTimer { 0 }; + + void BindToGMC(UGMC_MovementUtilityCmp* MovementComponent) + { + const EGMC_PredictionMode Prediction = ClientAuth ? EGMC_PredictionMode::ClientAuth_Input : EGMC_PredictionMode::ServerAuth_Output_ClientValidated; + + BI_OperationId = MovementComponent->BindInt( + CurrentOperation.OperationId, + Prediction, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::Periodic_Output, + EGMC_InterpolationFunction::TargetValue); + + BI_OperationType = MovementComponent->BindByte( + CurrentOperation.OperationType, + Prediction, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::Periodic_Output, + EGMC_InterpolationFunction::TargetValue); + + BI_OperationTag = MovementComponent->BindGameplayTag( + CurrentOperation.Tag, + Prediction, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::Periodic_Output, + EGMC_InterpolationFunction::TargetValue); + + BI_OperationClass = MovementComponent->BindName( + CurrentOperation.ItemClassName, + Prediction, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::Periodic_Output, + EGMC_InterpolationFunction::TargetValue); + + BI_OperationPayload = MovementComponent->BindInstancedStruct( + CurrentOperation.InstancedStruct, + Prediction, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::Periodic_Output, + EGMC_InterpolationFunction::TargetValue); + } + + void PreLocalMovement() + { + if (QueuedOperations.Num() > 0) + { + CurrentOperation = QueuedOperations.Pop(); + } + } + + int32 GenerateOperationId() const { return ActionTimer * 100; } + + void ClearCurrentOperation() + { + CurrentOperation.OperationId = -1; + CurrentOperation.Tag = FGameplayTag::EmptyTag; + CurrentOperation.ItemClassName = NAME_None; + CurrentOperation.OperationType = EGMASBoundQueueOperationType::None; + } + + void GenPredictionTick(float DeltaTime) + { + ActionTimer += DeltaTime; + } + + int32 QueueOperation(TGMASBoundQueueOperation& NewOperation, EGMASBoundQueueOperationType Type, FGameplayTag Tag, const T& Payload, TSubclassOf ItemClass = nullptr, bool bMovementSynced = true) + { + NewOperation.OperationId = GenerateOperationId(); + NewOperation.OperationType = Type; + NewOperation.Tag = Tag; + NewOperation.Payload = Payload; + NewOperation.InstancedPayload = FInstancedStruct::Make(Payload); + NewOperation.ItemClass = ItemClass; + if (ItemClass) + { + NewOperation.ItemClassName = ItemClass->GetFullName(); + } + + if (bMovementSynced) + { + // This needs to be handled via GMC, so add it to our queue. + QueuedOperations.Push(NewOperation); + } + + return NewOperation.OperationId; + } + + bool GetCurrentOperation(TGMASBoundQueueOperation& Operation) + { + Operation = CurrentOperation; + + return (CurrentOperation.OperationType != EGMASBoundQueueOperationType::None); + } + +}; From 137bc2687aba0580ec26ac062ade576abd5167b0 Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Fri, 27 Sep 2024 15:43:04 -0700 Subject: [PATCH 02/18] Further work on bound queues. --- .../Components/GMCAbilityComponent.cpp | 9 +++- .../Private/Utility/GMASBoundQueue.cpp | 1 + .../Public/Components/GMCAbilityComponent.h | 2 + .../Public/Utility/GMASBoundQueue.h | 51 +++++++++++++++---- 4 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 Source/GMCAbilitySystem/Private/Utility/GMASBoundQueue.cpp diff --git a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp index d8c2f254..72cf9f71 100644 --- a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp +++ b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp @@ -175,6 +175,8 @@ void UGMC_AbilitySystemComponent::BindReplicationData() EGMC_CombineMode::CombineIfUnchanged, EGMC_SimulationMode::None, EGMC_InterpolationFunction::TargetValue); + + QueuedAbilityOperations.BindToGMC(GMCMovementComponent); } void UGMC_AbilitySystemComponent::GenAncillaryTick(float DeltaTime, bool bIsCombinedClientMove) @@ -581,6 +583,11 @@ void UGMC_AbilitySystemComponent::GenPredictionTick(float DeltaTime) SendTaskDataToActiveAbility(true); + TGMASBoundQueueOperation Operation; + if (QueuedAbilityOperations.GetCurrentOperation(Operation)) + { + // We have a valid operation and ought to kick it off. + } } @@ -610,7 +617,7 @@ void UGMC_AbilitySystemComponent::PreLocalMoveExecution() TaskData = QueuedTaskData.Pop(); } - + QueuedAbilityOperations.PreLocalMovement(); } void UGMC_AbilitySystemComponent::BeginPlay() 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/Public/Components/GMCAbilityComponent.h b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h index e5c46edd..aab4c254 100644 --- a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h +++ b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h @@ -442,6 +442,8 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo TArray QueuedAbilities; TArray QueuedTaskData; + TGMASBoundQueue QueuedAbilityOperations; + // Current Ability Data being processed // Members of this struct are bound over GMC // FGMCAbilityData AbilityData; diff --git a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h index 34876a16..99687d71 100644 --- a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h +++ b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h @@ -7,7 +7,10 @@ #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, @@ -22,9 +25,8 @@ struct GMCABILITYSYSTEM_API TGMASBoundQueueOperation // A unique ID for this operation. int32 OperationId { -1 }; - // What this operation actually *is* (add, remove, etc.) - EGMASBoundQueueOperationType OperationType { EGMASBoundQueueOperationType::None }; - + uint8 OperationTypeRaw { 0 }; + // The tag associated with this operation (if any) FGameplayTag Tag { FGameplayTag::EmptyTag }; @@ -45,6 +47,16 @@ struct GMCABILITYSYSTEM_API TGMASBoundQueueOperation // movement history. If false, this item can be sent via standard Unreal RPC // separate from GMC. bool bMovementSync { true }; + + EGMASBoundQueueOperationType GetOperationType() const + { + return static_cast(OperationTypeRaw); + } + + void SetOperationType(EGMASBoundQueueOperationType OperationType) + { + OperationTypeRaw = static_cast(OperationType); + } }; @@ -61,7 +73,7 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue int BI_OperationTag { -1 }; int BI_OperationClass { -1 }; int BI_OperationPayload { -1 }; - + TArray> QueuedOperations; double ActionTimer { 0 }; @@ -78,7 +90,7 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue EGMC_InterpolationFunction::TargetValue); BI_OperationType = MovementComponent->BindByte( - CurrentOperation.OperationType, + CurrentOperation.OperationTypeRaw, Prediction, EGMC_CombineMode::CombineIfUnchanged, EGMC_SimulationMode::Periodic_Output, @@ -99,7 +111,7 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue EGMC_InterpolationFunction::TargetValue); BI_OperationPayload = MovementComponent->BindInstancedStruct( - CurrentOperation.InstancedStruct, + CurrentOperation.InstancedPayload, Prediction, EGMC_CombineMode::CombineIfUnchanged, EGMC_SimulationMode::Periodic_Output, @@ -121,7 +133,7 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue CurrentOperation.OperationId = -1; CurrentOperation.Tag = FGameplayTag::EmptyTag; CurrentOperation.ItemClassName = NAME_None; - CurrentOperation.OperationType = EGMASBoundQueueOperationType::None; + CurrentOperation.SetOperationType(EGMASBoundQueueOperationType::None); } void GenPredictionTick(float DeltaTime) @@ -132,7 +144,7 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue int32 QueueOperation(TGMASBoundQueueOperation& NewOperation, EGMASBoundQueueOperationType Type, FGameplayTag Tag, const T& Payload, TSubclassOf ItemClass = nullptr, bool bMovementSynced = true) { NewOperation.OperationId = GenerateOperationId(); - NewOperation.OperationType = Type; + NewOperation.SetOperationType(Type); NewOperation.Tag = Tag; NewOperation.Payload = Payload; NewOperation.InstancedPayload = FInstancedStruct::Make(Payload); @@ -151,11 +163,32 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue return NewOperation.OperationId; } + int Num() const + { + return QueuedOperations.Num(); + } + bool GetCurrentOperation(TGMASBoundQueueOperation& Operation) { Operation = CurrentOperation; - return (CurrentOperation.OperationType != EGMASBoundQueueOperationType::None); + if (Operation.GetOperationType() != EGMASBoundQueueOperationType::None) + { + Operation.Payload = CurrentOperation.InstancedPayload.template Get(); + + if (Operation.ItemClassName != NAME_None) + { + // Get a handle to our class, for instancing purposes. + ConstructorHelpers::FClassFinder ResolvedItemClass(*Operation.ItemClassName.ToString()); + if (ResolvedItemClass.Class != nullptr) + { + Operation.ItemClass = ResolvedItemClass.Class; + } + } + return true; + } + + return false; } }; From a5f9c92e6e8cd4533ba36f2a1c076103654f085a Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Fri, 27 Sep 2024 16:25:44 -0700 Subject: [PATCH 03/18] Convert abilities over to using the new bound operations queue. --- .../Ability/Tasks/WaitForInputKeyPress.cpp | 11 ++-- .../Ability/Tasks/WaitForInputKeyRelease.cpp | 2 +- .../Components/GMCAbilityComponent.cpp | 61 +++++++------------ .../Public/Components/GMCAbilityComponent.h | 6 +- .../Public/Utility/GMASBoundQueue.h | 31 +++++++--- 5 files changed, 54 insertions(+), 57 deletions(-) diff --git a/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForInputKeyPress.cpp b/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForInputKeyPress.cpp index 0492f27d..2e18f150 100644 --- a/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForInputKeyPress.cpp +++ b/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForInputKeyPress.cpp @@ -24,11 +24,14 @@ void UGMCAbilityTask_WaitForInputKeyPress::Activate() { UEnhancedInputComponent* const InputComponent = GetEnhancedInputComponent(); - const FEnhancedInputActionEventBinding& Binding = InputComponent->BindAction( - Ability->AbilityInputAction, ETriggerEvent::Started, this, - &UGMCAbilityTask_WaitForInputKeyPress::OnKeyPressed); + if (InputComponent) + { + const FEnhancedInputActionEventBinding& Binding = InputComponent->BindAction( + Ability->AbilityInputAction, ETriggerEvent::Started, this, + &UGMCAbilityTask_WaitForInputKeyPress::OnKeyPressed); - InputBindingHandle = Binding.GetHandle(); + InputBindingHandle = Binding.GetHandle(); + } } else { diff --git a/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForInputKeyRelease.cpp b/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForInputKeyRelease.cpp index d2184dc7..77d4396e 100644 --- a/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForInputKeyRelease.cpp +++ b/Source/GMCAbilitySystem/Private/Ability/Tasks/WaitForInputKeyRelease.cpp @@ -19,7 +19,7 @@ void UGMCAbilityTask_WaitForInputKeyRelease::Activate() UEnhancedInputComponent* const InputComponent = GetEnhancedInputComponent(); - if (Ability->AbilityInputAction != nullptr) + if (Ability->AbilityInputAction != nullptr && InputComponent != nullptr) { FEnhancedInputActionEventBinding& Binding = InputComponent->BindAction( Ability->AbilityInputAction, ETriggerEvent::Completed, this, diff --git a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp index 72cf9f71..4e4e94f0 100644 --- a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp +++ b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp @@ -142,20 +142,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, @@ -176,6 +162,7 @@ void UGMC_AbilitySystemComponent::BindReplicationData() EGMC_SimulationMode::None, EGMC_InterpolationFunction::TargetValue); + // Bind our ability operation queue. QueuedAbilityOperations.BindToGMC(GMCMovementComponent); } @@ -194,11 +181,14 @@ void UGMC_AbilitySystemComponent::GenAncillaryTick(float DeltaTime, bool bIsComb TickActiveCooldowns(DeltaTime); TickAncillaryActiveAbilities(DeltaTime); - - // Activate abilities from ancillary tick if they have bActivateOnMovementTick set to false - if (AbilityData.InputTag != FGameplayTag::EmptyTag) + + // Check if we have a valid operation + if (TGMASBoundQueueOperation Operation; QueuedAbilityOperations.GetCurrentOperation(Operation)) { - TryActivateAbilitiesByInputTag(AbilityData.InputTag, AbilityData.ActionInput, false); + if (Operation.GetOperationType() == EGMASBoundQueueOperationType::Activate) + { + TryActivateAbilitiesByInputTag(Operation.Tag, Operation.Payload.ActionInput, false); + } } SendTaskDataToActiveAbility(false); @@ -412,18 +402,14 @@ void UGMC_AbilitySystemComponent::QueueAbility(FGameplayTag InputTag, const UInp FGMCAbilityData Data; Data.InputTag = InputTag; Data.ActionInput = InputAction; - QueuedAbilities.Push(Data); + + 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) @@ -576,19 +562,17 @@ void UGMC_AbilitySystemComponent::GenPredictionTick(float DeltaTime) CleanupStaleAbilities(); // Was an ability used? - if (AbilityData.InputTag != FGameplayTag::EmptyTag) - { - TryActivateAbilitiesByInputTag(AbilityData.InputTag, AbilityData.ActionInput, true); - } - - SendTaskDataToActiveAbility(true); - - TGMASBoundQueueOperation Operation; - if (QueuedAbilityOperations.GetCurrentOperation(Operation)) + if (TGMASBoundQueueOperation Operation; + QueuedAbilityOperations.GetCurrentOperation(Operation)) { - // We have a valid operation and ought to kick it off. + // We have an operation! + if (Operation.GetOperationType() == EGMASBoundQueueOperationType::Activate) + { + TryActivateAbilitiesByInputTag(Operation.Tag, Operation.Payload.ActionInput, true); + } } + SendTaskDataToActiveAbility(true); } void UGMC_AbilitySystemComponent::GenSimulationTick(float DeltaTime) @@ -608,10 +592,6 @@ void UGMC_AbilitySystemComponent::GenSimulationTick(float DeltaTime) void UGMC_AbilitySystemComponent::PreLocalMoveExecution() { - if (QueuedAbilities.Num() > 0) - { - AbilityData = QueuedAbilities.Pop(); - } if (QueuedTaskData.Num() > 0) { TaskData = QueuedTaskData.Pop(); @@ -1066,6 +1046,7 @@ TArray> UGMC_AbilitySystemComponent::GetGrantedAbilitie void UGMC_AbilitySystemComponent::ClearAbilityAndTaskData() { AbilityData = FGMCAbilityData{}; + QueuedAbilityOperations.ClearCurrentOperation(); TaskData = FInstancedStruct::Make(FGMCAbilityTaskData{}); } diff --git a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h index aab4c254..c1eab91b 100644 --- a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h +++ b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h @@ -439,15 +439,11 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo // Add the starting ability tags to GrantedAbilities at start void InitializeStartingAbilities(); - TArray QueuedAbilities; TArray QueuedTaskData; + // Queued ability operations (activate, cancel, etc.) TGMASBoundQueue QueuedAbilityOperations; - // Current Ability Data being processed - // Members of this struct are bound over GMC - // FGMCAbilityData AbilityData; - // Predictions of Effect state changes FEffectStatePrediction EffectStatePrediction{}; diff --git a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h index 99687d71..663795e7 100644 --- a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h +++ b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h @@ -16,6 +16,7 @@ enum class EGMASBoundQueueOperationType : uint8 None, Add, Remove, + Activate, Cancel }; @@ -151,7 +152,11 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue NewOperation.ItemClass = ItemClass; if (ItemClass) { - NewOperation.ItemClassName = ItemClass->GetFullName(); + NewOperation.ItemClassName = FName(ItemClass->GetPathName()); + } + else + { + NewOperation.ItemClassName = NAME_None; } if (bMovementSynced) @@ -167,6 +172,21 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue { return QueuedOperations.Num(); } + + int NumMatching(FGameplayTag Tag, EGMASBoundQueueOperationType Type = EGMASBoundQueueOperationType::None) const + { + int Result = 0; + for (const auto& Operation : QueuedOperations) + { + if (Operation.Tag == Tag) + { + if (Type == EGMASBoundQueueOperationType::None || Operation.GetOperationType() == Type) Result++; + } + } + return Result; + } + + const TArray> &GetQueuedOperations() const { return QueuedOperations; } bool GetCurrentOperation(TGMASBoundQueueOperation& Operation) { @@ -176,14 +196,11 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue { Operation.Payload = CurrentOperation.InstancedPayload.template Get(); - if (Operation.ItemClassName != NAME_None) + if (Operation.ItemClassName != NAME_None && !Operation.ItemClass) { // Get a handle to our class, for instancing purposes. - ConstructorHelpers::FClassFinder ResolvedItemClass(*Operation.ItemClassName.ToString()); - if (ResolvedItemClass.Class != nullptr) - { - Operation.ItemClass = ResolvedItemClass.Class; - } + TSoftClassPtr ClassPtr = TSoftClassPtr(FSoftObjectPath(Operation.ItemClassName.ToString())); + Operation.ItemClass = ClassPtr.LoadSynchronous(); } return true; } From b38d417be5133b5ebd8f7652dfbcd7923790f503 Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Fri, 27 Sep 2024 16:53:22 -0700 Subject: [PATCH 04/18] Refactor to generalize ability queue processing --- .../Components/GMCAbilityComponent.cpp | 39 ++++++++++++++----- .../Public/Components/GMCAbilityComponent.h | 4 +- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp index 4e4e94f0..22c59a0b 100644 --- a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp +++ b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp @@ -185,10 +185,7 @@ void UGMC_AbilitySystemComponent::GenAncillaryTick(float DeltaTime, bool bIsComb // Check if we have a valid operation if (TGMASBoundQueueOperation Operation; QueuedAbilityOperations.GetCurrentOperation(Operation)) { - if (Operation.GetOperationType() == EGMASBoundQueueOperationType::Activate) - { - TryActivateAbilitiesByInputTag(Operation.Tag, Operation.Payload.ActionInput, false); - } + ProcessAbilityOperation(Operation, false); } SendTaskDataToActiveAbility(false); @@ -560,16 +557,15 @@ void UGMC_AbilitySystemComponent::GenPredictionTick(float DeltaTime) // Abilities CleanupStaleAbilities(); + + // Advance our queue action timers. + QueuedAbilityOperations.GenPredictionTick(DeltaTime); // Was an ability used? if (TGMASBoundQueueOperation Operation; QueuedAbilityOperations.GetCurrentOperation(Operation)) { - // We have an operation! - if (Operation.GetOperationType() == EGMASBoundQueueOperationType::Activate) - { - TryActivateAbilitiesByInputTag(Operation.Tag, Operation.Payload.ActionInput, true); - } + ProcessAbilityOperation(Operation, true); } SendTaskDataToActiveAbility(true); @@ -1146,6 +1142,31 @@ void UGMC_AbilitySystemComponent::InitializeStartingAbilities() } } +bool UGMC_AbilitySystemComponent::ProcessAbilityOperation( + const TGMASBoundQueueOperation& Operation, bool bFromMovementTick) +{ + EGMASBoundQueueOperationType OperationType = Operation.GetOperationType(); + if (OperationType == EGMASBoundQueueOperationType::Activate) + { + TryActivateAbilitiesByInputTag(Operation.Tag, Operation.Payload.ActionInput, bFromMovementTick); + return true; + } + + if (OperationType == EGMASBoundQueueOperationType::Cancel) + { + EndAbilitiesByTag(Operation.Tag); + 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.Tag.ToString()) + return false; +} + void UGMC_AbilitySystemComponent::OnRep_UnBoundAttributes() { diff --git a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h index c1eab91b..28812035 100644 --- a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h +++ b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h @@ -444,6 +444,8 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo // Queued ability operations (activate, cancel, etc.) TGMASBoundQueue QueuedAbilityOperations; + bool ProcessAbilityOperation(const TGMASBoundQueueOperation& Operation, bool bFromMovementTick); + // Predictions of Effect state changes FEffectStatePrediction EffectStatePrediction{}; @@ -459,8 +461,6 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo UPROPERTY() TMap ActiveCooldowns; - TGMASBoundQueueOperation QueuedEffects; - int GenerateAbilityID() const {return ActionTimer * 100;} // Set Attributes to either a default object or a provided TSubClassOf in BP defaults From 9a9828f457405f4a5c6743e89074d28bb89c7ea7 Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Fri, 27 Sep 2024 18:03:38 -0700 Subject: [PATCH 05/18] Convert effects to bound queue --- .../Components/GMCAbilityComponent.cpp | 265 ++++++------- .../Public/Components/GMCAbilityComponent.h | 31 +- .../Components/GMCAbilityOuterApplication.h | 78 +--- .../Public/Utility/GMASBoundQueue.h | 349 ++++++++++++++++-- 4 files changed, 447 insertions(+), 276 deletions(-) diff --git a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp index 22c59a0b..da7bbbbc 100644 --- a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp +++ b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp @@ -156,19 +156,13 @@ 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 ability operation queue. + // Bind our operation queues. QueuedAbilityOperations.BindToGMC(GMCMovementComponent); + QueuedEffectOperations.BindToGMC(GMCMovementComponent); } void UGMC_AbilitySystemComponent::GenAncillaryTick(float DeltaTime, bool bIsCombinedClientMove) { - OnAncillaryTick.Broadcast(DeltaTime); ClientHandlePendingEffect(); @@ -183,7 +177,7 @@ void UGMC_AbilitySystemComponent::GenAncillaryTick(float DeltaTime, bool bIsComb // Check if we have a valid operation - if (TGMASBoundQueueOperation Operation; QueuedAbilityOperations.GetCurrentOperation(Operation)) + if (TGMASBoundQueueOperation Operation; QueuedAbilityOperations.GetCurrentBoundOperation(Operation)) { ProcessAbilityOperation(Operation, false); } @@ -560,10 +554,11 @@ void UGMC_AbilitySystemComponent::GenPredictionTick(float DeltaTime) // Advance our queue action timers. QueuedAbilityOperations.GenPredictionTick(DeltaTime); + QueuedEffectOperations.GenPredictionTick(DeltaTime); // Was an ability used? if (TGMASBoundQueueOperation Operation; - QueuedAbilityOperations.GetCurrentOperation(Operation)) + QueuedAbilityOperations.GetCurrentBoundOperation(Operation)) { ProcessAbilityOperation(Operation, true); } @@ -856,63 +851,23 @@ 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]; - - if (Wrapper.ClientGraceTimeRemaining <= 0.f || AckId.Id.Contains(Wrapper.LateApplicationID)) { - - 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; + QueuedEffectOperations.DeductGracePeriod(DeltaTime); + auto Operations = QueuedEffectOperations.GetQueuedRPCOperations(); + for (auto& Operation : Operations) { + if (QueuedEffectOperations.IsAcknowledged(Operation.GetOperationId()) || Operation.GracePeriodExpired()) + { + if (Operation.GracePeriodExpired()) + { + UE_LOG(LogGMCAbilitySystem, Log, TEXT("Client removal of effect missed grace period, forcing a removal.")) } - PendingApplicationServer.RemoveAt(i); + ProcessEffectOperation(Operation); + QueuedEffectOperations.RemoveOperationById(Operation.GetOperationId()); } - else { - Wrapper.ClientGraceTimeRemaining -= DeltaTime; - } - - } } @@ -920,55 +875,18 @@ void UGMC_AbilitySystemComponent::ServerHandlePendingEffect(float DeltaTime) { void UGMC_AbilitySystemComponent::ClientHandlePendingEffect() { - - for (int i = PendingApplicationClient.Num() - 1; i >= 0; i--) - { - FGMCOuterApplicationWrapper& LateApplicationData = PendingApplicationClient[i]; - - switch (LateApplicationData.Type) { - case EGMC_AddEffect: { - const FGMCOuterEffectAdd& Data = LateApplicationData.OuterApplicationData.Get(); - - if (Data.EffectClass == nullptr) { - UE_LOG(LogGMCAbilitySystem, Error, TEXT("ClientHandlePendingEffect: EffectClass is null")); - break; - } - - UGMCAbilityEffect* CDO = Data.EffectClass->GetDefaultObject(); - - 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); + auto RPCOperations = QueuedEffectOperations.GetQueuedRPCOperations(); + for (auto& Operation : RPCOperations) { + if (!QueuedEffectOperations.IsAcknowledged(Operation.GetOperationId())) + { + ProcessEffectOperation(Operation); + QueuedEffectOperations.RemoveOperationById(Operation.GetOperationId()); + QueuedEffectOperations.Acknowledge(Operation.GetOperationId()); } -} - - -int UGMC_AbilitySystemComponent::GenerateLateApplicationID() { - int NewEffectID = static_cast(ActionTimer * 100); - while (ActiveEffects.Contains(NewEffectID)) - { - NewEffectID++; } - - return NewEffectID; + } - void UGMC_AbilitySystemComponent::RPCTaskHeartbeat_Implementation(int AbilityID, int TaskID) { if (ActiveAbilities.Contains(AbilityID) && ActiveAbilities[AbilityID] != nullptr) @@ -1148,13 +1066,13 @@ bool UGMC_AbilitySystemComponent::ProcessAbilityOperation( EGMASBoundQueueOperationType OperationType = Operation.GetOperationType(); if (OperationType == EGMASBoundQueueOperationType::Activate) { - TryActivateAbilitiesByInputTag(Operation.Tag, Operation.Payload.ActionInput, bFromMovementTick); + TryActivateAbilitiesByInputTag(Operation.GetTag(), Operation.Payload.ActionInput, bFromMovementTick); return true; } if (OperationType == EGMASBoundQueueOperationType::Cancel) { - EndAbilitiesByTag(Operation.Tag); + EndAbilitiesByTag(Operation.GetTag()); if (Operation.ItemClass) { EndAbilitiesByClass(Operation.ItemClass); @@ -1163,10 +1081,85 @@ bool UGMC_AbilitySystemComponent::ProcessAbilityOperation( } UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Received ability operation with invalid operation type %s for %s!"), - *UEnum::GetValueAsString(OperationType), *Operation.Tag.ToString()) + *UEnum::GetValueAsString(OperationType), *Operation.GetTag().ToString()) return false; } +UGMCAbilityEffect* UGMC_AbilitySystemComponent::ProcessEffectOperation( + const TGMASBoundQueueOperation& Operation) +{ + EGMASBoundQueueOperationType OperationType = Operation.GetOperationType(); + + if (OperationType == EGMASBoundQueueOperationType::Add) + { + UGMCAbilityEffect* Effect = DuplicateObject(Operation.ItemClass->GetDefaultObject(), this); + FGMCAbilityEffectData EffectData; + if (Operation.Payload.IsValid()) + { + EffectData = Operation.Payload; + } + else + { + EffectData = Effect->EffectData; + } + + ApplyAbilityEffect(Effect, EffectData); + 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::ClientQueueEffectOperation( + const TGMASBoundQueueOperation& Operation) +{ + FGMCAbilityEffectRPCWrapper Wrapper; + GetEffectWrapperFromOperation(Operation, Wrapper); + RPCClientQueueEffectOperation(Wrapper); +} + +bool UGMC_AbilitySystemComponent::GetEffectWrapperFromOperation( + const TGMASBoundQueueOperation& Operation, + FGMCAbilityEffectRPCWrapper& Wrapper) +{ + Wrapper.Payload = Operation.Payload; + Wrapper.Header = Operation.Header; + + return true; +} + +bool UGMC_AbilitySystemComponent::GetEffectOperationFromWrapper(const FGMCAbilityEffectRPCWrapper& Wrapper, + TGMASBoundQueueOperation& Operation) +{ + QueuedEffectOperations.MakeOperation(Operation, Wrapper.Header, Wrapper.Payload); + + return Operation.IsValid(); +} + +void UGMC_AbilitySystemComponent::RPCClientQueueEffectOperation_Implementation(const FGMCAbilityEffectRPCWrapper& Wrapper) +{ + TGMASBoundQueueOperation Operation; + GetEffectOperationFromWrapper(Wrapper, Operation); + + if (!Operation.IsValid()) return; + + QueuedEffectOperations.AddQueuedRPCOperation(Operation); +} + void UGMC_AbilitySystemComponent::OnRep_UnBoundAttributes() { @@ -1212,32 +1205,19 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf Operation; + QueuedEffectOperations.MakeOperation(Operation, EGMASBoundQueueOperationType::Add, FGameplayTag::EmptyTag, InitializationData, {}, Effect, 1.f); // 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.AddQueuedRPCOperation(Operation); + ClientQueueEffectOperation(Operation); } return nullptr; } - - - UGMCAbilityEffect* AbilityEffect = DuplicateObject(Effect->GetDefaultObject(), this); - - FGMCAbilityEffectData EffectData; - if (InitializationData.IsValid()) - { - EffectData = InitializationData; - } - else - { - EffectData = AbilityEffect->EffectData; - } - - ApplyAbilityEffect(AbilityEffect, EffectData); - return AbilityEffect; + return ProcessEffectOperation(Operation); } UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(UGMCAbilityEffect* Effect, FGMCAbilityEffectData InitializationData) @@ -1301,8 +1281,8 @@ int32 UGMC_AbilitySystemComponent::RemoveEffectByTag(FGameplayTag InEffectTag, i return 0; } - TMap EffectsToRemove; - int32 NumRemoved = 0; + TArray EffectsToRemove; + int NumRemoved = 0; for(const TTuple Effect : ActiveEffects) { @@ -1311,30 +1291,16 @@ int32 UGMC_AbilitySystemComponent::RemoveEffectByTag(FGameplayTag InEffectTag, i } if(Effect.Value->EffectData.EffectTag.IsValid() && Effect.Value->EffectData.EffectTag.MatchesTagExact(InEffectTag)){ - EffectsToRemove.Add(Effect.Key, Effect.Value); + EffectsToRemove.Add(Effect.Value->EffectData.EffectID); NumRemoved++; } } - - if (bOuterActivation) { - if (HasAuthority() && EffectsToRemove.Num() > 0) { - - TArray EffectIDsToRemove; - for (const auto& ToRemove : EffectsToRemove) { - EffectIDsToRemove.Add(ToRemove.Key); - } - - FGMCOuterApplicationWrapper Wrapper = FGMCOuterApplicationWrapper::Make(EffectIDsToRemove); - AddPendingEffectApplications(Wrapper); - } - return 0; + if (NumRemoved > 0) + { + RemoveEffectById(EffectsToRemove, bOuterActivation); } - for (auto& ToRemove : EffectsToRemove) { - ToRemove.Value->EndEffect(); - } - return NumRemoved; } @@ -1355,8 +1321,11 @@ bool UGMC_AbilitySystemComponent::RemoveEffectById(TArray Ids, bool bOuterA if (bOuterActivation) { if (HasAuthority()) { - FGMCOuterApplicationWrapper Wrapper = FGMCOuterApplicationWrapper::Make(Ids); - AddPendingEffectApplications(Wrapper); + TGMASBoundQueueOperation Operation; + FGMCAbilityEffectData Data; + QueuedEffectOperations.MakeOperation(Operation, EGMASBoundQueueOperationType::Remove, FGameplayTag::EmptyTag, Data, Ids); + QueuedEffectOperations.AddQueuedRPCOperation(Operation); + ClientQueueEffectOperation(Operation); } return true; } diff --git a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h index 28812035..92534a52 100644 --- a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h +++ b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h @@ -443,9 +443,19 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo // Queued ability operations (activate, cancel, etc.) TGMASBoundQueue QueuedAbilityOperations; - bool ProcessAbilityOperation(const TGMASBoundQueueOperation& Operation, bool bFromMovementTick); + TGMASBoundQueue QueuedEffectOperations; + UGMCAbilityEffect* ProcessEffectOperation(const TGMASBoundQueueOperation& Operation); + + void ClientQueueEffectOperation(const TGMASBoundQueueOperation& Operation); + + UFUNCTION(Client, Reliable) + void RPCClientQueueEffectOperation(const FGMCAbilityEffectRPCWrapper& Wrapper); + + bool GetEffectWrapperFromOperation(const TGMASBoundQueueOperation& Operation, FGMCAbilityEffectRPCWrapper& Wrapper); + bool GetEffectOperationFromWrapper(const FGMCAbilityEffectRPCWrapper& Wrapper, TGMASBoundQueueOperation& Operation); + // Predictions of Effect state changes FEffectStatePrediction EffectStatePrediction{}; @@ -511,33 +521,14 @@ 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; - // doesn't work ATM. UPROPERTY(BlueprintReadOnly, Category = "GMCAbilitySystem", meta=(AllowPrivateAccess="true")) bool bInGMCTime = false; - // 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{}); - - 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 ServerHandlePendingEffect(float DeltaTime); void ClientHandlePendingEffect(); - int GenerateLateApplicationID(); - int LateApplicationIDCounter = 0; // Effect IDs that have been processed and don't need to be remade when ActiveEffectsData is replicated diff --git a/Source/GMCAbilitySystem/Public/Components/GMCAbilityOuterApplication.h b/Source/GMCAbilitySystem/Public/Components/GMCAbilityOuterApplication.h index 85bffac5..abe2a431 100644 --- a/Source/GMCAbilitySystem/Public/Components/GMCAbilityOuterApplication.h +++ b/Source/GMCAbilitySystem/Public/Components/GMCAbilityOuterApplication.h @@ -1,86 +1,28 @@ #pragma once #include "CoreMinimal.h" -#include "InstancedStruct.h" +#include "Utility/GMASBoundQueue.h" #include "GMCAbilityOuterApplication.generated.h" -USTRUCT() -struct FGMCAcknowledgeId { - GENERATED_BODY() - - UPROPERTY() - TArray Id = {}; -}; - - -UENUM() -enum EGMCOuterApplicationType { - EGMC_AddEffect, - EGMC_RemoveEffect, -}; +class UGMCAbilityEffect; USTRUCT(BlueprintType) -struct FGMCOuterEffectAdd { +struct FGMCAbilityEffectRPCWrapper +{ GENERATED_BODY() UPROPERTY() - TSubclassOf EffectClass; - + FGMASBoundQueueRPCHeader Header; + UPROPERTY() - FGMCAbilityEffectData InitializationData; + FGMCAbilityEffectData Payload {}; }; USTRUCT(BlueprintType) -struct FGMCOuterEffectRemove { - GENERATED_BODY() - - UPROPERTY() - TArray Ids = {}; -}; - -USTRUCT(BlueprintType) -struct FGMCOuterApplicationWrapper { +struct FGMCAbilityEffectIdSet +{ 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; - } - - + TArray Ids = {}; }; -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/Utility/GMASBoundQueue.h b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h index 663795e7..cf419b14 100644 --- a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h +++ b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h @@ -18,25 +18,55 @@ enum class EGMASBoundQueueOperationType : uint8 Remove, Activate, Cancel -}; +}; -template -struct GMCABILITYSYSTEM_API TGMASBoundQueueOperation +USTRUCT(BlueprintType) +struct FGMASBoundQueueOperationIdSet +{ + GENERATED_BODY() + + TArray Ids = {}; +}; + +USTRUCT(BlueprintType) +struct GMCABILITYSYSTEM_API FGMASBoundQueueRPCHeader { - // A unique ID for this operation. + GENERATED_BODY() + + UPROPERTY() int32 OperationId { -1 }; + UPROPERTY() uint8 OperationTypeRaw { 0 }; + + EGMASBoundQueueOperationType GetOperationType() const + { + return static_cast(OperationTypeRaw); + } - // The tag associated with this operation (if any) + UPROPERTY() FGameplayTag Tag { FGameplayTag::EmptyTag }; + UPROPERTY() + FName ItemClassName { NAME_None }; + + UPROPERTY() + FGMASBoundQueueOperationIdSet PayloadIds {}; + + UPROPERTY() + float RPCGracePeriodSeconds { 1.f }; +}; + +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 }; - // An FName representation of this item, used for GMC replication. - FName ItemClassName { NAME_None }; - + FInstancedStruct InstancedPayloadIds; + // The struct payload for this item. T Payload; @@ -51,16 +81,75 @@ struct GMCABILITYSYSTEM_API TGMASBoundQueueOperation EGMASBoundQueueOperationType GetOperationType() const { - return static_cast(OperationTypeRaw); + return Header.GetOperationType(); } void SetOperationType(EGMASBoundQueueOperationType OperationType) { - OperationTypeRaw = static_cast(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) + { + Payload = InstancedPayload.template Get(); + Header.PayloadIds = InstancedPayloadIds.Get(); + } + + 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(); + } } }; +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 { @@ -69,13 +158,19 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue 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_OperationPayload { -1 }; + int BI_OperationPayloadIds { -1 }; - TArray> QueuedOperations; + TArray> QueuedBoundOperations; + TArray> QueuedRPCOperations; + + FInstancedStruct Acknowledgments; double ActionTimer { 0 }; @@ -83,29 +178,48 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue { const EGMC_PredictionMode Prediction = ClientAuth ? EGMC_PredictionMode::ClientAuth_Input : EGMC_PredictionMode::ServerAuth_Output_ClientValidated; + 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 + ); + + // Our acknowledgments queue is always client-auth. + BI_Acknowledgements = MovementComponent->BindInstancedStruct( + Acknowledgments, + EGMC_PredictionMode::ClientAuth_Input, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::None, + EGMC_InterpolationFunction::TargetValue); + BI_OperationId = MovementComponent->BindInt( - CurrentOperation.OperationId, + CurrentOperation.Header.OperationId, Prediction, EGMC_CombineMode::CombineIfUnchanged, EGMC_SimulationMode::Periodic_Output, EGMC_InterpolationFunction::TargetValue); BI_OperationType = MovementComponent->BindByte( - CurrentOperation.OperationTypeRaw, + CurrentOperation.Header.OperationTypeRaw, Prediction, EGMC_CombineMode::CombineIfUnchanged, EGMC_SimulationMode::Periodic_Output, EGMC_InterpolationFunction::TargetValue); BI_OperationTag = MovementComponent->BindGameplayTag( - CurrentOperation.Tag, + CurrentOperation.Header.Tag, Prediction, EGMC_CombineMode::CombineIfUnchanged, EGMC_SimulationMode::Periodic_Output, EGMC_InterpolationFunction::TargetValue); BI_OperationClass = MovementComponent->BindName( - CurrentOperation.ItemClassName, + CurrentOperation.Header.ItemClassName, Prediction, EGMC_CombineMode::CombineIfUnchanged, EGMC_SimulationMode::Periodic_Output, @@ -117,13 +231,20 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue 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); } void PreLocalMovement() { - if (QueuedOperations.Num() > 0) + if (QueuedBoundOperations.Num() > 0) { - CurrentOperation = QueuedOperations.Pop(); + CurrentOperation = QueuedBoundOperations.Pop(); } } @@ -131,81 +252,229 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue void ClearCurrentOperation() { - CurrentOperation.OperationId = -1; - CurrentOperation.Tag = FGameplayTag::EmptyTag; - CurrentOperation.ItemClassName = NAME_None; + CurrentOperation.Header.OperationId = -1; + CurrentOperation.Header.Tag = FGameplayTag::EmptyTag; + CurrentOperation.Header.ItemClassName = NAME_None; CurrentOperation.SetOperationType(EGMASBoundQueueOperationType::None); + CurrentOperation.Header.PayloadIds.Ids.Empty(); } void GenPredictionTick(float DeltaTime) { ActionTimer += DeltaTime; + ExpireStaleAcks(DeltaTime); } - int32 QueueOperation(TGMASBoundQueueOperation& NewOperation, EGMASBoundQueueOperationType Type, FGameplayTag Tag, const T& Payload, TSubclassOf ItemClass = nullptr, bool bMovementSynced = true) + int32 MakeOperation(TGMASBoundQueueOperation& NewOperation, EGMASBoundQueueOperationType Type, FGameplayTag Tag, const T& Payload, TArray PayloadIds = {}, TSubclassOf ItemClass = nullptr, float RPCGracePeriod = 1.f) { - NewOperation.OperationId = GenerateOperationId(); + NewOperation.Header.OperationId = GenerateOperationId(); NewOperation.SetOperationType(Type); - NewOperation.Tag = Tag; + NewOperation.Header.Tag = Tag; NewOperation.Payload = Payload; NewOperation.InstancedPayload = FInstancedStruct::Make(Payload); NewOperation.ItemClass = ItemClass; + NewOperation.Header.RPCGracePeriodSeconds = RPCGracePeriod; + NewOperation.Header.PayloadIds.Ids = PayloadIds; + NewOperation.InstancedPayloadIds = FInstancedStruct::Make(NewOperation.Header.PayloadIds); + if (ItemClass) { - NewOperation.ItemClassName = FName(ItemClass->GetPathName()); + NewOperation.Header.ItemClassName = FName(ItemClass->GetPathName()); } else { - NewOperation.ItemClassName = NAME_None; + NewOperation.Header.ItemClassName = NAME_None; } + 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(); + } + + 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); if (bMovementSynced) { // This needs to be handled via GMC, so add it to our queue. - QueuedOperations.Push(NewOperation); + QueuedBoundOperations.Push(NewOperation); + } + else + { + QueuedRPCOperations.Push(NewOperation); } - return NewOperation.OperationId; + return NewOperation.GetOperationId(); } int Num() const { - return QueuedOperations.Num(); + return QueuedBoundOperations.Num(); } int NumMatching(FGameplayTag Tag, EGMASBoundQueueOperationType Type = EGMASBoundQueueOperationType::None) const { int Result = 0; - for (const auto& Operation : QueuedOperations) + for (const auto& Operation : QueuedBoundOperations) { - if (Operation.Tag == Tag) + 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 QueuedOperations; } - - bool GetCurrentOperation(TGMASBoundQueueOperation& Operation) + const TArray>& GetQueuedOperations() const { return QueuedBoundOperations; } + + const TArray>& GetQueuedRPCOperations() const { return QueuedRPCOperations; } + + void AddQueuedRPCOperation(const TGMASBoundQueueOperation& NewOperation) { - Operation = CurrentOperation; + // This is used by the client-side to add the RPC-call operations. + // This allows us to still handle the application of effects within + // the GMC lifecycle, for the sake of everyone's sanity. + TGMASBoundQueueOperation TempOperation = TGMASBoundQueueOperation(); - if (Operation.GetOperationType() != EGMASBoundQueueOperationType::None) + if (!GetOperationById(NewOperation.GetOperationId(), TempOperation)) + { + TempOperation = NewOperation; + QueuedRPCOperations.Push(TempOperation); + } + } + + bool GetOperationById(int32 OperationId, TGMASBoundQueueOperation& OutOperation) const + { + for (const auto& Operation : QueuedBoundOperations) + { + if (Operation.GetOperationId() == OperationId) + { + OutOperation = Operation; + return true; + } + } + + for (const auto& Operation : QueuedRPCOperations) { - Operation.Payload = CurrentOperation.InstancedPayload.template Get(); + if (Operation.GetOperationId() == OperationId) + { + OutOperation = Operation; + return true; + } + } + + return false; + } - if (Operation.ItemClassName != NAME_None && !Operation.ItemClass) + bool RemoveOperationById(int32 OperationId) + { + int TargetIdx = -1; + + for (int Idx = 0; Idx < QueuedRPCOperations.Num() && TargetIdx == -1; Idx++) + { + if(QueuedRPCOperations[Idx].GetOperationId() == OperationId) { - // Get a handle to our class, for instancing purposes. - TSoftClassPtr ClassPtr = TSoftClassPtr(FSoftObjectPath(Operation.ItemClassName.ToString())); - Operation.ItemClass = ClassPtr.LoadSynchronous(); + TargetIdx = Idx; } + } + + if (TargetIdx != -1) + { + QueuedRPCOperations.RemoveAtSwap(TargetIdx, 1, false); + return true; + } + + TargetIdx = -1; + for (int Idx = 0; Idx < QueuedBoundOperations.Num() && TargetIdx == -1; Idx++) + { + if(QueuedBoundOperations[Idx].GetOperationId() == OperationId) + { + TargetIdx = Idx; + } + } + + if (TargetIdx != -1) + { + QueuedBoundOperations.RemoveAtSwap(TargetIdx, 1, false); return true; } return false; } + bool GetCurrentBoundOperation(TGMASBoundQueueOperation& Operation) + { + Operation = CurrentOperation; + + if (Operation.GetOperationType() != EGMASBoundQueueOperationType::None) + { + Operation.Refresh(); + return true; + } + + return false; + } + + 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; + auto& Acks = Acknowledgments.GetMutable(); + for (auto& Ack : Acks.AckSet) + { + Ack.Lifetime -= DeltaTime; + if (Ack.Lifetime > 0.f) + { + FreshAcks.Add(Ack); + } + } + Acks.AckSet = FreshAcks; + } + }; From 58bc8d1a974d4d9cc8e362f2131274df18eff9f3 Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Fri, 27 Sep 2024 23:36:38 -0700 Subject: [PATCH 06/18] Refactor and cleanup --- .../Components/GMCAbilityComponent.cpp | 44 ++++++++----------- .../Public/Components/GMCAbilityComponent.h | 7 +-- .../Components/GMCAbilityOuterApplication.h | 28 ------------ 3 files changed, 21 insertions(+), 58 deletions(-) delete mode 100644 Source/GMCAbilitySystem/Public/Components/GMCAbilityOuterApplication.h diff --git a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp index da7bbbbc..addf7fbc 100644 --- a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp +++ b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp @@ -859,7 +859,7 @@ void UGMC_AbilitySystemComponent::ServerHandlePendingEffect(float DeltaTime) { QueuedEffectOperations.DeductGracePeriod(DeltaTime); auto Operations = QueuedEffectOperations.GetQueuedRPCOperations(); for (auto& Operation : Operations) { - if (QueuedEffectOperations.IsAcknowledged(Operation.GetOperationId()) || Operation.GracePeriodExpired()) + if (ShouldProcessEffectOperation(Operation, true)) { if (Operation.GracePeriodExpired()) { @@ -877,11 +877,11 @@ void UGMC_AbilitySystemComponent::ClientHandlePendingEffect() { auto RPCOperations = QueuedEffectOperations.GetQueuedRPCOperations(); for (auto& Operation : RPCOperations) { - if (!QueuedEffectOperations.IsAcknowledged(Operation.GetOperationId())) + if (ShouldProcessEffectOperation(Operation, false)) { ProcessEffectOperation(Operation); - QueuedEffectOperations.RemoveOperationById(Operation.GetOperationId()); QueuedEffectOperations.Acknowledge(Operation.GetOperationId()); + QueuedEffectOperations.RemoveOperationById(Operation.GetOperationId()); } } @@ -1124,36 +1124,30 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ProcessEffectOperation( return nullptr; } -void UGMC_AbilitySystemComponent::ClientQueueEffectOperation( - const TGMASBoundQueueOperation& Operation) +bool UGMC_AbilitySystemComponent::ShouldProcessEffectOperation( + const TGMASBoundQueueOperation& Operation, bool bIsServer) const { - FGMCAbilityEffectRPCWrapper Wrapper; - GetEffectWrapperFromOperation(Operation, Wrapper); - RPCClientQueueEffectOperation(Wrapper); -} - -bool UGMC_AbilitySystemComponent::GetEffectWrapperFromOperation( - const TGMASBoundQueueOperation& Operation, - FGMCAbilityEffectRPCWrapper& Wrapper) -{ - Wrapper.Payload = Operation.Payload; - Wrapper.Header = Operation.Header; - - return true; + if (bIsServer) + { + return HasAuthority() && (QueuedEffectOperations.IsAcknowledged(Operation.GetOperationId()) || + Operation.GracePeriodExpired() || GetNetMode() == NM_Standalone); + } + else + { + return !QueuedEffectOperations.IsAcknowledged(Operation.GetOperationId()) && (!HasAuthority() || GMCMovementComponent->IsLocallyControlledServerPawn()); + } } -bool UGMC_AbilitySystemComponent::GetEffectOperationFromWrapper(const FGMCAbilityEffectRPCWrapper& Wrapper, - TGMASBoundQueueOperation& Operation) +void UGMC_AbilitySystemComponent::ClientQueueEffectOperation( + const TGMASBoundQueueOperation& Operation) { - QueuedEffectOperations.MakeOperation(Operation, Wrapper.Header, Wrapper.Payload); - - return Operation.IsValid(); + RPCClientQueueEffectOperation(Operation.Header, Operation.Payload); } -void UGMC_AbilitySystemComponent::RPCClientQueueEffectOperation_Implementation(const FGMCAbilityEffectRPCWrapper& Wrapper) +void UGMC_AbilitySystemComponent::RPCClientQueueEffectOperation_Implementation(const FGMASBoundQueueRPCHeader& Header, const FGMCAbilityEffectData& Payload) { TGMASBoundQueueOperation Operation; - GetEffectOperationFromWrapper(Wrapper, Operation); + QueuedEffectOperations.MakeOperation(Operation, Header, Payload); if (!Operation.IsValid()) return; diff --git a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h index 92534a52..0b3e2ef2 100644 --- a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h +++ b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h @@ -11,7 +11,6 @@ #include "Ability/Tasks/GMCAbilityTaskData.h" #include "Effects/GMCAbilityEffect.h" #include "Components/ActorComponent.h" -#include "GMCAbilityOuterApplication.h" #include "Utility/GMASBoundQueue.h" #include "GMCAbilityComponent.generated.h" @@ -448,14 +447,12 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo TGMASBoundQueue QueuedEffectOperations; UGMCAbilityEffect* ProcessEffectOperation(const TGMASBoundQueueOperation& Operation); + bool ShouldProcessEffectOperation(const TGMASBoundQueueOperation& Operation, bool bIsServer = true) const; void ClientQueueEffectOperation(const TGMASBoundQueueOperation& Operation); UFUNCTION(Client, Reliable) - void RPCClientQueueEffectOperation(const FGMCAbilityEffectRPCWrapper& Wrapper); + void RPCClientQueueEffectOperation(const FGMASBoundQueueRPCHeader& Header, const FGMCAbilityEffectData& Payload); - bool GetEffectWrapperFromOperation(const TGMASBoundQueueOperation& Operation, FGMCAbilityEffectRPCWrapper& Wrapper); - bool GetEffectOperationFromWrapper(const FGMCAbilityEffectRPCWrapper& Wrapper, TGMASBoundQueueOperation& Operation); - // Predictions of Effect state changes FEffectStatePrediction EffectStatePrediction{}; diff --git a/Source/GMCAbilitySystem/Public/Components/GMCAbilityOuterApplication.h b/Source/GMCAbilitySystem/Public/Components/GMCAbilityOuterApplication.h deleted file mode 100644 index abe2a431..00000000 --- a/Source/GMCAbilitySystem/Public/Components/GMCAbilityOuterApplication.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "Utility/GMASBoundQueue.h" -#include "GMCAbilityOuterApplication.generated.h" - -class UGMCAbilityEffect; - -USTRUCT(BlueprintType) -struct FGMCAbilityEffectRPCWrapper -{ - GENERATED_BODY() - - UPROPERTY() - FGMASBoundQueueRPCHeader Header; - - UPROPERTY() - FGMCAbilityEffectData Payload {}; -}; - -USTRUCT(BlueprintType) -struct FGMCAbilityEffectIdSet -{ - GENERATED_BODY() - - TArray Ids = {}; -}; - From 5ff84189a079459d920bce85858eed7cf0210ad8 Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Fri, 27 Sep 2024 23:48:35 -0700 Subject: [PATCH 07/18] A little more refactoring --- .../Components/GMCAbilityComponent.cpp | 8 +++-- .../Public/Utility/GMASBoundQueue.h | 31 ++++++++----------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp index addf7fbc..b9899d14 100644 --- a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp +++ b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp @@ -1151,7 +1151,7 @@ void UGMC_AbilitySystemComponent::RPCClientQueueEffectOperation_Implementation(c if (!Operation.IsValid()) return; - QueuedEffectOperations.AddQueuedRPCOperation(Operation); + QueuedEffectOperations.QueuePreparedOperation(Operation, false); } void UGMC_AbilitySystemComponent::OnRep_UnBoundAttributes() @@ -1205,7 +1205,7 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf Ids, bool bOuterA TGMASBoundQueueOperation Operation; FGMCAbilityEffectData Data; QueuedEffectOperations.MakeOperation(Operation, EGMASBoundQueueOperationType::Remove, FGameplayTag::EmptyTag, Data, Ids); - QueuedEffectOperations.AddQueuedRPCOperation(Operation); + QueuedEffectOperations.QueuePreparedOperation(Operation, false); + + // Send the operation over to our client via standard RPC. ClientQueueEffectOperation(Operation); } return true; diff --git a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h index cf419b14..e6d870c4 100644 --- a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h +++ b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h @@ -298,10 +298,14 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue return NewOperation.GetOperationId(); } - - int32 QueueOperation(TGMASBoundQueueOperation& NewOperation, EGMASBoundQueueOperationType Type, FGameplayTag Tag, const T& Payload, TArray PayloadIds = {}, TSubclassOf ItemClass = nullptr, bool bMovementSynced = true, float RPCGracePeriod = 1.f) + + void QueuePreparedOperation(TGMASBoundQueueOperation& NewOperation, bool bMovementSynced = true) { - MakeOperation(NewOperation, Type, Tag, Payload, PayloadIds, ItemClass, RPCGracePeriod); + 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. @@ -311,7 +315,12 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue { 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(); } @@ -344,20 +353,6 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue const TArray>& GetQueuedRPCOperations() const { return QueuedRPCOperations; } - void AddQueuedRPCOperation(const TGMASBoundQueueOperation& NewOperation) - { - // This is used by the client-side to add the RPC-call operations. - // This allows us to still handle the application of effects within - // the GMC lifecycle, for the sake of everyone's sanity. - TGMASBoundQueueOperation TempOperation = TGMASBoundQueueOperation(); - - if (!GetOperationById(NewOperation.GetOperationId(), TempOperation)) - { - TempOperation = NewOperation; - QueuedRPCOperations.Push(TempOperation); - } - } - bool GetOperationById(int32 OperationId, TGMASBoundQueueOperation& OutOperation) const { for (const auto& Operation : QueuedBoundOperations) From 935db5d9c44fd02c527e629d1dd01a183e822733 Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Sat, 28 Sep 2024 00:30:05 -0700 Subject: [PATCH 08/18] Support server-auth effects queued via GMC moves. Note that this requires adding an SV_PreRemoteMoveExecution to your movement component, and calling into GMAS's PreRemoteMove call in that. --- .../Components/GMCAbilityComponent.cpp | 48 +++++++++++++++++-- .../Public/Components/GMCAbilityComponent.h | 18 ++++--- .../Public/Utility/GMASBoundQueue.h | 16 +++++-- 3 files changed, 68 insertions(+), 14 deletions(-) diff --git a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp index b9899d14..47a78d97 100644 --- a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp +++ b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp @@ -589,6 +589,13 @@ void UGMC_AbilitySystemComponent::PreLocalMoveExecution() } QueuedAbilityOperations.PreLocalMovement(); + QueuedEffectOperations.PreLocalMovement(); +} + +void UGMC_AbilitySystemComponent::PreRemoteMoveExecution() +{ + QueuedAbilityOperations.PreRemoteMovement(); + QueuedEffectOperations.PreRemoteMovement(); } void UGMC_AbilitySystemComponent::BeginPlay() @@ -856,6 +863,19 @@ void UGMC_AbilitySystemComponent::ServerHandlePendingEffect(float DeltaTime) { return; } + // Handle our GMC-replicated effect operation, if any. + TGMASBoundQueueOperation BoundOperation; + QueuedEffectOperations.GetCurrentBoundOperation(BoundOperation); + if (ShouldProcessEffectOperation(BoundOperation, true)) + { + if (BoundOperation.GracePeriodExpired()) + { + UE_LOG(LogGMCAbilitySystem, Log, TEXT("Client effect operation missed grace period, forcing on server.")) + } + ProcessEffectOperation(BoundOperation); + } + + // Handle our 'outer' RPC effect operations. QueuedEffectOperations.DeductGracePeriod(DeltaTime); auto Operations = QueuedEffectOperations.GetQueuedRPCOperations(); for (auto& Operation : Operations) { @@ -863,7 +883,7 @@ void UGMC_AbilitySystemComponent::ServerHandlePendingEffect(float DeltaTime) { { if (Operation.GracePeriodExpired()) { - UE_LOG(LogGMCAbilitySystem, Log, TEXT("Client removal of effect missed grace period, forcing a removal.")) + UE_LOG(LogGMCAbilitySystem, Log, TEXT("Client effect operation missed grace period, forcing on server.")) } ProcessEffectOperation(Operation); QueuedEffectOperations.RemoveOperationById(Operation.GetOperationId()); @@ -875,6 +895,16 @@ void UGMC_AbilitySystemComponent::ServerHandlePendingEffect(float DeltaTime) { void UGMC_AbilitySystemComponent::ClientHandlePendingEffect() { + // Handle our queued GMC-bound effect operation, if any. + TGMASBoundQueueOperation BoundOperation; + QueuedEffectOperations.GetCurrentBoundOperation(BoundOperation); + if (ShouldProcessEffectOperation(BoundOperation, false)) + { + ProcessEffectOperation(BoundOperation); + QueuedEffectOperations.Acknowledge(BoundOperation.GetOperationId()); + } + + // Handle our 'Outer' RPC effect operations auto RPCOperations = QueuedEffectOperations.GetQueuedRPCOperations(); for (auto& Operation : RPCOperations) { if (ShouldProcessEffectOperation(Operation, false)) @@ -1127,6 +1157,8 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ProcessEffectOperation( bool UGMC_AbilitySystemComponent::ShouldProcessEffectOperation( const TGMASBoundQueueOperation& Operation, bool bIsServer) const { + if (!Operation.IsValid()) return false; + if (bIsServer) { return HasAuthority() && (QueuedEffectOperations.IsAcknowledged(Operation.GetOperationId()) || @@ -1134,7 +1166,8 @@ bool UGMC_AbilitySystemComponent::ShouldProcessEffectOperation( } else { - return !QueuedEffectOperations.IsAcknowledged(Operation.GetOperationId()) && (!HasAuthority() || GMCMovementComponent->IsLocallyControlledServerPawn()); + return !QueuedEffectOperations.IsAcknowledged(Operation.GetOperationId()) && + (!HasAuthority() || GMCMovementComponent->IsLocallyControlledServerPawn()); } } @@ -1191,7 +1224,7 @@ void UGMC_AbilitySystemComponent::OnRep_UnBoundAttributes() } //BP Version -UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf Effect, FGMCAbilityEffectData InitializationData, bool bOuterActivation) +UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf Effect, FGMCAbilityEffectData InitializationData, bool bOuterActivation, bool bQueueViaGMC) { if (Effect == nullptr) { @@ -1211,6 +1244,15 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf Effect, FGMCAbilityEffectData InitializationData, bool bOuterActivation = false); + UFUNCTION(BlueprintCallable, Category="GMAS|Effects") + UGMCAbilityEffect* ApplyAbilityEffect(TSubclassOf Effect, FGMCAbilityEffectData InitializationData, bool bOuterActivation = false, bool bQueueViaGMC = false); UGMCAbilityEffect* ApplyAbilityEffect(UGMCAbilityEffect* Effect, FGMCAbilityEffectData InitializationData); @@ -364,6 +365,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 diff --git a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h index e6d870c4..36a87ba9 100644 --- a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h +++ b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h @@ -177,7 +177,8 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue void BindToGMC(UGMC_MovementUtilityCmp* MovementComponent) { const EGMC_PredictionMode Prediction = ClientAuth ? EGMC_PredictionMode::ClientAuth_Input : EGMC_PredictionMode::ServerAuth_Output_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. @@ -189,10 +190,9 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue EGMC_InterpolationFunction::TargetValue ); - // Our acknowledgments queue is always client-auth. BI_Acknowledgements = MovementComponent->BindInstancedStruct( Acknowledgments, - EGMC_PredictionMode::ClientAuth_Input, + AckPrediction, EGMC_CombineMode::CombineIfUnchanged, EGMC_SimulationMode::None, EGMC_InterpolationFunction::TargetValue); @@ -242,7 +242,15 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue void PreLocalMovement() { - if (QueuedBoundOperations.Num() > 0) + if (QueuedBoundOperations.Num() > 0 && ClientAuth) + { + CurrentOperation = QueuedBoundOperations.Pop(); + } + } + + void PreRemoteMovement() + { + if (QueuedBoundOperations.Num() > 0 && !ClientAuth) { CurrentOperation = QueuedBoundOperations.Pop(); } From ac58d82c357960d300b6a0ce15cfde2940799646 Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Sat, 28 Sep 2024 00:45:05 -0700 Subject: [PATCH 09/18] Ensure that Queue-via-GMC effects work in standalone as well. --- .../Private/Components/GMCAbilityComponent.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp index 47a78d97..08d291ce 100644 --- a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp +++ b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp @@ -589,7 +589,14 @@ void UGMC_AbilitySystemComponent::PreLocalMoveExecution() } QueuedAbilityOperations.PreLocalMovement(); - QueuedEffectOperations.PreLocalMovement(); + + if (GetNetMode() == NM_Standalone) + { + // In standalone, we never get the pre remote movement, so + // we need to kick this off to ensure we shuffle the new operation + // into the queue. + QueuedEffectOperations.PreRemoteMovement(); + } } void UGMC_AbilitySystemComponent::PreRemoteMoveExecution() @@ -873,6 +880,9 @@ void UGMC_AbilitySystemComponent::ServerHandlePendingEffect(float DeltaTime) { UE_LOG(LogGMCAbilitySystem, Log, TEXT("Client effect operation missed grace period, forcing on server.")) } ProcessEffectOperation(BoundOperation); + + // Operation has been done, clear it out. + QueuedEffectOperations.ClearCurrentOperation(); } // Handle our 'outer' RPC effect operations. @@ -1252,7 +1262,7 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf Date: Sat, 28 Sep 2024 21:26:01 -0700 Subject: [PATCH 10/18] Queue rework and cleanup finished! --- .../Components/GMCAbilityComponent.cpp | 383 ++++++++++++++---- .../Private/Effects/GMCAbilityEffect.cpp | 2 +- .../Public/Components/GMCAbilityComponent.h | 84 +++- .../Public/Utility/GMASBoundQueue.h | 59 ++- 4 files changed, 407 insertions(+), 121 deletions(-) diff --git a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp index 08d291ce..d39d4f86 100644 --- a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp +++ b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp @@ -177,7 +177,8 @@ void UGMC_AbilitySystemComponent::GenAncillaryTick(float DeltaTime, bool bIsComb // Check if we have a valid operation - if (TGMASBoundQueueOperation Operation; QueuedAbilityOperations.GetCurrentBoundOperation(Operation)) + TGMASBoundQueueOperation Operation; + if (QueuedAbilityOperations.GetCurrentBoundOperation(Operation)) { ProcessAbilityOperation(Operation, false); } @@ -186,14 +187,14 @@ void UGMC_AbilitySystemComponent::GenAncillaryTick(float DeltaTime, bool bIsComb ClearAbilityAndTaskData(); bInGMCTime = false; + QueuedEffectOperations.ClearCurrentOperation(); } -TArray UGMC_AbilitySystemComponent::GetActivesEffectByTag(FGameplayTag GameplayTag) const { +TArray UGMC_AbilitySystemComponent::GetActiveEffectsByTag(FGameplayTag GameplayTag) 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)) { ActiveEffectsFound.Add(EffectFound.Value); @@ -204,7 +205,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; @@ -324,7 +326,6 @@ 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(); @@ -590,18 +591,15 @@ void UGMC_AbilitySystemComponent::PreLocalMoveExecution() QueuedAbilityOperations.PreLocalMovement(); - if (GetNetMode() == NM_Standalone) + if (GetNetMode() == NM_Standalone || GMCMovementComponent->IsLocallyControlledServerPawn()) { - // In standalone, we never get the pre remote movement, so - // we need to kick this off to ensure we shuffle the new operation - // into the queue. + // We'll never get the pre remote movement trigger in this case, soooo... QueuedEffectOperations.PreRemoteMovement(); } } void UGMC_AbilitySystemComponent::PreRemoteMoveExecution() { - QueuedAbilityOperations.PreRemoteMovement(); QueuedEffectOperations.PreRemoteMovement(); } @@ -777,7 +775,10 @@ void UGMC_AbilitySystemComponent::TickActiveEffects(float DeltaTime) } 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() && @@ -872,18 +873,14 @@ void UGMC_AbilitySystemComponent::ServerHandlePendingEffect(float DeltaTime) { // Handle our GMC-replicated effect operation, if any. TGMASBoundQueueOperation BoundOperation; - QueuedEffectOperations.GetCurrentBoundOperation(BoundOperation); - if (ShouldProcessEffectOperation(BoundOperation, true)) + if (QueuedEffectOperations.GetCurrentBoundOperation(BoundOperation)) { - if (BoundOperation.GracePeriodExpired()) - { - UE_LOG(LogGMCAbilitySystem, Log, TEXT("Client effect operation missed grace period, forcing on server.")) - } - ProcessEffectOperation(BoundOperation); + // Move this into our RPC queue to wait on acknowledgment. + QueuedEffectOperations.QueuePreparedOperation(BoundOperation, false); - // Operation has been done, clear it out. - QueuedEffectOperations.ClearCurrentOperation(); - } + // And send it via RPC, so that the client gets it, as the binding goes away at the end of the move. + ClientQueueEffectOperation(BoundOperation); + }; // Handle our 'outer' RPC effect operations. QueuedEffectOperations.DeductGracePeriod(DeltaTime); @@ -893,7 +890,7 @@ void UGMC_AbilitySystemComponent::ServerHandlePendingEffect(float DeltaTime) { { if (Operation.GracePeriodExpired()) { - UE_LOG(LogGMCAbilitySystem, Log, TEXT("Client effect operation missed grace period, forcing on server.")) + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Client effect operation missed grace period, forcing on server.")) } ProcessEffectOperation(Operation); QueuedEffectOperations.RemoveOperationById(Operation.GetOperationId()); @@ -907,11 +904,19 @@ void UGMC_AbilitySystemComponent::ClientHandlePendingEffect() { // Handle our queued GMC-bound effect operation, if any. TGMASBoundQueueOperation BoundOperation; - QueuedEffectOperations.GetCurrentBoundOperation(BoundOperation); - if (ShouldProcessEffectOperation(BoundOperation, false)) + if (QueuedEffectOperations.GetCurrentBoundOperation(BoundOperation)) { - ProcessEffectOperation(BoundOperation); - QueuedEffectOperations.Acknowledge(BoundOperation.GetOperationId()); + if (ShouldProcessEffectOperation(BoundOperation, false)) + { + // Process and ack this. + ProcessEffectOperation(BoundOperation); + QueuedEffectOperations.Acknowledge(BoundOperation.GetOperationId()); + } + else + { + // Toss it into the RPC queue until we're good; either way it's getting cleared after this. + QueuedEffectOperations.QueuePreparedOperation(BoundOperation, false); + } } // Handle our 'Outer' RPC effect operations @@ -1132,17 +1137,24 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ProcessEffectOperation( if (OperationType == EGMASBoundQueueOperationType::Add) { - UGMCAbilityEffect* Effect = DuplicateObject(Operation.ItemClass->GetDefaultObject(), this); - FGMCAbilityEffectData EffectData; - if (Operation.Payload.IsValid()) + if (Operation.ItemClass == nullptr) { - EffectData = Operation.Payload; + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Attempting to process an add effect operation with no set class!")) } - else + + 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]; + } + ApplyAbilityEffect(Effect, EffectData); return Effect; } @@ -1167,7 +1179,16 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ProcessEffectOperation( bool UGMC_AbilitySystemComponent::ShouldProcessEffectOperation( const TGMASBoundQueueOperation& Operation, bool bIsServer) const { - if (!Operation.IsValid()) return false; + if (!Operation.IsValid()) + { + if (Operation.Header.OperationId != -1) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%s] %s received invalid queued effect operation %d!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), Operation.Header.OperationId) + return false; + } + return false; + } if (bIsServer) { @@ -1176,25 +1197,19 @@ bool UGMC_AbilitySystemComponent::ShouldProcessEffectOperation( } else { - return !QueuedEffectOperations.IsAcknowledged(Operation.GetOperationId()) && - (!HasAuthority() || GMCMovementComponent->IsLocallyControlledServerPawn()); + return !QueuedEffectOperations.IsAcknowledged(Operation.GetOperationId()) && (GMCMovementComponent->IsLocallyControlledServerPawn() || GMCMovementComponent->IsAutonomousProxy()); } } void UGMC_AbilitySystemComponent::ClientQueueEffectOperation( const TGMASBoundQueueOperation& Operation) { - RPCClientQueueEffectOperation(Operation.Header, Operation.Payload); + RPCClientQueueEffectOperation(Operation.Header); } -void UGMC_AbilitySystemComponent::RPCClientQueueEffectOperation_Implementation(const FGMASBoundQueueRPCHeader& Header, const FGMCAbilityEffectData& Payload) +void UGMC_AbilitySystemComponent::RPCClientQueueEffectOperation_Implementation(const FGMASBoundQueueRPCHeader& Header) { - TGMASBoundQueueOperation Operation; - QueuedEffectOperations.MakeOperation(Operation, Header, Payload); - - if (!Operation.IsValid()) return; - - QueuedEffectOperations.QueuePreparedOperation(Operation, false); + QueuedEffectOperations.QueueOperationFromHeader(Header, false); } void UGMC_AbilitySystemComponent::OnRep_UnBoundAttributes() @@ -1233,8 +1248,57 @@ void UGMC_AbilitySystemComponent::OnRep_UnBoundAttributes() } +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)) + { + NewEffectID++; + } + UE_LOG(LogGMCAbilitySystem, VeryVerbose, TEXT("[Server: %hhd] Generated Effect ID: %d"), HasAuthority(), NewEffectID); + + return NewEffectID; +} + +int UGMC_AbilitySystemComponent::CreateEffectOperation( + TGMASBoundQueueOperation& OutOperation, + const TSubclassOf& EffectClass, + const FGMCAbilityEffectData& EffectData, + bool bForcedEffectId) +{ + TArray PayloadIds {}; + + FGMCAbilityEffectData PayloadData; + if (EffectData.IsValid()) + { + PayloadData = EffectData; + } + else + { + PayloadData = EffectClass->GetDefaultObject()->EffectData; + } + + if (bForcedEffectId) + { + if (PayloadData.EffectID == 0) + { + PayloadData.EffectID = GetNextAvailableEffectID(); + } + PayloadIds.Add(PayloadData.EffectID); + } + + QueuedEffectOperations.MakeOperation(OutOperation, EGMASBoundQueueOperationType::Add, PayloadData.EffectTag, PayloadData, PayloadIds, EffectClass, 1.f); + return PayloadData.EffectID; +} + //BP Version -UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf Effect, FGMCAbilityEffectData InitializationData, bool bOuterActivation, bool bQueueViaGMC) +UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf Effect, FGMCAbilityEffectData InitializationData, bool bOuterActivation) { if (Effect == nullptr) { @@ -1243,8 +1307,13 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf Operation; - QueuedEffectOperations.MakeOperation(Operation, EGMASBoundQueueOperationType::Add, FGameplayTag::EmptyTag, InitializationData, {}, Effect, 1.f); - + if (CreateEffectOperation(Operation, Effect, InitializationData, bOuterActivation) == -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()) { @@ -1254,16 +1323,96 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOfIsExecutingMove() && GetNetMode() != NM_Standalone) { - if (HasAuthority()) + // 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 ProcessEffectOperation(Operation); +} + +void UGMC_AbilitySystemComponent::ApplyAbilityEffectSafe(TSubclassOf EffectClass, + FGMCAbilityEffectData InitializationData, EGMCAbilityEffectQueueType QueueType, bool& OutSuccess, int& OutEffectId, + UGMCAbilityEffect*& OutEffect) +{ + OutSuccess = ApplyAbilityEffect(EffectClass, InitializationData, QueueType, OutEffectId, OutEffect); +} + +bool UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf EffectClass, + FGMCAbilityEffectData InitializationData, EGMCAbilityEffectQueueType QueueType, int& OutEffectId, UGMCAbilityEffect*& OutEffect) +{ + OutEffect = nullptr; + OutEffectId = -1; + if (EffectClass == nullptr) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Trying to apply Effect, but effect is null!")); + return false; + } + + const bool bPregenerateEffectId = QueueType != EGMCAbilityEffectQueueType::Predicted; + + TGMASBoundQueueOperation Operation; + const int EffectID = CreateEffectOperation(Operation, EffectClass, InitializationData, bPregenerateEffectId); + if (bPregenerateEffectId && EffectID == -1) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s could not create an effect of type %s!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), *EffectClass->GetName()) + return false; + } + + switch(QueueType) + { + case EGMCAbilityEffectQueueType::Predicted: { - QueuedEffectOperations.QueuePreparedOperation(Operation, true); + if (!GMCMovementComponent->IsExecutingMove() && GetNetMode() != NM_Standalone) + { + 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 = ProcessEffectOperation(Operation); + OutEffectId = OutEffect->EffectData.EffectID; + return true; + } + case EGMCAbilityEffectQueueType::MoveCycle: + case EGMCAbilityEffectQueueType::Outside: + { + 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::MoveCycle) Operation.Header.RPCGracePeriodSeconds = 5.f; + + QueuedEffectOperations.QueuePreparedOperation(Operation, QueueType == EGMCAbilityEffectQueueType::MoveCycle); + + if (QueueType == EGMCAbilityEffectQueueType::Outside) + { + // Queue for RPC and throw this to our client. + ClientQueueEffectOperation(Operation); + } + + OutEffectId = EffectID; + return true; } - return nullptr; } - - return ProcessEffectOperation(Operation); + + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted to apply effect %s 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]; } UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(UGMCAbilityEffect* Effect, FGMCAbilityEffectData InitializationData) @@ -1281,19 +1430,7 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(UGMCAbilityEf 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 @@ -1305,8 +1442,9 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(UGMCAbilityEf { ProcessedEffectIDs.Add(Effect->EffectData.EffectID, false); } - + ActiveEffects.Add(Effect->EffectData.EffectID, Effect); + return Effect; } @@ -1318,13 +1456,22 @@ 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::RemoveActiveAbilityEffectSafe(UGMCAbilityEffect* Effect, + EGMCAbilityEffectQueueType QueueType) +{ + if (Effect == nullptr) return; - if (NumToRemove < -1 || !InEffectTag.IsValid()) { - return 0; + RemoveEffectByIdSafe({ Effect->EffectData.EffectID }, QueueType); +} + +TArray UGMC_AbilitySystemComponent::EffectsMatchingTag(const FGameplayTag& Tag, int32 NumToRemove) const +{ + if (NumToRemove < -1 || !Tag.IsValid()) { + return {}; } TArray EffectsToRemove; @@ -1336,23 +1483,50 @@ int32 UGMC_AbilitySystemComponent::RemoveEffectByTag(FGameplayTag InEffectTag, i break; } - if(Effect.Value->EffectData.EffectTag.IsValid() && Effect.Value->EffectData.EffectTag.MatchesTagExact(InEffectTag)){ + if(Effect.Value->EffectData.EffectTag.IsValid() && Effect.Value->EffectData.EffectTag.MatchesTagExact(Tag)){ EffectsToRemove.Add(Effect.Value->EffectData.EffectID); NumRemoved++; } } - if (NumRemoved > 0) + return EffectsToRemove; +} + +int32 UGMC_AbilitySystemComponent::RemoveEffectByTag(FGameplayTag InEffectTag, int32 NumToRemove, bool bOuterActivation) { + + if (NumToRemove < -1 || !InEffectTag.IsValid()) { + return 0; + } + + TArray EffectsToRemove = EffectsMatchingTag(InEffectTag, NumToRemove); + + if (EffectsToRemove.Num() > 0) { RemoveEffectById(EffectsToRemove, bOuterActivation); } - return NumRemoved; + return EffectsToRemove.Num(); } +int32 UGMC_AbilitySystemComponent::RemoveEffectByTagSafe(FGameplayTag InEffectTag, int32 NumToRemove, + EGMCAbilityEffectQueueType QueueType) +{ + if (NumToRemove < -1 || !InEffectTag.IsValid()) { + return 0; + } -bool UGMC_AbilitySystemComponent::RemoveEffectById(TArray Ids, bool bOuterActivation) { + TArray EffectsToRemove = EffectsMatchingTag(InEffectTag, NumToRemove); + + if (EffectsToRemove.Num() > 0) + { + RemoveEffectByIdSafe(EffectsToRemove, QueueType); + } + return EffectsToRemove.Num(); +} + +bool UGMC_AbilitySystemComponent::RemoveEffectByIdSafe(TArray Ids, EGMCAbilityEffectQueueType QueueType) +{ if (!Ids.Num()) { return true; } @@ -1365,26 +1539,61 @@ bool UGMC_AbilitySystemComponent::RemoveEffectById(TArray Ids, bool bOuterA } } - if (bOuterActivation) { - if (HasAuthority()) { + switch(QueueType) + { + case EGMCAbilityEffectQueueType::Predicted: + { + if (!GMCMovementComponent->IsExecutingMove() && GetNetMode() != NM_Standalone) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted a predicted removal of effects outside of a movement cycle!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName()) + return false; + } + + for (auto& Effect : ActiveEffects) { + if (Ids.Contains(Effect.Key)) { + RemoveActiveAbilityEffect(Effect.Value); + } + } + + return true; + } + case EGMCAbilityEffectQueueType::MoveCycle: + case EGMCAbilityEffectQueueType::Outside: + { + if (!HasAuthority()) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted a server-auth removal of %d effects on a client!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), Ids.Num()) + return false; + } + TGMASBoundQueueOperation Operation; FGMCAbilityEffectData Data; QueuedEffectOperations.MakeOperation(Operation, EGMASBoundQueueOperationType::Remove, FGameplayTag::EmptyTag, Data, Ids); - QueuedEffectOperations.QueuePreparedOperation(Operation, false); - - // Send the operation over to our client via standard RPC. - ClientQueueEffectOperation(Operation); + QueuedEffectOperations.QueuePreparedOperation(Operation, QueueType == EGMCAbilityEffectQueueType::MoveCycle); + + if (QueueType == EGMCAbilityEffectQueueType::Outside) + { + // Send the operation over to our client via standard RPC. + ClientQueueEffectOperation(Operation); + } } + return true; } + + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted a removal of effects but something went horribly wrong!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName()) + return false; +} - for (auto& Effect : ActiveEffects) { - if (Ids.Contains(Effect.Key)) { - Effect.Value->EndEffect(); - } - } - return true; +bool UGMC_AbilitySystemComponent::RemoveEffectById(TArray Ids, bool bOuterActivation) { + + // Just hit up the newer version. + return RemoveEffectByIdSafe(Ids, bOuterActivation ? EGMCAbilityEffectQueueType::Outside : EGMCAbilityEffectQueueType::Predicted); + } @@ -1490,7 +1699,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"); } @@ -1498,7 +1707,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"); } diff --git a/Source/GMCAbilitySystem/Private/Effects/GMCAbilityEffect.cpp b/Source/GMCAbilitySystem/Private/Effects/GMCAbilityEffect.cpp index f2d0f280..0310e026 100644 --- a/Source/GMCAbilitySystem/Private/Effects/GMCAbilityEffect.cpp +++ b/Source/GMCAbilitySystem/Private/Effects/GMCAbilityEffect.cpp @@ -233,7 +233,7 @@ void UGMCAbilityEffect::RemoveTagsFromOwner(bool bPreserveOnMultipleInstances) { if (bPreserveOnMultipleInstances && EffectData.EffectTag.IsValid()) { - TArray ActiveEffect = OwnerAbilityComponent->GetActivesEffectByTag(EffectData.EffectTag); + TArray ActiveEffect = OwnerAbilityComponent->GetActiveEffectsByTag(EffectData.EffectTag); if (ActiveEffect.Num() > 1) { return; diff --git a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h index 995154f6..549e14e3 100644 --- a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h +++ b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h @@ -28,8 +28,6 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAncillaryTick, float, DeltaTime); DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnActiveTagsChanged, FGameplayTagContainer, AddedTags, FGameplayTagContainer, RemovedTags); DECLARE_MULTICAST_DELEGATE_TwoParams(FGameplayTagFilteredMulticastDelegate, const FGameplayTagContainer&, const FGameplayTagContainer&); - - USTRUCT() struct FEffectStatePrediction { @@ -44,6 +42,14 @@ struct FEffectStatePrediction uint8 State; }; +UENUM(BlueprintType) +enum class EGMCAbilityEffectQueueType : uint8 +{ + Predicted UMETA(DisplayName="Immediate [Predicted]", Tooltip="Predicted effect, goes into effect immediately, should be added on both client and server within the GMC movement cycle."), + Outside UMETA(DisplayName="RPC [Server Only]", ToolTip="Queues a server-authoritative effect to be added on clients. Can be used outside of the GMC movement cycle, but effects will not be actually active until the next GMC movement cycle, and they will not be preserved in the move history."), + MoveCycle UMETA(DisplayName="Movement Cycle [Server Only]", ToolTip="Queues a server-authoritative effect to be added as part of the next GMC movement cycle. Potentially slightly slower, but will preserve the effect in the move history.") +}; + class UGMCAbility; UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent, DisplayName="GMC Ability System Component"), meta=(Categories="GMAS")) @@ -78,7 +84,7 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo // Return active Effect with tag UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMAS|Abilities") - TArray GetActivesEffectByTag(FGameplayTag GameplayTag) const; + TArray GetActiveEffectsByTag(FGameplayTag GameplayTag) const; // Get the first active effect with the Effecttag UFUNCTION(BlueprintCallable, BlueprintPure, Category="GMAS|Abilities") @@ -219,39 +225,91 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo UFUNCTION() void OnRep_UnBoundAttributes(); + int GetNextAvailableEffectID() const; + int CreateEffectOperation(TGMASBoundQueueOperation& OutOperation, const TSubclassOf& Effect, const FGMCAbilityEffectData& EffectData, bool bForcedEffectId = true); + /** - * Applies an effect to the Ability Component. If both bOuterActivation and bQueueViaGMC are false, the effect - * will be immediately applied; if either is true, the operation will be queued but no valid effect will be returned. - * If either Outer Activation or Queue via GMC is true, the effect *must* be applied on the server. + * 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 InitializationData Effect initialization data. * @param bOuterActivation Whether this effect should be replicated outside of GMC, via normal Unreal RPC - * @param bQueueViaGMC Whether this effect should be queued via GMC's normal moves. */ - UFUNCTION(BlueprintCallable, Category="GMAS|Effects") - UGMCAbilityEffect* ApplyAbilityEffect(TSubclassOf Effect, FGMCAbilityEffectData InitializationData, bool bOuterActivation = false, bool bQueueViaGMC = false); + 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. + */ + 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 ID") int& OutEffectId, UPARAM(DisplayName="Effect Instance") UGMCAbilityEffect*& OutEffect); + + /** + * 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 OutEffectId The newly-created effect's ID, if successful. + * @param OutEffect The newly-created effect instance, if a predicted add. + * @return true if the effect was applied, false otherwise. + */ + bool ApplyAbilityEffect(TSubclassOf EffectClass, FGMCAbilityEffectData InitializationData, EGMCAbilityEffectQueueType QueueType, 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; + + 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", DisplayName="Remove Active Ability Effect (Safe)") + void RemoveActiveAbilityEffectSafe(UGMCAbilityEffect* Effect, EGMCAbilityEffectQueueType QueueType = EGMCAbilityEffectQueueType::Predicted); + /** * 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); + + /** * Gets the number of active effects with the inputted tag. * Returns -1 if tag is invalid. @@ -455,7 +513,7 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo void ClientQueueEffectOperation(const TGMASBoundQueueOperation& Operation); UFUNCTION(Client, Reliable) - void RPCClientQueueEffectOperation(const FGMASBoundQueueRPCHeader& Header, const FGMCAbilityEffectData& Payload); + void RPCClientQueueEffectOperation(const FGMASBoundQueueRPCHeader& Header); // Predictions of Effect state changes FEffectStatePrediction EffectStatePrediction{}; diff --git a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h index 36a87ba9..9b5b3823 100644 --- a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h +++ b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h @@ -24,7 +24,8 @@ USTRUCT(BlueprintType) struct FGMASBoundQueueOperationIdSet { GENERATED_BODY() - + + UPROPERTY() TArray Ids = {}; }; @@ -53,6 +54,9 @@ struct GMCABILITYSYSTEM_API FGMASBoundQueueRPCHeader UPROPERTY() FGMASBoundQueueOperationIdSet PayloadIds {}; + UPROPERTY() + FInstancedStruct InstancedPayload {}; + UPROPERTY() float RPCGracePeriodSeconds { 1.f }; }; @@ -74,11 +78,6 @@ struct GMCABILITYSYSTEM_API TGMASBoundQueueOperation // bind for replication. FInstancedStruct InstancedPayload; - // If true, this item must be replicated via GMC and thus preserved in the - // movement history. If false, this item can be sent via standard Unreal RPC - // separate from GMC. - bool bMovementSync { true }; - EGMASBoundQueueOperationType GetOperationType() const { return Header.GetOperationType(); @@ -109,8 +108,18 @@ struct GMCABILITYSYSTEM_API TGMASBoundQueueOperation { if (bDecodePayload) { - Payload = InstancedPayload.template Get(); - Header.PayloadIds = InstancedPayloadIds.Get(); + // 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); } if (Header.ItemClassName != NAME_None && !ItemClass) @@ -119,6 +128,11 @@ struct GMCABILITYSYSTEM_API TGMASBoundQueueOperation TSoftClassPtr ClassPtr = TSoftClassPtr(FSoftObjectPath(Header.ItemClassName.ToString())); ItemClass = ClassPtr.LoadSynchronous(); } + + if (ItemClass && Header.ItemClassName == NAME_None) + { + Header.ItemClassName = FName(ItemClass->GetPathName()); + } } }; @@ -177,7 +191,7 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue void BindToGMC(UGMC_MovementUtilityCmp* MovementComponent) { const EGMC_PredictionMode Prediction = ClientAuth ? EGMC_PredictionMode::ClientAuth_Input : EGMC_PredictionMode::ServerAuth_Output_ClientValidated; - const EGMC_PredictionMode AckPrediction = ClientAuth ? EGMC_PredictionMode::ServerAuth_Output_ClientValidated : EGMC_PredictionMode::ClientAuth_Input; + const EGMC_PredictionMode AckPrediction = ClientAuth ? EGMC_PredictionMode::ServerAuth_Input_ClientValidated : EGMC_PredictionMode::ClientAuth_Input; Acknowledgments = FInstancedStruct::Make(FGMASBoundQueueAcknowledgements()); @@ -226,7 +240,7 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue EGMC_InterpolationFunction::TargetValue); BI_OperationPayload = MovementComponent->BindInstancedStruct( - CurrentOperation.InstancedPayload, + CurrentOperation.Header.InstancedPayload, Prediction, EGMC_CombineMode::CombineIfUnchanged, EGMC_SimulationMode::Periodic_Output, @@ -245,6 +259,7 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue if (QueuedBoundOperations.Num() > 0 && ClientAuth) { CurrentOperation = QueuedBoundOperations.Pop(); + CurrentOperation.Refresh(false); } } @@ -253,6 +268,7 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue if (QueuedBoundOperations.Num() > 0 && !ClientAuth) { CurrentOperation = QueuedBoundOperations.Pop(); + CurrentOperation.Refresh(false); } } @@ -265,6 +281,8 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue CurrentOperation.Header.ItemClassName = NAME_None; CurrentOperation.SetOperationType(EGMASBoundQueueOperationType::None); CurrentOperation.Header.PayloadIds.Ids.Empty(); + CurrentOperation.Payload = T(); + CurrentOperation.Refresh(false); } void GenPredictionTick(float DeltaTime) @@ -279,20 +297,12 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue NewOperation.SetOperationType(Type); NewOperation.Header.Tag = Tag; NewOperation.Payload = Payload; - NewOperation.InstancedPayload = FInstancedStruct::Make(Payload); NewOperation.ItemClass = ItemClass; NewOperation.Header.RPCGracePeriodSeconds = RPCGracePeriod; NewOperation.Header.PayloadIds.Ids = PayloadIds; - NewOperation.InstancedPayloadIds = FInstancedStruct::Make(NewOperation.Header.PayloadIds); - if (ItemClass) - { - NewOperation.Header.ItemClassName = FName(ItemClass->GetPathName()); - } - else - { - NewOperation.Header.ItemClassName = NAME_None; - } + NewOperation.Refresh(false); + return NewOperation.GetOperationId(); } @@ -332,6 +342,15 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue 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(); From cc707efa0254d71d103b4556e17a87ad6b0648da Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Sat, 28 Sep 2024 21:41:08 -0700 Subject: [PATCH 11/18] Last few tweaks to the queue setup. --- .../Components/GMCAbilityComponent.cpp | 27 +++++-------------- .../Public/Utility/GMASBoundQueue.h | 5 ++-- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp index d39d4f86..11d1f0b4 100644 --- a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp +++ b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp @@ -187,7 +187,6 @@ void UGMC_AbilitySystemComponent::GenAncillaryTick(float DeltaTime, bool bIsComb ClearAbilityAndTaskData(); bInGMCTime = false; - QueuedEffectOperations.ClearCurrentOperation(); } @@ -871,14 +870,16 @@ void UGMC_AbilitySystemComponent::ServerHandlePendingEffect(float DeltaTime) { return; } - // Handle our GMC-replicated effect operation, if any. + // 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)) { // Move this into our RPC queue to wait on acknowledgment. QueuedEffectOperations.QueuePreparedOperation(BoundOperation, false); - // And send it via RPC, so that the client gets it, as the binding goes away at the end of the move. + // And send it via RPC, so that the client gets it. ClientQueueEffectOperation(BoundOperation); }; @@ -902,24 +903,8 @@ void UGMC_AbilitySystemComponent::ServerHandlePendingEffect(float DeltaTime) { void UGMC_AbilitySystemComponent::ClientHandlePendingEffect() { - // Handle our queued GMC-bound effect operation, if any. - TGMASBoundQueueOperation BoundOperation; - if (QueuedEffectOperations.GetCurrentBoundOperation(BoundOperation)) - { - if (ShouldProcessEffectOperation(BoundOperation, false)) - { - // Process and ack this. - ProcessEffectOperation(BoundOperation); - QueuedEffectOperations.Acknowledge(BoundOperation.GetOperationId()); - } - else - { - // Toss it into the RPC queue until we're good; either way it's getting cleared after this. - QueuedEffectOperations.QueuePreparedOperation(BoundOperation, false); - } - } - - // Handle our 'Outer' RPC effect operations + // 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 = QueuedEffectOperations.GetQueuedRPCOperations(); for (auto& Operation : RPCOperations) { if (ShouldProcessEffectOperation(Operation, false)) diff --git a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h index 9b5b3823..0682dba5 100644 --- a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h +++ b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h @@ -190,8 +190,8 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue void BindToGMC(UGMC_MovementUtilityCmp* MovementComponent) { - const EGMC_PredictionMode Prediction = ClientAuth ? EGMC_PredictionMode::ClientAuth_Input : EGMC_PredictionMode::ServerAuth_Output_ClientValidated; - const EGMC_PredictionMode AckPrediction = ClientAuth ? EGMC_PredictionMode::ServerAuth_Input_ClientValidated : EGMC_PredictionMode::ClientAuth_Input; + 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()); @@ -265,6 +265,7 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue void PreRemoteMovement() { + ClearCurrentOperation(); if (QueuedBoundOperations.Num() > 0 && !ClientAuth) { CurrentOperation = QueuedBoundOperations.Pop(); From a49cbd8570619904542c3c4ca852683b13a552e2 Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Sun, 29 Sep 2024 16:17:28 -0700 Subject: [PATCH 12/18] Further queue refactor and cleanup. New queue types (ClientAuth, PredictedQueued) added. --- .../Components/GMCAbilityComponent.cpp | 130 +++++++++++++++--- .../Public/Components/GMCAbilityComponent.h | 26 +++- .../Public/Utility/GMASBoundQueue.h | 54 ++++++-- 3 files changed, 176 insertions(+), 34 deletions(-) diff --git a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp index 11d1f0b4..4094ab8c 100644 --- a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp +++ b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp @@ -159,6 +159,7 @@ void UGMC_AbilitySystemComponent::BindReplicationData() // Bind our operation queues. QueuedAbilityOperations.BindToGMC(GMCMovementComponent); QueuedEffectOperations.BindToGMC(GMCMovementComponent); + QueuedEffectOperations_ClientAuth.BindToGMC(GMCMovementComponent); } void UGMC_AbilitySystemComponent::GenAncillaryTick(float DeltaTime, bool bIsCombinedClientMove) @@ -178,7 +179,7 @@ void UGMC_AbilitySystemComponent::GenAncillaryTick(float DeltaTime, bool bIsComb // Check if we have a valid operation TGMASBoundQueueOperation Operation; - if (QueuedAbilityOperations.GetCurrentBoundOperation(Operation)) + if (QueuedAbilityOperations.GetCurrentBoundOperation(Operation, true)) { ProcessAbilityOperation(Operation, false); } @@ -186,6 +187,7 @@ void UGMC_AbilitySystemComponent::GenAncillaryTick(float DeltaTime, bool bIsComb SendTaskDataToActiveAbility(false); ClearAbilityAndTaskData(); + QueuedEffectOperations_ClientAuth.ClearCurrentOperation(); bInGMCTime = false; } @@ -558,10 +560,12 @@ void UGMC_AbilitySystemComponent::GenPredictionTick(float DeltaTime) // Was an ability used? if (TGMASBoundQueueOperation Operation; - QueuedAbilityOperations.GetCurrentBoundOperation(Operation)) + QueuedAbilityOperations.GetCurrentBoundOperation(Operation, true)) { ProcessAbilityOperation(Operation, true); } + + ServerHandlePredictedPendingEffect(DeltaTime); SendTaskDataToActiveAbility(true); } @@ -588,7 +592,9 @@ void UGMC_AbilitySystemComponent::PreLocalMoveExecution() TaskData = QueuedTaskData.Pop(); } + // Advance our client-auth queues. QueuedAbilityOperations.PreLocalMovement(); + QueuedEffectOperations_ClientAuth.PreLocalMovement(); if (GetNetMode() == NM_Standalone || GMCMovementComponent->IsLocallyControlledServerPawn()) { @@ -599,6 +605,7 @@ void UGMC_AbilitySystemComponent::PreLocalMoveExecution() void UGMC_AbilitySystemComponent::PreRemoteMoveExecution() { + // Advance our server-auth queues. QueuedEffectOperations.PreRemoteMovement(); } @@ -874,7 +881,7 @@ void UGMC_AbilitySystemComponent::ServerHandlePendingEffect(float DeltaTime) { // 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)) + if (QueuedEffectOperations.GetCurrentBoundOperation(BoundOperation, true)) { // Move this into our RPC queue to wait on acknowledgment. QueuedEffectOperations.QueuePreparedOperation(BoundOperation, false); @@ -897,6 +904,25 @@ void UGMC_AbilitySystemComponent::ServerHandlePendingEffect(float DeltaTime) { QueuedEffectOperations.RemoveOperationById(Operation.GetOperationId()); } } +} + +void UGMC_AbilitySystemComponent::ServerHandlePredictedPendingEffect(float DeltaTime) +{ + if (!HasAuthority()) return; + + // Check for any client-auth effects. + TGMASBoundQueueOperation BoundOperation; + if (QueuedEffectOperations_ClientAuth.GetCurrentBoundOperation(BoundOperation, true)) + { + ProcessEffectOperation(BoundOperation); + } + + // 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)) + { + ProcessEffectOperation(BoundOperation); + } } @@ -914,7 +940,21 @@ void UGMC_AbilitySystemComponent::ClientHandlePendingEffect() { QueuedEffectOperations.RemoveOperationById(Operation.GetOperationId()); } } +} + +void UGMC_AbilitySystemComponent::ClientHandlePredictedPendingEffect() +{ + TGMASBoundQueueOperation BoundOperation; + if (QueuedEffectOperations_ClientAuth.GetCurrentBoundOperation(BoundOperation)) + { + ProcessEffectOperation(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)) + { + ProcessEffectOperation(BoundOperation); + } } void UGMC_AbilitySystemComponent::RPCTaskHeartbeat_Implementation(int AbilityID, int TaskID) @@ -1124,7 +1164,8 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ProcessEffectOperation( { if (Operation.ItemClass == nullptr) { - UE_LOG(LogGMCAbilitySystem, Error, TEXT("Attempting to process an add effect operation with no set class!")) + 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); @@ -1139,6 +1180,14 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ProcessEffectOperation( { 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); return Effect; @@ -1255,7 +1304,8 @@ int UGMC_AbilitySystemComponent::CreateEffectOperation( TGMASBoundQueueOperation& OutOperation, const TSubclassOf& EffectClass, const FGMCAbilityEffectData& EffectData, - bool bForcedEffectId) + bool bForcedEffectId, + EGMCAbilityEffectQueueType QueueType) { TArray PayloadIds {}; @@ -1278,7 +1328,7 @@ int UGMC_AbilitySystemComponent::CreateEffectOperation( PayloadIds.Add(PayloadData.EffectID); } - QueuedEffectOperations.MakeOperation(OutOperation, EGMASBoundQueueOperationType::Add, PayloadData.EffectTag, PayloadData, PayloadIds, EffectClass, 1.f); + QueuedEffectOperations.MakeOperation(OutOperation, EGMASBoundQueueOperationType::Add, PayloadData.EffectTag, PayloadData, PayloadIds, EffectClass, 1.f, static_cast(QueueType)); return PayloadData.EffectID; } @@ -1292,7 +1342,7 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf Operation; - if (CreateEffectOperation(Operation, Effect, InitializationData, bOuterActivation) == -1) + 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()) @@ -1336,10 +1386,10 @@ bool UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf Operation; - const int EffectID = CreateEffectOperation(Operation, EffectClass, InitializationData, bPregenerateEffectId); + const int EffectID = CreateEffectOperation(Operation, EffectClass, InitializationData, bPregenerateEffectId, QueueType); if (bPregenerateEffectId && EffectID == -1) { UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s could not create an effect of type %s!"), @@ -1363,8 +1413,14 @@ bool UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOfEffectData.EffectID; return true; } - case EGMCAbilityEffectQueueType::MoveCycle: - case EGMCAbilityEffectQueueType::Outside: + case EGMCAbilityEffectQueueType::PredictedQueued: + { + // We utilize the ClientAuth queue's RPC queue for the sake of convenience. + QueuedEffectOperations_ClientAuth.QueuePreparedOperation(Operation, false); + return true; + } + case EGMCAbilityEffectQueueType::ServerAuthMove: + case EGMCAbilityEffectQueueType::ServerAuth: { if (!HasAuthority()) { @@ -1373,11 +1429,11 @@ bool UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOfIsAutonomousProxy() && !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 %s of type %s but something has gone BADLY wrong!"), @@ -1519,7 +1589,8 @@ bool UGMC_AbilitySystemComponent::RemoveEffectByIdSafe(TArray Ids, EGMCAbil // 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; } } @@ -1543,8 +1614,27 @@ bool UGMC_AbilitySystemComponent::RemoveEffectByIdSafe(TArray Ids, EGMCAbil return true; } - case EGMCAbilityEffectQueueType::MoveCycle: - case EGMCAbilityEffectQueueType::Outside: + case EGMCAbilityEffectQueueType::PredictedQueued: + case EGMCAbilityEffectQueueType::ClientAuth: + { + if (QueueType == EGMCAbilityEffectQueueType::ClientAuth) + { + if (GetNetMode() != NM_Standalone && (HasAuthority() && !GMCMovementComponent->IsLocallyControlledServerPawn())) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted a client-auth removal of %d effects on a server!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), Ids.Num()) + 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()) { @@ -1556,9 +1646,9 @@ bool UGMC_AbilitySystemComponent::RemoveEffectByIdSafe(TArray Ids, EGMCAbil TGMASBoundQueueOperation Operation; FGMCAbilityEffectData Data; QueuedEffectOperations.MakeOperation(Operation, EGMASBoundQueueOperationType::Remove, FGameplayTag::EmptyTag, Data, Ids); - QueuedEffectOperations.QueuePreparedOperation(Operation, QueueType == EGMCAbilityEffectQueueType::MoveCycle); + QueuedEffectOperations.QueuePreparedOperation(Operation, QueueType == EGMCAbilityEffectQueueType::ServerAuthMove); - if (QueueType == EGMCAbilityEffectQueueType::Outside) + if (QueueType == EGMCAbilityEffectQueueType::ServerAuth) { // Send the operation over to our client via standard RPC. ClientQueueEffectOperation(Operation); @@ -1577,7 +1667,7 @@ bool UGMC_AbilitySystemComponent::RemoveEffectByIdSafe(TArray Ids, EGMCAbil bool UGMC_AbilitySystemComponent::RemoveEffectById(TArray Ids, bool bOuterActivation) { // Just hit up the newer version. - return RemoveEffectByIdSafe(Ids, bOuterActivation ? EGMCAbilityEffectQueueType::Outside : EGMCAbilityEffectQueueType::Predicted); + return RemoveEffectByIdSafe(Ids, bOuterActivation ? EGMCAbilityEffectQueueType::ServerAuth : EGMCAbilityEffectQueueType::Predicted); } diff --git a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h index 549e14e3..6727b3ca 100644 --- a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h +++ b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h @@ -45,9 +45,24 @@ struct FEffectStatePrediction UENUM(BlueprintType) enum class EGMCAbilityEffectQueueType : uint8 { - Predicted UMETA(DisplayName="Immediate [Predicted]", Tooltip="Predicted effect, goes into effect immediately, should be added on both client and server within the GMC movement cycle."), - Outside UMETA(DisplayName="RPC [Server Only]", ToolTip="Queues a server-authoritative effect to be added on clients. Can be used outside of the GMC movement cycle, but effects will not be actually active until the next GMC movement cycle, and they will not be preserved in the move history."), - MoveCycle UMETA(DisplayName="Movement Cycle [Server Only]", ToolTip="Queues a server-authoritative effect to be added as part of the next GMC movement cycle. Potentially slightly slower, but will preserve the effect in the move history.") + /// Immediately applied, only valid within the GMC movement cycle. Should be applied on both client and server. + Predicted UMETA(DisplayName="Predicted"), + + /// 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. Should PROBABLY only be used for cosmetic effects! + ClientAuth UMETA(DisplayName="Client Auth"), + + /// 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. WILL NOT RETURN AN EFFECT ID. + PredictedQueued UMETA(Hidden, DisplayName="ADVANCED: Predicted [Queued]"), + + /// 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. + ServerAuthMove UMETA(Hidden, DisplayName="ADVANCED: Server Auth [Movement Cycle]") }; class UGMCAbility; @@ -226,7 +241,7 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo void OnRep_UnBoundAttributes(); int GetNextAvailableEffectID() const; - int CreateEffectOperation(TGMASBoundQueueOperation& OutOperation, const TSubclassOf& Effect, const FGMCAbilityEffectData& EffectData, bool bForcedEffectId = true); + int CreateEffectOperation(TGMASBoundQueueOperation& OutOperation, const TSubclassOf& Effect, const FGMCAbilityEffectData& EffectData, bool bForcedEffectId = true, EGMCAbilityEffectQueueType QueueType = EGMCAbilityEffectQueueType::Predicted); /** * Applies an effect to the Ability Component. If bOuterActivation is false, the effect will be immediately @@ -507,6 +522,7 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo bool ProcessAbilityOperation(const TGMASBoundQueueOperation& Operation, bool bFromMovementTick); TGMASBoundQueue QueuedEffectOperations; + TGMASBoundQueue QueuedEffectOperations_ClientAuth; UGMCAbilityEffect* ProcessEffectOperation(const TGMASBoundQueueOperation& Operation); bool ShouldProcessEffectOperation(const TGMASBoundQueueOperation& Operation, bool bIsServer = true) const; @@ -585,8 +601,10 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo bool bInGMCTime = false; void ServerHandlePendingEffect(float DeltaTime); + void ServerHandlePredictedPendingEffect(float DeltaTime); void ClientHandlePendingEffect(); + void ClientHandlePredictedPendingEffect(); int LateApplicationIDCounter = 0; diff --git a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h index 0682dba5..b7ace3cf 100644 --- a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h +++ b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h @@ -59,6 +59,9 @@ struct GMCABILITYSYSTEM_API FGMASBoundQueueRPCHeader UPROPERTY() float RPCGracePeriodSeconds { 1.f }; + + UPROPERTY() + uint8 ExtraFlags { 0 }; }; template @@ -122,6 +125,11 @@ struct GMCABILITYSYSTEM_API TGMASBoundQueueOperation InstancedPayloadIds = FInstancedStruct::Make(Header.PayloadIds); } + RefreshClass(); + } + + void RefreshClass() + { if (Header.ItemClassName != NAME_None && !ItemClass) { // Get a handle to our class, for instancing purposes. @@ -178,6 +186,7 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue int BI_OperationType { -1 }; int BI_OperationTag { -1 }; int BI_OperationClass { -1 }; + int BI_OperationExtraFlags { -1 }; int BI_OperationPayload { -1 }; int BI_OperationPayloadIds { -1 }; @@ -246,6 +255,13 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue 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, @@ -253,7 +269,7 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue EGMC_SimulationMode::Periodic_Output, EGMC_InterpolationFunction::TargetValue); } - + void PreLocalMovement() { if (QueuedBoundOperations.Num() > 0 && ClientAuth) @@ -277,11 +293,14 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue void ClearCurrentOperation() { - CurrentOperation.Header.OperationId = -1; - CurrentOperation.Header.Tag = FGameplayTag::EmptyTag; - CurrentOperation.Header.ItemClassName = NAME_None; - CurrentOperation.SetOperationType(EGMASBoundQueueOperationType::None); - CurrentOperation.Header.PayloadIds.Ids.Empty(); + 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); } @@ -292,7 +311,7 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue ExpireStaleAcks(DeltaTime); } - int32 MakeOperation(TGMASBoundQueueOperation& NewOperation, EGMASBoundQueueOperationType Type, FGameplayTag Tag, const T& Payload, TArray PayloadIds = {}, TSubclassOf ItemClass = nullptr, float RPCGracePeriod = 1.f) + 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); @@ -301,6 +320,7 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue NewOperation.ItemClass = ItemClass; NewOperation.Header.RPCGracePeriodSeconds = RPCGracePeriod; NewOperation.Header.PayloadIds.Ids = PayloadIds; + NewOperation.Header.ExtraFlags = ExtraFlags; NewOperation.Refresh(false); @@ -440,19 +460,33 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue return false; } - bool GetCurrentBoundOperation(TGMASBoundQueueOperation& Operation) + bool GetCurrentBoundOperation(TGMASBoundQueueOperation& Operation, bool bRefresh = false) { Operation = CurrentOperation; - if (Operation.GetOperationType() != EGMASBoundQueueOperationType::None) { - Operation.Refresh(); + 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) From 732cbbe5010f809bf530e45b562a288f1b6a6eb8 Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Sun, 29 Sep 2024 17:27:10 -0700 Subject: [PATCH 13/18] Add effect handles, so that PredictedQueued works right. --- .../Components/GMCAbilityComponent.cpp | 124 +++++++++++++++++- .../Public/Components/GMCAbilityComponent.h | 54 ++++++-- 2 files changed, 161 insertions(+), 17 deletions(-) diff --git a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp index 4094ab8c..c420c590 100644 --- a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp +++ b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp @@ -806,6 +806,21 @@ void UGMC_AbilitySystemComponent::TickActiveEffects(float DeltaTime) ActiveEffects.Remove(EffectID); ActiveEffectsData.RemoveAll([EffectID](const FGMCAbilityEffectData& EffectData) {return EffectData.EffectID == EffectID;}); } + + // Clean effect handles + TArray ExpiredHandles; + for (auto& [ID, HandleData] : EffectHandles) + { + if (HandleData.NetworkId > 0 && !ActiveEffects.Contains(HandleData.NetworkId)) + { + ExpiredHandles.Add(HandleData.Handle); + } + } + for (const int& Handle : ExpiredHandles) + { + EffectHandles.Remove(Handle); + } + } void UGMC_AbilitySystemComponent::TickActiveAbilities(float DeltaTime) @@ -1190,6 +1205,16 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ProcessEffectOperation( } 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; } @@ -1368,18 +1393,75 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf(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& OutEffectId, - UGMCAbilityEffect*& OutEffect) + FGMCAbilityEffectData InitializationData, EGMCAbilityEffectQueueType QueueType, bool& OutSuccess, int& OutEffectHandle, int& OutEffectId, + UGMCAbilityEffect*& OutEffect) { - OutSuccess = ApplyAbilityEffect(EffectClass, InitializationData, QueueType, OutEffectId, OutEffect); + OutSuccess = ApplyAbilityEffect(EffectClass, InitializationData, QueueType, OutEffectHandle, OutEffectId, OutEffect); } bool UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf EffectClass, - FGMCAbilityEffectData InitializationData, EGMCAbilityEffectQueueType QueueType, int& OutEffectId, UGMCAbilityEffect*& OutEffect) + FGMCAbilityEffectData InitializationData, EGMCAbilityEffectQueueType QueueType, int& OutEffectHandle, int& OutEffectId, UGMCAbilityEffect*& OutEffect) { OutEffect = nullptr; OutEffectId = -1; + OutEffectHandle = -1; if (EffectClass == nullptr) { UE_LOG(LogGMCAbilitySystem, Error, TEXT("Trying to apply Effect, but effect is null!")); @@ -1396,6 +1478,12 @@ bool UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOfGetName(), *EffectClass->GetName()) return false; } + + FGMASQueueOperationHandle HandleData; + HandleData.Handle = GetNextAvailableEffectHandle(); + HandleData.NetworkId = EffectID; + HandleData.OperationId = Operation.Header.OperationId; + EffectHandles.Add(HandleData.Handle, HandleData); switch(QueueType) { @@ -1411,12 +1499,23 @@ bool UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOfEffectData.EffectID; + OutEffectHandle = HandleData.Handle; return true; } case EGMCAbilityEffectQueueType::PredictedQueued: { - // We utilize the ClientAuth queue's RPC queue for the sake of convenience. - QueuedEffectOperations_ClientAuth.QueuePreparedOperation(Operation, false); + if (GMCMovementComponent->IsExecutingMove()) + { + // We're in a move context, just add it directly rather than queuing. + OutEffect = ProcessEffectOperation(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: @@ -1440,6 +1539,7 @@ bool UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf Ids, EGMCAbil 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) { diff --git a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h index 6727b3ca..8194f387 100644 --- a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h +++ b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h @@ -42,26 +42,42 @@ 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. Should PROBABLY only be used for cosmetic effects! - ClientAuth UMETA(DisplayName="Client Auth"), - - /// 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. WILL NOT RETURN AN EFFECT ID. - PredictedQueued UMETA(Hidden, DisplayName="ADVANCED: Predicted [Queued]"), + /// 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. + /// 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]") }; @@ -264,7 +280,7 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo */ 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 ID") int& OutEffectId, UPARAM(DisplayName="Effect Instance") UGMCAbilityEffect*& OutEffect); + UPARAM(DisplayName="Success") bool& OutSuccess, UPARAM(DisplayName="Effect Handle") int& OutEffectHandle, UPARAM(DisplayName="Effect Network ID") int& OutEffectId, UPARAM(DisplayName="Effect Instance") UGMCAbilityEffect*& OutEffect); /** * Applies an effect to the ability component. If the Queue Type is Predicted, the effect will be immediately added @@ -274,11 +290,12 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo * @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 OutEffectId The newly-created effect's ID, if successful. - * @param OutEffect The newly-created effect instance, if a predicted add. + * @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& OutEffectId, UGMCAbilityEffect*& OutEffect); + 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); @@ -324,6 +341,8 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo 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); /** * Gets the number of active effects with the inputted tag. @@ -596,6 +615,19 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo UPROPERTY() TMap ActiveEffects; + UPROPERTY() + TMap EffectHandles; + + int GetNextAvailableEffectHandle() const; + + 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 RemoveEffectHandle(int EffectHandle); + // doesn't work ATM. UPROPERTY(BlueprintReadOnly, Category = "GMCAbilitySystem", meta=(AllowPrivateAccess="true")) bool bInGMCTime = false; From 8369933e197aa8cc3f90b11862f529c27e3407df Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Thu, 3 Oct 2024 16:06:36 -0700 Subject: [PATCH 14/18] Small cleanup on effect handle expiry. --- .../Private/Components/GMCAbilityComponent.cpp | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp index c420c590..d1405336 100644 --- a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp +++ b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp @@ -808,18 +808,16 @@ void UGMC_AbilitySystemComponent::TickActiveEffects(float DeltaTime) } // Clean effect handles - TArray ExpiredHandles; - for (auto& [ID, HandleData] : EffectHandles) + TArray CurrentHandles; + EffectHandles.GetKeys(CurrentHandles); + for (const int Handle : CurrentHandles) { - if (HandleData.NetworkId > 0 && !ActiveEffects.Contains(HandleData.NetworkId)) + const auto& Data = EffectHandles[Handle]; + if (Data.NetworkId > 0 && !ActiveEffects.Contains(Data.NetworkId)) { - ExpiredHandles.Add(HandleData.Handle); + EffectHandles.Remove(Handle); } } - for (const int& Handle : ExpiredHandles) - { - EffectHandles.Remove(Handle); - } } @@ -1558,7 +1556,7 @@ bool UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOfGetName(), *EffectClass->GetName()) return false; } From e6b13a6e453aa4ee7562d64b99d3e74f400468e0 Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Thu, 3 Oct 2024 20:33:26 -0700 Subject: [PATCH 15/18] Correct desync for server-auth queue. --- .../Public/Utility/GMASBoundQueue.h | 121 ++++++++++-------- 1 file changed, 67 insertions(+), 54 deletions(-) diff --git a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h index b7ace3cf..ed78963e 100644 --- a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h +++ b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h @@ -213,61 +213,74 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue EGMC_InterpolationFunction::TargetValue ); - BI_Acknowledgements = MovementComponent->BindInstancedStruct( - Acknowledgments, - AckPrediction, - EGMC_CombineMode::CombineIfUnchanged, - EGMC_SimulationMode::None, - EGMC_InterpolationFunction::TargetValue); - - 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); + 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); + } - 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() From d29834268b933aa3f56f4c44b8b4212ee18428d0 Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Tue, 8 Oct 2024 16:36:53 -0700 Subject: [PATCH 16/18] Ensure we don't use the same ID if queuing two server-auth things in the same frame. --- .../Components/GMCAbilityComponent.cpp | 2 +- .../Public/Utility/GMASBoundQueue.h | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp index d1405336..035f0fe6 100644 --- a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp +++ b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp @@ -1314,7 +1314,7 @@ int UGMC_AbilitySystemComponent::GetNextAvailableEffectID() const } int NewEffectID = static_cast(ActionTimer * 100); - while (ActiveEffects.Contains(NewEffectID)) + while (ActiveEffects.Contains(NewEffectID) || QueuedEffectOperations.HasOperationWithPayloadId(NewEffectID) || QueuedEffectOperations_ClientAuth.HasOperationWithPayloadId(NewEffectID)) { NewEffectID++; } diff --git a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h index ed78963e..093b5312 100644 --- a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h +++ b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h @@ -437,6 +437,27 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue 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; From f207ab0ca7a3c36f650313bc44f64533f1b8652d Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Tue, 8 Oct 2024 18:35:33 -0700 Subject: [PATCH 17/18] Fix for Reznok's two-effects-in-one-frame issue. --- .../Components/GMCAbilityComponent.cpp | 23 ++++++++++++++++++- .../Public/Components/GMCAbilityComponent.h | 1 + .../Public/Utility/GMASBoundQueue.h | 13 ++++++++++- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp index 035f0fe6..a2a9c0e1 100644 --- a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp +++ b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp @@ -1314,7 +1314,7 @@ int UGMC_AbilitySystemComponent::GetNextAvailableEffectID() const } int NewEffectID = static_cast(ActionTimer * 100); - while (ActiveEffects.Contains(NewEffectID) || QueuedEffectOperations.HasOperationWithPayloadId(NewEffectID) || QueuedEffectOperations_ClientAuth.HasOperationWithPayloadId(NewEffectID)) + while (ActiveEffects.Contains(NewEffectID) || CheckIfEffectIDQueued(NewEffectID)) { NewEffectID++; } @@ -1323,6 +1323,27 @@ int UGMC_AbilitySystemComponent::GetNextAvailableEffectID() const 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, diff --git a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h index 8194f387..ad6eab8e 100644 --- a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h +++ b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h @@ -257,6 +257,7 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo void OnRep_UnBoundAttributes(); 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); /** diff --git a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h index 093b5312..49c9961d 100644 --- a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h +++ b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h @@ -302,7 +302,18 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue } } - int32 GenerateOperationId() const { return ActionTimer * 100; } + int32 GenerateOperationId() const + { + int32 NewOperationId = ActionTimer * 100; + + TGMASBoundQueueOperation Operation; + + if (GetOperationById(NewOperationId, Operation)) + { + NewOperationId++; + } + return NewOperationId; + } void ClearCurrentOperation() { From 28e42e1600512e4be56411fd30db73c8984deecd Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Fri, 11 Oct 2024 11:12:44 -0700 Subject: [PATCH 18/18] Correct issue with 2+ PredictedQueued operations in one tick, or 3+ ServerAuth queued. --- .../Private/Components/GMCAbilityComponent.cpp | 9 ++++++++- Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h | 6 +++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp index a2a9c0e1..258ecf04 100644 --- a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp +++ b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp @@ -1372,7 +1372,14 @@ int UGMC_AbilitySystemComponent::CreateEffectOperation( PayloadIds.Add(PayloadData.EffectID); } - QueuedEffectOperations.MakeOperation(OutOperation, EGMASBoundQueueOperationType::Add, PayloadData.EffectTag, PayloadData, PayloadIds, EffectClass, 1.f, static_cast(QueueType)); + 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; } diff --git a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h index 49c9961d..fb91525a 100644 --- a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h +++ b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h @@ -308,7 +308,7 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue TGMASBoundQueueOperation Operation; - if (GetOperationById(NewOperationId, Operation)) + while (GetOperationById(NewOperationId, Operation)) { NewOperationId++; } @@ -427,7 +427,7 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue bool GetOperationById(int32 OperationId, TGMASBoundQueueOperation& OutOperation) const { - for (const auto& Operation : QueuedBoundOperations) + for (const auto& Operation : GetQueuedOperations()) { if (Operation.GetOperationId() == OperationId) { @@ -436,7 +436,7 @@ class GMCABILITYSYSTEM_API TGMASBoundQueue } } - for (const auto& Operation : QueuedRPCOperations) + for (const auto& Operation : GetQueuedRPCOperations()) { if (Operation.GetOperationId() == OperationId) {