Let's talk

RSAbilitySystemLibrary.cpp

Source/RSGasHelpers/Private/RSAbilitySystemLibrary.cpp Raw Download ZIP

Click a line number to link it. Shift-click to select a range.

1// Copyright (c) 2026 Reignance Studios. MIT License, see LICENSE.
2
3#include "RSAbilitySystemLibrary.h"
4
5#include "Abilities/GameplayAbility.h"
6#include "AbilitySystemComponent.h"
7#include "AbilitySystemGlobals.h"
8#include "AbilitySystemInterface.h"
9#include "GameplayEffect.h"
10#include "GameFramework/Controller.h"
11#include "GameFramework/Pawn.h"
12#include "GameFramework/PlayerState.h"
13#include "Misc/EngineVersionComparison.h"
14
15namespace RSGasHelpers
16{
17 static UAbilitySystemComponent* FromActorOnly(const AActor* Actor)
18 {
19 // LookForComponent=false: only trust IAbilitySystemInterface here, the
20 // component search below would also find an ASC on a child actor.
21 return Actor ? UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(Actor, false) : nullptr;
22 }
23
24 /** Handles of granted abilities whose asset tags include Tag. Copied out, because activation can change the spec list. */
25 static TArray<FGameplayAbilitySpecHandle> MatchingSpecHandles(UAbilitySystemComponent* ASC, const FGameplayTag& Tag)
26 {
27 TArray<FGameplayAbilitySpecHandle> Handles;
28 for (const FGameplayAbilitySpec& Spec : ASC->GetActivatableAbilities())
29 {
30 if (Spec.Ability && Spec.Ability->GetAssetTags().HasTag(Tag))
31 {
32 Handles.Add(Spec.Handle);
33 }
34 }
35 return Handles;
36 }
37
38 /** The prediction key the ability was activated with, which WaitInputPress/Release listen on. */
39 static FPredictionKey ActivationKey(const FGameplayAbilitySpec& Spec)
40 {
41 if (const UGameplayAbility* Instance = Spec.GetPrimaryInstance())
42 {
43 return Instance->GetCurrentActivationInfo().GetActivationPredictionKey();
44 }
45 PRAGMA_DISABLE_DEPRECATION_WARNINGS
46 return Spec.ActivationInfo.GetActivationPredictionKey(); // non-instanced abilities
47 PRAGMA_ENABLE_DEPRECATION_WARNINGS
48 }
49
50 static const TCHAR* RoleName(ENetRole Role)
51 {
52 switch (Role)
53 {
54 case ROLE_Authority: return TEXT("Authority");
55 case ROLE_AutonomousProxy: return TEXT("AutonomousProxy");
56 case ROLE_SimulatedProxy: return TEXT("SimulatedProxy");
57 default: return TEXT("None");
58 }
59 }
60}
61
62UAbilitySystemComponent* URSAbilitySystemLibrary::FindAbilitySystemComponent(const AActor* Actor)
63{
64 if (!Actor)
65 {
66 return nullptr;
67 }
68
69 if (UAbilitySystemComponent* ASC = RSGasHelpers::FromActorOnly(Actor))
70 {
71 return ASC;
72 }
73
74 if (const APawn* Pawn = Cast<APawn>(Actor))
75 {
76 if (UAbilitySystemComponent* ASC = RSGasHelpers::FromActorOnly(Pawn->GetPlayerState()))
77 {
78 return ASC;
79 }
80 }
81
82 if (const AController* Controller = Cast<AController>(Actor))
83 {
84 if (UAbilitySystemComponent* ASC = RSGasHelpers::FromActorOnly(Controller->PlayerState))
85 {
86 return ASC;
87 }
88 }
89
90 // An ASC component on the actor itself wins over its owner's, so a projectile
91 // with its own ASC is not mistaken for the pawn that fired it.
92 if (UAbilitySystemComponent* ASC = Actor->FindComponentByClass<UAbilitySystemComponent>())
93 {
94 return ASC;
95 }
96
97 return RSGasHelpers::FromActorOnly(Actor->GetOwner());
98}
99
100ERSActivateResult URSAbilitySystemLibrary::TryActivateAbilityByTag(AActor* Actor, FGameplayTag AbilityTag, bool bAllowRemoteActivation)
101{
102 UAbilitySystemComponent* ASC = FindAbilitySystemComponent(Actor);
103 if (!ASC)
104 {
105 return ERSActivateResult::NoASC;
106 }
107
108 const FGameplayTagContainer Tags(AbilityTag);
109
110 // bOnlyAbilitiesThatSatisfyTagRequirements=false: we want "is it granted at all",
111 // so a blocked ability still counts as found.
112 TArray<FGameplayAbilitySpec*> Specs;
113 ASC->GetActivatableGameplayAbilitySpecsByAllMatchingTags(Tags, Specs, false);
114 if (Specs.Num() == 0)
115 {
116 return ERSActivateResult::NotGranted;
117 }
118
119 return ASC->TryActivateAbilitiesByTag(Tags, bAllowRemoteActivation)
120 ? ERSActivateResult::Activated
121 : ERSActivateResult::Blocked;
122}
123
124bool URSAbilitySystemLibrary::AddReplicatedLooseTag(AActor* Actor, FGameplayTag Tag)
125{
126 UAbilitySystemComponent* ASC = FindAbilitySystemComponent(Actor);
127 if (!ASC || !Tag.IsValid() || !ASC->IsOwnerActorAuthoritative())
128 {
129 return false;
130 }
131
132#if UE_VERSION_NEWER_THAN(5, 6, 99)
133 // 5.7 removed AddReplicatedLooseGameplayTag; the replication state does both jobs.
134 // TagOnly replicates presence, not count: clients see 1 however many times it was added.
135 ASC->AddLooseGameplayTag(Tag, 1, EGameplayTagReplicationState::TagOnly);
136#else
137 // The loose tag updates the server's tag counts right away; the replicated
138 // loose tag container carries it to clients, which apply it in their OnRep.
139 ASC->AddLooseGameplayTag(Tag);
140 ASC->AddReplicatedLooseGameplayTag(Tag);
141#endif
142 return true;
143}
144
145bool URSAbilitySystemLibrary::RemoveReplicatedLooseTag(AActor* Actor, FGameplayTag Tag)
146{
147 UAbilitySystemComponent* ASC = FindAbilitySystemComponent(Actor);
148 if (!ASC || !Tag.IsValid() || !ASC->IsOwnerActorAuthoritative())
149 {
150 return false;
151 }
152
153#if UE_VERSION_NEWER_THAN(5, 6, 99)
154 // The container overload, not RemoveLooseGameplayTag(Tag): in 5.7 and 5.8 the single-tag
155 // path drops AnyCountChange callbacks unless the count reaches 0, so a 2 -> 1 change
156 // would never reach WaitGameplayTagCount or RegisterGameplayTagEvent listeners.
157 ASC->RemoveLooseGameplayTags(FGameplayTagContainer(Tag), 1, EGameplayTagReplicationState::TagOnly);
158#else
159 ASC->RemoveLooseGameplayTag(Tag);
160 ASC->RemoveReplicatedLooseGameplayTag(Tag);
161#endif
162 return true;
163}
164
165FGameplayEffectSpecHandle URSAbilitySystemLibrary::MakeSpecWithSetByCaller(
166 UAbilitySystemComponent* ASC,
167 TSubclassOf<UGameplayEffect> EffectClass,
168 float Level,
169 const TMap<FGameplayTag, float>& SetByCallerMagnitudes)
170{
171 if (!ASC || !EffectClass)
172 {
173 return FGameplayEffectSpecHandle();
174 }
175
176 FGameplayEffectContextHandle Context = ASC->MakeEffectContext();
177 Context.AddSourceObject(ASC->GetAvatarActor());
178
179 FGameplayEffectSpecHandle Spec = ASC->MakeOutgoingSpec(EffectClass, Level, Context);
180 if (Spec.IsValid())
181 {
182 for (const TPair<FGameplayTag, float>& Pair : SetByCallerMagnitudes)
183 {
184 Spec.Data->SetSetByCallerMagnitude(Pair.Key, Pair.Value);
185 }
186 }
187 return Spec;
188}
189
190bool URSAbilitySystemLibrary::GetCooldownRemainingByTag(const AActor* Actor, FGameplayTag CooldownTag, float& TimeRemaining, float& Duration)
191{
192 TimeRemaining = 0.f;
193 Duration = 0.f;
194
195 const UAbilitySystemComponent* ASC = FindAbilitySystemComponent(Actor);
196 if (!ASC || !CooldownTag.IsValid())
197 {
198 return false;
199 }
200
201 // Owning tags include tags added at runtime through DynamicGrantedTags, the usual
202 // way one cooldown effect class serves many abilities.
203 const FGameplayEffectQuery Query = FGameplayEffectQuery::MakeQuery_MatchAnyOwningTags(FGameplayTagContainer(CooldownTag));
204 bool bFound = false;
205 for (const TPair<float, float>& RemainingAndDuration : ASC->GetActiveEffectsTimeRemainingAndDuration(Query))
206 {
207 // Several effects can grant the same tag; the longest one decides when it ends.
208 if (!bFound || RemainingAndDuration.Key > TimeRemaining)
209 {
210 TimeRemaining = RemainingAndDuration.Key;
211 Duration = RemainingAndDuration.Value;
212 bFound = true;
213 }
214 }
215 return bFound;
216}
217
218int32 URSAbilitySystemLibrary::PressAbilityInputByTag(AActor* Actor, FGameplayTag AbilityTag)
219{
220 UAbilitySystemComponent* ASC = FindAbilitySystemComponent(Actor);
221 if (!ASC || !AbilityTag.IsValid())
222 {
223 return 0;
224 }
225
226 const TArray<FGameplayAbilitySpecHandle> Handles = RSGasHelpers::MatchingSpecHandles(ASC, AbilityTag);
227 for (const FGameplayAbilitySpecHandle& Handle : Handles)
228 {
229 FGameplayAbilitySpec* Spec = ASC->FindAbilitySpecFromHandle(Handle);
230 if (!Spec)
231 {
232 continue;
233 }
234 Spec->InputPressed = true;
235 if (Spec->IsActive())
236 {
237 // Same as the engine's input-ID path: tell the ability, then fire the event that
238 // WaitInputPress listens on. The task replicates it with the activation's key.
239 ASC->AbilitySpecInputPressed(*Spec);
240 ASC->InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, Spec->Handle, RSGasHelpers::ActivationKey(*Spec));
241 }
242 else
243 {
244 ASC->TryActivateAbility(Handle);
245 }
246 }
247 return Handles.Num();
248}
249
250int32 URSAbilitySystemLibrary::ReleaseAbilityInputByTag(AActor* Actor, FGameplayTag AbilityTag)
251{
252 UAbilitySystemComponent* ASC = FindAbilitySystemComponent(Actor);
253 if (!ASC || !AbilityTag.IsValid())
254 {
255 return 0;
256 }
257
258 const TArray<FGameplayAbilitySpecHandle> Handles = RSGasHelpers::MatchingSpecHandles(ASC, AbilityTag);
259 for (const FGameplayAbilitySpecHandle& Handle : Handles)
260 {
261 FGameplayAbilitySpec* Spec = ASC->FindAbilitySpecFromHandle(Handle);
262 if (!Spec)
263 {
264 continue;
265 }
266 Spec->InputPressed = false;
267 if (Spec->IsActive())
268 {
269 ASC->AbilitySpecInputReleased(*Spec);
270 ASC->InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputReleased, Spec->Handle, RSGasHelpers::ActivationKey(*Spec));
271 }
272 }
273 return Handles.Num();
274}
275
276FString URSAbilitySystemLibrary::DumpAbilitySystemState(const AActor* Actor)
277{
278 UAbilitySystemComponent* ASC = FindAbilitySystemComponent(Actor);
279 if (!ASC)
280 {
281 return FString::Printf(TEXT("[RS GAS] %s: no ability system component"), *GetNameSafe(Actor));
282 }
283
284 const AActor* Owner = ASC->GetOwnerActor();
285 const AActor* Avatar = ASC->GetAvatarActor_Direct();
286
287 FString Out;
288 Out += FString::Printf(TEXT("[RS GAS] %s | role %s | owner %s | avatar %s\n"),
289 *ASC->GetName(),
290 Owner ? RSGasHelpers::RoleName(Owner->GetLocalRole()) : TEXT("None"),
291 *GetNameSafe(Owner),
292 *GetNameSafe(Avatar));
293
294 Out += FString::Printf(TEXT(" Tags: %s\n"), *ASC->GetOwnedGameplayTags().ToStringSimple());
295
296 Out += TEXT(" Abilities:\n");
297 for (const FGameplayAbilitySpec& Spec : ASC->GetActivatableAbilities())
298 {
299 Out += FString::Printf(TEXT(" %s lvl %d %s\n"),
300 *GetNameSafe(Spec.Ability),
301 Spec.Level,
302 Spec.IsActive() ? TEXT("[ACTIVE]") : TEXT(""));
303 }
304
305 Out += TEXT(" Effects:\n");
306 for (const FActiveGameplayEffectHandle& Handle : ASC->GetActiveEffects(FGameplayEffectQuery()))
307 {
308 if (const FActiveGameplayEffect* Effect = ASC->GetActiveGameplayEffect(Handle))
309 {
310 Out += FString::Printf(TEXT(" %s x%d\n"),
311 *GetNameSafe(Effect->Spec.Def),
312 Effect->Spec.GetStackCount());
313 }
314 }
315
316 Out += TEXT(" Attributes:\n");
317 TArray<FGameplayAttribute> Attributes;
318 ASC->GetAllAttributes(Attributes);
319 for (const FGameplayAttribute& Attribute : Attributes)
320 {
321 Out += FString::Printf(TEXT(" %s = %.2f\n"),
322 *Attribute.GetName(),
323 ASC->GetNumericAttribute(Attribute));
324 }
325
326 return Out;
327}

Still desyncing in packaged builds?

Free code fixes one symptom. A Diagnostic finds the cause across your project in two days.