diff --git a/Source/GMCAbilitySystem/Private/Ability/GMCAbility.cpp b/Source/GMCAbilitySystem/Private/Ability/GMCAbility.cpp index f1ebb91e..c4fa30a7 100644 --- a/Source/GMCAbilitySystem/Private/Ability/GMCAbility.cpp +++ b/Source/GMCAbilitySystem/Private/Ability/GMCAbility.cpp @@ -59,18 +59,20 @@ void UGMCAbility::AncillaryTick(float DeltaTime){ void UGMCAbility::TickTasks(float DeltaTime) { - for (const TPair& Task : RunningTasks) + for (int i=0; i < RunningTasks.Num(); i++) { - if (Task.Value == nullptr) {continue;} - Task.Value->Tick(DeltaTime); + UGMCAbilityTaskBase* Task = RunningTasks[i]; + if (Task == nullptr) {continue;} + Task->Tick(DeltaTime); } } void UGMCAbility::AncillaryTickTasks(float DeltaTime){ - for (const TPair& Task : RunningTasks) + for (int i=0; i < RunningTasks.Num(); i++) { - if (Task.Value == nullptr) {continue;} - Task.Value->AncillaryTick(DeltaTime); + UGMCAbilityTaskBase* Task = RunningTasks[i]; + if (Task == nullptr) {continue;} + Task->AncillaryTick(DeltaTime); } } 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 35aa879a..e36a892f 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, @@ -170,16 +156,14 @@ void UGMC_AbilitySystemComponent::BindReplicationData() EGMC_SimulationMode::PeriodicAndOnChange_Output, EGMC_InterpolationFunction::TargetValue); - GMCMovementComponent->BindInstancedStruct(AcknowledgeId, - EGMC_PredictionMode::ClientAuth_Input, - EGMC_CombineMode::CombineIfUnchanged, - EGMC_SimulationMode::None, - EGMC_InterpolationFunction::TargetValue); + // Bind our operation queues. + QueuedAbilityOperations.BindToGMC(GMCMovementComponent); + QueuedEffectOperations.BindToGMC(GMCMovementComponent); + QueuedEffectOperations_ClientAuth.BindToGMC(GMCMovementComponent); } void UGMC_AbilitySystemComponent::GenAncillaryTick(float DeltaTime, bool bIsCombinedClientMove) { - OnAncillaryTick.Broadcast(DeltaTime); ClientHandlePendingEffect(); @@ -192,25 +176,26 @@ 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 + TGMASBoundQueueOperation Operation; + if (QueuedAbilityOperations.GetCurrentBoundOperation(Operation, true)) { - TryActivateAbilitiesByInputTag(AbilityData.InputTag, AbilityData.ActionInput, false); + ProcessAbilityOperation(Operation, false); } SendTaskDataToActiveAbility(false); ClearAbilityAndTaskData(); + QueuedEffectOperations_ClientAuth.ClearCurrentOperation(); bInGMCTime = false; } -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); @@ -221,7 +206,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; @@ -341,7 +327,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(); @@ -410,18 +395,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) @@ -572,16 +553,21 @@ void UGMC_AbilitySystemComponent::GenPredictionTick(float DeltaTime) // Abilities CleanupStaleAbilities(); + + // Advance our queue action timers. + QueuedAbilityOperations.GenPredictionTick(DeltaTime); + QueuedEffectOperations.GenPredictionTick(DeltaTime); // Was an ability used? - if (AbilityData.InputTag != FGameplayTag::EmptyTag) + if (TGMASBoundQueueOperation Operation; + QueuedAbilityOperations.GetCurrentBoundOperation(Operation, true)) { - TryActivateAbilitiesByInputTag(AbilityData.InputTag, AbilityData.ActionInput, true); + ProcessAbilityOperation(Operation, true); } - SendTaskDataToActiveAbility(true); - + ServerHandlePredictedPendingEffect(DeltaTime); + SendTaskDataToActiveAbility(true); } void UGMC_AbilitySystemComponent::GenSimulationTick(float DeltaTime) @@ -601,16 +587,26 @@ void UGMC_AbilitySystemComponent::GenSimulationTick(float DeltaTime) void UGMC_AbilitySystemComponent::PreLocalMoveExecution() { - if (QueuedAbilities.Num() > 0) - { - AbilityData = QueuedAbilities.Pop(); - } if (QueuedTaskData.Num() > 0) { TaskData = QueuedTaskData.Pop(); } - + // Advance our client-auth queues. + QueuedAbilityOperations.PreLocalMovement(); + QueuedEffectOperations_ClientAuth.PreLocalMovement(); + + if (GetNetMode() == NM_Standalone || GMCMovementComponent->IsLocallyControlledServerPawn()) + { + // We'll never get the pre remote movement trigger in this case, soooo... + QueuedEffectOperations.PreRemoteMovement(); + } +} + +void UGMC_AbilitySystemComponent::PreRemoteMoveExecution() +{ + // Advance our server-auth queues. + QueuedEffectOperations.PreRemoteMovement(); } void UGMC_AbilitySystemComponent::BeginPlay() @@ -780,7 +776,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() && @@ -802,6 +801,19 @@ void UGMC_AbilitySystemComponent::TickActiveEffects(float DeltaTime) ActiveEffects.Remove(EffectID); ActiveEffectsData.RemoveAll([EffectID](const FGMCAbilityEffectData& EffectData) {return EffectData.EffectID == EffectID;}); } + + // Clean effect handles + TArray CurrentHandles; + EffectHandles.GetKeys(CurrentHandles); + for (const int Handle : CurrentHandles) + { + const auto& Data = EffectHandles[Handle]; + if (Data.NetworkId > 0 && !ActiveEffects.Contains(Data.NetworkId)) + { + EffectHandles.Remove(Handle); + } + } + } void UGMC_AbilitySystemComponent::TickActiveAbilities(float DeltaTime) @@ -868,119 +880,94 @@ void UGMC_AbilitySystemComponent::CheckRemovedEffects() } } -void UGMC_AbilitySystemComponent::AddPendingEffectApplications(FGMCOuterApplicationWrapper& Wrapper) { - check(HasAuthority()) - - Wrapper.ClientGraceTimeRemaining = 1.f; - Wrapper.LateApplicationID = GenerateLateApplicationID(); - - PendingApplicationServer.Add(Wrapper); - RPCClientAddPendingEffectApplication(Wrapper); -} - - -void UGMC_AbilitySystemComponent::RPCClientAddPendingEffectApplication_Implementation( - FGMCOuterApplicationWrapper Wrapper) { - PendingApplicationClient.Add(Wrapper); -} - - - void UGMC_AbilitySystemComponent::ServerHandlePendingEffect(float DeltaTime) { if (!HasAuthority()) { return; } - FGMCAcknowledgeId& AckId = AcknowledgeId.GetMutable(); - - - for (int i = PendingApplicationServer.Num() - 1; i >= 0; i--) { - FGMCOuterApplicationWrapper& Wrapper = PendingApplicationServer[i]; + // Handle our GMC-replicated effect operation, if any. We can't actually replicate + // the message server-to-client via GMC, but we *can* preserve it in the move history in + // case it is relevant to replay. + TGMASBoundQueueOperation BoundOperation; + if (QueuedEffectOperations.GetCurrentBoundOperation(BoundOperation, true)) + { + // Move this into our RPC queue to wait on acknowledgment. + QueuedEffectOperations.QueuePreparedOperation(BoundOperation, false); - if (Wrapper.ClientGraceTimeRemaining <= 0.f || AckId.Id.Contains(Wrapper.LateApplicationID)) { + // And send it via RPC, so that the client gets it. + ClientQueueEffectOperation(BoundOperation); + }; - switch (Wrapper.Type) { - case EGMC_AddEffect: { - const FGMCOuterEffectAdd& Data = Wrapper.OuterApplicationData.Get(); - UGMCAbilityEffect* AbilityEffect = DuplicateObject(Data.EffectClass->GetDefaultObject(), this); - AbilityEffect->EffectData.EffectID = Wrapper.LateApplicationID; - FGMCAbilityEffectData EffectData = Data.InitializationData.IsValid() ? Data.InitializationData : AbilityEffect->EffectData; - UGMCAbilityEffect* FX = ApplyAbilityEffect(AbilityEffect, EffectData); - if (Wrapper.ClientGraceTimeRemaining <= 0.f) { - UE_LOG(LogGMCAbilitySystem, Log, TEXT("Client Add Effect `%s ` Missed Grace time, Force application : id: %d"), *GetNameSafe(Data.EffectClass), FX->EffectData.EffectID); - } - } break; - case EGMC_RemoveEffect: { - const FGMCOuterEffectRemove& Data = Wrapper.OuterApplicationData.Get(); - RemoveEffectById(Data.Ids); - if (Wrapper.ClientGraceTimeRemaining <= 0.f) { - UE_LOG(LogGMCAbilitySystem, Log, TEXT("Client Remove Effect Missed Grace time, Force remove")); - } - } break; + // Handle our 'outer' RPC effect operations. + QueuedEffectOperations.DeductGracePeriod(DeltaTime); + auto Operations = QueuedEffectOperations.GetQueuedRPCOperations(); + for (auto& Operation : Operations) { + if (ShouldProcessEffectOperation(Operation, true)) + { + if (Operation.GracePeriodExpired()) + { + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Client effect operation missed grace period, forcing on server.")) } - PendingApplicationServer.RemoveAt(i); - } - else { - Wrapper.ClientGraceTimeRemaining -= DeltaTime; + ProcessEffectOperation(Operation); + QueuedEffectOperations.RemoveOperationById(Operation.GetOperationId()); } - - } - } - -void UGMC_AbilitySystemComponent::ClientHandlePendingEffect() { - - - for (int i = PendingApplicationClient.Num() - 1; i >= 0; i--) +void UGMC_AbilitySystemComponent::ServerHandlePredictedPendingEffect(float DeltaTime) +{ + if (!HasAuthority()) return; + + // Check for any client-auth effects. + TGMASBoundQueueOperation BoundOperation; + if (QueuedEffectOperations_ClientAuth.GetCurrentBoundOperation(BoundOperation, true)) { - FGMCOuterApplicationWrapper& LateApplicationData = PendingApplicationClient[i]; - - 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(); + ProcessEffectOperation(BoundOperation); + } - 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); - } + // 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); + } + } -int UGMC_AbilitySystemComponent::GenerateLateApplicationID() { - int NewEffectID = static_cast(ActionTimer * 100); - while (ActiveEffects.Contains(NewEffectID)) +void UGMC_AbilitySystemComponent::ClientHandlePendingEffect() { + + // 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 (QueuedEffectOperations.IsAcknowledged(Operation.GetOperationId())) + { + ProcessEffectOperation(Operation); + QueuedEffectOperations.RemoveOperationById(Operation.GetOperationId()); + } + if (ShouldProcessEffectOperation(Operation, false)) + { + QueuedEffectOperations.Acknowledge(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)) { - NewEffectID++; + ProcessEffectOperation(BoundOperation); } - - return NewEffectID; } - void UGMC_AbilitySystemComponent::RPCTaskHeartbeat_Implementation(int AbilityID, int TaskID) { if (ActiveAbilities.Contains(AbilityID) && ActiveAbilities[AbilityID] != nullptr) @@ -1054,6 +1041,7 @@ TArray> UGMC_AbilitySystemComponent::GetGrantedAbilitie void UGMC_AbilitySystemComponent::ClearAbilityAndTaskData() { AbilityData = FGMCAbilityData{}; + QueuedAbilityOperations.ClearCurrentOperation(); TaskData = FInstancedStruct::Make(FGMCAbilityTaskData{}); } @@ -1153,6 +1141,132 @@ void UGMC_AbilitySystemComponent::InitializeStartingAbilities() } } +bool UGMC_AbilitySystemComponent::ProcessAbilityOperation( + const TGMASBoundQueueOperation& Operation, bool bFromMovementTick) +{ + EGMASBoundQueueOperationType OperationType = Operation.GetOperationType(); + if (OperationType == EGMASBoundQueueOperationType::Activate) + { + TryActivateAbilitiesByInputTag(Operation.GetTag(), Operation.Payload.ActionInput, bFromMovementTick); + return true; + } + + if (OperationType == EGMASBoundQueueOperationType::Cancel) + { + EndAbilitiesByTag(Operation.GetTag()); + if (Operation.ItemClass) + { + EndAbilitiesByClass(Operation.ItemClass); + } + return true; + } + + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Received ability operation with invalid operation type %s for %s!"), + *UEnum::GetValueAsString(OperationType), *Operation.GetTag().ToString()) + return false; +} + +UGMCAbilityEffect* UGMC_AbilitySystemComponent::ProcessEffectOperation( + const TGMASBoundQueueOperation& Operation) +{ + EGMASBoundQueueOperationType OperationType = Operation.GetOperationType(); + + if (OperationType == EGMASBoundQueueOperationType::Add) + { + if (Operation.ItemClass == nullptr) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted to process an add effect operation with no set class!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName()) + } + + UGMCAbilityEffect* Effect = DuplicateObject(Operation.ItemClass->GetDefaultObject(), this); + FGMCAbilityEffectData EffectData = Operation.Payload; + + if (!EffectData.IsValid()) + { + EffectData = Effect->EffectData; + } + + if (Operation.Header.PayloadIds.Ids.Num() > 0) + { + EffectData.EffectID = Operation.Header.PayloadIds.Ids[0]; + } + + if (EffectData.EffectID > 0 && ActiveEffects.Contains(EffectData.EffectID)) + { + const auto& ExistingEffect = ActiveEffects[EffectData.EffectID]; + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("[%20s] %s attempted to process an explicit ID add effect operation for %s with existing effect %d [%s]"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), *Effect->GetClass()->GetName(), EffectData.EffectID, *ExistingEffect->GetClass()->GetName()) + return nullptr; + } + + ApplyAbilityEffect(Effect, EffectData); + + for (auto& [EffectHandle, EffectHandleData] : EffectHandles) + { + // If we don't already have a known effect ID, attach it to our handle now. + if (EffectHandleData.NetworkId <= 0 && EffectHandleData.OperationId == Operation.Header.OperationId) + { + EffectHandleData.NetworkId = Effect->EffectData.EffectID; + } + } + + return Effect; + } + + if (OperationType == EGMASBoundQueueOperationType::Remove) + { + for (auto& [Id, Effect]: ActiveEffects) + { + if (Operation.GetPayloadIds().Contains(Id)) + { + RemoveActiveAbilityEffect(Effect); + } + } + return nullptr; + } + + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Received ability operation with invalid operation type %s for %s!"), + *UEnum::GetValueAsString(OperationType), *Operation.ItemClass->GetName()) + return nullptr; +} + +bool UGMC_AbilitySystemComponent::ShouldProcessEffectOperation( + const TGMASBoundQueueOperation& Operation, bool bIsServer) const +{ + 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) + { + return HasAuthority() && (QueuedEffectOperations.IsAcknowledged(Operation.GetOperationId()) || + Operation.GracePeriodExpired() || GetNetMode() == NM_Standalone); + } + else + { + return !QueuedEffectOperations.IsAcknowledged(Operation.GetOperationId()) && (GMCMovementComponent->IsLocallyControlledServerPawn() || GMCMovementComponent->IsAutonomousProxy()); + } +} + +void UGMC_AbilitySystemComponent::ClientQueueEffectOperation( + const TGMASBoundQueueOperation& Operation) +{ + RPCClientQueueEffectOperation(Operation.Header); +} + +void UGMC_AbilitySystemComponent::RPCClientQueueEffectOperation_Implementation(const FGMASBoundQueueRPCHeader& Header) +{ + QueuedEffectOperations.QueueOperationFromHeader(Header, false); +} + void UGMC_AbilitySystemComponent::OnRep_UnBoundAttributes() { @@ -1189,6 +1303,84 @@ void UGMC_AbilitySystemComponent::CheckUnBoundAttributeChanges() } } +int UGMC_AbilitySystemComponent::GetNextAvailableEffectID() const +{ + if (ActionTimer == 0) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[ApplyAbilityEffect] Action Timer is 0, cannot generate Effect ID. Is it a listen server smoothed pawn?")); + return -1; + } + + int NewEffectID = static_cast(ActionTimer * 100); + while (ActiveEffects.Contains(NewEffectID) || CheckIfEffectIDQueued(NewEffectID)) + { + NewEffectID++; + } + UE_LOG(LogGMCAbilitySystem, VeryVerbose, TEXT("[Server: %hhd] Generated Effect ID: %d"), HasAuthority(), NewEffectID); + + return NewEffectID; +} + +bool UGMC_AbilitySystemComponent::CheckIfEffectIDQueued(int EffectID) const +{ + for (const auto& Operation : QueuedEffectOperations.GetQueuedRPCOperations()) + { + if (Operation.Payload.EffectID == EffectID) + { + return true; + } + } + + for (const auto& Operation : QueuedEffectOperations_ClientAuth.GetQueuedRPCOperations()) + { + if (Operation.Payload.EffectID == EffectID) + { + return true; + } + } + + return false; +} + +int UGMC_AbilitySystemComponent::CreateEffectOperation( + TGMASBoundQueueOperation& OutOperation, + const TSubclassOf& EffectClass, + const FGMCAbilityEffectData& EffectData, + bool bForcedEffectId, + EGMCAbilityEffectQueueType QueueType) +{ + TArray PayloadIds {}; + + FGMCAbilityEffectData PayloadData; + if (EffectData.IsValid()) + { + PayloadData = EffectData; + } + else + { + PayloadData = EffectClass->GetDefaultObject()->EffectData; + } + + if (bForcedEffectId) + { + if (PayloadData.EffectID == 0) + { + PayloadData.EffectID = GetNextAvailableEffectID(); + } + PayloadIds.Add(PayloadData.EffectID); + } + + if (QueueType == EGMCAbilityEffectQueueType::PredictedQueued || QueueType == EGMCAbilityEffectQueueType::ClientAuth) + { + QueuedEffectOperations_ClientAuth.MakeOperation(OutOperation, EGMASBoundQueueOperationType::Add, PayloadData.EffectTag, PayloadData, PayloadIds, EffectClass, 1.f, static_cast(QueueType)); + } + else + { + QueuedEffectOperations.MakeOperation(OutOperation, EGMASBoundQueueOperationType::Add, PayloadData.EffectTag, PayloadData, PayloadIds, EffectClass, 1.f, static_cast(QueueType)); + } + return PayloadData.EffectID; +} + //BP Version UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf Effect, FGMCAbilityEffectData InitializationData, bool bOuterActivation) { @@ -1198,32 +1390,208 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf Operation; + if (CreateEffectOperation(Operation, Effect, InitializationData, bOuterActivation, bOuterActivation ? EGMCAbilityEffectQueueType::ServerAuth : EGMCAbilityEffectQueueType::Predicted) == -1) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s could not create an effect of type %s!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), *Effect->GetName()) + return nullptr; + } + // We are trying to apply an effect from an outside source, so we will need to go trough a different routing to apply it if (bOuterActivation) { if (HasAuthority()) { - FGMCOuterApplicationWrapper Wrapper = FGMCOuterApplicationWrapper::Make(Effect, InitializationData); - AddPendingEffectApplications(Wrapper); + QueuedEffectOperations.QueuePreparedOperation(Operation, false); + ClientQueueEffectOperation(Operation); } return nullptr; } - + if (!GMCMovementComponent->IsExecutingMove() && GetNetMode() != NM_Standalone) + { + // For backwards compatibility, we do not reject this if we're outside a movement cycle. However, we will at least + // log it. + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("[%20s] %s tried to apply a predicted effect of type %s outside a movement cycle!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), *Effect->GetName()) + } + return ProcessEffectOperation(Operation); +} + +int32 UGMC_AbilitySystemComponent::GetNextAvailableEffectHandle() const +{ + if (ActionTimer == 0) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[ApplyAbilityEffect] Action Timer is 0, cannot generate Effect ID. Is it a listen server smoothed pawn?")); + return -1; + } + + int NewEffectHandle = static_cast(ActionTimer * 100); + while (EffectHandles.Contains(NewEffectHandle)) + { + NewEffectHandle++; + } - UGMCAbilityEffect* AbilityEffect = DuplicateObject(Effect->GetDefaultObject(), this); - - FGMCAbilityEffectData EffectData; - if (InitializationData.IsValid()) + 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) { - EffectData = InitializationData; + OutEffect = ActiveEffects[HandleData.NetworkId]; } - else + return true; +} + +bool UGMC_AbilitySystemComponent::GetEffectHandle(int EffectHandle, FGMASQueueOperationHandle& HandleData) const +{ + for (auto& [ID, Handle] : EffectHandles) { - EffectData = AbilityEffect->EffectData; + if (Handle.Handle == EffectHandle) + { + HandleData = Handle; + return true; + } + } + return false; +} + +void UGMC_AbilitySystemComponent::RemoveEffectHandle(int EffectHandle) +{ + EffectHandles.Remove(EffectHandle); +} + +void UGMC_AbilitySystemComponent::ApplyAbilityEffectSafe(TSubclassOf EffectClass, + FGMCAbilityEffectData InitializationData, EGMCAbilityEffectQueueType QueueType, bool& OutSuccess, int& OutEffectHandle, int& OutEffectId, + UGMCAbilityEffect*& OutEffect) +{ + OutSuccess = ApplyAbilityEffect(EffectClass, InitializationData, QueueType, OutEffectHandle, OutEffectId, OutEffect); +} + +bool UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf EffectClass, + FGMCAbilityEffectData InitializationData, EGMCAbilityEffectQueueType QueueType, int& OutEffectHandle, int& OutEffectId, UGMCAbilityEffect*& OutEffect) +{ + OutEffect = nullptr; + OutEffectId = -1; + OutEffectHandle = -1; + if (EffectClass == nullptr) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Trying to apply Effect, but effect is null!")); + return false; + } + + const bool bPregenerateEffectId = QueueType != EGMCAbilityEffectQueueType::Predicted && QueueType != EGMCAbilityEffectQueueType::PredictedQueued; + + TGMASBoundQueueOperation Operation; + 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!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), *EffectClass->GetName()) + return false; } + + FGMASQueueOperationHandle HandleData; + HandleData.Handle = GetNextAvailableEffectHandle(); + HandleData.NetworkId = EffectID; + HandleData.OperationId = Operation.Header.OperationId; + EffectHandles.Add(HandleData.Handle, HandleData); - ApplyAbilityEffect(AbilityEffect, EffectData); - return AbilityEffect; + switch(QueueType) + { + case EGMCAbilityEffectQueueType::Predicted: + { + if (!GMCMovementComponent->IsExecutingMove() && GetNetMode() != NM_Standalone) + { + 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; + OutEffectHandle = HandleData.Handle; + return true; + } + case EGMCAbilityEffectQueueType::PredictedQueued: + { + 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: + case EGMCAbilityEffectQueueType::ServerAuth: + { + if (!HasAuthority()) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted to apply server-queued effect %d of type %s on a client!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), EffectID, *EffectClass->GetName()) + return false; + } + + if (QueueType == EGMCAbilityEffectQueueType::ServerAuthMove) Operation.Header.RPCGracePeriodSeconds = 5.f; + + QueuedEffectOperations.QueuePreparedOperation(Operation, QueueType == EGMCAbilityEffectQueueType::ServerAuthMove); + + if (QueueType == EGMCAbilityEffectQueueType::ServerAuth) + { + // Queue for RPC and throw this to our client. + ClientQueueEffectOperation(Operation); + } + + OutEffectId = EffectID; + OutEffectHandle = HandleData.Handle; + return true; + } + + case EGMCAbilityEffectQueueType::ClientAuth: + { + if (GetNetMode() != NM_Standalone && !GMCMovementComponent->IsAutonomousProxy() && !GMCMovementComponent->IsLocallyControlledServerPawn()) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted to apply client-auth effect %d of type %s on a server!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), EffectID, *EffectClass->GetName()) + return false; + } + + QueuedEffectOperations_ClientAuth.QueuePreparedOperation(Operation, true); + OutEffectId = EffectID; + return true; + } + } + + UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted to apply effect of type %s but something has gone BADLY wrong!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), *EffectClass->GetName()) + return false; +} + +UGMCAbilityEffect* UGMC_AbilitySystemComponent::GetEffectById(const int EffectId) const +{ + if (!ActiveEffects.Contains(EffectId)) return nullptr; + + return ActiveEffects[EffectId]; } UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(UGMCAbilityEffect* Effect, FGMCAbilityEffectData InitializationData) @@ -1241,19 +1609,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 @@ -1265,8 +1621,9 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(UGMCAbilityEf { ProcessedEffectIDs.Add(Effect->EffectData.EffectID, false); } - + ActiveEffects.Add(Effect->EffectData.EffectID, Effect); + return Effect; } @@ -1278,17 +1635,26 @@ 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 {}; } - TMap EffectsToRemove; - int32 NumRemoved = 0; + TArray EffectsToRemove; + int NumRemoved = 0; for(const TTuple Effect : ActiveEffects) { @@ -1296,37 +1662,50 @@ int32 UGMC_AbilitySystemComponent::RemoveEffectByTag(FGameplayTag InEffectTag, i break; } - if(Effect.Value->EffectData.EffectTag.IsValid() && Effect.Value->EffectData.EffectTag.MatchesTagExact(InEffectTag)){ - EffectsToRemove.Add(Effect.Key, Effect.Value); + if(Effect.Value->EffectData.EffectTag.IsValid() && Effect.Value->EffectData.EffectTag.MatchesTagExact(Tag)){ + EffectsToRemove.Add(Effect.Value->EffectData.EffectID); NumRemoved++; } } - - if (bOuterActivation) { - if (HasAuthority() && EffectsToRemove.Num() > 0) { + return EffectsToRemove; +} - TArray EffectIDsToRemove; - for (const auto& ToRemove : EffectsToRemove) { - EffectIDsToRemove.Add(ToRemove.Key); - } - - FGMCOuterApplicationWrapper Wrapper = FGMCOuterApplicationWrapper::Make(EffectIDsToRemove); - AddPendingEffectApplications(Wrapper); - } +int32 UGMC_AbilitySystemComponent::RemoveEffectByTag(FGameplayTag InEffectTag, int32 NumToRemove, bool bOuterActivation) { + + if (NumToRemove < -1 || !InEffectTag.IsValid()) { return 0; } - for (auto& ToRemove : EffectsToRemove) { - ToRemove.Value->EndEffect(); + TArray EffectsToRemove = EffectsMatchingTag(InEffectTag, NumToRemove); + + if (EffectsToRemove.Num() > 0) + { + RemoveEffectById(EffectsToRemove, bOuterActivation); } - - return 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; } @@ -1334,26 +1713,98 @@ bool UGMC_AbilitySystemComponent::RemoveEffectById(TArray Ids, bool bOuterA // check all IDs exists for (int Id : Ids) { if (!ActiveEffects.Contains(Id)) { - UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Trying to remove effect with ID %d, but it doesn't exist!"), Id); + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("[%20s] %s tried to remove effect with ID %d, but it doesn't exist!"), + *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), Id); return false; } } - if (bOuterActivation) { - if (HasAuthority()) { - FGMCOuterApplicationWrapper Wrapper = FGMCOuterApplicationWrapper::Make(Ids); - AddPendingEffectApplications(Wrapper); + 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::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()) + { + 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, QueueType == EGMCAbilityEffectQueueType::ServerAuthMove); + + if (QueueType == EGMCAbilityEffectQueueType::ServerAuth) + { + // 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(); - } +bool UGMC_AbilitySystemComponent::RemoveEffectByHandle(int EffectHandle, EGMCAbilityEffectQueueType QueueType) +{ + int32 EffectID; + UGMCAbilityEffect* Effect; + if (GetEffectFromHandle(EffectHandle, EffectID, Effect) && EffectID > 0) + { + return RemoveEffectByIdSafe({ EffectID }, QueueType); } + + return false; +} + + +bool UGMC_AbilitySystemComponent::RemoveEffectById(TArray Ids, bool bOuterActivation) { + + // Just hit up the newer version. + return RemoveEffectByIdSafe(Ids, bOuterActivation ? EGMCAbilityEffectQueueType::ServerAuth : EGMCAbilityEffectQueueType::Predicted); - return true; } @@ -1460,7 +1911,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"); } @@ -1468,7 +1919,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..992731f8 100644 --- a/Source/GMCAbilitySystem/Private/Effects/GMCAbilityEffect.cpp +++ b/Source/GMCAbilitySystem/Private/Effects/GMCAbilityEffect.cpp @@ -74,7 +74,7 @@ void UGMCAbilityEffect::StartEffect() { for (const FGMCAttributeModifier& Modifier : EffectData.Modifiers) { - OwnerAbilityComponent->ApplyAbilityEffectModifier(Modifier, true); + OwnerAbilityComponent->ApplyAbilityEffectModifier(Modifier, true, false, SourceAbilityComponent); } EndEffect(); return; @@ -86,7 +86,7 @@ void UGMCAbilityEffect::StartEffect() EffectData.bNegateEffectAtEnd = true; for (const FGMCAttributeModifier& Modifier : EffectData.Modifiers) { - OwnerAbilityComponent->ApplyAbilityEffectModifier(Modifier, false); + OwnerAbilityComponent->ApplyAbilityEffectModifier(Modifier, false, false, SourceAbilityComponent); } } @@ -128,7 +128,7 @@ void UGMCAbilityEffect::EndEffect() } } - RemoveTagsFromOwner(); + RemoveTagsFromOwner(EffectData.bPreserveGrantedTagsIfMultiple); RemoveAbilitiesFromOwner(); } @@ -201,7 +201,7 @@ void UGMCAbilityEffect::PeriodTick() if (AttributeDynamicCondition()) { for (const FGMCAttributeModifier& AttributeModifier : EffectData.Modifiers) { - OwnerAbilityComponent->ApplyAbilityEffectModifier(AttributeModifier, true); + OwnerAbilityComponent->ApplyAbilityEffectModifier(AttributeModifier, true, false, SourceAbilityComponent); } } } @@ -231,14 +231,22 @@ void UGMCAbilityEffect::AddTagsToOwner() void UGMCAbilityEffect::RemoveTagsFromOwner(bool bPreserveOnMultipleInstances) { - - if (bPreserveOnMultipleInstances && EffectData.EffectTag.IsValid()) { - TArray ActiveEffect = OwnerAbilityComponent->GetActivesEffectByTag(EffectData.EffectTag); - - if (ActiveEffect.Num() > 1) { - return; + if (bPreserveOnMultipleInstances) + { + if (EffectData.EffectTag.IsValid()) { + TArray ActiveEffect = OwnerAbilityComponent->GetActiveEffectsByTag(EffectData.EffectTag); + + if (ActiveEffect.Num() > 1) { + return; + } + } + else + { + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Effect Tag is not valid with PreserveMultipleInstances in UGMCAbilityEffect::RemoveTagsFromOwner")); } } + + for (const FGameplayTag Tag : EffectData.GrantedTags) { 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 42ec16ea..b12c7511 100644 --- a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h +++ b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h @@ -11,7 +11,7 @@ #include "Ability/Tasks/GMCAbilityTaskData.h" #include "Effects/GMCAbilityEffect.h" #include "Components/ActorComponent.h" -#include "GMCAbilityOuterApplication.h" +#include "Utility/GMASBoundQueue.h" #include "GMCAbilityComponent.generated.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,45 @@ struct FEffectStatePrediction uint8 State; }; +USTRUCT() +struct FGMASQueueOperationHandle +{ + GENERATED_BODY() + + UPROPERTY() + int32 Handle { -1 }; + + UPROPERTY() + int32 OperationId { -1 }; + + UPROPERTY() + int32 NetworkId { -1 }; +}; + +UENUM(BlueprintType) +enum class EGMCAbilityEffectQueueType : uint8 +{ + /// Immediately applied, only valid within the GMC movement cycle. Should be applied on both client and server. + Predicted UMETA(DisplayName="Predicted"), + + /// Predicted effect, not replicated but will be queued for addition in the next GMC movement cycle. Valid even + /// outside of the GMC movement cycle. Should be applied on both client and server. If used during the GMC + /// movement cycle, this is silently turned into Predicted. + PredictedQueued UMETA(DisplayName="Predicted [Queued]"), + + /// Only valid on server; queued from server and sent to client via RPC. Valid even outside of the GMC movement cycle. + ServerAuth UMETA(DisplayName="Server Auth"), + + /// Only valid on client; queued from client and sent to the server via GMC bindings. Valid even outside of the + /// GMC movement cycle. You almost certainly don't want to use this, but it's here for the sake of completeness. + ClientAuth UMETA(Hidden, DisplayName="Client Auth"), + + /// Only valid on server; queued from server and recorded in the GMC move history. Valid even outside of the GMC + /// movement cycle. Slower than ServerAuth, only use this if you really need to preserve the effect application in + /// the movement history. you almost certainly don't want to use this, but it's here for the sake of completeness. + ServerAuthMove UMETA(Hidden, DisplayName="ADVANCED: Server Auth [Movement Cycle]") +}; + class UGMCAbility; UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent, DisplayName="GMC Ability System Component"), meta=(Categories="GMAS")) @@ -78,7 +115,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") @@ -221,38 +258,95 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo void CheckUnBoundAttributeChanges(); + 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); + /** - * Applies an effect to the Ability Component + * Applies an effect to the Ability Component. If bOuterActivation is false, the effect will be immediately + * applied; if either is true, the operation will be queued but no valid effect will be returned. If + * Outer Activation is true, the effect *must* be applied on the server. * * @param Effect Effect to apply - * @param AdditionalModifiers Additional Modifiers to apply with this effect application - * @param SourceAbilityComponent Ability Component from which this effect originated - * @param bOverwriteExistingModifiers Whether or not to replace existing modifiers that have the same name as additional modifiers. If false, will add them. - * @param bAppliedByServer Is this Effect only applied by server? Used to help client predict the unpredictable. + * @param InitializationData Effect initialization data. + * @param bOuterActivation Whether this effect should be replicated outside of GMC, via normal Unreal RPC */ - UFUNCTION(BlueprintCallable, Category="GMAS|Effects", meta = (AutoCreateRefTerm = "AdditionalModifiers")) + UFUNCTION(BlueprintCallable, Category="GMAS|Effects", DisplayName="Apply Ability Effect (Legacy)", meta=(DeprecatedFunction, DeprecationMessage="Please use the more modern ApplyAbilityEffect which takes a queue type.")) UGMCAbilityEffect* ApplyAbilityEffect(TSubclassOf Effect, FGMCAbilityEffectData InitializationData, bool bOuterActivation = false); + + // BP-specific version of + /** + * Applies an effect to the ability component. If the Queue Type is Predicted, the effect will be immediately added + * on both client and server; this must happen within the GMC movement lifecycle for it to be valid. If the + * Queue Type is anything else, the effect must be queued on the server and will be replicated to the client. + */ + UFUNCTION(BlueprintCallable, Category="GMAS|Effects", DisplayName="Apply Ability Effect") + void ApplyAbilityEffectSafe(TSubclassOf EffectClass, FGMCAbilityEffectData InitializationData, EGMCAbilityEffectQueueType QueueType, + UPARAM(DisplayName="Success") bool& OutSuccess, UPARAM(DisplayName="Effect Handle") int& OutEffectHandle, UPARAM(DisplayName="Effect Network ID") int& OutEffectId, UPARAM(DisplayName="Effect Instance") UGMCAbilityEffect*& OutEffect); + + /** + * Applies an effect to the ability component. If the Queue Type is Predicted, the effect will be immediately added + * on both client and server; this must happen within the GMC movement lifecycle for it to be valid. If the + * Queue Type is anything else, the effect must be queued on the server and will be replicated to the client. + * + * @param EffectClass The class of ability effect to add. + * @param InitializationData The initialization data for the ability effect. + * @param QueueType How to queue the effect. + * @param OutEffectHandle A local handle to this effect, only valid locally. + * @param OutEffectId The newly-created effect's network ID, if one is available. Valid across server/client. + * @param OutEffect The newly-created effect instance, if available. + * @return true if the effect was applied, false otherwise. + */ + bool ApplyAbilityEffect(TSubclassOf EffectClass, FGMCAbilityEffectData InitializationData, EGMCAbilityEffectQueueType QueueType, int& OutEffectHandle, int& OutEffectId, UGMCAbilityEffect*& OutEffect); + + // Do not call this directly unless you know what you are doing. Otherwise, always go through the above ApplyAbilityEffect variant! UGMCAbilityEffect* ApplyAbilityEffect(UGMCAbilityEffect* Effect, FGMCAbilityEffectData InitializationData); - + UFUNCTION(BlueprintCallable, Category="GMAS|Effects") + UGMCAbilityEffect* GetEffectById(const int EffectId) const; + + 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); + + 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. * Returns -1 if tag is invalid. @@ -366,6 +460,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 @@ -440,12 +537,21 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo // Add the starting ability tags to GrantedAbilities at start void InitializeStartingAbilities(); - TArray QueuedAbilities; TArray QueuedTaskData; - // Current Ability Data being processed - // Members of this struct are bound over GMC - // FGMCAbilityData AbilityData; + // Queued ability operations (activate, cancel, etc.) + TGMASBoundQueue QueuedAbilityOperations; + bool ProcessAbilityOperation(const TGMASBoundQueueOperation& Operation, bool bFromMovementTick); + + TGMASBoundQueue QueuedEffectOperations; + TGMASBoundQueue QueuedEffectOperations_ClientAuth; + 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 FGMASBoundQueueRPCHeader& Header); // Predictions of Effect state changes FEffectStatePrediction EffectStatePrediction{}; @@ -461,8 +567,7 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo UPROPERTY() TMap ActiveCooldowns; - - + int GenerateAbilityID() const {return ActionTimer * 100;} // Set Attributes to either a default object or a provided TSubClassOf in BP defaults @@ -513,32 +618,28 @@ 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; + TMap EffectHandles; - UPROPERTY() - TArray PendingApplicationClient; + 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; - // 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 ServerHandlePredictedPendingEffect(float DeltaTime); void ClientHandlePendingEffect(); - - int GenerateLateApplicationID(); + void ClientHandlePredictedPendingEffect(); int LateApplicationIDCounter = 0; diff --git a/Source/GMCAbilitySystem/Public/Components/GMCAbilityOuterApplication.h b/Source/GMCAbilitySystem/Public/Components/GMCAbilityOuterApplication.h deleted file mode 100644 index 85bffac5..00000000 --- a/Source/GMCAbilitySystem/Public/Components/GMCAbilityOuterApplication.h +++ /dev/null @@ -1,86 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "InstancedStruct.h" -#include "GMCAbilityOuterApplication.generated.h" - -USTRUCT() -struct FGMCAcknowledgeId { - GENERATED_BODY() - - UPROPERTY() - TArray Id = {}; -}; - - -UENUM() -enum EGMCOuterApplicationType { - EGMC_AddEffect, - EGMC_RemoveEffect, -}; - -USTRUCT(BlueprintType) -struct FGMCOuterEffectAdd { - GENERATED_BODY() - - UPROPERTY() - TSubclassOf EffectClass; - - UPROPERTY() - FGMCAbilityEffectData InitializationData; -}; - -USTRUCT(BlueprintType) -struct FGMCOuterEffectRemove { - GENERATED_BODY() - - UPROPERTY() - TArray Ids = {}; -}; - -USTRUCT(BlueprintType) -struct FGMCOuterApplicationWrapper { - GENERATED_BODY() - - UPROPERTY() - TEnumAsByte Type = EGMC_AddEffect; - - UPROPERTY() - FInstancedStruct OuterApplicationData; - - UPROPERTY() - int LateApplicationID = 0; - - float ClientGraceTimeRemaining = 0.f; - - template - static FGMCOuterApplicationWrapper Make(Args... args) - { - FGMCOuterApplicationWrapper Wrapper; - Wrapper.OuterApplicationData = FInstancedStruct::Make(args...); - return Wrapper; - } - - -}; - -template<> inline FGMCOuterApplicationWrapper FGMCOuterApplicationWrapper::Make(TSubclassOf Effect, FGMCAbilityEffectData InitializationData) -{ - FGMCOuterApplicationWrapper Wrapper; - Wrapper.Type = EGMC_AddEffect; - Wrapper.OuterApplicationData = FInstancedStruct::Make(); - FGMCOuterEffectAdd& Data = Wrapper.OuterApplicationData.GetMutable(); - Data.EffectClass = Effect; - Data.InitializationData = InitializationData; - return Wrapper; -} - -template<> inline FGMCOuterApplicationWrapper FGMCOuterApplicationWrapper::Make(TArray Ids) -{ - FGMCOuterApplicationWrapper Wrapper; - Wrapper.Type = EGMC_RemoveEffect; - Wrapper.OuterApplicationData = FInstancedStruct::Make(); - FGMCOuterEffectRemove& Data = Wrapper.OuterApplicationData.GetMutable(); - Data.Ids = Ids; - return Wrapper; -} diff --git a/Source/GMCAbilitySystem/Public/Effects/GMCAbilityEffect.h b/Source/GMCAbilitySystem/Public/Effects/GMCAbilityEffect.h index d631ac30..340ee8d9 100644 --- a/Source/GMCAbilitySystem/Public/Effects/GMCAbilityEffect.h +++ b/Source/GMCAbilitySystem/Public/Effects/GMCAbilityEffect.h @@ -108,6 +108,11 @@ struct FGMCAbilityEffectData UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem") FGameplayTagContainer GrantedTags; + // Whether to preserve the granted tags if multiple instances of the same effect are applied + // If false, will remove all stacks of the tag + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "GMCAbilitySystem") + bool bPreserveGrantedTagsIfMultiple = false; + // Tags that the owner must have to apply this effect UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GMCAbilitySystem") FGameplayTagContainer ApplicationMustHaveTags; diff --git a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h new file mode 100644 index 00000000..fb91525a --- /dev/null +++ b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h @@ -0,0 +1,582 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "GMCMovementUtilityComponent.h" +#include "InstancedStruct.h" +#include "UObject/Object.h" +#include "UObject/ConstructorHelpers.h" +#include "GMASBoundQueue.generated.h" + +UENUM(BlueprintType) +enum class EGMASBoundQueueOperationType : uint8 +{ + None, + Add, + Remove, + Activate, + Cancel +}; + +USTRUCT(BlueprintType) +struct FGMASBoundQueueOperationIdSet +{ + GENERATED_BODY() + + UPROPERTY() + TArray Ids = {}; +}; + +USTRUCT(BlueprintType) +struct GMCABILITYSYSTEM_API FGMASBoundQueueRPCHeader +{ + GENERATED_BODY() + + UPROPERTY() + int32 OperationId { -1 }; + + UPROPERTY() + uint8 OperationTypeRaw { 0 }; + + EGMASBoundQueueOperationType GetOperationType() const + { + return static_cast(OperationTypeRaw); + } + + UPROPERTY() + FGameplayTag Tag { FGameplayTag::EmptyTag }; + + UPROPERTY() + FName ItemClassName { NAME_None }; + + UPROPERTY() + FGMASBoundQueueOperationIdSet PayloadIds {}; + + UPROPERTY() + FInstancedStruct InstancedPayload {}; + + UPROPERTY() + float RPCGracePeriodSeconds { 1.f }; + + UPROPERTY() + uint8 ExtraFlags { 0 }; +}; + +template +struct GMCABILITYSYSTEM_API TGMASBoundQueueOperation +{ + FGMASBoundQueueRPCHeader Header {}; + + // An actual class to be utilized with this, in case we need to instance it. + TSubclassOf ItemClass { nullptr }; + + FInstancedStruct InstancedPayloadIds; + + // The struct payload for this item. + T Payload; + + // The instanced struct representation of this payload, used to actually + // bind for replication. + FInstancedStruct InstancedPayload; + + EGMASBoundQueueOperationType GetOperationType() const + { + return Header.GetOperationType(); + } + + void SetOperationType(EGMASBoundQueueOperationType OperationType) + { + Header.OperationTypeRaw = static_cast(OperationType); + } + + int32 GetOperationId() const { return Header.OperationId; } + + TArray GetPayloadIds() const { return Header.PayloadIds.Ids; } + + FGameplayTag GetTag() const { return Header.Tag; } + + bool GracePeriodExpired() const + { + return Header.RPCGracePeriodSeconds <= 0.f; + } + + bool IsValid() const + { + return Header.OperationTypeRaw != 0 && (Header.ItemClassName != NAME_None || Header.Tag != FGameplayTag::EmptyTag || Header.PayloadIds.Ids.Num() > 0); + } + + void Refresh(bool bDecodePayload = false) + { + if (bDecodePayload) + { + // Incoming from remote. + Payload = Header.InstancedPayload.Get(); + if (Header.PayloadIds.Ids.Num() == 0 && InstancedPayloadIds.IsValid()) + { + Header.PayloadIds = InstancedPayloadIds.Get(); + } + } + else + { + // Outgoing + Header.InstancedPayload = FInstancedStruct::Make(Payload); + InstancedPayloadIds = FInstancedStruct::Make(Header.PayloadIds); + } + + RefreshClass(); + } + + void RefreshClass() + { + if (Header.ItemClassName != NAME_None && !ItemClass) + { + // Get a handle to our class, for instancing purposes. + TSoftClassPtr ClassPtr = TSoftClassPtr(FSoftObjectPath(Header.ItemClassName.ToString())); + ItemClass = ClassPtr.LoadSynchronous(); + } + + if (ItemClass && Header.ItemClassName == NAME_None) + { + Header.ItemClassName = FName(ItemClass->GetPathName()); + } + } + +}; + +USTRUCT(BlueprintType) +struct GMCABILITYSYSTEM_API FGMASBoundQueueAcknowledgement +{ + GENERATED_BODY() + + UPROPERTY() + int32 Id { -1 }; + + UPROPERTY() + float Lifetime { 5.f }; +}; + +USTRUCT(BlueprintType) +struct GMCABILITYSYSTEM_API FGMASBoundQueueAcknowledgements +{ + GENERATED_BODY() + + UPROPERTY() + TArray AckSet; +}; + +USTRUCT(BlueprintType) +struct GMCABILITYSYSTEM_API FGMASBoundQueueEmptyData +{ + GENERATED_BODY() +}; + +template +class GMCABILITYSYSTEM_API TGMASBoundQueue +{ + +public: + + TGMASBoundQueueOperation CurrentOperation; + + int BI_ActionTimer { -1 }; + int BI_Acknowledgements { -1 }; + int BI_OperationId { -1 }; + int BI_OperationType { -1 }; + int BI_OperationTag { -1 }; + int BI_OperationClass { -1 }; + int BI_OperationExtraFlags { -1 }; + int BI_OperationPayload { -1 }; + int BI_OperationPayloadIds { -1 }; + + TArray> QueuedBoundOperations; + TArray> QueuedRPCOperations; + + FInstancedStruct Acknowledgments; + + double ActionTimer { 0 }; + + void BindToGMC(UGMC_MovementUtilityCmp* MovementComponent) + { + const EGMC_PredictionMode Prediction = ClientAuth ? EGMC_PredictionMode::ClientAuth_Input : EGMC_PredictionMode::ServerAuth_Input_ClientValidated; + const EGMC_PredictionMode AckPrediction = ClientAuth ? EGMC_PredictionMode::ServerAuth_Output_ClientValidated : EGMC_PredictionMode::ClientAuth_Input; + + Acknowledgments = FInstancedStruct::Make(FGMASBoundQueueAcknowledgements()); + + // Our queue's action timer is always server-auth. + BI_ActionTimer = MovementComponent->BindDoublePrecisionFloat( + ActionTimer, + EGMC_PredictionMode::ServerAuth_Output_ClientValidated, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::None, + EGMC_InterpolationFunction::TargetValue + ); + + if (!ClientAuth) + { + // Acknowledgements are bound client-auth for server-auth effects. + BI_Acknowledgements = MovementComponent->BindInstancedStruct( + Acknowledgments, + AckPrediction, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::None, + EGMC_InterpolationFunction::TargetValue); + } + else + { + // For client-auth stuff, we bind the individual pieces of the queue. + // We will probably not use this often (if ever), but it exists just-in-case. + + BI_OperationId = MovementComponent->BindInt( + CurrentOperation.Header.OperationId, + Prediction, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::Periodic_Output, + EGMC_InterpolationFunction::TargetValue); + + BI_OperationType = MovementComponent->BindByte( + CurrentOperation.Header.OperationTypeRaw, + Prediction, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::Periodic_Output, + EGMC_InterpolationFunction::TargetValue); + + BI_OperationTag = MovementComponent->BindGameplayTag( + CurrentOperation.Header.Tag, + Prediction, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::Periodic_Output, + EGMC_InterpolationFunction::TargetValue); + + BI_OperationClass = MovementComponent->BindName( + CurrentOperation.Header.ItemClassName, + Prediction, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::Periodic_Output, + EGMC_InterpolationFunction::TargetValue); + + BI_OperationPayload = MovementComponent->BindInstancedStruct( + CurrentOperation.Header.InstancedPayload, + Prediction, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::Periodic_Output, + EGMC_InterpolationFunction::TargetValue); + + BI_OperationExtraFlags = MovementComponent->BindByte( + CurrentOperation.Header.ExtraFlags, + Prediction, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::Periodic_Output, + EGMC_InterpolationFunction::TargetValue); + + BI_OperationPayloadIds = MovementComponent->BindInstancedStruct( + CurrentOperation.InstancedPayloadIds, + Prediction, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::Periodic_Output, + EGMC_InterpolationFunction::TargetValue); + } + + CurrentOperation = TGMASBoundQueueOperation(); + CurrentOperation.Header = FGMASBoundQueueRPCHeader(); + CurrentOperation.Refresh(false); + } + + void PreLocalMovement() + { + if (QueuedBoundOperations.Num() > 0 && ClientAuth) + { + CurrentOperation = QueuedBoundOperations.Pop(); + CurrentOperation.Refresh(false); + } + } + + void PreRemoteMovement() + { + ClearCurrentOperation(); + if (QueuedBoundOperations.Num() > 0 && !ClientAuth) + { + CurrentOperation = QueuedBoundOperations.Pop(); + CurrentOperation.Refresh(false); + } + } + + int32 GenerateOperationId() const + { + int32 NewOperationId = ActionTimer * 100; + + TGMASBoundQueueOperation Operation; + + while (GetOperationById(NewOperationId, Operation)) + { + NewOperationId++; + } + return NewOperationId; + } + + void ClearCurrentOperation() + { + CurrentOperation.Header = FGMASBoundQueueRPCHeader(); + + // CurrentOperation.Header.OperationId = -1; + // CurrentOperation.Header.Tag = FGameplayTag::EmptyTag; + // CurrentOperation.Header.ItemClassName = NAME_None; + // CurrentOperation.SetOperationType(EGMASBoundQueueOperationType::None); + // CurrentOperation.Header.PayloadIds.Ids.Empty(); + + CurrentOperation.Payload = T(); + CurrentOperation.Refresh(false); + } + + void GenPredictionTick(float DeltaTime) + { + ActionTimer += DeltaTime; + ExpireStaleAcks(DeltaTime); + } + + int32 MakeOperation(TGMASBoundQueueOperation& NewOperation, EGMASBoundQueueOperationType Type, FGameplayTag Tag, const T& Payload, TArray PayloadIds = {}, TSubclassOf ItemClass = nullptr, float RPCGracePeriod = 1.f, uint8 ExtraFlags = 0) + { + NewOperation.Header.OperationId = GenerateOperationId(); + NewOperation.SetOperationType(Type); + NewOperation.Header.Tag = Tag; + NewOperation.Payload = Payload; + NewOperation.ItemClass = ItemClass; + NewOperation.Header.RPCGracePeriodSeconds = RPCGracePeriod; + NewOperation.Header.PayloadIds.Ids = PayloadIds; + NewOperation.Header.ExtraFlags = ExtraFlags; + + NewOperation.Refresh(false); + + return NewOperation.GetOperationId(); + } + + int32 MakeOperation(TGMASBoundQueueOperation& NewOperation, const FGMASBoundQueueRPCHeader& Header, const T& Payload) + { + NewOperation.Header = Header; + NewOperation.Payload = Payload; + NewOperation.Refresh(); + NewOperation.InstancedPayload = FInstancedStruct::Make(Payload); + NewOperation.InstancedPayloadIds = FInstancedStruct::Make(NewOperation.Header.PayloadIds); + + return NewOperation.GetOperationId(); + } + + void QueuePreparedOperation(TGMASBoundQueueOperation& NewOperation, bool bMovementSynced = true) + { + TGMASBoundQueueOperation TestOperation = TGMASBoundQueueOperation(); + + // Don't bother queueing it if it already exists. + if (GetOperationById(NewOperation.GetOperationId(), TestOperation)) return; + + if (bMovementSynced) + { + // This needs to be handled via GMC, so add it to our queue. + QueuedBoundOperations.Push(NewOperation); + } + else + { + QueuedRPCOperations.Push(NewOperation); + } + } + + int32 QueueOperation(TGMASBoundQueueOperation& NewOperation, EGMASBoundQueueOperationType Type, FGameplayTag Tag, const T& Payload, TArray PayloadIds = {}, TSubclassOf ItemClass = nullptr, bool bMovementSynced = true, float RPCGracePeriod = 1.f) + { + MakeOperation(NewOperation, Type, Tag, Payload, PayloadIds, ItemClass, RPCGracePeriod); + QueuePreparedOperation(NewOperation, bMovementSynced); + return NewOperation.GetOperationId(); + } + + void QueueOperationFromHeader(const FGMASBoundQueueRPCHeader& Header, bool bMovementSynced) + { + TGMASBoundQueueOperation NewOperation; + + NewOperation.Header = Header; + NewOperation.Refresh(true); + QueuePreparedOperation(NewOperation, bMovementSynced); + } + + int Num() const + { + return QueuedBoundOperations.Num(); + } + + int NumMatching(FGameplayTag Tag, EGMASBoundQueueOperationType Type = EGMASBoundQueueOperationType::None) const + { + int Result = 0; + for (const auto& Operation : QueuedBoundOperations) + { + if (Operation.GetTag() == Tag) + { + if (Type == EGMASBoundQueueOperationType::None || Operation.GetOperationType() == Type) Result++; + } + } + for (const auto& Operation : QueuedRPCOperations) + { + if (Operation.GetTag() == Tag) + { + if (Type == EGMASBoundQueueOperationType::None || Operation.GetOperationType() == Type) Result++; + } + } + return Result; + } + + const TArray>& GetQueuedOperations() const { return QueuedBoundOperations; } + + const TArray>& GetQueuedRPCOperations() const { return QueuedRPCOperations; } + + bool GetOperationById(int32 OperationId, TGMASBoundQueueOperation& OutOperation) const + { + for (const auto& Operation : GetQueuedOperations()) + { + if (Operation.GetOperationId() == OperationId) + { + OutOperation = Operation; + return true; + } + } + + for (const auto& Operation : GetQueuedRPCOperations()) + { + if (Operation.GetOperationId() == OperationId) + { + OutOperation = Operation; + return true; + } + } + + return false; + } + + bool HasOperationWithPayloadId(int32 PayloadId) const + { + for (const auto& Operation : QueuedBoundOperations) + { + if (Operation.GetPayloadIds().Contains(PayloadId)) + { + return true; + } + } + + for (const auto& Operation : QueuedRPCOperations) + { + if (Operation.GetPayloadIds().Contains(PayloadId)) + { + return true; + } + } + + return false; + } + + bool RemoveOperationById(int32 OperationId) + { + int TargetIdx = -1; + + for (int Idx = 0; Idx < QueuedRPCOperations.Num() && TargetIdx == -1; Idx++) + { + if(QueuedRPCOperations[Idx].GetOperationId() == OperationId) + { + TargetIdx = Idx; + } + } + + if (TargetIdx != -1) + { + 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, bool bRefresh = false) + { + Operation = CurrentOperation; + if (Operation.GetOperationType() != EGMASBoundQueueOperationType::None) + { + if (bRefresh) + { + Operation.Refresh(true); + } + else + { + Operation.RefreshClass(); + } + return true; + } + + return false; + } + + bool PopNextRPCOperation(TGMASBoundQueueOperation& Operation) + { + if (QueuedRPCOperations.Num() == 0) return false; + + Operation = QueuedRPCOperations.Pop(); + return true; + } + + void DeductGracePeriod(float DeltaTime) + { + for (auto& Operation : QueuedRPCOperations) + { + Operation.Header.RPCGracePeriodSeconds -= DeltaTime; + } + } + + void Acknowledge(int32 OperationId, float AckLifetime = 5.f) + { + if (!IsAcknowledged(OperationId)) + { + FGMASBoundQueueAcknowledgement NewAck; + NewAck.Id = OperationId; + NewAck.Lifetime = AckLifetime; + + auto& Acks = Acknowledgments.GetMutable(); + Acks.AckSet.Add(NewAck); + } + } + + bool IsAcknowledged(int32 OperationId) const + { + const auto& Acks = Acknowledgments.Get(); + for (const auto& Ack : Acks.AckSet) + { + if (Ack.Id == OperationId) return true; + } + return false; + } + + void ExpireStaleAcks(float DeltaTime) + { + // Deduct from our ack lifetime; if we've gone stale, remove the stale acks to avoid it just growing forever. + TArray FreshAcks; + auto& Acks = Acknowledgments.GetMutable(); + for (auto& Ack : Acks.AckSet) + { + Ack.Lifetime -= DeltaTime; + if (Ack.Lifetime > 0.f) + { + FreshAcks.Add(Ack); + } + } + Acks.AckSet = FreshAcks; + } + +};