From 8c384f9c556382c7c63a0630847e6b77f556d61c Mon Sep 17 00:00:00 2001 From: Gonzalo Martinez Date: Sun, 28 Sep 2025 19:10:50 -0300 Subject: [PATCH] Added plugin --- .../GameplayMessageRouter.uplugin | 35 +++ .../GameplayMessageNodes.Build.cs | 43 ++++ .../Private/GameplayMessageNodesModule.cpp | 5 + ..._AsyncAction_ListenForGameplayMessages.cpp | 229 +++++++++++++++++ ...de_AsyncAction_ListenForGameplayMessages.h | 58 +++++ .../GameplayMessageRuntime.Build.cs | 30 +++ .../AsyncAction_ListenForGameplayMessage.cpp | 110 ++++++++ .../GameFramework/GameplayMessageRuntime.cpp | 5 + .../GameplayMessageSubsystem.cpp | 191 ++++++++++++++ .../AsyncAction_ListenForGameplayMessage.h | 74 ++++++ .../GameFramework/GameplayMessageSubsystem.h | 240 ++++++++++++++++++ .../GameFramework/GameplayMessageTypes2.h | 51 ++++ 12 files changed, 1071 insertions(+) create mode 100644 Plugins/GameplayMessageRouter/GameplayMessageRouter.uplugin create mode 100644 Plugins/GameplayMessageRouter/Source/GameplayMessageNodes/GameplayMessageNodes.Build.cs create mode 100644 Plugins/GameplayMessageRouter/Source/GameplayMessageNodes/Private/GameplayMessageNodesModule.cpp create mode 100644 Plugins/GameplayMessageRouter/Source/GameplayMessageNodes/Private/K2Node_AsyncAction_ListenForGameplayMessages.cpp create mode 100644 Plugins/GameplayMessageRouter/Source/GameplayMessageNodes/Public/K2Node_AsyncAction_ListenForGameplayMessages.h create mode 100644 Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/GameplayMessageRuntime.Build.cs create mode 100644 Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Private/GameFramework/AsyncAction_ListenForGameplayMessage.cpp create mode 100644 Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Private/GameFramework/GameplayMessageRuntime.cpp create mode 100644 Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Private/GameFramework/GameplayMessageSubsystem.cpp create mode 100644 Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Public/GameFramework/AsyncAction_ListenForGameplayMessage.h create mode 100644 Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Public/GameFramework/GameplayMessageSubsystem.h create mode 100644 Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Public/GameFramework/GameplayMessageTypes2.h diff --git a/Plugins/GameplayMessageRouter/GameplayMessageRouter.uplugin b/Plugins/GameplayMessageRouter/GameplayMessageRouter.uplugin new file mode 100644 index 0000000..13c0e39 --- /dev/null +++ b/Plugins/GameplayMessageRouter/GameplayMessageRouter.uplugin @@ -0,0 +1,35 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.0", + "FriendlyName": "Gameplay Message Subsystem", + "Description": "A subsystem that allows registering for and sending messages between unconnected gameplay objects.", + "Category": "Gameplay", + "CreatedBy": "Epic Games, Inc.", + "CreatedByURL": "http://epicgames.com", + "DocsURL": "", + "MarketplaceURL": "", + "SupportURL": "", + "EnabledByDefault": true, + "CanContainContent": false, + "IsBetaVersion": true, + "Installed": false, + "Modules": [ + { + "Name": "GameplayMessageRuntime", + "Type": "Runtime", + "LoadingPhase": "Default" + }, + { + "Name": "GameplayMessageNodes", + "Type": "UncookedOnly", + "LoadingPhase": "Default" + } + ], + "Plugins": [ + { + "Name": "GameplayTagsEditor", + "Enabled": true + } + ] +} \ No newline at end of file diff --git a/Plugins/GameplayMessageRouter/Source/GameplayMessageNodes/GameplayMessageNodes.Build.cs b/Plugins/GameplayMessageRouter/Source/GameplayMessageNodes/GameplayMessageNodes.Build.cs new file mode 100644 index 0000000..1442757 --- /dev/null +++ b/Plugins/GameplayMessageRouter/Source/GameplayMessageNodes/GameplayMessageNodes.Build.cs @@ -0,0 +1,43 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class GameplayMessageNodes : ModuleRules +{ + public GameplayMessageNodes(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreUObject", + "Engine", + "KismetCompiler", + "PropertyEditor", + "GameplayMessageRuntime", + "UnrealEd" + } + ); + + PublicDependencyModuleNames.AddRange( + new string[] + { + "BlueprintGraph", + } + ); + + PrivateIncludePaths.AddRange( + new string[] + { + } + ); + + PublicIncludePaths.AddRange( + new string[] + { + } + ); + } +} diff --git a/Plugins/GameplayMessageRouter/Source/GameplayMessageNodes/Private/GameplayMessageNodesModule.cpp b/Plugins/GameplayMessageRouter/Source/GameplayMessageNodes/Private/GameplayMessageNodesModule.cpp new file mode 100644 index 0000000..47c9ac9 --- /dev/null +++ b/Plugins/GameplayMessageRouter/Source/GameplayMessageNodes/Private/GameplayMessageNodesModule.cpp @@ -0,0 +1,5 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "Modules/ModuleManager.h" + +IMPLEMENT_MODULE(FDefaultModuleImpl, GameplayMessageNodes); diff --git a/Plugins/GameplayMessageRouter/Source/GameplayMessageNodes/Private/K2Node_AsyncAction_ListenForGameplayMessages.cpp b/Plugins/GameplayMessageRouter/Source/GameplayMessageNodes/Private/K2Node_AsyncAction_ListenForGameplayMessages.cpp new file mode 100644 index 0000000..fe80ce4 --- /dev/null +++ b/Plugins/GameplayMessageRouter/Source/GameplayMessageNodes/Private/K2Node_AsyncAction_ListenForGameplayMessages.cpp @@ -0,0 +1,229 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "K2Node_AsyncAction_ListenForGameplayMessages.h" + +#include "BlueprintActionDatabaseRegistrar.h" +#include "BlueprintFunctionNodeSpawner.h" +#include "EdGraph/EdGraph.h" +#include "GameFramework/AsyncAction_ListenForGameplayMessage.h" +#include "K2Node_AssignmentStatement.h" +#include "K2Node_AsyncAction.h" +#include "K2Node_CallFunction.h" +#include "K2Node_TemporaryVariable.h" +#include "KismetCompiler.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(K2Node_AsyncAction_ListenForGameplayMessages) + +class UEdGraph; + +#define LOCTEXT_NAMESPACE "K2Node" + +namespace UK2Node_AsyncAction_ListenForGameplayMessagesHelper +{ + static FName ActualChannelPinName = "ActualChannel"; + static FName PayloadPinName = "Payload"; + static FName PayloadTypePinName = "PayloadType"; + static FName DelegateProxyPinName = "ProxyObject"; +}; + +void UK2Node_AsyncAction_ListenForGameplayMessages::PostReconstructNode() +{ + Super::PostReconstructNode(); + + RefreshOutputPayloadType(); +} + +void UK2Node_AsyncAction_ListenForGameplayMessages::PinDefaultValueChanged(UEdGraphPin* ChangedPin) +{ + if (ChangedPin == GetPayloadTypePin()) + { + if (ChangedPin->LinkedTo.Num() == 0) + { + RefreshOutputPayloadType(); + } + } +} + +void UK2Node_AsyncAction_ListenForGameplayMessages::GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextOut) const +{ + Super::GetPinHoverText(Pin, HoverTextOut); + if (Pin.PinName == UK2Node_AsyncAction_ListenForGameplayMessagesHelper::PayloadPinName) + { + HoverTextOut = HoverTextOut + LOCTEXT("PayloadOutTooltip", "\n\nThe message structure that we received").ToString(); + } +} + +void UK2Node_AsyncAction_ListenForGameplayMessages::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const +{ + struct GetMenuActions_Utils + { + static void SetNodeFunc(UEdGraphNode* NewNode, bool /*bIsTemplateNode*/, TWeakObjectPtr FunctionPtr) + { + UK2Node_AsyncAction_ListenForGameplayMessages* AsyncTaskNode = CastChecked(NewNode); + if (FunctionPtr.IsValid()) + { + UFunction* Func = FunctionPtr.Get(); + FObjectProperty* ReturnProp = CastFieldChecked(Func->GetReturnProperty()); + + AsyncTaskNode->ProxyFactoryFunctionName = Func->GetFName(); + AsyncTaskNode->ProxyFactoryClass = Func->GetOuterUClass(); + AsyncTaskNode->ProxyClass = ReturnProp->PropertyClass; + } + } + }; + + UClass* NodeClass = GetClass(); + ActionRegistrar.RegisterClassFactoryActions(FBlueprintActionDatabaseRegistrar::FMakeFuncSpawnerDelegate::CreateLambda([NodeClass](const UFunction* FactoryFunc)->UBlueprintNodeSpawner* + { + UBlueprintNodeSpawner* NodeSpawner = UBlueprintFunctionNodeSpawner::Create(FactoryFunc); + check(NodeSpawner != nullptr); + NodeSpawner->NodeClass = NodeClass; + + TWeakObjectPtr FunctionPtr = MakeWeakObjectPtr(const_cast(FactoryFunc)); + NodeSpawner->CustomizeNodeDelegate = UBlueprintNodeSpawner::FCustomizeNodeDelegate::CreateStatic(GetMenuActions_Utils::SetNodeFunc, FunctionPtr); + + return NodeSpawner; + }) ); +} + +void UK2Node_AsyncAction_ListenForGameplayMessages::AllocateDefaultPins() +{ + Super::AllocateDefaultPins(); + + // The output of the UAsyncAction_ListenForGameplayMessage delegates is a proxy object which is used in the follow up call of GetPayload when triggered + // This is only needed in the internals of this node so hide the pin from the editor. + UEdGraphPin* DelegateProxyPin = FindPin(UK2Node_AsyncAction_ListenForGameplayMessagesHelper::DelegateProxyPinName); + if (ensure(DelegateProxyPin)) + { + DelegateProxyPin->bHidden = true; + } + + CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Wildcard, UK2Node_AsyncAction_ListenForGameplayMessagesHelper::PayloadPinName); +} + +bool UK2Node_AsyncAction_ListenForGameplayMessages::HandleDelegates(const TArray& VariableOutputs, UEdGraphPin* ProxyObjectPin, UEdGraphPin*& InOutLastThenPin, UEdGraph* SourceGraph, FKismetCompilerContext& CompilerContext) +{ + bool bIsErrorFree = true; + + if (VariableOutputs.Num() != 3) + { + ensureMsgf(false, TEXT("UK2Node_AsyncAction_ListenForGameplayMessages::HandleDelegates - Variable output array not valid. Output delegates must only have the single proxy object output and than must have pin for payload.")); + return false; + } + + for (TFieldIterator PropertyIt(ProxyClass); PropertyIt && bIsErrorFree; ++PropertyIt) + { + UEdGraphPin* LastActivatedThenPin = nullptr; + bIsErrorFree &= FBaseAsyncTaskHelper::HandleDelegateImplementation(*PropertyIt, VariableOutputs, ProxyObjectPin, InOutLastThenPin, LastActivatedThenPin, this, SourceGraph, CompilerContext); + + bIsErrorFree &= HandlePayloadImplementation(*PropertyIt, VariableOutputs[0], VariableOutputs[2], VariableOutputs[1], LastActivatedThenPin, SourceGraph, CompilerContext); + } + + return bIsErrorFree; +} + +bool UK2Node_AsyncAction_ListenForGameplayMessages::HandlePayloadImplementation(FMulticastDelegateProperty* CurrentProperty, const FBaseAsyncTaskHelper::FOutputPinAndLocalVariable& ProxyObjectVar, const FBaseAsyncTaskHelper::FOutputPinAndLocalVariable& PayloadVar, const FBaseAsyncTaskHelper::FOutputPinAndLocalVariable& ActualChannelVar, UEdGraphPin*& InOutLastActivatedThenPin, UEdGraph* SourceGraph, FKismetCompilerContext& CompilerContext) +{ + bool bIsErrorFree = true; + const UEdGraphPin* PayloadPin = GetPayloadPin(); + const UEdGraphSchema_K2* Schema = CompilerContext.GetSchema(); + + check(CurrentProperty && SourceGraph && Schema); + + const FEdGraphPinType& PinType = PayloadPin->PinType; + + if (PinType.PinCategory == UEdGraphSchema_K2::PC_Wildcard) + { + if (PayloadPin->LinkedTo.Num() == 0) + { + // If no payload type is specified and we're not trying to connect the output to anything ignore the rest of this step + return true; + } + else + { + return false; + } + } + + UK2Node_TemporaryVariable* TempVarOutput = CompilerContext.SpawnInternalVariable( + this, PinType.PinCategory, PinType.PinSubCategory, PinType.PinSubCategoryObject.Get(), PinType.ContainerType, PinType.PinValueType); + + UK2Node_CallFunction* const CallGetPayloadNode = CompilerContext.SpawnIntermediateNode(this, SourceGraph); + CallGetPayloadNode->FunctionReference.SetExternalMember(TEXT("GetPayload"), CurrentProperty->GetOwnerClass()); + CallGetPayloadNode->AllocateDefaultPins(); + + // Hook up the self connection + UEdGraphPin* GetPayloadCallSelfPin = Schema->FindSelfPin(*CallGetPayloadNode, EGPD_Input); + if (GetPayloadCallSelfPin) + { + bIsErrorFree &= Schema->TryCreateConnection(GetPayloadCallSelfPin, ProxyObjectVar.TempVar->GetVariablePin()); + + // Hook the activate node up in the exec chain + UEdGraphPin* GetPayloadExecPin = CallGetPayloadNode->FindPinChecked(UEdGraphSchema_K2::PN_Execute); + UEdGraphPin* GetPayloadThenPin = CallGetPayloadNode->FindPinChecked(UEdGraphSchema_K2::PN_Then); + + UEdGraphPin* LastThenPin = nullptr; + UEdGraphPin* GetPayloadPin = CallGetPayloadNode->FindPinChecked(TEXT("OutPayload")); + bIsErrorFree &= Schema->TryCreateConnection(TempVarOutput->GetVariablePin(), GetPayloadPin); + + + UK2Node_AssignmentStatement* AssignNode = CompilerContext.SpawnIntermediateNode(this, SourceGraph); + AssignNode->AllocateDefaultPins(); + bIsErrorFree &= Schema->TryCreateConnection(GetPayloadThenPin, AssignNode->GetExecPin()); + bIsErrorFree &= Schema->TryCreateConnection(PayloadVar.TempVar->GetVariablePin(), AssignNode->GetVariablePin()); + AssignNode->NotifyPinConnectionListChanged(AssignNode->GetVariablePin()); + bIsErrorFree &= Schema->TryCreateConnection(AssignNode->GetValuePin(), TempVarOutput->GetVariablePin()); + AssignNode->NotifyPinConnectionListChanged(AssignNode->GetValuePin()); + + + bIsErrorFree &= CompilerContext.MovePinLinksToIntermediate(*InOutLastActivatedThenPin, *AssignNode->GetThenPin()).CanSafeConnect(); + bIsErrorFree &= Schema->TryCreateConnection(InOutLastActivatedThenPin, GetPayloadExecPin); + + // Hook up the actual channel connection + UEdGraphPin* OutActualChannelPin = GetOutputChannelPin(); + bIsErrorFree &= CompilerContext.MovePinLinksToIntermediate(*OutActualChannelPin, *ActualChannelVar.TempVar->GetVariablePin()).CanSafeConnect(); + } + + return bIsErrorFree; +} + +void UK2Node_AsyncAction_ListenForGameplayMessages::RefreshOutputPayloadType() +{ + UEdGraphPin* PayloadPin = GetPayloadPin(); + UEdGraphPin* PayloadTypePin = GetPayloadTypePin(); + + if (PayloadTypePin->DefaultObject != PayloadPin->PinType.PinSubCategoryObject) + { + if (PayloadPin->SubPins.Num() > 0) + { + GetSchema()->RecombinePin(PayloadPin); + } + + PayloadPin->PinType.PinSubCategoryObject = PayloadTypePin->DefaultObject; + PayloadPin->PinType.PinCategory = (PayloadTypePin->DefaultObject == nullptr) ? UEdGraphSchema_K2::PC_Wildcard : UEdGraphSchema_K2::PC_Struct; + } +} + +UEdGraphPin* UK2Node_AsyncAction_ListenForGameplayMessages::GetPayloadPin() const +{ + UEdGraphPin* Pin = FindPinChecked(UK2Node_AsyncAction_ListenForGameplayMessagesHelper::PayloadPinName); + check(Pin->Direction == EGPD_Output); + return Pin; +} + +UEdGraphPin* UK2Node_AsyncAction_ListenForGameplayMessages::GetPayloadTypePin() const +{ + UEdGraphPin* Pin = FindPinChecked(UK2Node_AsyncAction_ListenForGameplayMessagesHelper::PayloadTypePinName); + check(Pin->Direction == EGPD_Input); + return Pin; +} + +UEdGraphPin* UK2Node_AsyncAction_ListenForGameplayMessages::GetOutputChannelPin() const +{ + UEdGraphPin* Pin = FindPinChecked(UK2Node_AsyncAction_ListenForGameplayMessagesHelper::ActualChannelPinName); + check(Pin->Direction == EGPD_Output); + return Pin; +} + +#undef LOCTEXT_NAMESPACE + diff --git a/Plugins/GameplayMessageRouter/Source/GameplayMessageNodes/Public/K2Node_AsyncAction_ListenForGameplayMessages.h b/Plugins/GameplayMessageRouter/Source/GameplayMessageNodes/Public/K2Node_AsyncAction_ListenForGameplayMessages.h new file mode 100644 index 0000000..68787cf --- /dev/null +++ b/Plugins/GameplayMessageRouter/Source/GameplayMessageNodes/Public/K2Node_AsyncAction_ListenForGameplayMessages.h @@ -0,0 +1,58 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "K2Node_AsyncAction.h" + +#include "K2Node_AsyncAction_ListenForGameplayMessages.generated.h" + +class FBlueprintActionDatabaseRegistrar; +class FKismetCompilerContext; +class FMulticastDelegateProperty; +class FString; +class UEdGraph; +class UEdGraphPin; +class UObject; + +/** + * Blueprint node which is spawned to handle the async logic for UAsyncAction_RegisterGameplayMessageReceiver + */ + +UCLASS() +class UK2Node_AsyncAction_ListenForGameplayMessages : public UK2Node_AsyncAction +{ + GENERATED_BODY() + + //~UEdGraphNode interface + virtual void PostReconstructNode() override; + virtual void PinDefaultValueChanged(UEdGraphPin* ChangedPin) override; + virtual void GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextOut) const override; + //~End of UEdGraphNode interface + + //~UK2Node interface + virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override; + virtual void AllocateDefaultPins() override; + //~End of UK2Node interface + +protected: + virtual bool HandleDelegates( + const TArray& VariableOutputs, UEdGraphPin* ProxyObjectPin, + UEdGraphPin*& InOutLastThenPin, UEdGraph* SourceGraph, FKismetCompilerContext& CompilerContext) override; + +private: + + // Add the GetPayload flow to the end of the delegate handler's logic chain + bool HandlePayloadImplementation( + FMulticastDelegateProperty* CurrentProperty, + const FBaseAsyncTaskHelper::FOutputPinAndLocalVariable& ProxyObjectVar, + const FBaseAsyncTaskHelper::FOutputPinAndLocalVariable& PayloadVar, + const FBaseAsyncTaskHelper::FOutputPinAndLocalVariable& ActualChannelVar, + UEdGraphPin*& InOutLastActivatedThenPin, UEdGraph* SourceGraph, FKismetCompilerContext& CompilerContext); + + // Make sure the output Payload wildcard matches the input PayloadType + void RefreshOutputPayloadType(); + + UEdGraphPin* GetPayloadPin() const; + UEdGraphPin* GetPayloadTypePin() const; + UEdGraphPin* GetOutputChannelPin() const; +}; diff --git a/Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/GameplayMessageRuntime.Build.cs b/Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/GameplayMessageRuntime.Build.cs new file mode 100644 index 0000000..6e1194e --- /dev/null +++ b/Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/GameplayMessageRuntime.Build.cs @@ -0,0 +1,30 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class GameplayMessageRuntime : ModuleRules +{ + public GameplayMessageRuntime(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "Engine", + "GameplayTags" + }); + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + }); + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + }); + } +} diff --git a/Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Private/GameFramework/AsyncAction_ListenForGameplayMessage.cpp b/Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Private/GameFramework/AsyncAction_ListenForGameplayMessage.cpp new file mode 100644 index 0000000..42e2c6e --- /dev/null +++ b/Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Private/GameFramework/AsyncAction_ListenForGameplayMessage.cpp @@ -0,0 +1,110 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "GameFramework/AsyncAction_ListenForGameplayMessage.h" + +#include "Engine/Engine.h" +#include "Engine/World.h" +#include "GameFramework/GameplayMessageSubsystem.h" +#include "UObject/ScriptMacros.h" +#include "UObject/Stack.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(AsyncAction_ListenForGameplayMessage) + +UAsyncAction_ListenForGameplayMessage* UAsyncAction_ListenForGameplayMessage::ListenForGameplayMessages(UObject* WorldContextObject, FGameplayTag Channel, UScriptStruct* PayloadType, EGameplayMessageMatch MatchType) +{ + UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull); + if (!World) + { + return nullptr; + } + + UAsyncAction_ListenForGameplayMessage* Action = NewObject(); + Action->WorldPtr = World; + Action->ChannelToRegister = Channel; + Action->MessageStructType = PayloadType; + Action->MessageMatchType = MatchType; + Action->RegisterWithGameInstance(World); + + return Action; +} + +void UAsyncAction_ListenForGameplayMessage::Activate() +{ + if (UWorld* World = WorldPtr.Get()) + { + if (UGameplayMessageSubsystem::HasInstance(World)) + { + UGameplayMessageSubsystem& Router = UGameplayMessageSubsystem::Get(World); + + TWeakObjectPtr WeakThis(this); + ListenerHandle = Router.RegisterListenerInternal(ChannelToRegister, + [WeakThis](FGameplayTag Channel, const UScriptStruct* StructType, const void* Payload) + { + if (UAsyncAction_ListenForGameplayMessage* StrongThis = WeakThis.Get()) + { + StrongThis->HandleMessageReceived(Channel, StructType, Payload); + } + }, + MessageStructType.Get(), + MessageMatchType); + + return; + } + } + + SetReadyToDestroy(); +} + +void UAsyncAction_ListenForGameplayMessage::SetReadyToDestroy() +{ + ListenerHandle.Unregister(); + + Super::SetReadyToDestroy(); +} + +bool UAsyncAction_ListenForGameplayMessage::GetPayload(int32& OutPayload) +{ + checkNoEntry(); + return false; +} + +DEFINE_FUNCTION(UAsyncAction_ListenForGameplayMessage::execGetPayload) +{ + Stack.MostRecentPropertyAddress = nullptr; + Stack.StepCompiledIn(nullptr); + void* MessagePtr = Stack.MostRecentPropertyAddress; + FStructProperty* StructProp = CastField(Stack.MostRecentProperty); + P_FINISH; + + bool bSuccess = false; + + // Make sure the type we are trying to get through the blueprint node matches the type of the message payload received. + if ((StructProp != nullptr) && (StructProp->Struct != nullptr) && (MessagePtr != nullptr) && (StructProp->Struct == P_THIS->MessageStructType.Get()) && (P_THIS->ReceivedMessagePayloadPtr != nullptr)) + { + StructProp->Struct->CopyScriptStruct(MessagePtr, P_THIS->ReceivedMessagePayloadPtr); + bSuccess = true; + } + + *(bool*)RESULT_PARAM = bSuccess; +} + +void UAsyncAction_ListenForGameplayMessage::HandleMessageReceived(FGameplayTag Channel, const UScriptStruct* StructType, const void* Payload) +{ + if (!MessageStructType.Get() || (MessageStructType.Get() == StructType)) + { + ReceivedMessagePayloadPtr = Payload; + + OnMessageReceived.Broadcast(this, Channel); + + ReceivedMessagePayloadPtr = nullptr; + } + + if (!OnMessageReceived.IsBound()) + { + // If the BP object that created the async node is destroyed, OnMessageReceived will be unbound after calling the broadcast. + // In this case we can safely mark this receiver as ready for destruction. + // Need to support a more proactive mechanism for cleanup FORT-340994 + SetReadyToDestroy(); + } +} + diff --git a/Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Private/GameFramework/GameplayMessageRuntime.cpp b/Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Private/GameFramework/GameplayMessageRuntime.cpp new file mode 100644 index 0000000..8890d8e --- /dev/null +++ b/Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Private/GameFramework/GameplayMessageRuntime.cpp @@ -0,0 +1,5 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "Modules/ModuleManager.h" + +IMPLEMENT_MODULE(FDefaultModuleImpl, GameplayMessageRuntime) diff --git a/Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Private/GameFramework/GameplayMessageSubsystem.cpp b/Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Private/GameFramework/GameplayMessageSubsystem.cpp new file mode 100644 index 0000000..cfa0306 --- /dev/null +++ b/Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Private/GameFramework/GameplayMessageSubsystem.cpp @@ -0,0 +1,191 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "GameFramework/GameplayMessageSubsystem.h" +#include "Engine/Engine.h" +#include "Engine/GameInstance.h" +#include "Engine/World.h" +#include "UObject/ScriptMacros.h" +#include "UObject/Stack.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GameplayMessageSubsystem) + +DEFINE_LOG_CATEGORY(LogGameplayMessageSubsystem); + +namespace UE +{ + namespace GameplayMessageSubsystem + { + static int32 ShouldLogMessages = 0; + static FAutoConsoleVariableRef CVarShouldLogMessages(TEXT("GameplayMessageSubsystem.LogMessages"), + ShouldLogMessages, + TEXT("Should messages broadcast through the gameplay message subsystem be logged?")); + } +} + +////////////////////////////////////////////////////////////////////// +// FGameplayMessageListenerHandle + +void FGameplayMessageListenerHandle::Unregister() +{ + if (UGameplayMessageSubsystem* StrongSubsystem = Subsystem.Get()) + { + StrongSubsystem->UnregisterListener(*this); + Subsystem.Reset(); + Channel = FGameplayTag(); + ID = 0; + } +} + +////////////////////////////////////////////////////////////////////// +// UGameplayMessageSubsystem + +UGameplayMessageSubsystem& UGameplayMessageSubsystem::Get(const UObject* WorldContextObject) +{ + UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::Assert); + check(World); + UGameplayMessageSubsystem* Router = UGameInstance::GetSubsystem(World->GetGameInstance()); + check(Router); + return *Router; +} + +bool UGameplayMessageSubsystem::HasInstance(const UObject* WorldContextObject) +{ + UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::Assert); + UGameplayMessageSubsystem* Router = World != nullptr ? UGameInstance::GetSubsystem(World->GetGameInstance()) : nullptr; + return Router != nullptr; +} + +void UGameplayMessageSubsystem::Deinitialize() +{ + ListenerMap.Reset(); + + Super::Deinitialize(); +} + +void UGameplayMessageSubsystem::BroadcastMessageInternal(FGameplayTag Channel, const UScriptStruct* StructType, const void* MessageBytes) +{ + // Log the message if enabled + if (UE::GameplayMessageSubsystem::ShouldLogMessages != 0) + { + FString* pContextString = nullptr; +#if WITH_EDITOR + if (GIsEditor) + { + extern ENGINE_API FString GPlayInEditorContextString; + pContextString = &GPlayInEditorContextString; + } +#endif + + FString HumanReadableMessage; + StructType->ExportText(/*out*/ HumanReadableMessage, MessageBytes, /*Defaults=*/ nullptr, /*OwnerObject=*/ nullptr, PPF_None, /*ExportRootScope=*/ nullptr); + UE_LOG(LogGameplayMessageSubsystem, Log, TEXT("BroadcastMessage(%s, %s, %s)"), pContextString ? **pContextString : *GetPathNameSafe(this), *Channel.ToString(), *HumanReadableMessage); + } + + // Broadcast the message + bool bOnInitialTag = true; + for (FGameplayTag Tag = Channel; Tag.IsValid(); Tag = Tag.RequestDirectParent()) + { + if (const FChannelListenerList* pList = ListenerMap.Find(Tag)) + { + // Copy in case there are removals while handling callbacks + TArray ListenerArray(pList->Listeners); + + for (const FGameplayMessageListenerData& Listener : ListenerArray) + { + if (bOnInitialTag || (Listener.MatchType == EGameplayMessageMatch::PartialMatch)) + { + if (Listener.bHadValidType && !Listener.ListenerStructType.IsValid()) + { + UE_LOG(LogGameplayMessageSubsystem, Warning, TEXT("Listener struct type has gone invalid on Channel %s. Removing listener from list"), *Channel.ToString()); + UnregisterListenerInternal(Channel, Listener.HandleID); + continue; + } + + // The receiving type must be either a parent of the sending type or completely ambiguous (for internal use) + if (!Listener.bHadValidType || StructType->IsChildOf(Listener.ListenerStructType.Get())) + { + Listener.ReceivedCallback(Channel, StructType, MessageBytes); + } + else + { + UE_LOG(LogGameplayMessageSubsystem, Error, TEXT("Struct type mismatch on channel %s (broadcast type %s, listener at %s was expecting type %s)"), + *Channel.ToString(), + *StructType->GetPathName(), + *Tag.ToString(), + *Listener.ListenerStructType->GetPathName()); + } + } + } + } + bOnInitialTag = false; + } +} + +void UGameplayMessageSubsystem::K2_BroadcastMessage(FGameplayTag Channel, const int32& Message) +{ + // This will never be called, the exec version below will be hit instead + checkNoEntry(); +} + +DEFINE_FUNCTION(UGameplayMessageSubsystem::execK2_BroadcastMessage) +{ + P_GET_STRUCT(FGameplayTag, Channel); + + Stack.MostRecentPropertyAddress = nullptr; + Stack.StepCompiledIn(nullptr); + void* MessagePtr = Stack.MostRecentPropertyAddress; + FStructProperty* StructProp = CastField(Stack.MostRecentProperty); + + P_FINISH; + + if (ensure((StructProp != nullptr) && (StructProp->Struct != nullptr) && (MessagePtr != nullptr))) + { + P_THIS->BroadcastMessageInternal(Channel, StructProp->Struct, MessagePtr); + } +} + +FGameplayMessageListenerHandle UGameplayMessageSubsystem::RegisterListenerInternal(FGameplayTag Channel, TFunction&& Callback, const UScriptStruct* StructType, EGameplayMessageMatch MatchType) +{ + FChannelListenerList& List = ListenerMap.FindOrAdd(Channel); + + FGameplayMessageListenerData& Entry = List.Listeners.AddDefaulted_GetRef(); + Entry.ReceivedCallback = MoveTemp(Callback); + Entry.ListenerStructType = StructType; + Entry.bHadValidType = StructType != nullptr; + Entry.HandleID = ++List.HandleID; + Entry.MatchType = MatchType; + + return FGameplayMessageListenerHandle(this, Channel, Entry.HandleID); +} + +void UGameplayMessageSubsystem::UnregisterListener(FGameplayMessageListenerHandle Handle) +{ + if (Handle.IsValid()) + { + check(Handle.Subsystem == this); + + UnregisterListenerInternal(Handle.Channel, Handle.ID); + } + else + { + UE_LOG(LogGameplayMessageSubsystem, Warning, TEXT("Trying to unregister an invalid Handle.")); + } +} + +void UGameplayMessageSubsystem::UnregisterListenerInternal(FGameplayTag Channel, int32 HandleID) +{ + if (FChannelListenerList* pList = ListenerMap.Find(Channel)) + { + int32 MatchIndex = pList->Listeners.IndexOfByPredicate([ID = HandleID](const FGameplayMessageListenerData& Other) { return Other.HandleID == ID; }); + if (MatchIndex != INDEX_NONE) + { + pList->Listeners.RemoveAtSwap(MatchIndex); + } + + if (pList->Listeners.Num() == 0) + { + ListenerMap.Remove(Channel); + } + } +} + diff --git a/Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Public/GameFramework/AsyncAction_ListenForGameplayMessage.h b/Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Public/GameFramework/AsyncAction_ListenForGameplayMessage.h new file mode 100644 index 0000000..f452462 --- /dev/null +++ b/Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Public/GameFramework/AsyncAction_ListenForGameplayMessage.h @@ -0,0 +1,74 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Engine/CancellableAsyncAction.h" +#include "GameplayMessageSubsystem.h" +#include "GameplayMessageTypes2.h" + +#include "AsyncAction_ListenForGameplayMessage.generated.h" + +#define UE_API GAMEPLAYMESSAGERUNTIME_API + +class UScriptStruct; +class UWorld; +struct FFrame; + +/** + * Proxy object pin will be hidden in K2Node_GameplayMessageAsyncAction. Is used to get a reference to the object triggering the delegate for the follow up call of 'GetPayload'. + * + * @param ActualChannel The actual message channel that we received Payload from (will always start with Channel, but may be more specific if partial matches were enabled) + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FAsyncGameplayMessageDelegate, UAsyncAction_ListenForGameplayMessage*, ProxyObject, FGameplayTag, ActualChannel); + +UCLASS(MinimalAPI, BlueprintType, meta=(HasDedicatedAsyncNode)) +class UAsyncAction_ListenForGameplayMessage : public UCancellableAsyncAction +{ + GENERATED_BODY() + +public: + /** + * Asynchronously waits for a gameplay message to be broadcast on the specified channel. + * + * @param Channel The message channel to listen for + * @param PayloadType The kind of message structure to use (this must match the same type that the sender is broadcasting) + * @param MatchType The rule used for matching the channel with broadcasted messages + */ + UFUNCTION(BlueprintCallable, Category = Messaging, meta = (WorldContext = "WorldContextObject", BlueprintInternalUseOnly = "true")) + static UE_API UAsyncAction_ListenForGameplayMessage* ListenForGameplayMessages(UObject* WorldContextObject, FGameplayTag Channel, UScriptStruct* PayloadType, EGameplayMessageMatch MatchType = EGameplayMessageMatch::ExactMatch); + + /** + * Attempt to copy the payload received from the broadcasted gameplay message into the specified wildcard. + * The wildcard's type must match the type from the received message. + * + * @param OutPayload The wildcard reference the payload should be copied into + * @return If the copy was a success + */ + UFUNCTION(BlueprintCallable, CustomThunk, Category = "Messaging", meta = (CustomStructureParam = "OutPayload")) + UE_API bool GetPayload(UPARAM(ref) int32& OutPayload); + + DECLARE_FUNCTION(execGetPayload); + + UE_API virtual void Activate() override; + UE_API virtual void SetReadyToDestroy() override; + +public: + /** Called when a message is broadcast on the specified channel. Use GetPayload() to request the message payload. */ + UPROPERTY(BlueprintAssignable) + FAsyncGameplayMessageDelegate OnMessageReceived; + +private: + void HandleMessageReceived(FGameplayTag Channel, const UScriptStruct* StructType, const void* Payload); + +private: + const void* ReceivedMessagePayloadPtr = nullptr; + + TWeakObjectPtr WorldPtr; + FGameplayTag ChannelToRegister; + TWeakObjectPtr MessageStructType = nullptr; + EGameplayMessageMatch MessageMatchType = EGameplayMessageMatch::ExactMatch; + + FGameplayMessageListenerHandle ListenerHandle; +}; + +#undef UE_API diff --git a/Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Public/GameFramework/GameplayMessageSubsystem.h b/Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Public/GameFramework/GameplayMessageSubsystem.h new file mode 100644 index 0000000..2af6898 --- /dev/null +++ b/Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Public/GameFramework/GameplayMessageSubsystem.h @@ -0,0 +1,240 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "GameplayMessageTypes2.h" +#include "GameplayTagContainer.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "UObject/WeakObjectPtr.h" + +#include "GameplayMessageSubsystem.generated.h" + +#define UE_API GAMEPLAYMESSAGERUNTIME_API + +class UGameplayMessageSubsystem; +struct FFrame; + +GAMEPLAYMESSAGERUNTIME_API DECLARE_LOG_CATEGORY_EXTERN(LogGameplayMessageSubsystem, Log, All); + +class UAsyncAction_ListenForGameplayMessage; + +/** + * An opaque handle that can be used to remove a previously registered message listener + * @see UGameplayMessageSubsystem::RegisterListener and UGameplayMessageSubsystem::UnregisterListener + */ +USTRUCT(BlueprintType) +struct FGameplayMessageListenerHandle +{ +public: + GENERATED_BODY() + + FGameplayMessageListenerHandle() {} + + UE_API void Unregister(); + + bool IsValid() const { return ID != 0; } + +private: + UPROPERTY(Transient) + TWeakObjectPtr Subsystem; + + UPROPERTY(Transient) + FGameplayTag Channel; + + UPROPERTY(Transient) + int32 ID = 0; + + FDelegateHandle StateClearedHandle; + + friend UGameplayMessageSubsystem; + + FGameplayMessageListenerHandle(UGameplayMessageSubsystem* InSubsystem, FGameplayTag InChannel, int32 InID) : Subsystem(InSubsystem), Channel(InChannel), ID(InID) {} +}; + +/** + * Entry information for a single registered listener + */ +USTRUCT() +struct FGameplayMessageListenerData +{ + GENERATED_BODY() + + // Callback for when a message has been received + TFunction ReceivedCallback; + + int32 HandleID; + EGameplayMessageMatch MatchType; + + // Adding some logging and extra variables around some potential problems with this + TWeakObjectPtr ListenerStructType = nullptr; + bool bHadValidType = false; +}; + +/** + * This system allows event raisers and listeners to register for messages without + * having to know about each other directly, though they must agree on the format + * of the message (as a USTRUCT() type). + * + * + * You can get to the message router from the game instance: + * UGameInstance::GetSubsystem(GameInstance) + * or directly from anything that has a route to a world: + * UGameplayMessageSubsystem::Get(WorldContextObject) + * + * Note that call order when there are multiple listeners for the same channel is + * not guaranteed and can change over time! + */ +UCLASS(MinimalAPI) +class UGameplayMessageSubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + + friend UAsyncAction_ListenForGameplayMessage; + +public: + + /** + * @return the message router for the game instance associated with the world of the specified object + */ + static UE_API UGameplayMessageSubsystem& Get(const UObject* WorldContextObject); + + /** + * @return true if a valid GameplayMessageRouter subsystem if active in the provided world + */ + static UE_API bool HasInstance(const UObject* WorldContextObject); + + //~USubsystem interface + UE_API virtual void Deinitialize() override; + //~End of USubsystem interface + + /** + * Broadcast a message on the specified channel + * + * @param Channel The message channel to broadcast on + * @param Message The message to send (must be the same type of UScriptStruct expected by the listeners for this channel, otherwise an error will be logged) + */ + template + void BroadcastMessage(FGameplayTag Channel, const FMessageStructType& Message) + { + const UScriptStruct* StructType = TBaseStructure::Get(); + BroadcastMessageInternal(Channel, StructType, &Message); + } + + /** + * Register to receive messages on a specified channel + * + * @param Channel The message channel to listen to + * @param Callback Function to call with the message when someone broadcasts it (must be the same type of UScriptStruct provided by broadcasters for this channel, otherwise an error will be logged) + * + * @return a handle that can be used to unregister this listener (either by calling Unregister() on the handle or calling UnregisterListener on the router) + */ + template + FGameplayMessageListenerHandle RegisterListener(FGameplayTag Channel, TFunction&& Callback, EGameplayMessageMatch MatchType = EGameplayMessageMatch::ExactMatch) + { + auto ThunkCallback = [InnerCallback = MoveTemp(Callback)](FGameplayTag ActualTag, const UScriptStruct* SenderStructType, const void* SenderPayload) + { + InnerCallback(ActualTag, *reinterpret_cast(SenderPayload)); + }; + + const UScriptStruct* StructType = TBaseStructure::Get(); + return RegisterListenerInternal(Channel, ThunkCallback, StructType, MatchType); + } + + /** + * Register to receive messages on a specified channel and handle it with a specified member function + * Executes a weak object validity check to ensure the object registering the function still exists before triggering the callback + * + * @param Channel The message channel to listen to + * @param Object The object instance to call the function on + * @param Function Member function to call with the message when someone broadcasts it (must be the same type of UScriptStruct provided by broadcasters for this channel, otherwise an error will be logged) + * + * @return a handle that can be used to unregister this listener (either by calling Unregister() on the handle or calling UnregisterListener on the router) + */ + template + FGameplayMessageListenerHandle RegisterListener(FGameplayTag Channel, TOwner* Object, void(TOwner::* Function)(FGameplayTag, const FMessageStructType&)) + { + TWeakObjectPtr WeakObject(Object); + return RegisterListener(Channel, + [WeakObject, Function](FGameplayTag Channel, const FMessageStructType& Payload) + { + if (TOwner* StrongObject = WeakObject.Get()) + { + (StrongObject->*Function)(Channel, Payload); + } + }); + } + + /** + * Register to receive messages on a specified channel with extra parameters to support advanced behavior + * The stateful part of this logic should probably be separated out to a separate system + * + * @param Channel The message channel to listen to + * @param Params Structure containing details for advanced behavior + * + * @return a handle that can be used to unregister this listener (either by calling Unregister() on the handle or calling UnregisterListener on the router) + */ + template + FGameplayMessageListenerHandle RegisterListener(FGameplayTag Channel, FGameplayMessageListenerParams& Params) + { + FGameplayMessageListenerHandle Handle; + + // Register to receive any future messages broadcast on this channel + if (Params.OnMessageReceivedCallback) + { + auto ThunkCallback = [InnerCallback = Params.OnMessageReceivedCallback](FGameplayTag ActualTag, const UScriptStruct* SenderStructType, const void* SenderPayload) + { + InnerCallback(ActualTag, *reinterpret_cast(SenderPayload)); + }; + + const UScriptStruct* StructType = TBaseStructure::Get(); + Handle = RegisterListenerInternal(Channel, ThunkCallback, StructType, Params.MatchType); + } + + return Handle; + } + + /** + * Remove a message listener previously registered by RegisterListener + * + * @param Handle The handle returned by RegisterListener + */ + UE_API void UnregisterListener(FGameplayMessageListenerHandle Handle); + +protected: + /** + * Broadcast a message on the specified channel + * + * @param Channel The message channel to broadcast on + * @param Message The message to send (must be the same type of UScriptStruct expected by the listeners for this channel, otherwise an error will be logged) + */ + UFUNCTION(BlueprintCallable, CustomThunk, Category=Messaging, meta=(CustomStructureParam="Message", AllowAbstract="false", DisplayName="Broadcast Message")) + UE_API void K2_BroadcastMessage(FGameplayTag Channel, const int32& Message); + + DECLARE_FUNCTION(execK2_BroadcastMessage); + +private: + // Internal helper for broadcasting a message + UE_API void BroadcastMessageInternal(FGameplayTag Channel, const UScriptStruct* StructType, const void* MessageBytes); + + // Internal helper for registering a message listener + UE_API FGameplayMessageListenerHandle RegisterListenerInternal( + FGameplayTag Channel, + TFunction&& Callback, + const UScriptStruct* StructType, + EGameplayMessageMatch MatchType); + + UE_API void UnregisterListenerInternal(FGameplayTag Channel, int32 HandleID); + +private: + // List of all entries for a given channel + struct FChannelListenerList + { + TArray Listeners; + int32 HandleID = 0; + }; + +private: + TMap ListenerMap; +}; + +#undef UE_API diff --git a/Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Public/GameFramework/GameplayMessageTypes2.h b/Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Public/GameFramework/GameplayMessageTypes2.h new file mode 100644 index 0000000..e750cf9 --- /dev/null +++ b/Plugins/GameplayMessageRouter/Source/GameplayMessageRuntime/Public/GameFramework/GameplayMessageTypes2.h @@ -0,0 +1,51 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "GameplayTagContainer.h" +#include "Kismet/BlueprintFunctionLibrary.h" + +#include "GameplayMessageTypes2.generated.h" + +class UGameplayMessageRouter; + +// Match rule for message listeners +UENUM(BlueprintType) +enum class EGameplayMessageMatch : uint8 +{ + // An exact match will only receive messages with exactly the same channel + // (e.g., registering for "A.B" will match a broadcast of A.B but not A.B.C) + ExactMatch, + + // A partial match will receive any messages rooted in the same channel + // (e.g., registering for "A.B" will match a broadcast of A.B as well as A.B.C) + PartialMatch +}; + +/** + * Struct used to specify advanced behavior when registering a listener for gameplay messages + */ +template +struct FGameplayMessageListenerParams +{ + /** Whether Callback should be called for broadcasts of more derived channels or if it will only be called for exact matches. */ + EGameplayMessageMatch MatchType = EGameplayMessageMatch::ExactMatch; + + /** If bound this callback will trigger when a message is broadcast on the specified channel. */ + TFunction OnMessageReceivedCallback; + + /** Helper to bind weak member function to OnMessageReceivedCallback */ + template + void SetMessageReceivedCallback(TOwner* Object, void(TOwner::* Function)(FGameplayTag, const FMessageStructType&)) + { + TWeakObjectPtr WeakObject(Object); + OnMessageReceivedCallback = [WeakObject, Function](FGameplayTag Channel, const FMessageStructType& Payload) + { + if (TOwner* StrongObject = WeakObject.Get()) + { + (StrongObject->*Function)(Channel, Payload); + } + }; + } +}; +