// Copyright (c) 2026 Reignance Studios. MIT License, see LICENSE. #include "RSAbilitySystemLibrary.h" #include "AbilitySystemComponent.h" #include "AbilitySystemGlobals.h" #include "AbilitySystemInterface.h" #include "GameplayEffect.h" #include "GameFramework/Controller.h" #include "GameFramework/Pawn.h" #include "GameFramework/PlayerState.h" #include "Misc/EngineVersionComparison.h" namespace RSGasHelpers { static UAbilitySystemComponent* FromActorOnly(const AActor* Actor) { // LookForComponent=false: only trust IAbilitySystemInterface here, the // component search below would also find an ASC on a child actor. return Actor ? UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(Actor, false) : nullptr; } static const TCHAR* RoleName(ENetRole Role) { switch (Role) { case ROLE_Authority: return TEXT("Authority"); case ROLE_AutonomousProxy: return TEXT("AutonomousProxy"); case ROLE_SimulatedProxy: return TEXT("SimulatedProxy"); default: return TEXT("None"); } } } UAbilitySystemComponent* URSAbilitySystemLibrary::FindAbilitySystemComponent(const AActor* Actor) { if (!Actor) { return nullptr; } if (UAbilitySystemComponent* ASC = RSGasHelpers::FromActorOnly(Actor)) { return ASC; } if (const APawn* Pawn = Cast(Actor)) { if (UAbilitySystemComponent* ASC = RSGasHelpers::FromActorOnly(Pawn->GetPlayerState())) { return ASC; } } if (const AController* Controller = Cast(Actor)) { if (UAbilitySystemComponent* ASC = RSGasHelpers::FromActorOnly(Controller->PlayerState)) { return ASC; } } // An ASC component on the actor itself wins over its owner's, so a projectile // with its own ASC is not mistaken for the pawn that fired it. if (UAbilitySystemComponent* ASC = Actor->FindComponentByClass()) { return ASC; } return RSGasHelpers::FromActorOnly(Actor->GetOwner()); } ERSActivateResult URSAbilitySystemLibrary::TryActivateAbilityByTag(AActor* Actor, FGameplayTag AbilityTag, bool bAllowRemoteActivation) { UAbilitySystemComponent* ASC = FindAbilitySystemComponent(Actor); if (!ASC) { return ERSActivateResult::NoASC; } const FGameplayTagContainer Tags(AbilityTag); // bOnlyAbilitiesThatSatisfyTagRequirements=false: we want "is it granted at all", // so a blocked ability still counts as found. TArray Specs; ASC->GetActivatableGameplayAbilitySpecsByAllMatchingTags(Tags, Specs, false); if (Specs.Num() == 0) { return ERSActivateResult::NotGranted; } return ASC->TryActivateAbilitiesByTag(Tags, bAllowRemoteActivation) ? ERSActivateResult::Activated : ERSActivateResult::Blocked; } bool URSAbilitySystemLibrary::AddReplicatedLooseTag(AActor* Actor, FGameplayTag Tag) { UAbilitySystemComponent* ASC = FindAbilitySystemComponent(Actor); if (!ASC || !Tag.IsValid() || !ASC->IsOwnerActorAuthoritative()) { return false; } #if UE_VERSION_NEWER_THAN(5, 6, 99) // 5.7 removed AddReplicatedLooseGameplayTag; the replication state does both jobs. // TagOnly replicates presence, not count: clients see 1 however many times it was added. ASC->AddLooseGameplayTag(Tag, 1, EGameplayTagReplicationState::TagOnly); #else // The loose tag updates the server's tag counts right away; the replicated // loose tag container carries it to clients, which apply it in their OnRep. ASC->AddLooseGameplayTag(Tag); ASC->AddReplicatedLooseGameplayTag(Tag); #endif return true; } bool URSAbilitySystemLibrary::RemoveReplicatedLooseTag(AActor* Actor, FGameplayTag Tag) { UAbilitySystemComponent* ASC = FindAbilitySystemComponent(Actor); if (!ASC || !Tag.IsValid() || !ASC->IsOwnerActorAuthoritative()) { return false; } #if UE_VERSION_NEWER_THAN(5, 6, 99) // The container overload, not RemoveLooseGameplayTag(Tag): in 5.7 and 5.8 the single-tag // path drops AnyCountChange callbacks unless the count reaches 0, so a 2 -> 1 change // would never reach WaitGameplayTagCount or RegisterGameplayTagEvent listeners. ASC->RemoveLooseGameplayTags(FGameplayTagContainer(Tag), 1, EGameplayTagReplicationState::TagOnly); #else ASC->RemoveLooseGameplayTag(Tag); ASC->RemoveReplicatedLooseGameplayTag(Tag); #endif return true; } FGameplayEffectSpecHandle URSAbilitySystemLibrary::MakeSpecWithSetByCaller( UAbilitySystemComponent* ASC, TSubclassOf EffectClass, float Level, const TMap& SetByCallerMagnitudes) { if (!ASC || !EffectClass) { return FGameplayEffectSpecHandle(); } FGameplayEffectContextHandle Context = ASC->MakeEffectContext(); Context.AddSourceObject(ASC->GetAvatarActor()); FGameplayEffectSpecHandle Spec = ASC->MakeOutgoingSpec(EffectClass, Level, Context); if (Spec.IsValid()) { for (const TPair& Pair : SetByCallerMagnitudes) { Spec.Data->SetSetByCallerMagnitude(Pair.Key, Pair.Value); } } return Spec; } FString URSAbilitySystemLibrary::DumpAbilitySystemState(const AActor* Actor) { UAbilitySystemComponent* ASC = FindAbilitySystemComponent(Actor); if (!ASC) { return FString::Printf(TEXT("[RS GAS] %s: no ability system component"), *GetNameSafe(Actor)); } const AActor* Owner = ASC->GetOwnerActor(); const AActor* Avatar = ASC->GetAvatarActor_Direct(); FString Out; Out += FString::Printf(TEXT("[RS GAS] %s | role %s | owner %s | avatar %s\n"), *ASC->GetName(), Owner ? RSGasHelpers::RoleName(Owner->GetLocalRole()) : TEXT("None"), *GetNameSafe(Owner), *GetNameSafe(Avatar)); Out += FString::Printf(TEXT(" Tags: %s\n"), *ASC->GetOwnedGameplayTags().ToStringSimple()); Out += TEXT(" Abilities:\n"); for (const FGameplayAbilitySpec& Spec : ASC->GetActivatableAbilities()) { Out += FString::Printf(TEXT(" %s lvl %d %s\n"), *GetNameSafe(Spec.Ability), Spec.Level, Spec.IsActive() ? TEXT("[ACTIVE]") : TEXT("")); } Out += TEXT(" Effects:\n"); for (const FActiveGameplayEffectHandle& Handle : ASC->GetActiveEffects(FGameplayEffectQuery())) { if (const FActiveGameplayEffect* Effect = ASC->GetActiveGameplayEffect(Handle)) { Out += FString::Printf(TEXT(" %s x%d\n"), *GetNameSafe(Effect->Spec.Def), Effect->Spec.GetStackCount()); } } Out += TEXT(" Attributes:\n"); TArray Attributes; ASC->GetAllAttributes(Attributes); for (const FGameplayAttribute& Attribute : Attributes) { Out += FString::Printf(TEXT(" %s = %.2f\n"), *Attribute.GetName(), ASC->GetNumericAttribute(Attribute)); } return Out; }