diff --git a/GMCAbilitySystem.uplugin b/GMCAbilitySystem.uplugin index c6b7528a..4264c312 100644 --- a/GMCAbilitySystem.uplugin +++ b/GMCAbilitySystem.uplugin @@ -34,6 +34,10 @@ { "Name": "StructUtils", "Enabled": true + }, + { + "Name": "Niagara", + "Enabled": true } ] } \ No newline at end of file diff --git a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp index c05a7e47..b158f6d7 100644 --- a/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp +++ b/Source/GMCAbilitySystem/Private/Components/GMCAbilityComponent.cpp @@ -119,23 +119,39 @@ void UGMC_AbilitySystemComponent::BindReplicationData() EGMC_InterpolationFunction::TargetValue); // Bind our operation queues. - QueuedAbilityOperations.BindToGMC(GMCMovementComponent); - QueuedEffectOperations.BindToGMC(GMCMovementComponent); - QueuedEffectOperations_ClientAuth.BindToGMC(GMCMovementComponent); - QueuedEventOperations.BindToGMC(GMCMovementComponent); + // QueuedAbilityOperations.BindToGMC(GMCMovementComponent); + // QueuedEffectOperations.BindToGMC(GMCMovementComponent); + // QueuedEffectOperations_ClientAuth.BindToGMC(GMCMovementComponent); + // QueuedEventOperations.BindToGMC(GMCMovementComponent); + BoundQueueV2.BindToGMC(GMCMovementComponent); } void UGMC_AbilitySystemComponent::GenAncillaryTick(float DeltaTime, bool bIsCombinedClientMove) { + TRACE_CPUPROFILER_EVENT_SCOPE(UGMC_AbilitySystemComponent::GenAncillaryTick) + // Caution if you override Ancillarytick, this value should wrap up the override. bInAncillaryTick = true; OnAncillaryTick.Broadcast(DeltaTime); - ClientHandlePendingOperation(QueuedEffectOperations); - ServerHandlePendingEffect(DeltaTime); - - ClientHandlePendingOperation(QueuedEventOperations); + if (HasAuthority()) + { + // Server processes client output payloads + if (GMCMovementComponent->IsPlayerControlledPawn()) + { + const FGMC_PawnState OutputState = GMCMovementComponent->SV_GetLastClientData().OutputState; + const FInstancedStruct ClientPayloadOperationData = GMCMovementComponent->GetBoundInstancedStruct(BoundQueueV2.BI_OperationData, OutputState); + ServerProcessOperation(ClientPayloadOperationData, false); + } + // Server owned pawns + BoundQueueV2.GenPreLocalMoveExecution(); + ProcessOperation(BoundQueueV2.OperationData, false); + } + else + { + ProcessOperation(BoundQueueV2.OperationData, false); + } CheckActiveTagsChanged(); @@ -144,21 +160,14 @@ void UGMC_AbilitySystemComponent::GenAncillaryTick(float DeltaTime, bool bIsComb CheckAttributeChanged(); CheckUnBoundAttributeChanged(); - TickActiveCooldowns(DeltaTime); + BoundQueueV2.GenAncillaryTick(DeltaTime); + SendTaskDataToActiveAbility(false); TickAncillaryActiveAbilities(DeltaTime); - - // Check if we have a valid operation - TGMASBoundQueueOperation Operation; - if (QueuedAbilityOperations.GetCurrentBoundOperation(Operation, true)) - { - ProcessAbilityOperation(Operation, false); - } - ClearAbilityAndTaskData(); - QueuedEffectOperations_ClientAuth.ClearCurrentOperation(); + ClearAbilityAndTaskData(); bInAncillaryTick = false; } @@ -301,16 +310,41 @@ TArray UGMC_AbilitySystemComponent::GetActiveTagsByParentTag(const return MatchedTags; } -void UGMC_AbilitySystemComponent::TryActivateAbilitiesByInputTag(const FGameplayTag& InputTag, const UInputAction* InputAction, bool bFromMovementTick) +bool UGMC_AbilitySystemComponent::TryActivateAbilitiesByInputTag(const FGameplayTag& InputTag, const UInputAction* InputAction, + const bool bFromMovementTick, const bool bForce) { - for (const TSubclassOf& ActivatedAbility : GetGrantedAbilitiesByTag(InputTag)) + + auto GrantedAbilities = GetGrantedAbilitiesByTag(InputTag); + if (GrantedAbilities.Num() == 0) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("No Abilities Granted for InputTag: %s"), *InputTag.ToString()); + return false; + } + bool bFirstAbilityActivatesDuringMovementTick = GrantedAbilities[0]->GetDefaultObject()->bActivateOnMovementTick; + + if (!bForce && bFirstAbilityActivatesDuringMovementTick != bFromMovementTick) + { + // If the first ability doesn't match the bFromMovementTick state, we can't activate any abilities + return false; + } + + // If any abilities don't match the first ability's state, we can't activate them (no mixing movement and ancillary abilities) + for (const TSubclassOf& ActivatedAbility : GrantedAbilities) { const UGMCAbility* AbilityCDO = ActivatedAbility->GetDefaultObject(); - if(AbilityCDO && bFromMovementTick == AbilityCDO->bActivateOnMovementTick){ - UE_LOG(LogGMCAbilitySystem, VeryVerbose, TEXT("Trying to Activate Ability: %s from %s"), *GetNameSafe(ActivatedAbility), bFromMovementTick ? TEXT("Movement") : TEXT("Ancillary")); - TryActivateAbility(ActivatedAbility, InputAction, InputTag); + if (AbilityCDO && AbilityCDO->bActivateOnMovementTick != bFirstAbilityActivatesDuringMovementTick) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Trying to Activate Ability: %s from %s, but it doesn't match bFromMovementTick state of other abilities"), *GetNameSafe(ActivatedAbility), bFromMovementTick ? TEXT("Movement") : TEXT("Ancillary")); + return false; } } + + for (const TSubclassOf& ActivatedAbility : GrantedAbilities) + { + TryActivateAbility(ActivatedAbility, InputAction, InputTag); + } + + return true; } bool UGMC_AbilitySystemComponent::TryActivateAbility(const TSubclassOf ActivatedAbility, const UInputAction* InputAction, const FGameplayTag ActivationTag) @@ -363,34 +397,26 @@ void UGMC_AbilitySystemComponent::QueueAbility(FGameplayTag InputTag, const UInp { if (GetOwnerRole() != ROLE_AutonomousProxy && GetOwnerRole() != ROLE_Authority) return; - FGMCAbilityData Data; - Data.InputTag = InputTag; - Data.ActionInput = InputAction; - - - // Early local check for ability - FAbilityMapData* MapEntry = AbilityMap.Find(InputTag); - if (MapEntry == nullptr || MapEntry->Abilities.IsEmpty()) return; + FGMASBoundQueueV2AbilityActivationOperation ActivationData; + ActivationData.InputTag = InputTag; + ActivationData.InputAction = InputAction; + const int OperationID = BoundQueueV2.MakeOperationData(ActivationData); - // This local check will prevent concurrent activation of the same ability if the ability is contain in the input map - if (bPreventConcurrentActivation) { - for (const TPair& ActiveAbility : ActiveAbilities) - { - if (ActiveAbility.Value && MapEntry->Abilities.ContainsByPredicate([&ActiveAbility](const TSubclassOf& Ability) { - return ActiveAbility.Value->IsA(Ability); - })) return; - } + if (!HasAuthority()) + { + BoundQueueV2.QueueClientOperation(OperationID); + } + else + { + BoundQueueV2.QueueServerOperation(OperationID); } - - - TGMASBoundQueueOperation Operation; - QueuedAbilityOperations.QueueOperation(Operation, EGMASBoundQueueOperationType::Activate, InputTag, Data); } int32 UGMC_AbilitySystemComponent::GetQueuedAbilityCount(FGameplayTag AbilityTag) { - return QueuedAbilityOperations.NumMatching(AbilityTag, EGMASBoundQueueOperationType::Activate); + return 0; + // return QueuedAbilityOperations.NumMatching(AbilityTag, EGMASBoundQueueOperationType::Activate); } int32 UGMC_AbilitySystemComponent::GetActiveAbilityCount(TSubclassOf AbilityClass) @@ -552,9 +578,24 @@ bool UGMC_AbilitySystemComponent::IsServerOnly() const void UGMC_AbilitySystemComponent::GenPredictionTick(float DeltaTime) { + TRACE_CPUPROFILER_EVENT_SCOPE(UGMC_AbilitySystemComponent::GenPredictionTick) + bJustTeleported = false; - // ActionTimer += DeltaTime; ActionTimer = GMCMovementComponent->GetMoveTimestamp(); + + + if (HasAuthority() && GMCMovementComponent->IsPlayerControlledPawn()) + { + // Server processes client output payloads + const FGMC_PawnState OutputState = GMCMovementComponent->SV_GetLastClientData().OutputState; + const FInstancedStruct ClientPayloadOperationData = GMCMovementComponent->GetBoundInstancedStruct(BoundQueueV2.BI_OperationData, OutputState); + ServerProcessOperation(ClientPayloadOperationData, true); + } + else + { + ProcessOperation(BoundQueueV2.OperationData, true); + } + ApplyStartingEffects(); @@ -576,27 +617,11 @@ void UGMC_AbilitySystemComponent::GenPredictionTick(float DeltaTime) // Abilities CleanupStaleAbilities(); - - // Advance our queue action timers. - QueuedAbilityOperations.GenPredictionTick(DeltaTime); - QueuedEffectOperations.GenPredictionTick(DeltaTime); - QueuedEventOperations.GenPredictionTick(DeltaTime); - - // Was an ability used? - if (TGMASBoundQueueOperation Operation; - QueuedAbilityOperations.GetCurrentBoundOperation(Operation, true)) - { - ProcessAbilityOperation(Operation, true); - } - - ServerHandlePredictedPendingEffect(DeltaTime); - - } void UGMC_AbilitySystemComponent::GenSimulationTick(float DeltaTime) { - + TRACE_CPUPROFILER_EVENT_SCOPE(UGMC_AbilitySystemComponent::GenSimulationTick) if (!GMCMovementComponent->IsSmoothedListenServerPawn()) { CheckActiveTagsChanged(); @@ -620,24 +645,32 @@ void UGMC_AbilitySystemComponent::PreLocalMoveExecution() { TaskData = QueuedTaskData.Pop(); } + BoundQueueV2.GenPreLocalMoveExecution(); +} - // Advance our client-auth queues. - QueuedAbilityOperations.PreLocalMovement(); - QueuedEffectOperations_ClientAuth.PreLocalMovement(); +void UGMC_AbilitySystemComponent::RPCOnServerOperationAdded_Implementation(const int OperationID, const FInstancedStruct Operation) +{ + UE_LOG(LogTemp, Warning, TEXT("RPCOnServerOperationAdded: %d"), OperationID); + BoundQueueV2.CacheOperationPayload(OperationID, Operation); + BoundQueueV2.ClientQueuedOperations.Add(OperationID); +} - if (GetNetMode() == NM_Standalone || GMCMovementComponent->IsLocallyControlledServerPawn()) +void UGMC_AbilitySystemComponent::BoundQueueV2Debug(TSubclassOf Effect) +{ + if (HasAuthority()) { - // We'll never get the pre remote movement trigger in this case, soooo... - QueuedEffectOperations.PreRemoteMovement(); - QueuedEventOperations.PreRemoteMovement(); + FGMASBoundQueueV2ApplyEffectOperation EffectActivationData; + EffectActivationData.EffectClass = Effect; + EffectActivationData.EffectID = GetNextAvailableEffectID(); + int OperationID = BoundQueueV2.MakeOperationData(EffectActivationData); + BoundQueueV2.QueueServerOperation(OperationID); } -} +} -void UGMC_AbilitySystemComponent::PreRemoteMoveExecution() +void UGMC_AbilitySystemComponent::OnServerOperationForced(FInstancedStruct OperationData) { - // Advance our server-auth queues. - QueuedEffectOperations.PreRemoteMovement(); - QueuedEventOperations.PreRemoteMovement(); + UE_LOG(LogTemp, Warning, TEXT("Forcing Operation On Server")); + ProcessOperation(OperationData, false, true); } void UGMC_AbilitySystemComponent::BeginPlay() @@ -647,6 +680,10 @@ void UGMC_AbilitySystemComponent::BeginPlay() InitializeStartingAbilities(); InitializeAbilityMap(); SetStartingTags(); + + // Bind to Queue Events + BoundQueueV2.OnServerOperationAdded.AddDynamic(this, &UGMC_AbilitySystemComponent::RPCOnServerOperationAdded); + BoundQueueV2.OnServerOperationForced.AddDynamic(this, &UGMC_AbilitySystemComponent::OnServerOperationForced); } void UGMC_AbilitySystemComponent::InstantiateAttributes() @@ -834,7 +871,7 @@ void UGMC_AbilitySystemComponent::TickActiveEffects(float DeltaTime) if (HasAuthority()) {RPCClientEndEffect(EffectID);} ActiveEffects.Remove(EffectID); - ActiveEffectsData.RemoveAll([EffectID](const FGMCAbilityEffectData& EffectData) {return EffectData.EffectID == EffectID;}); + ActiveEffectIDs.Remove(EffectID); } // Clean effect handles @@ -857,7 +894,6 @@ void UGMC_AbilitySystemComponent::ProcessAttributes(bool bInGenPredictionTick) { if (Attribute->IsDirty() && Attribute->bIsGMCBound == bInGenPredictionTick) { - Attribute->CalculateValue(); // Broadcast dirty change if unbound if (!Attribute->bIsGMCBound) @@ -897,26 +933,6 @@ void UGMC_AbilitySystemComponent::TickActiveCooldowns(float DeltaTime) } } -void UGMC_AbilitySystemComponent::OnRep_ActiveEffectsData() -{ - for (FGMCAbilityEffectData ActiveEffectData : ActiveEffectsData) - { - if (ActiveEffectData.EffectID == 0) continue; - - if (!ProcessedEffectIDs.Contains(ActiveEffectData.EffectID) || ProcessedEffectIDs[ActiveEffectData.EffectID] == EGMCEffectAnswerState::Timeout) - { - // The client never predicted this effect, so we process it as a new effect. - UGMCAbilityEffect* EffectCDO = DuplicateObject(UGMCAbilityEffect::StaticClass()->GetDefaultObject(), this); - - ApplyAbilityEffect(EffectCDO, ActiveEffectData); - ProcessedEffectIDs.Add(ActiveEffectData.EffectID, EGMCEffectAnswerState::Validated); - UE_LOG(LogGMCAbilitySystem, Warning, TEXT("[Client] Effect [%d] %s has been force apply by the server"), ActiveEffectData.EffectID, *ActiveEffectData.EffectTag.ToString()); - } - - ProcessedEffectIDs[ActiveEffectData.EffectID] = EGMCEffectAnswerState::Validated; - } -} - void UGMC_AbilitySystemComponent::CheckRemovedEffects() { for (TPair Effect : ActiveEffects) @@ -928,98 +944,10 @@ void UGMC_AbilitySystemComponent::CheckRemovedEffects() // it means the server removed it if (ProcessedEffectIDs[Effect.Key] == EGMCEffectAnswerState::Pending){return;} - if (!ActiveEffectsData.ContainsByPredicate([Effect](const FGMCAbilityEffectData& EffectData) {return EffectData.EffectID == Effect.Key;})) - { - RemoveActiveAbilityEffect(Effect.Value); - } - } -} - -void UGMC_AbilitySystemComponent::ServerHandlePendingEffect(float DeltaTime) { - if (!HasAuthority()) { - return; - } - - // 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); - - // And send it via RPC, so that the client gets it. - ClientQueueOperation(BoundOperation); - }; - - // Handle our 'outer' RPC effect operations. - QueuedEffectOperations.DeductGracePeriod(DeltaTime); - auto Operations = QueuedEffectOperations.GetQueuedRPCOperations(); - for (auto& Operation : Operations) { - if (ShouldProcessOperation(Operation, QueuedEffectOperations, true)) - { - if (Operation.GracePeriodExpired()) - { - UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Client effect operation missed grace period, forcing on server.")) - } - ProcessOperation(Operation); - 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)) - { - ProcessOperation(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)) - { - ProcessOperation(BoundOperation); - } - -} - -template -void UGMC_AbilitySystemComponent::ClientHandlePendingOperation(TGMASBoundQueue& QueuedOperations) { - -// Handle our RPC effect operations. MoveCycle operations will be sent via RPC - // just like the Outer ones, but will be preserved in the movement history. - auto RPCOperations = QueuedOperations.GetQueuedRPCOperations(); - for (auto& Operation : RPCOperations) { - if (QueuedOperations.IsAcknowledged(Operation.GetOperationId())) - { - ProcessOperation(Operation); - QueuedOperations.RemoveOperationById(Operation.GetOperationId()); - } - if (ShouldProcessOperation(Operation, QueuedOperations, false)) - { - QueuedOperations.Acknowledge(Operation.GetOperationId()); - } - } -} - -void UGMC_AbilitySystemComponent::ClientHandlePredictedPendingEffect() -{ - TGMASBoundQueueOperation BoundOperation; - if (QueuedEffectOperations_ClientAuth.GetCurrentBoundOperation(BoundOperation)) - { - ProcessOperation(BoundOperation); - } - - // We use the client auth effect queue's (otherwise-unused) RPC operations queue to avoid creating an entire new one. - while (QueuedEffectOperations_ClientAuth.PopNextRPCOperation(BoundOperation)) - { - ProcessOperation(BoundOperation); + // if (!ActiveEffectsData.ContainsByPredicate([Effect](const FGMCAbilityEffectData& EffectData) {return EffectData.EffectID == Effect.Key;})) + // { + // RemoveActiveAbilityEffect(Effect.Value); + // } } } @@ -1106,7 +1034,7 @@ TArray> UGMC_AbilitySystemComponent::GetGrantedAbilitie void UGMC_AbilitySystemComponent::ClearAbilityAndTaskData() { AbilityData = FGMCAbilityData{}; - QueuedAbilityOperations.ClearCurrentOperation(); + // QueuedAbilityOperations.ClearCurrentOperation(); TaskData = FInstancedStruct::Make(FGMCAbilityTaskData{}); } @@ -1233,240 +1161,246 @@ void UGMC_AbilitySystemComponent::InitializeStartingAbilities() } } -bool UGMC_AbilitySystemComponent::ProcessAbilityOperation( - const TGMASBoundQueueOperation& Operation, bool bFromMovementTick) +void UGMC_AbilitySystemComponent::ExecuteSyncedEvent(FGMASSyncedEventContainer EventData) { - EGMASBoundQueueOperationType OperationType = Operation.GetOperationType(); - if (OperationType == EGMASBoundQueueOperationType::Activate) + if (!HasAuthority()) { - TryActivateAbilitiesByInputTag(Operation.GetTag(), Operation.Payload.ActionInput, bFromMovementTick); - return true; + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Client attempted to create a SyncedEvent")); + return; } - if (OperationType == EGMASBoundQueueOperationType::Cancel) + if (EventData.EventTag == FGameplayTag::EmptyTag) { - EndAbilitiesByTag(Operation.GetTag()); - if (Operation.ItemClass) - { - EndAbilitiesByClass(Operation.ItemClass); - } - return true; + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Attempted to create a SyncedEvent with an empty tag")); + return; } + + EventData.EventType = EGMASSyncedEventType::BlueprintImplemented; - UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Received ability operation with invalid operation type %s for %s!"), - *UEnum::GetValueAsString(OperationType), *Operation.GetTag().ToString()) - return false; + // TGMASBoundQueueOperation Operation; + // if (CreateSyncedEventOperation(Operation, EventData) == -1 ) + // { + // UE_LOG(LogGMCAbilitySystem, Error, TEXT("Failed to create SyncedEvent")); + // return; + // } + + // ClientQueueOperation(Operation); } -UGMCAbilityEffect* UGMC_AbilitySystemComponent::ProcessOperation( - const TGMASBoundQueueOperation& Operation) +bool UGMC_AbilitySystemComponent::ProcessOperation(FInstancedStruct OperationData, bool bFromMovementTick, bool bForce) { - EGMASBoundQueueOperationType OperationType = Operation.GetOperationType(); + if (!BoundQueueV2.IsValidGMASOperation(OperationData)) return false; - if (OperationType == EGMASBoundQueueOperationType::Add) + const FGMASBoundQueueV2OperationBaseData* BaseData = OperationData.GetPtr(); + const int OperationID = BaseData->OperationID; + + if (OperationID == 0) { - 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; + return false; // Empty/Default Operation, Ignore + } + + // Payload data should be in the cache + // Possible if it isn't with abilities being double processed anc/movement tick until that's checked later + if (!BoundQueueV2.HasPayloadByID(OperationID)) + { + // UE_LOG(LogGMCAbilitySystem, Error, TEXT("OperationID %d not found in OperationPayloads"), OperationID); + return false; + } - if (!EffectData.IsValid()) - { - EffectData = Effect->EffectData; - } - - if (Operation.Header.PayloadIds.Ids.Num() > 0) - { - EffectData.EffectID = Operation.Header.PayloadIds.Ids[0]; - } + // Pull actual payload from operation cache + FInstancedStruct PayloadData = BoundQueueV2.GetPayloadByID(OperationID); - 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); + // Server only ever processes operations once so it doesn't need them cached + if (HasAuthority()) + { + BoundQueueV2.RemovePayloadByID(OperationID); + } + + const UScriptStruct* StructType = PayloadData.GetScriptStruct(); - 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; + // Activate Ability + if (StructType == FGMASBoundQueueV2AbilityActivationOperation::StaticStruct()) + { + const FGMASBoundQueueV2AbilityActivationOperation Data = PayloadData.Get(); + BoundQueueV2.OperationData = PayloadData; + return TryActivateAbilitiesByInputTag(Data.InputTag, Data.InputAction, bFromMovementTick, bForce); + } + + // Everything below happens only during the Prediction tick + if (!bFromMovementTick && !bForce){ + // BoundQueueV2.OperationData = FInstancedStruct::Make(FGMASBoundQueueV2OperationBaseData{}); + return false; } - if (OperationType == EGMASBoundQueueOperationType::Remove) + ///////////// Server-Auth Events + // Todo: Every event is setting the same AcknowledgedOperation. Could probably pull that out of each event and just set it once at the end. + // + // Apply Effect + if (StructType == FGMASBoundQueueV2ApplyEffectOperation::StaticStruct()) { - for (auto& [Id, Effect]: ActiveEffects) + const FGMASBoundQueueV2ApplyEffectOperation Data = PayloadData.Get(); + ProcessEffectApplicationFromOperation(Data); + if (!HasAuthority()) { - if (Operation.GetPayloadIds().Contains(Id)) - { - RemoveActiveAbilityEffect(Effect); - } + // Make an operation to confirm the effect application + BoundQueueV2.OperationData = FInstancedStruct::Make(FGMASBoundQueueV2AcknowledgeOperation{OperationID}); } - return nullptr; + return true; } - UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Received ability operation with invalid operation type %s for %s!"), - *UEnum::GetValueAsString(OperationType), *Operation.ItemClass->GetName()) - return nullptr; -} - -void UGMC_AbilitySystemComponent::ExecuteSyncedEvent(FGMASSyncedEventContainer EventData) -{ - if (!HasAuthority()) + // Remove Effect + if (StructType == FGMASBoundQueueV2RemoveEffectOperation::StaticStruct()) { - UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Client attempted to create a SyncedEvent")); - return; + const FGMASBoundQueueV2RemoveEffectOperation Data = PayloadData.Get(); + RemoveEffectByIdSafe(Data.EffectIDs, EGMCAbilityEffectQueueType::Predicted); + + if (!HasAuthority()) + { + // Make an operation to confirm the effect application + BoundQueueV2.OperationData = FInstancedStruct::Make(FGMASBoundQueueV2AcknowledgeOperation{OperationID}); + } + + return true; } - if (EventData.EventTag == FGameplayTag::EmptyTag) + // Add Impulse + if (StructType == FGMASBoundQueueV2AddImpulseOperation::StaticStruct()) { - UE_LOG(LogGMCAbilitySystem, Error, TEXT("Attempted to create a SyncedEvent with an empty tag")); - return; + const FGMASBoundQueueV2AddImpulseOperation KBData = PayloadData.Get(); + GMCMovementComponent->AddImpulse(KBData.Impulse, KBData.bVelocityChange); + if (!HasAuthority()) + { + // Make an operation to confirm the impulse application + BoundQueueV2.OperationData = FInstancedStruct::Make(FGMASBoundQueueV2AcknowledgeOperation{OperationID}); + } + return true; } - EventData.EventType = EGMASSyncedEventType::BlueprintImplemented; - - TGMASBoundQueueOperation Operation; - if (CreateSyncedEventOperation(Operation, EventData) == -1 ) + // Set Actor Location + if (StructType == FGMASBoundQueueV2SetActorLocationOperation::StaticStruct()) { - UE_LOG(LogGMCAbilitySystem, Error, TEXT("Failed to create SyncedEvent")); - return; + const FGMASBoundQueueV2SetActorLocationOperation LocationData = PayloadData.Get(); + GetOwner()->SetActorLocation(LocationData.Location); + if (!HasAuthority()) + { + // Make an operation to confirm the location change + BoundQueueV2.OperationData = FInstancedStruct::Make(FGMASBoundQueueV2AcknowledgeOperation{OperationID}); + } + return true; } - - QueuedEventOperations.QueuePreparedOperation(Operation, false); - ClientQueueOperation(Operation); + + // No valid event found, so nothing to ack. Send a blank. + BoundQueueV2.OperationData = FInstancedStruct::Make(FGMASBoundQueueV2OperationBaseData{}); + return false; } -void UGMC_AbilitySystemComponent::ProcessOperation(const TGMASBoundQueueOperation& Operation) +void UGMC_AbilitySystemComponent::ProcessEffectApplicationFromOperation(const FGMASBoundQueueV2ApplyEffectOperation& Data) { - FGMASSyncedEventContainer EventDataContainer = Operation.Payload; - - switch (EventDataContainer.EventType) + if (Data.EffectClass) { - case EGMASSyncedEventType::BlueprintImplemented: + UGMCAbilityEffect* Effect; + int OutEffectHandle; + int OutEffectId; + + if (Data.EffectData != FGMCAbilityEffectData{}) { - OnSyncedEvent.Broadcast(EventDataContainer); - break; + ApplyAbilityEffect(Data.EffectClass, Data.EffectData, EGMCAbilityEffectQueueType::Predicted, OutEffectHandle, OutEffectId, Effect); } - case EGMASSyncedEventType::AddImpulse: + else { - AddImpulseEvent(EventDataContainer); - break; + // Otherwise, we can apply the default effect data + FGMCAbilityEffectData DefaultData = Data.EffectClass->GetDefaultObject()->EffectData; + DefaultData.EffectID = Data.EffectID; // Need to slam the effect ID in there + ApplyAbilityEffect(Data.EffectClass,DefaultData, EGMCAbilityEffectQueueType::Predicted, OutEffectHandle, OutEffectId, Effect); + } + + // Auto validate the effect since this was added via a server operation + if (!HasAuthority() && Effect != nullptr) + { + ProcessedEffectIDs[Effect->EffectData.EffectID] = EGMCEffectAnswerState::Validated; + UE_LOG(LogGMCAbilitySystem, VeryVerbose, TEXT("Applied Effect: %s"), *GetNameSafe(Data.EffectClass)); } - case EGMASSyncedEventType::PlayMontage: - { - break; - } - default: - break; } - } -template -bool UGMC_AbilitySystemComponent::IsOperationValid(const TGMASBoundQueueOperation& Operation) const +void UGMC_AbilitySystemComponent::ServerProcessOperation(const FInstancedStruct& OperationData, bool bFromMovementTick) { - if (!Operation.IsValid()) + if (!HasAuthority()) return; + if (!BoundQueueV2.IsValidGMASOperation(OperationData)) return; + + const FGMASBoundQueueV2OperationBaseData* BaseData = OperationData.GetPtr(); + const int OperationID = BaseData->OperationID; + + // Empty Operation, Ignore + if (OperationID == 0) return; + + // Security check + // These are operations the client has requested, ie Activate Ability + if (BoundQueueV2.IsValidClientOperation(OperationData)) { - if (Operation.Header.OperationId != -1) + // A server-auth operation is ack'd, should have everything needed to go process it + if (OperationData.GetScriptStruct() == FGMASBoundQueueV2AcknowledgeOperation::StaticStruct()) { - UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%s] %s received invalid queued operation %d!"), - *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), Operation.Header.OperationId) - return false; + ServerProcessAcknowledgedOperation(BaseData->OperationID, bFromMovementTick); + return; } - return false; + + if (!BoundQueueV2.HasPayloadByID(OperationID)) + { + BoundQueueV2.CacheOperationPayload(OperationID, OperationData); + } + + ProcessOperation(OperationData, bFromMovementTick); } - return true; } -template -bool UGMC_AbilitySystemComponent::ShouldProcessOperation( - const TGMASBoundQueueOperation& Operation, TGMASBoundQueue& QueuedOperations, bool bIsServer) const +void UGMC_AbilitySystemComponent::ServerProcessAcknowledgedOperation(int OperationID, bool bFromMovementTick) { - if (!IsOperationValid(Operation)) - { - return false; - } + // Everything else should be server built operations that the client has confirmed + // Ie, applied server-auth effects or server-auth events - if (bIsServer) + if (!BoundQueueV2.HasPayloadByID(OperationID) || !BoundQueueV2.ServerQueuedBoundOperationsGracePeriods.Contains(OperationID)) { - return HasAuthority() && (QueuedOperations.IsAcknowledged(Operation.GetOperationId()) || - Operation.GracePeriodExpired() || GetNetMode() == NM_Standalone); + return; } - else + + + FInstancedStruct PayloadData = BoundQueueV2.GetPayloadByID(OperationID); + + if (ProcessOperation(PayloadData, bFromMovementTick)) { - return !QueuedOperations.IsAcknowledged(Operation.GetOperationId()) && (GMCMovementComponent->IsLocallyControlledServerPawn() || GMCMovementComponent->IsAutonomousProxy()); + BoundQueueV2.ServerAcknowledgeOperation(OperationID); } } + void UGMC_AbilitySystemComponent::AddImpulse(FVector Impulse, bool bVelChange) { if (!HasAuthority()) { - UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Client attempted to apply knock back effect")); + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Client attempted to apply server-auth event")); return; } - - TGMASBoundQueueOperation Operation; - FGMASSyncedEventContainer EventData; + // + FGMASBoundQueueV2AddImpulseOperation ImpulseOperation; + ImpulseOperation.Impulse = Impulse; + ImpulseOperation.bVelocityChange = bVelChange; + const int OperationID = BoundQueueV2.MakeOperationData(ImpulseOperation); + BoundQueueV2.QueueServerOperation(OperationID); +} - FGMASSyncedEventData_AddImpulse ImpulseData; - ImpulseData.Impulse = Impulse; - ImpulseData.bVelocityChange = bVelChange; - - EventData.EventType = EGMASSyncedEventType::AddImpulse; - EventData.InstancedPayload = FInstancedStruct::Make(ImpulseData); - - if (CreateSyncedEventOperation(Operation, EventData) == -1 ) +void UGMC_AbilitySystemComponent::SetActorLocation(FVector Location) +{ + if (!HasAuthority()) { - UE_LOG(LogGMCAbilitySystem, Error, TEXT("Failed to create knock back effect")); + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("Client attempted to apply server-auth event")); return; } - QueuedEventOperations.QueuePreparedOperation(Operation, false); - ClientQueueOperation(Operation); - -} - -void UGMC_AbilitySystemComponent::AddImpulseEvent(const FGMASSyncedEventContainer& EventData) const -{ - const FGMASSyncedEventData_AddImpulse KBData = EventData.InstancedPayload.Get(); - GMCMovementComponent->AddImpulse(KBData.Impulse, KBData.bVelocityChange); -} - -void UGMC_AbilitySystemComponent::ClientQueueOperation( - const TGMASBoundQueueOperation& Operation) -{ - RPCClientQueueEffectOperation(Operation.Header); -} - -void UGMC_AbilitySystemComponent::ClientQueueOperation( - const TGMASBoundQueueOperation& Operation) -{ - RPCClientQueueEventOperation(Operation.Header); -} - -void UGMC_AbilitySystemComponent::RPCClientQueueEventOperation_Implementation(const FGMASBoundQueueRPCHeader& Header) -{ - QueuedEventOperations.QueueOperationFromHeader(Header, false); -} - -void UGMC_AbilitySystemComponent::RPCClientQueueEffectOperation_Implementation(const FGMASBoundQueueRPCHeader& Header) -{ - QueuedEffectOperations.QueueOperationFromHeader(Header, false); + FGMASBoundQueueV2SetActorLocationOperation ImpulseOperation; + ImpulseOperation.Location = Location; + const int OperationID = BoundQueueV2.MakeOperationData(ImpulseOperation); + BoundQueueV2.QueueServerOperation(OperationID); } void UGMC_AbilitySystemComponent::OnRep_UnBoundAttributes() @@ -1510,7 +1444,7 @@ int UGMC_AbilitySystemComponent::GetNextAvailableEffectID() const } int NewEffectID = static_cast(ActionTimer * 100); - while (ActiveEffects.Contains(NewEffectID) || CheckIfEffectIDQueued(NewEffectID)) + while (ActiveEffects.Contains(NewEffectID) || ReservedEffectIDs.Contains(NewEffectID)) { NewEffectID++; } @@ -1519,126 +1453,6 @@ 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, - const FGMCAbilityEffectData& EffectData, - bool bForcedEffectId, - EGMCAbilityEffectQueueType QueueType) -{ - TArray PayloadIds {}; - - FGMCAbilityEffectData PayloadData; - if (EffectData.IsValid()) - { - PayloadData = EffectData; - } - else - { - PayloadData = EffectClass->GetDefaultObject()->EffectData; - } - - if (QueueType == EGMCAbilityEffectQueueType::ServerAuth) - { - PayloadData.bServerAuth = true; - } - - - if (bForcedEffectId) - { - if (PayloadData.EffectID == 0) - { - PayloadData.EffectID = GetNextAvailableEffectID(); - } - PayloadIds.Add(PayloadData.EffectID); - } - - if (QueueType == EGMCAbilityEffectQueueType::PredictedQueued || QueueType == EGMCAbilityEffectQueueType::ClientAuth) - { - QueuedEffectOperations_ClientAuth.MakeOperation(OutOperation, EGMASBoundQueueOperationType::Add, PayloadData.EffectTag, PayloadData, PayloadIds, EffectClass, 1.f, static_cast(QueueType)); - } - else - { - QueuedEffectOperations.MakeOperation(OutOperation, EGMASBoundQueueOperationType::Add, PayloadData.EffectTag, PayloadData, PayloadIds, EffectClass, 1.f, static_cast(QueueType)); - } - return PayloadData.EffectID; -} - -int UGMC_AbilitySystemComponent::CreateSyncedEventOperation( - TGMASBoundQueueOperation& OutOperation, - const FGMASSyncedEventContainer& EventData) -{ - if (!HasAuthority()) - { - UE_LOG(LogGMCAbilitySystem, Error, TEXT("Client attempted to create a synced event operation")); - return -1; - } - - TArray PayloadIds {}; - int ID = GetNextAvailableEffectID(); - PayloadIds.Add(ID); - QueuedEventOperations.MakeOperation(OutOperation, EGMASBoundQueueOperationType::Add, FGameplayTag::EmptyTag, EventData, PayloadIds, nullptr, 1.f, static_cast(EGMCAbilityEffectQueueType::ServerAuth)); - - return ID; -} - -//BP Version -UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf Effect, FGMCAbilityEffectData InitializationData, bool bOuterActivation) -{ - if (Effect == nullptr) - { - UE_LOG(LogGMCAbilitySystem, Error, TEXT("Trying to apply Effect, but effect is null!")); - return nullptr; - } - - TGMASBoundQueueOperation 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()) { - QueuedEffectOperations.QueuePreparedOperation(Operation, false); - ClientQueueOperation(Operation); - } - return nullptr; - } - - if (!GMCMovementComponent->IsExecutingMove() && GetNetMode() != NM_Standalone) - { - // For backwards compatibility, we do not reject this if we're outside a movement cycle. However, we will at least - // log it. - UE_LOG(LogGMCAbilitySystem, Warning, TEXT("[%20s] %s tried to apply a predicted effect of type %s outside a movement cycle!"), - *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), *Effect->GetName()) - } - return ProcessOperation(Operation); -} - int32 UGMC_AbilitySystemComponent::GetNextAvailableEffectHandle() const { if (ActionTimer == 0) @@ -1699,10 +1513,18 @@ void UGMC_AbilitySystemComponent::ApplyAbilityEffectSafe(TSubclassOfGetDefaultObject()->EffectData; + } + else + { + EffectData = InitializationData; + } + OutSuccess = ApplyAbilityEffect(EffectClass, EffectData, QueueType, OutEffectHandle, OutEffectId, OutEffect); if (OutSuccess && HandlingAbility) { @@ -1726,7 +1548,11 @@ UGMCAbilityEffect* UGMC_AbilitySystemComponent::ApplyAbilityEffectShort(TSubclas bool UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf EffectClass, - FGMCAbilityEffectData InitializationData, EGMCAbilityEffectQueueType QueueType, int& OutEffectHandle, int& OutEffectId, UGMCAbilityEffect*& OutEffect) + FGMCAbilityEffectData InitializationData, + EGMCAbilityEffectQueueType QueueType, + int& OutEffectHandle, + int& OutEffectId, + UGMCAbilityEffect*& OutEffect) { OutEffect = nullptr; OutEffectId = -1; @@ -1736,97 +1562,47 @@ bool UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOf 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); switch(QueueType) { case EGMCAbilityEffectQueueType::Predicted: { - if (!GMCMovementComponent->IsExecutingMove() && GetNetMode() != NM_Standalone && !bInAncillaryTick) - { - UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted to apply predicted effect %d of type %s outside of a GMC move!"), - *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), EffectID, *EffectClass->GetName()) - return false; - } - // Apply effect immediately. - OutEffect = ProcessOperation(Operation); + UGMCAbilityEffect* Effect = DuplicateObject(EffectClass->GetDefaultObject(), this); + OutEffect = ApplyAbilityEffect(Effect, InitializationData); OutEffectId = OutEffect->EffectData.EffectID; - OutEffectHandle = HandleData.Handle; + // OutEffectHandle = HandleData.Handle; return true; } case EGMCAbilityEffectQueueType::PredictedQueued: - { - if (GMCMovementComponent->IsExecutingMove() || bInAncillaryTick) - { - // We're in a move context, just add it directly rather than queuing. - OutEffect = ProcessOperation(Operation); - OutEffectId = OutEffect->EffectData.EffectID; - } - else - { - // We utilize the ClientAuth queue's RPC queue for the sake of convenience. - QueuedEffectOperations_ClientAuth.QueuePreparedOperation(Operation, false); - } - OutEffectHandle = HandleData.Handle; - return true; - } + return false; case EGMCAbilityEffectQueueType::ServerAuthMove: case EGMCAbilityEffectQueueType::ServerAuth: { + // Client does not apply effects in these queues, only the server does if (!HasAuthority()) { - UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted to apply server-queued effect %d of type %s on a client!"), - *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), EffectID, *EffectClass->GetName()) return false; } - if (QueueType == EGMCAbilityEffectQueueType::ServerAuthMove) Operation.Header.RPCGracePeriodSeconds = Operation.Payload.ClientGraceTime;; - - QueuedEffectOperations.QueuePreparedOperation(Operation, QueueType == EGMCAbilityEffectQueueType::ServerAuthMove); - - if (QueueType == EGMCAbilityEffectQueueType::ServerAuth) - { - // Queue for RPC and throw this to our client. - ClientQueueOperation(Operation); - } - - OutEffectId = EffectID; - OutEffectHandle = HandleData.Handle; + + FGMASBoundQueueV2ApplyEffectOperation EffectActivationData; + EffectActivationData.EffectClass = EffectClass; + EffectActivationData.EffectData = InitializationData; + EffectActivationData.EffectID = GetNextAvailableEffectID(); + ReservedEffectIDs.Add(EffectActivationData.EffectID); + + // We return back the Operation ID instead of the Effect ID which isn't great + OutEffectId =EffectActivationData.EffectID; + int OperationID = BoundQueueV2.MakeOperationData(EffectActivationData); + OutEffectHandle = OutEffectId; + BoundQueueV2.QueueServerOperation(OperationID); 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; - } + 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!"), @@ -1834,6 +1610,28 @@ bool UGMC_AbilitySystemComponent::ApplyAbilityEffect(TSubclassOfGetDefaultObject()->EffectData; + } + + + UGMCAbilityEffect* Effect = DuplicateObject(Operation.EffectClass->GetDefaultObject(), this); + Effect = ApplyAbilityEffect(Effect, EffectData); + FString isExecutingMove = GMCMovementComponent->IsExecutingMove() ? TEXT("True") : TEXT("False"); + UE_LOG(LogTemp, Warning, TEXT("Applied Effect With Action Timer: %f | IsPredTick: %s | IsServer: %s"), + ActionTimer, *isExecutingMove, HasAuthority() ? TEXT("True") : TEXT("False")); + + return Effect; +} + UGMCAbilityEffect* UGMC_AbilitySystemComponent::GetEffectById(const int EffectId) const { if (!ActiveEffects.Contains(EffectId)) return nullptr; @@ -1870,11 +1668,10 @@ FString UGMC_AbilitySystemComponent::GetEffectsNameAsString(const TArrayEffectData); + // If this was a server-auth, the ID is already generated and needs to be cleaned up from reserved + ReservedEffectIDs.Remove(Effect->EffectData.EffectID); + ActiveEffectIDs.Push(Effect->EffectData.EffectID); } else { @@ -2077,72 +1876,22 @@ bool UGMC_AbilitySystemComponent::RemoveEffectByIdSafe(TArray Ids, EGMCAbil return true; } case EGMCAbilityEffectQueueType::PredictedQueued: - { - // If in move, silenttly remove the effect as predicted - if (GMCMovementComponent->IsExecutingMove() || bInAncillaryTick) - { - TArray EffectsToRemove; - for (int Id : Ids) { - if (ActiveEffects.Contains(Id)) { - EffectsToRemove.Add(ActiveEffects[Id]); - } - } - - for (auto Effect : EffectsToRemove) { - RemoveActiveAbilityEffect(Effect); - } - - } - else { - TGMASBoundQueueOperation Operation; - FGMCAbilityEffectData Data; - QueuedEffectOperations_ClientAuth.MakeOperation(Operation, EGMASBoundQueueOperationType::Remove, FGameplayTag::EmptyTag, Data, Ids); - QueuedEffectOperations_ClientAuth.QueuePreparedOperation(Operation, false); - } - return true; - } + return false; case EGMCAbilityEffectQueueType::ClientAuth: - { - if (QueueType == EGMCAbilityEffectQueueType::ClientAuth) - { - if (GetNetMode() != NM_Standalone && (HasAuthority() && !GMCMovementComponent->IsLocallyControlledServerPawn())) - { - ensureMsgf(false, TEXT("[%20s] %s attempted a client-auth removal of %d effects on a server! (%s)"), - *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), Ids.Num(), *GetEffectsNameAsString(GetEffectsByIds(Ids))); - UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted a client-auth removal of %d effects on a server! (%s)"), - *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), Ids.Num(), *GetEffectsNameAsString(GetEffectsByIds(Ids))) - return false; - } - } - - TGMASBoundQueueOperation Operation; - FGMCAbilityEffectData Data; - QueuedEffectOperations_ClientAuth.MakeOperation(Operation, EGMASBoundQueueOperationType::Remove, FGameplayTag::EmptyTag, Data, Ids); - QueuedEffectOperations_ClientAuth.QueuePreparedOperation(Operation, QueueType == EGMCAbilityEffectQueueType::ClientAuth); - return true; - } + return false; + case EGMCAbilityEffectQueueType::ServerAuthMove: case EGMCAbilityEffectQueueType::ServerAuth: { if (!HasAuthority()) { - ensureMsgf(false, TEXT("[%20s] %s attempted a server-auth removal of %d effects on a client! (%s)"), - *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), Ids.Num(), *GetEffectsNameAsString(GetEffectsByIds(Ids))); - UE_LOG(LogGMCAbilitySystem, Error, TEXT("[%20s] %s attempted a server-auth removal of %d effects on a client! (%s)"), - *GetNetRoleAsString(GetOwnerRole()), *GetOwner()->GetName(), Ids.Num(), *GetEffectsNameAsString(GetEffectsByIds(Ids))) return false; } - TGMASBoundQueueOperation Operation; - FGMCAbilityEffectData Data; - QueuedEffectOperations.MakeOperation(Operation, EGMASBoundQueueOperationType::Remove, FGameplayTag::EmptyTag, Data, Ids); - QueuedEffectOperations.QueuePreparedOperation(Operation, QueueType == EGMCAbilityEffectQueueType::ServerAuthMove); - - if (QueueType == EGMCAbilityEffectQueueType::ServerAuth) - { - // Send the operation over to our client via standard RPC. - ClientQueueOperation(Operation); - } + FGMASBoundQueueV2RemoveEffectOperation EffectRemovalData; + EffectRemovalData.EffectIDs = Ids; + const int OperationID = BoundQueueV2.MakeOperationData(EffectRemovalData); + BoundQueueV2.QueueServerOperation(OperationID); return true; } } @@ -2289,11 +2038,12 @@ FString UGMC_AbilitySystemComponent::GetAllAttributesString() const{ } FString UGMC_AbilitySystemComponent::GetActiveEffectsDataString() const{ - FString FinalString = FString::Printf(TEXT("%d total\n"), ActiveEffectsData.Num()); - for(const FGMCAbilityEffectData& ActiveEffectData : ActiveEffectsData){ - FinalString += ActiveEffectData.ToString() + TEXT("\n"); - } - return FinalString; + // FString FinalString = FString::Printf(TEXT("%d total\n"), ActiveEffectsData.Num()); + // for(const FGMCAbilityEffectData& ActiveEffectData : ActiveEffectsData){ + // FinalString += ActiveEffectData.ToString() + TEXT("\n"); + // } + // return FinalString; + return "UNDER CONSTRUCTION"; } FString UGMC_AbilitySystemComponent::GetActiveEffectsString() const{ @@ -2458,12 +2208,25 @@ void UGMC_AbilitySystemComponent::MC_SpawnSound_Implementation(USoundBase* Sound SpawnSound(Sound, Location, VolumeMultiplier, PitchMultiplier, bIsClientPredicted); } +//ActiveEffectIds OnRep +void UGMC_AbilitySystemComponent::OnRep_ActiveEffectIDs() +{ + // This is called when the ActiveEffectIDs array is replicated to the client + // We need to update the ActiveEffects map based on the replicated IDs + for (int EffectId : ActiveEffectIDs) + { + if (ProcessedEffectIDs.Contains(EffectId)) + { + ProcessedEffectIDs[EffectId] = EGMCEffectAnswerState::Validated; + } + } +} + // ReplicatedProps void UGMC_AbilitySystemComponent::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); - DOREPLIFETIME_CONDITION(UGMC_AbilitySystemComponent, ActiveEffectsData, COND_OwnerOnly); DOREPLIFETIME(UGMC_AbilitySystemComponent, UnBoundAttributes); + DOREPLIFETIME(UGMC_AbilitySystemComponent, ActiveEffectIDs); } - diff --git a/Source/GMCAbilitySystem/Private/Debug/GameplayDebuggerCategory_GMCAbilitySystem.cpp b/Source/GMCAbilitySystem/Private/Debug/GameplayDebuggerCategory_GMCAbilitySystem.cpp index aae0ddf2..1d17e75d 100644 --- a/Source/GMCAbilitySystem/Private/Debug/GameplayDebuggerCategory_GMCAbilitySystem.cpp +++ b/Source/GMCAbilitySystem/Private/Debug/GameplayDebuggerCategory_GMCAbilitySystem.cpp @@ -31,9 +31,10 @@ void FGameplayDebuggerCategory_GMCAbilitySystem::CollectData(APlayerController* DataPack.ActiveEffects = AbilityComponent->GetActiveEffectsString(); DataPack.NBActiveEffects = AbilityComponent->GetActiveEffects().Num(); DataPack.ActiveEffectData = AbilityComponent->GetActiveEffectsDataString(); - DataPack.NBActiveEffectData = AbilityComponent->ActiveEffectsData.Num(); + DataPack.NBActiveEffectData = AbilityComponent->ActiveEffectIDs.Num(); DataPack.ActiveAbilities = AbilityComponent->GetActiveAbilitiesString(); DataPack.NBActiveAbilities = AbilityComponent->GetActiveAbilities().Num(); + DataPack.NBCachedOperationPayloads = AbilityComponent->BoundQueueV2.GetPayloadCount(); AbilityComponent->GMCMovementComponent->SV_SwapServerState(); } @@ -55,7 +56,7 @@ void FGameplayDebuggerCategory_GMCAbilitySystem::DrawData(APlayerController* Own // Abilities CanvasContext.Printf(TEXT("{blue}[server] {yellow}Granted Abilities (%d): {white}%s%s"), DataPack.NBGrantedAbilities, *DataPack.GrantedAbilities.Left(MaxCharDisplayAbilities), DataPack.GrantedAbilities.Len() > MaxCharDisplayAbilities ? TEXT("...") : TEXT("")); // Show client-side data - if (AbilityComponent) + if (AbilityComponent) // Todo: Stop having every dang thing check for AbilityComponent being null { if (DataPack.NBGrantedAbilities != AbilityComponent->GetGrantedAbilities().Num()) { @@ -113,9 +114,9 @@ void FGameplayDebuggerCategory_GMCAbilitySystem::DrawData(APlayerController* Own if (AbilityComponent) { if (DataPack.NBActiveEffects != AbilityComponent->GetActiveEffects().Num()) - CanvasContext.Printf(TEXT("{green}[client] {yellow}Active Effects: {red} [INCOHERENCY] {white}%s"), *AbilityComponent->GetActiveEffectsString()); + CanvasContext.Printf(TEXT("{green}[client] {yellow}Active Effects: {red} [INCOHERENCY] {white}%s\n"), *AbilityComponent->GetActiveEffectsString()); else - CanvasContext.Printf(TEXT("{green}[client] {yellow}Active Effects: {white}%s"), *AbilityComponent->GetActiveEffectsString()); + CanvasContext.Printf(TEXT("{green}[client] {yellow}Active Effects: {white}%s\n"), *AbilityComponent->GetActiveEffectsString()); } // Active Effects Data @@ -123,10 +124,21 @@ void FGameplayDebuggerCategory_GMCAbilitySystem::DrawData(APlayerController* Own // Show client-side data if (AbilityComponent) { - if (DataPack.NBActiveEffectData != AbilityComponent->ActiveEffectsData.Num()) - CanvasContext.Printf(TEXT("{green}[client] {yellow}Active Effects Data: {red} [INCOHERENCY] {white}%s"), *AbilityComponent->GetActiveEffectsDataString()); + if (DataPack.NBActiveEffectData != AbilityComponent->ActiveEffectIDs.Num()) + CanvasContext.Printf(TEXT("{green}[client] {yellow}Active Effects Data: {red} [INCOHERENCY] {white}%s\n"), *AbilityComponent->GetActiveEffectsDataString()); + else + CanvasContext.Printf(TEXT("{green}[client] {yellow}Active Effects Data: {white}%s\n"), *AbilityComponent->GetActiveEffectsDataString()); + } + + // Cached Operations Data + CanvasContext.Printf(TEXT("{blue}[server] {yellow}Cached Operations: {white}%d"), DataPack.NBCachedOperationPayloads); + // Show client-side data + if (AbilityComponent) + { + if (DataPack.NBActiveEffectData != AbilityComponent->ActiveEffectIDs.Num()) + CanvasContext.Printf(TEXT("{green}[client] {yellow}Cached Operations: {red} [INCOHERENCY] {white}%d\n"), AbilityComponent->BoundQueueV2.GetPayloadCount()); else - CanvasContext.Printf(TEXT("{green}[client] {yellow}Active Effects Data: {white}%s"), *AbilityComponent->GetActiveEffectsDataString()); + CanvasContext.Printf(TEXT("{green}[client] {yellow}Cached Operations: {white}%d\n"), AbilityComponent->BoundQueueV2.GetPayloadCount()); } } @@ -152,6 +164,7 @@ void FGameplayDebuggerCategory_GMCAbilitySystem::FRepData::Serialize(FArchive& A Ar << NBActiveEffects; Ar << NBActiveEffectData; Ar << NBActiveAbilities; + Ar << NBCachedOperationPayloads; } #endif \ No newline at end of file diff --git a/Source/GMCAbilitySystem/Private/Utility/GMASBoundQueue.cpp b/Source/GMCAbilitySystem/Private/Utility/GMASBoundQueue.cpp deleted file mode 100644 index 1a3be8b2..00000000 --- a/Source/GMCAbilitySystem/Private/Utility/GMASBoundQueue.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "Utility/GMASBoundQueue.h" diff --git a/Source/GMCAbilitySystem/Private/Utility/GMASBoundQueueV2.cpp b/Source/GMCAbilitySystem/Private/Utility/GMASBoundQueueV2.cpp new file mode 100644 index 00000000..6c71b8c3 --- /dev/null +++ b/Source/GMCAbilitySystem/Private/Utility/GMASBoundQueueV2.cpp @@ -0,0 +1,196 @@ +#include "Utility/GMASBoundQueueV2.h" + +#include + +#include "GMCAbilitySystem.h" +#include "GMCMovementUtilityComponent.h" + + +bool FGMASBoundQueueV2::IsValidGMASOperation(const FInstancedStruct& Data) const +{ + // Check if the operation is a valid type + if (!OperationData.IsValid()) return false; + + const FGMASBoundQueueV2OperationBaseData* BaseData = OperationData.GetPtr(); + if (!BaseData) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("OperationData is not a valid type")); + return false; + } + + return true; +} + +bool FGMASBoundQueueV2::IsValidClientOperation(const FInstancedStruct& Data) const +{ + if (Data.IsValid()) + { + const UScriptStruct* StructType = Data.GetScriptStruct(); + if (ValidClientInputOperationTypes.Contains(StructType)) + { + return true; + } + UE_LOG(LogGMCAbilitySystem, Error, TEXT("Client operation type %s is not allowed by server as client input"), *StructType->GetName()); + } + return false; +} + +void FGMASBoundQueueV2::ClearStaleOperationData() +{ + const int MaximumFreshMoveIndex = GMCMoveCounter - GMCMovementComponent->MoveHistoryMaxSize; + for (auto It = OperationDataCacheExpiration.CreateIterator(); It; ++It) + { + const int64 MoveAddedAt = It->ModeAddedAt; + if (MoveAddedAt < MaximumFreshMoveIndex) + { + const int OperationID = It->OperationID; + if (!OperationPayloads.Contains(OperationID)) + { + UE_LOG(LogGMCAbilitySystem, Warning, TEXT("OperationID %d not found in OperationPayloads, but still in cache expiration map"), OperationID); + continue; + } + // Remove stale operation data + OperationPayloads.Remove(OperationID); + It.RemoveCurrent(); + } + } +} + +void FGMASBoundQueueV2::BindToGMC(UGMC_MovementUtilityCmp* MovementComponent) +{ + OperationData = FInstancedStruct::Make(); + + BI_OperationData = MovementComponent->BindInstancedStruct( + OperationData, + EGMC_PredictionMode::ClientAuth_InputOutput, + EGMC_CombineMode::CombineIfUnchanged, + EGMC_SimulationMode::None, + EGMC_InterpolationFunction::TargetValue); + + GMCMovementComponent = MovementComponent; +} + +void FGMASBoundQueueV2::GenPreLocalMoveExecution() +{ + // UE_LOG(LogTemp, Warning, TEXT("OperationDataType: %s"), *OperationData.GetScriptStruct()->GetName()); + // Client Logic + if (GMCMovementComponent->GetNetMode() == NM_Client || + GMCMovementComponent->GetNetMode() == NM_Standalone || + GMCMovementComponent->IsLocallyControlledListenServerPawn() || + GMCMovementComponent->IsLocallyControlledDedicatedServerPawn()) + { + // Get a pending operation + if (ClientQueuedOperations.Num() > 0) + { + const int OperationIDToProcess = ClientQueuedOperations.Pop(); + if (OperationPayloads.Contains(OperationIDToProcess)) + { + FInstancedStruct OperationPayload; + OperationPayload.InitializeAs(OperationIDToProcess); + OperationData = OperationPayload; + } + } + else + { + OperationData = FInstancedStruct::Make(); + } + } +} + +void FGMASBoundQueueV2::GenAncillaryTick(const float DeltaTime) +{ + CheckValidState(); + + if (GMCMovementComponent->GetNetMode() >= NM_Client) + { + GMCMoveCounter++; + ClearStaleOperationData(); + } + + // Tick all Server Queued Operations + for (auto It = ServerQueuedBoundOperationsGracePeriods.CreateIterator(); It; ++It) + { + It.Value() -= DeltaTime; + + if (It.Value() <= 0) + { + if (OperationPayloads.Contains(It.Key())) + { + OnServerOperationForced.Broadcast(OperationPayloads[It.Key()]); + OperationPayloads.Remove(It.Key()); + } + It.RemoveCurrent(); + } + } +} + +void FGMASBoundQueueV2::CacheOperationPayload(const int OperationID, const FInstancedStruct& Payload) +{ + OperationPayloads.Add(OperationID, Payload); + OperationDataCacheExpiration.Add({OperationID, GMCMoveCounter}); +} + +void FGMASBoundQueueV2::QueueClientOperation(const int OperationID) +{ + ClientQueuedOperations.Add(OperationID); +} + +void FGMASBoundQueueV2::QueueServerOperation(const int OperationID, const float Timeout) +{ + if (!OperationPayloads.Contains(OperationID)) + { + UE_LOG(LogTemp, Error, TEXT("Tried to queue server operation, but server operation %d not found in payloads"), OperationID); + return; + } + + const FInstancedStruct QueuedOperation = OperationPayloads[OperationID]; + + // Add to server timeout map + ServerQueuedBoundOperationsGracePeriods.Add(OperationID, Timeout); + + // Notify + OnServerOperationAdded.Broadcast(OperationID, QueuedOperation); +} + +void FGMASBoundQueueV2::ServerAcknowledgeOperation(int ID) +{ + if (OperationPayloads.Contains(ID)) + { + OperationPayloads.Remove(ID); + } + + if (ServerQueuedBoundOperationsGracePeriods.Contains(ID)) + { + ServerQueuedBoundOperationsGracePeriods.Remove(ID); + } +} + +void FGMASBoundQueueV2::CheckValidState() const +{ + // Server Logic + if (GMCMovementComponent->GetNetMode() < NM_Client) + { + // Check Client Queued Operations is empty + if (ClientQueuedOperations.Num() > 0) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("ClientQueuedOperations has %d pending operations on server"), ClientQueuedOperations.Num()); + } + + // Check OperationPayloads for invalid IDs (-1 is reserved for client-made operations) + for (auto operation : OperationPayloads) + { + if (operation.Key < 0) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("OperationPayloads has invalid operation ID %d on server"), operation.Key); + } + } + } + else + { + // Check Server Queued Operations is empty + if (ServerQueuedBoundOperationsGracePeriods.Num() > 0) + { + UE_LOG(LogGMCAbilitySystem, Error, TEXT("ServerQueuedBoundOperationsGracePeriods has %d pending operations on client"), ServerQueuedBoundOperationsGracePeriods.Num());; + } + } +} diff --git a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h index d4b7c600..b1afef26 100644 --- a/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h +++ b/Source/GMCAbilitySystem/Public/Components/GMCAbilityComponent.h @@ -10,8 +10,7 @@ #include "Ability/Tasks/GMCAbilityTaskData.h" #include "Effects/GMCAbilityEffect.h" #include "Components/ActorComponent.h" -#include "Containers/Deque.h" -#include "Utility/GMASBoundQueue.h" +#include "Utility/GMASBoundQueueV2.h" #include "Utility/GMASSyncedEvent.h" #include "GMCAbilityComponent.generated.h" @@ -213,7 +212,7 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo TArray GetActiveTagsByParentTag(const FGameplayTag ParentTag); // Do not call directly on client, go through QueueAbility - void TryActivateAbilitiesByInputTag(const FGameplayTag& InputTag, const UInputAction* InputAction = nullptr, bool bFromMovementTick=true); + bool TryActivateAbilitiesByInputTag(const FGameplayTag& InputTag, const UInputAction* InputAction = nullptr, const bool bFromMovementTick=true, const bool bForce=false); // Do not call directly on client, go through QueueAbility. Can be used to call server-side abilities (like AI). bool TryActivateAbility(TSubclassOf ActivatedAbility, const UInputAction* InputAction = nullptr, const FGameplayTag ActivationTag = FGameplayTag::EmptyTag); @@ -305,22 +304,9 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo int GetNextAvailableEffectID() const; bool CheckIfEffectIDQueued(int EffectID) const; - int CreateEffectOperation(TGMASBoundQueueOperation& OutOperation, const TSubclassOf& Effect, const FGMCAbilityEffectData& EffectData, bool bForcedEffectId = true, EGMCAbilityEffectQueueType QueueType = EGMCAbilityEffectQueueType::Predicted); - int CreateSyncedEventOperation(TGMASBoundQueueOperation& OutOperation, const FGMASSyncedEventContainer& EventData); + // int CreateEffectOperation(TGMASBoundQueueOperation& OutOperation, const TSubclassOf& Effect, const FGMCAbilityEffectData& EffectData, bool bForcedEffectId = true, EGMCAbilityEffectQueueType QueueType = EGMCAbilityEffectQueueType::Predicted); + // int CreateSyncedEventOperation(TGMASBoundQueueOperation& OutOperation, const FGMASSyncedEventContainer& EventData); - - /** - * Applies an effect to the Ability Component. 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 - */ - 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 /** @@ -360,6 +346,8 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo */ bool ApplyAbilityEffect(TSubclassOf EffectClass, FGMCAbilityEffectData InitializationData, EGMCAbilityEffectQueueType QueueType, int& OutEffectHandle, int& OutEffectId, UGMCAbilityEffect*& OutEffect); + UGMCAbilityEffect* ApplyAbilityEffectViaOperation(const FGMASBoundQueueV2ApplyEffectOperation& Operatio); + // 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); @@ -563,9 +551,7 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo UFUNCTION(BlueprintCallable, Category="GMAS") virtual void PreLocalMoveExecution(); - - UFUNCTION(BlueprintCallable, Category="GMAS") - virtual void PreRemoteMoveExecution(); + #pragma endregion GMC @@ -585,8 +571,20 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo #pragma endregion ToStringHelpers protected: - virtual void BeginPlay() override; + //TODO: Move these functions + + UFUNCTION(Client, Reliable) + void RPCOnServerOperationAdded(int OperationID, const FInstancedStruct Operation); + + UFUNCTION(BlueprintCallable) + void BoundQueueV2Debug(TSubclassOf Effect); + + UFUNCTION() + void OnServerOperationForced(FInstancedStruct OperationData); + + virtual void BeginPlay() override; + // Abilities that are granted to the player (bound) FGameplayTagContainer GrantedAbilityTags; @@ -658,56 +656,27 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo void InitializeStartingAbilities(); TArray QueuedTaskData; - - // Queued ability operations (activate, cancel, etc.) - TGMASBoundQueue QueuedAbilityOperations; - bool ProcessAbilityOperation(const TGMASBoundQueueOperation& Operation, bool bFromMovementTick); - - TGMASBoundQueue QueuedEffectOperations; - TGMASBoundQueue QueuedEffectOperations_ClientAuth; - - TGMASBoundQueue QueuedEventOperations; - - - template - bool IsOperationValid(const TGMASBoundQueueOperation& Operation) const; - - template - bool ShouldProcessOperation(const TGMASBoundQueueOperation& Operation, TGMASBoundQueue& QueuedOperations, bool bIsServer = true) const; + FGMASBoundQueueV2 BoundQueueV2 = {}; // Events - virtual void ProcessOperation(const TGMASBoundQueueOperation& Operation); + virtual bool ProcessOperation(FInstancedStruct OperationData, bool bFromMovementTick = true, bool bForce = false); + virtual void ProcessEffectApplicationFromOperation(const FGMASBoundQueueV2ApplyEffectOperation& Data); + + virtual void ServerProcessOperation(const FInstancedStruct& OperationData, bool bFromMovementTick = true); + virtual void ServerProcessAcknowledgedOperation(int OperationID, bool bFromMovementTick = true); // Event Implementations // Execute an event that is created by the server where execution is synced between server and client UFUNCTION(BlueprintCallable, Category = "GMASSyncedEvent") void ExecuteSyncedEvent(FGMASSyncedEventContainer EventData); - - - UFUNCTION(BlueprintCallable, DisplayName="Add Impulse (Synced Event)", Category = "Impulse") + UFUNCTION(BlueprintCallable, DisplayName="Add Impulse (Synced Event)", Category = "GMASSyncedEvent") void AddImpulse(FVector Impulse, bool bVelChange = false); - void AddImpulseEvent(const FGMASSyncedEventContainer& EventData) const; - - // Effects - virtual UGMCAbilityEffect* ProcessOperation(const TGMASBoundQueueOperation& Operation); + UFUNCTION(BlueprintCallable, DisplayName="Set Actor Location (Synced Event)", Category = "GMASSyncedEvent") + void SetActorLocation(FVector Location); - void ClientQueueOperation(const TGMASBoundQueueOperation& Operation); - void ClientQueueOperation(const TGMASBoundQueueOperation& Operation); - - UFUNCTION(Client, Reliable) - void RPCClientQueueEffectOperation(const FGMASBoundQueueRPCHeader& Header); - - UFUNCTION(Client, Reliable) - void RPCClientQueueEventOperation(const FGMASBoundQueueRPCHeader& Header); - - // Predictions of Effect state changes - FEffectStatePrediction EffectStatePrediction{}; - - TArray QueuedEffectStates; - UPROPERTY() TMap ActiveAbilities; @@ -752,20 +721,23 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo // Can be just normally replicated since if the client doesn't have them already // then prediction is already out the window - UPROPERTY(ReplicatedUsing = OnRep_ActiveEffectsData) - TArray ActiveEffectsData; + UPROPERTY(ReplicatedUsing=OnRep_ActiveEffectIDs) + TArray ActiveEffectIDs; + + UFUNCTION() + void OnRep_ActiveEffectIDs(); // Max time a client will predict an effect without it being confirmed by the server before cancelling float ClientEffectApplicationTimeout = 1.f; - UFUNCTION() - void OnRep_ActiveEffectsData(); - // Check if any effects have been removed by the server and remove them locally void CheckRemovedEffects(); UPROPERTY() TMap ActiveEffects; + + // IDs that have been claimed by server-auth effect applications + TArray ReservedEffectIDs; UPROPERTY() TMap EffectHandles; @@ -782,14 +754,7 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo UPROPERTY(BlueprintReadOnly, Category = "GMCAbilitySystem", meta=(AllowPrivateAccess="true")) bool bInAncillaryTick = false; - - void ServerHandlePendingEffect(float DeltaTime); - void ServerHandlePredictedPendingEffect(float DeltaTime); - - template - void ClientHandlePendingOperation(TGMASBoundQueue& QueuedOperations); - void ClientHandlePredictedPendingEffect(); int LateApplicationIDCounter = 0; @@ -847,6 +812,7 @@ class GMCABILITYSYSTEM_API UGMC_AbilitySystemComponent : public UGameplayTasksCo UFUNCTION(BlueprintCallable, Category="GMAS|FX") void SpawnSound(USoundBase* Sound, FVector Location, float VolumeMultiplier = 1.f, float PitchMultiplier = 1.f, bool bIsClientPredicted = false); + UFUNCTION(NetMulticast, Unreliable) void MC_SpawnSound(USoundBase* Sound, FVector Location, float VolumeMultiplier = 1.f, float PitchMultiplier = 1.f, bool bIsClientPredicted = false); diff --git a/Source/GMCAbilitySystem/Public/Debug/GameplayDebuggerCategory_GMCAbilitySystem.h b/Source/GMCAbilitySystem/Public/Debug/GameplayDebuggerCategory_GMCAbilitySystem.h index ae373ce0..e1c18f40 100644 --- a/Source/GMCAbilitySystem/Public/Debug/GameplayDebuggerCategory_GMCAbilitySystem.h +++ b/Source/GMCAbilitySystem/Public/Debug/GameplayDebuggerCategory_GMCAbilitySystem.h @@ -36,6 +36,8 @@ class GMCABILITYSYSTEM_API FGameplayDebuggerCategory_GMCAbilitySystem : public F int NBActiveEffectData; FString ActiveAbilities; int NBActiveAbilities; + + int NBCachedOperationPayloads; void Serialize(FArchive& Ar); }; diff --git a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h deleted file mode 100644 index 0a1869a3..00000000 --- a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueue.h +++ /dev/null @@ -1,594 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#pragma once - -#include "CoreMinimal.h" -#include "GameplayTagContainer.h" -#include "GMCMovementUtilityComponent.h" -#include "InstancedStruct.h" -#include "UObject/Object.h" -#include "UObject/ConstructorHelpers.h" -#include "GMASBoundQueue.generated.h" - -UENUM(BlueprintType) -enum class EGMASBoundQueueOperationType : uint8 -{ - None, - Add, - Remove, - Activate, - Cancel -}; - -USTRUCT(BlueprintType) -struct FGMASBoundQueueOperationIdSet -{ - GENERATED_BODY() - - UPROPERTY() - TArray Ids = {}; -}; - -USTRUCT(BlueprintType) -struct GMCABILITYSYSTEM_API FGMASBoundQueueRPCHeader -{ - GENERATED_BODY() - - UPROPERTY() - int32 OperationId { -1 }; - - UPROPERTY() - uint8 OperationTypeRaw { 0 }; - - EGMASBoundQueueOperationType GetOperationType() const - { - return static_cast(OperationTypeRaw); - } - - UPROPERTY() - FGameplayTag Tag { FGameplayTag::EmptyTag }; - - UPROPERTY() - FName ItemClassName { NAME_None }; - - UPROPERTY() - FGMASBoundQueueOperationIdSet PayloadIds {}; - - UPROPERTY() - FInstancedStruct InstancedPayload {}; - - UPROPERTY() - float RPCGracePeriodSeconds { 1.f }; - - UPROPERTY() - uint8 ExtraFlags { 0 }; -}; - -template -struct GMCABILITYSYSTEM_API TGMASBoundQueueOperation -{ - FGMASBoundQueueRPCHeader Header {}; - - // An actual class to be utilized with this, in case we need to instance it. - TSubclassOf ItemClass { nullptr }; - - FInstancedStruct InstancedPayloadIds; - - // The struct payload for this item. - T Payload; - - // The instanced struct representation of this payload, used to actually - // bind for replication. - FInstancedStruct InstancedPayload; - - EGMASBoundQueueOperationType GetOperationType() const - { - return Header.GetOperationType(); - } - - void SetOperationType(EGMASBoundQueueOperationType OperationType) - { - Header.OperationTypeRaw = static_cast(OperationType); - } - - int32 GetOperationId() const { return Header.OperationId; } - - TArray GetPayloadIds() const { return Header.PayloadIds.Ids; } - - FGameplayTag GetTag() const { return Header.Tag; } - - bool GracePeriodExpired() const - { - return Header.RPCGracePeriodSeconds <= 0.f; - } - - bool IsValid() const - { - return Header.OperationTypeRaw != 0 && (Header.ItemClassName != NAME_None || Header.Tag != FGameplayTag::EmptyTag || Header.PayloadIds.Ids.Num() > 0); - } - - void Refresh(bool bDecodePayload = false) - { - if (bDecodePayload) - { - // Incoming from remote. - Payload = Header.InstancedPayload.Get(); - if (Header.PayloadIds.Ids.Num() == 0 && InstancedPayloadIds.IsValid()) - { - Header.PayloadIds = InstancedPayloadIds.Get(); - } - } - else - { - // Outgoing - Header.InstancedPayload = FInstancedStruct::Make(Payload); - InstancedPayloadIds = FInstancedStruct::Make(Header.PayloadIds); - } - - RefreshClass(); - } - - void RefreshClass() - { - if (Header.ItemClassName != NAME_None && !ItemClass) - { - // Get a handle to our class, for instancing purposes. - TSoftClassPtr ClassPtr = TSoftClassPtr(FSoftObjectPath(Header.ItemClassName.ToString())); - ItemClass = ClassPtr.LoadSynchronous(); - } - - if (ItemClass && Header.ItemClassName == NAME_None) - { - Header.ItemClassName = FName(ItemClass->GetPathName()); - } - } - -}; - -USTRUCT(BlueprintType) -struct GMCABILITYSYSTEM_API FGMASBoundQueueAcknowledgement -{ - GENERATED_BODY() - - UPROPERTY() - int32 Id { -1 }; - - UPROPERTY() - float Lifetime { 5.f }; -}; - -USTRUCT(BlueprintType) -struct GMCABILITYSYSTEM_API FGMASBoundQueueAcknowledgements -{ - GENERATED_BODY() - - UPROPERTY() - TArray AckSet; -}; - -USTRUCT(BlueprintType) -struct GMCABILITYSYSTEM_API FGMASBoundQueueEmptyData -{ - GENERATED_BODY() -}; - -template -class GMCABILITYSYSTEM_API TGMASBoundQueue -{ - -public: - - TGMASBoundQueueOperation CurrentOperation; - - int BI_ActionTimer { -1 }; - int BI_Acknowledgements { -1 }; - int BI_OperationId { -1 }; - int BI_OperationType { -1 }; - int BI_OperationTag { -1 }; - int BI_OperationClass { -1 }; - int BI_OperationExtraFlags { -1 }; - int BI_OperationPayload { -1 }; - int BI_OperationPayloadIds { -1 }; - - TArray> QueuedBoundOperations; - TArray> QueuedRPCOperations; - - FInstancedStruct Acknowledgments; - - double ActionTimer { 0 }; - - void BindToGMC(UGMC_MovementUtilityCmp* MovementComponent) - { - const EGMC_PredictionMode Prediction = ClientAuth ? EGMC_PredictionMode::ClientAuth_Input : EGMC_PredictionMode::ServerAuth_Input_ClientValidated; - const EGMC_PredictionMode AckPrediction = ClientAuth ? EGMC_PredictionMode::ServerAuth_Output_ClientValidated : EGMC_PredictionMode::ClientAuth_Input; - - Acknowledgments = FInstancedStruct::Make(FGMASBoundQueueAcknowledgements()); - - // Our queue's action timer is always server-auth. - BI_ActionTimer = MovementComponent->BindDoublePrecisionFloat( - ActionTimer, - EGMC_PredictionMode::ServerAuth_Output_ClientValidated, - EGMC_CombineMode::CombineIfUnchanged, - EGMC_SimulationMode::None, - EGMC_InterpolationFunction::TargetValue - ); - - if (!ClientAuth) - { - // Acknowledgements are bound client-auth for server-auth effects. - BI_Acknowledgements = MovementComponent->BindInstancedStruct( - Acknowledgments, - AckPrediction, - EGMC_CombineMode::CombineIfUnchanged, - EGMC_SimulationMode::None, - EGMC_InterpolationFunction::TargetValue); - } - else - { - // For client-auth stuff, we bind the individual pieces of the queue. - // We will probably not use this often (if ever), but it exists just-in-case. - - BI_OperationId = MovementComponent->BindInt( - CurrentOperation.Header.OperationId, - Prediction, - EGMC_CombineMode::CombineIfUnchanged, - EGMC_SimulationMode::Periodic_Output, - EGMC_InterpolationFunction::TargetValue); - - BI_OperationType = MovementComponent->BindByte( - CurrentOperation.Header.OperationTypeRaw, - Prediction, - EGMC_CombineMode::CombineIfUnchanged, - EGMC_SimulationMode::Periodic_Output, - EGMC_InterpolationFunction::TargetValue); - - BI_OperationTag = MovementComponent->BindGameplayTag( - CurrentOperation.Header.Tag, - Prediction, - EGMC_CombineMode::CombineIfUnchanged, - EGMC_SimulationMode::Periodic_Output, - EGMC_InterpolationFunction::TargetValue); - - BI_OperationClass = MovementComponent->BindName( - CurrentOperation.Header.ItemClassName, - Prediction, - EGMC_CombineMode::CombineIfUnchanged, - EGMC_SimulationMode::Periodic_Output, - EGMC_InterpolationFunction::TargetValue); - - BI_OperationPayload = MovementComponent->BindInstancedStruct( - CurrentOperation.Header.InstancedPayload, - Prediction, - EGMC_CombineMode::CombineIfUnchanged, - EGMC_SimulationMode::Periodic_Output, - EGMC_InterpolationFunction::TargetValue); - - BI_OperationExtraFlags = MovementComponent->BindByte( - CurrentOperation.Header.ExtraFlags, - Prediction, - EGMC_CombineMode::CombineIfUnchanged, - EGMC_SimulationMode::Periodic_Output, - EGMC_InterpolationFunction::TargetValue); - - BI_OperationPayloadIds = MovementComponent->BindInstancedStruct( - CurrentOperation.InstancedPayloadIds, - Prediction, - EGMC_CombineMode::CombineIfUnchanged, - EGMC_SimulationMode::Periodic_Output, - EGMC_InterpolationFunction::TargetValue); - } - - CurrentOperation = TGMASBoundQueueOperation(); - CurrentOperation.Header = FGMASBoundQueueRPCHeader(); - CurrentOperation.Refresh(false); - } - - void PreLocalMovement() - { - if (QueuedBoundOperations.Num() > 0 && ClientAuth) - { - CurrentOperation = QueuedBoundOperations.Pop(); - CurrentOperation.Refresh(false); - } - } - - void PreRemoteMovement() - { - ClearCurrentOperation(); - if (QueuedBoundOperations.Num() > 0 && !ClientAuth) - { - CurrentOperation = QueuedBoundOperations.Pop(); - CurrentOperation.Refresh(false); - } - } - - int32 GenerateOperationId() const - { - int32 NewOperationId = ActionTimer * 100; - - TGMASBoundQueueOperation Operation; - - while (GetOperationById(NewOperationId, Operation)) - { - NewOperationId++; - } - return NewOperationId; - } - - void ClearCurrentOperation() - { - CurrentOperation.Header = FGMASBoundQueueRPCHeader(); - - // CurrentOperation.Header.OperationId = -1; - // CurrentOperation.Header.Tag = FGameplayTag::EmptyTag; - // CurrentOperation.Header.ItemClassName = NAME_None; - // CurrentOperation.SetOperationType(EGMASBoundQueueOperationType::None); - // CurrentOperation.Header.PayloadIds.Ids.Empty(); - - CurrentOperation.Payload = T(); - CurrentOperation.Refresh(false); - } - - void GenPredictionTick(float DeltaTime) - { - ActionTimer += DeltaTime; - ExpireStaleAcks(DeltaTime); - } - - int32 MakeOperation(TGMASBoundQueueOperation& NewOperation, EGMASBoundQueueOperationType Type, FGameplayTag Tag, const T& Payload, TArray PayloadIds = {}, TSubclassOf ItemClass = nullptr, float RPCGracePeriod = 1.f, uint8 ExtraFlags = 0) - { - NewOperation.Header.OperationId = GenerateOperationId(); - NewOperation.SetOperationType(Type); - NewOperation.Header.Tag = Tag; - NewOperation.Payload = Payload; - NewOperation.ItemClass = ItemClass; - NewOperation.Header.RPCGracePeriodSeconds = RPCGracePeriod; - NewOperation.Header.PayloadIds.Ids = PayloadIds; - NewOperation.Header.ExtraFlags = ExtraFlags; - - NewOperation.Refresh(false); - - return NewOperation.GetOperationId(); - } - - int32 MakeOperation(TGMASBoundQueueOperation& NewOperation, const FGMASBoundQueueRPCHeader& Header, const T& Payload) - { - NewOperation.Header = Header; - NewOperation.Payload = Payload; - NewOperation.Refresh(); - NewOperation.InstancedPayload = FInstancedStruct::Make(Payload); - NewOperation.InstancedPayloadIds = FInstancedStruct::Make(NewOperation.Header.PayloadIds); - - return NewOperation.GetOperationId(); - } - - void QueuePreparedOperation(TGMASBoundQueueOperation& NewOperation, bool bMovementSynced = true) - { - TGMASBoundQueueOperation TestOperation = TGMASBoundQueueOperation(); - - // Don't bother queueing it if it already exists. - if (GetOperationById(NewOperation.GetOperationId(), TestOperation)) return; - - if (bMovementSynced) - { - // This needs to be handled via GMC, so add it to our queue. - QueuedBoundOperations.Push(NewOperation); - } - else - { - QueuedRPCOperations.Push(NewOperation); - } - } - - int32 QueueOperation(TGMASBoundQueueOperation& NewOperation, EGMASBoundQueueOperationType Type, FGameplayTag Tag, const T& Payload, TArray PayloadIds = {}, TSubclassOf ItemClass = nullptr, bool bMovementSynced = true, float RPCGracePeriod = 1.f) - { - MakeOperation(NewOperation, Type, Tag, Payload, PayloadIds, ItemClass, RPCGracePeriod); - QueuePreparedOperation(NewOperation, bMovementSynced); - return NewOperation.GetOperationId(); - } - - void QueueOperationFromHeader(const FGMASBoundQueueRPCHeader& Header, bool bMovementSynced) - { - TGMASBoundQueueOperation NewOperation; - - NewOperation.Header = Header; - NewOperation.Refresh(true); - QueuePreparedOperation(NewOperation, bMovementSynced); - } - - int Num() const - { - return QueuedBoundOperations.Num(); - } - - int NumMatching(FGameplayTag Tag, EGMASBoundQueueOperationType Type = EGMASBoundQueueOperationType::None) const - { - int Result = 0; - for (const auto& Operation : QueuedBoundOperations) - { - if (Operation.GetTag() == Tag) - { - if (Type == EGMASBoundQueueOperationType::None || Operation.GetOperationType() == Type) Result++; - } - } - for (const auto& Operation : QueuedRPCOperations) - { - if (Operation.GetTag() == Tag) - { - if (Type == EGMASBoundQueueOperationType::None || Operation.GetOperationType() == Type) Result++; - } - } - return Result; - } - - const TArray>& GetQueuedOperations() const { return QueuedBoundOperations; } - - const TArray>& GetQueuedRPCOperations() const { return QueuedRPCOperations; } - - bool GetOperationById(int32 OperationId, TGMASBoundQueueOperation& OutOperation) const - { - for (const auto& Operation : GetQueuedOperations()) - { - if (Operation.GetOperationId() == OperationId) - { - OutOperation = Operation; - return true; - } - } - - for (const auto& Operation : GetQueuedRPCOperations()) - { - if (Operation.GetOperationId() == OperationId) - { - OutOperation = Operation; - return true; - } - } - - return false; - } - - bool HasOperationWithPayloadId(int32 PayloadId) const - { - for (const auto& Operation : QueuedBoundOperations) - { - if (Operation.GetPayloadIds().Contains(PayloadId)) - { - return true; - } - } - - for (const auto& Operation : QueuedRPCOperations) - { - if (Operation.GetPayloadIds().Contains(PayloadId)) - { - return true; - } - } - - return false; - } - - bool RemoveOperationById(int32 OperationId) - { - int TargetIdx = -1; - - for (int Idx = 0; Idx < QueuedRPCOperations.Num() && TargetIdx == -1; Idx++) - { - if(QueuedRPCOperations[Idx].GetOperationId() == OperationId) - { - TargetIdx = Idx; - } - } - - if (TargetIdx != -1) - { -#if ENGINE_MAJOR_VERSION >=5 && ENGINE_MINOR_VERSION >= 4 - QueuedRPCOperations.RemoveAtSwap(TargetIdx, 1, EAllowShrinking::No); -#else - QueuedRPCOperations.RemoveAtSwap(TargetIdx, 1, false); -#endif - return true; - } - - TargetIdx = -1; - for (int Idx = 0; Idx < QueuedBoundOperations.Num() && TargetIdx == -1; Idx++) - { - if(QueuedBoundOperations[Idx].GetOperationId() == OperationId) - { - TargetIdx = Idx; - } - } - - if (TargetIdx != -1) - { -#if ENGINE_MAJOR_VERSION >=5 && ENGINE_MINOR_VERSION >= 4 - QueuedBoundOperations.RemoveAtSwap(TargetIdx, 1, EAllowShrinking::No); -#else - QueuedBoundOperations.RemoveAtSwap(TargetIdx, 1, false); -#endif - return true; - } - - return false; - } - - bool GetCurrentBoundOperation(TGMASBoundQueueOperation& Operation, bool bRefresh = false) - { - Operation = CurrentOperation; - if (Operation.GetOperationType() != EGMASBoundQueueOperationType::None) - { - if (bRefresh) - { - Operation.Refresh(true); - } - else - { - Operation.RefreshClass(); - } - return true; - } - - return false; - } - - bool PopNextRPCOperation(TGMASBoundQueueOperation& Operation) - { - if (QueuedRPCOperations.Num() == 0) return false; - - Operation = QueuedRPCOperations.Pop(); - return true; - } - - void DeductGracePeriod(float DeltaTime) - { - for (auto& Operation : QueuedRPCOperations) - { - Operation.Header.RPCGracePeriodSeconds -= DeltaTime; - } - } - - void Acknowledge(int32 OperationId, float AckLifetime = 5.f) - { - if (!IsAcknowledged(OperationId)) - { - FGMASBoundQueueAcknowledgement NewAck; - NewAck.Id = OperationId; - NewAck.Lifetime = AckLifetime; - - auto& Acks = Acknowledgments.GetMutable(); - Acks.AckSet.Add(NewAck); - } - } - - bool IsAcknowledged(int32 OperationId) const - { - const auto& Acks = Acknowledgments.Get(); - for (const auto& Ack : Acks.AckSet) - { - if (Ack.Id == OperationId) return true; - } - return false; - } - - void ExpireStaleAcks(float DeltaTime) - { - // Deduct from our ack lifetime; if we've gone stale, remove the stale acks to avoid it just growing forever. - TArray FreshAcks; - if (!Acknowledgments.IsValid()) { // that fix the crash, but doesn't fix the issue. @packetdancer - return; - } - - auto& Acks = Acknowledgments.GetMutable(); - for (auto& Ack : Acks.AckSet) - { - Ack.Lifetime -= DeltaTime; - if (Ack.Lifetime > 0.f) - { - FreshAcks.Add(Ack); - } - } - Acks.AckSet = FreshAcks; - } - -}; diff --git a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueueV2.h b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueueV2.h new file mode 100644 index 00000000..279c00c2 --- /dev/null +++ b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueueV2.h @@ -0,0 +1,173 @@ +#pragma once + +#include "StructUtils/InstancedStruct.h" +#include "GMASBoundQueueV2_Operations.h" +#include "GMCMovementUtilityComponent.h" +#include "GMASBoundQueueV2.generated.h" + +class UGMC_MovementUtilityCmp; +class UGMCMovementComponent; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnServerOperationAdded, int, OperationID, FInstancedStruct, OperationData); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnServerOperationForced, FInstancedStruct, OperationData); + + +USTRUCT() +struct FOperationDataCacheExpiration +{ + GENERATED_BODY() + + // Operation ID + int OperationID = -1; + + // The GMC move # (GMCMoveCounter) when this operation was added + int64 ModeAddedAt = -1; +}; + +USTRUCT() +struct FGMASBoundQueueV2 +{ + GENERATED_BODY() + // Events + FOnServerOperationAdded OnServerOperationAdded; + FOnServerOperationForced OnServerOperationForced; + + UPROPERTY() + UGMC_MovementUtilityCmp* GMCMovementComponent = nullptr; + + // Every time a GMC move is processed, this counter is incremented + // Used to expire stale operation data + int64 GMCMoveCounter = 0; + TArray OperationDataCacheExpiration; + + void ClearStaleOperationData(); + + int NextOperationID = 0; + + // Get the next operation ID + // Any positive ID is a server generated operation + // Any negative ID is a client generated operation + int GetNextOperationID() + { + if (GMCMovementComponent->GetNetMode() != NM_Client) + { + return ++NextOperationID; + } + return --NextOperationID; + } + + // OperationID, OperationPayload +protected: + TMap OperationPayloads; + +public: + // Accessors for OperationPayloads + + void RemovePayloadByID(const int OperationID) + { + if (OperationPayloads.Contains(OperationID)) + { + OperationPayloads.Remove(OperationID); + } + } + + // Make a GetOperationByID + FInstancedStruct GetPayloadByID(const int OperationID) + { + if (OperationPayloads.Contains(OperationID)) + { + return OperationPayloads[OperationID]; + } + return FInstancedStruct(); + } + + bool HasPayloadByID(const int OperationID) const + { + return OperationPayloads.Contains(OperationID); + } + + int GetPayloadCount() const + { + return OperationPayloads.Num(); + } + + + TArray OperationQueue; + + // GMC + void BindToGMC(UGMC_MovementUtilityCmp* MovementComponent); + void GenPreLocalMoveExecution(); + void GenAncillaryTick(float DeltaTime); + + //// GMC Bound + // + // Operation Data used to actually process the operation + // These are only ever sent via RPC from the server to the client then cached + // GMC moves will access the caches instead of storing it all in moves + int BI_OperationData; + FInstancedStruct OperationData; + //// End GMC Bound + + void CacheOperationPayload(const int OperationID, const FInstancedStruct& Payload); + + // Wrappers for building instanced structs for each data type + // Adds the Operation ID to the data and stores the payload in OperationPayloads + template + int MakeOperationData(const T& Data) + { + static_assert(TIsDerivedFrom::IsDerived, "T must be derived from FGMASBoundQueueV2BaseData"); + + T BuiltData = Data; + BuiltData.OperationID = GetNextOperationID(); + + FInstancedStruct OutStruct; + OutStruct.InitializeAs(BuiltData); + + // Add to payload cache to reference it later + CacheOperationPayload(BuiltData.OperationID, OutStruct); + + return BuiltData.OperationID; + } + + // Operation types that the client can supply for the server to run + // Treat these as unsafe client-supplied data + TArray ValidClientInputOperationTypes = { + FGMASBoundQueueV2AbilityActivationOperation::StaticStruct(), + FGMASBoundQueueV2AcknowledgeOperation::StaticStruct() + }; + + bool IsValidGMASOperation(const FInstancedStruct& Data) const; + + bool IsValidClientOperation(const FInstancedStruct& Data) const; + + // Queue a Client operation + void QueueClientOperation(const int OperationID); + + // Queue a ServerAuth operation + void QueueServerOperation(const int OperationID, const float Timeout = 1.0f); + + bool CurrentOperationIsOfType(const UScriptStruct* T) const + { + return OperationData.GetScriptStruct() == T; + } + + // Process a server operation that the client has sent an ack for + void ServerAcknowledgeOperation(int ID); + + // Operations (referenced by ID to OperationPayloads) that the Client has queued + // Key: Operation Id + TArray ClientQueuedOperations; + + // Operations that the server has sent to the client but haven't been acknowledged yet + // If the client doesn't acknowledge the operation in time, the server will force it + // Map: OperationId -> GracePeriod + TMap ServerQueuedBoundOperationsGracePeriods; + + // Runs checks on the current state of the queue and logs any issues found + void CheckValidState() const; +}; + +// Operations +#pragma region Operations + +#pragma endregion Operations diff --git a/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueueV2_Operations.h b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueueV2_Operations.h new file mode 100644 index 00000000..63894754 --- /dev/null +++ b/Source/GMCAbilitySystem/Public/Utility/GMASBoundQueueV2_Operations.h @@ -0,0 +1,125 @@ +#pragma once +#include "GameplayTagContainer.h" +#include "InputAction.h" +#include "Effects/GMCAbilityEffect.h" +#include "InstancedStruct.h" +#include "GMASBoundQueueV2_Operations.generated.h" + +USTRUCT() +struct GMCABILITYSYSTEM_API FGMASBoundQueueV2OperationBaseData +{ + GENERATED_BODY() + + FGMASBoundQueueV2OperationBaseData(){} + FGMASBoundQueueV2OperationBaseData(const int ID){OperationID = ID;} + + // The ID of the operation + // If this is a server operation, this will be a positive number + // If this is a client operation, this will be a negative number + // If a move contains a positive ID, it means the client is ACKing the server operation + // If a move contains a negative ID, it means the client is providing operation data (ie: Ability Activation) + UPROPERTY() + int32 OperationID { 0 }; +}; + + + +// Client Auth Operations +// UNTRUSTED DATA!!! +// Operations sent by clients to the server to request an action. +// Currently used for Ability Activation but can be extended for other actions +// Make sure to add operations here to the ValidClientInputOperationTypes in GMASBoundQueueV2.h + +USTRUCT() +struct GMCABILITYSYSTEM_API FGMASBoundQueueV2AbilityActivationOperation : public FGMASBoundQueueV2OperationBaseData +{ + GENERATED_BODY() + + FGMASBoundQueueV2AbilityActivationOperation() + : InputTag(FGameplayTag::EmptyTag), InputAction(nullptr) + { + } + UPROPERTY() + FGameplayTag InputTag; + + UPROPERTY() + const UInputAction* InputAction; +}; + +// Operation sent by clients when they process server-auth events. Technically a client operation. +USTRUCT() +struct GMCABILITYSYSTEM_API FGMASBoundQueueV2AcknowledgeOperation: public FGMASBoundQueueV2OperationBaseData +{ + GENERATED_BODY() +}; + + +// Server Auth Operations +// Operations generated on the server that need to be sync-executed on clients. They are RPC'd to clients. +// A client should never send these operations to the server as part of their OperationData Output (bound var) +// Currently these will always happen during Prediction tick +// Examples: Apply effect, add impulse, server-auth events, etc. + +// Apply Effect +USTRUCT() +struct GMCABILITYSYSTEM_API FGMASBoundQueueV2ApplyEffectOperation : public FGMASBoundQueueV2OperationBaseData +{ + GENERATED_BODY() + + UPROPERTY() + int EffectID {-1}; + + UPROPERTY() + TSubclassOf EffectClass; + + UPROPERTY() + FGMCAbilityEffectData EffectData; +}; + +// Remove Effect +USTRUCT() +struct GMCABILITYSYSTEM_API FGMASBoundQueueV2RemoveEffectOperation : public FGMASBoundQueueV2OperationBaseData +{ + GENERATED_BODY() + + UPROPERTY() + TArray EffectIDs{}; +}; + +// Add Impulse +USTRUCT() +struct GMCABILITYSYSTEM_API FGMASBoundQueueV2AddImpulseOperation : public FGMASBoundQueueV2OperationBaseData +{ + GENERATED_BODY() + + UPROPERTY() + FVector Impulse {FVector::Zero()}; + + UPROPERTY() + bool bVelocityChange {false}; +}; + +// Set Actor Location (Teleport) +USTRUCT() +struct GMCABILITYSYSTEM_API FGMASBoundQueueV2SetActorLocationOperation : public FGMASBoundQueueV2OperationBaseData +{ + GENERATED_BODY() + + UPROPERTY() + FVector Location {FVector::Zero()}; + +}; + +// Custom Events (Replaces Synced-Events) +// Use a Tag and whatever payload you want to send +USTRUCT(BlueprintType) +struct GMCABILITYSYSTEM_API FGMASBoundQueueV2CustomEventOperation : public FGMASBoundQueueV2OperationBaseData +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadWrite, Category = "GMASSyncedEvent") + FGameplayTag EventTag; + + UPROPERTY(BlueprintReadWrite, Category = "GMASSyncedEvent") + FInstancedStruct InstancedPayload; +};