Skip to content

Commit

Permalink
[#8] Implement a Custom, Generic "Invert" Blueprint Node for TMaps
Browse files Browse the repository at this point in the history
This finally adds the ability to easily invert any type of map in a
blueprint, as long as that map has a value type that can be used as a
key (the type must implement `GetTypeHash()`).

This functionality is too complex to express through normal metadata on
a `UFUNCTION` because the key and value types on the return value are
expected to be the inverse of the key and value types of the input
value. To accomplish that feat, a custom K2Node was implemented that
initially appears with a wildcard input and output pin. Once the input
pin has been attached to a non-wildcard map input, the output will
automatically assume the inverse of the input type.

At blueprint compilation time, the custom K2Node is expanded to a
hidden, internal-only node from the new `PF2MapLibrary` blueprint
function library. The internal node is defined with a custom thunk and
accepts a wildcard input and wildcard map output. Blueprint would never
allow this node to be used directly since the output pin provides no
type information to later nodes that receive input from it. Instead, the
custom K2Node forces the types of the pins on the internal node to match
the types on its own pins. For example, if the K2Node received an input
of type "const Map<String, Int>" then the internal node will be wired up
to accept "const Map<String, Int>" and return "const Map<Int, String>".

Since the internal node accepts wildcard parameters, it has a custom
thunk to extract the input and output map references off the stack. The
implementation then calls the `GenericMap_Invert` native code to perform
the actual invert operation by swapping the pointers of every pair from
the source map.

This is a LOT of code for what should be a straightforward use case, but
it is understandable given that the type safety built into Blueprint is
there for a reason and can't reasonably be expected to support this use
case easily since the types on the map are changing radically during the
call and are generic to begin win.

The only known limitation is that the constness of map values is not
preserved during the inversion. This is a side effect of the fact that
blueprint doesn't have a way to track the constness of map keys; it only
tracks the constness of map values and the map itself. So, after
inversion, we will lose the constness of the map values (which are now
the keys).
  • Loading branch information
GuyPaddock committed May 1, 2023
1 parent f964fc6 commit 002c64e
Show file tree
Hide file tree
Showing 9 changed files with 793 additions and 2 deletions.
8 changes: 6 additions & 2 deletions Source/OpenPF2Core/OpenPF2Core.Build.cs
Expand Up @@ -4,6 +4,7 @@
// distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
using UnrealBuildTool;

// ReSharper disable once InconsistentNaming
public class OpenPF2Core: ModuleRules
{
public OpenPF2Core(ReadOnlyTargetRules Target) : base(Target)
Expand All @@ -13,7 +14,7 @@ public OpenPF2Core(ReadOnlyTargetRules Target) : base(Target)
PrivatePCHHeaderFile = "Public/OpenPF2Core.h";

PublicDependencyModuleNames.AddRange(
new string[]
new[]
{
"Core",
"AIModule",
Expand All @@ -24,11 +25,14 @@ public OpenPF2Core(ReadOnlyTargetRules Target) : base(Target)
);

PrivateDependencyModuleNames.AddRange(
new string[]
new[]
{
"BlueprintGraph",
"CoreUObject",
"Engine",
"GameplayAbilities",
"KismetCompiler",
"UnrealEd",
"Slate",
"SlateCore",
}
Expand Down
292 changes: 292 additions & 0 deletions Source/OpenPF2Core/Private/Libraries/PF2K2Node_MapInvert.cpp
@@ -0,0 +1,292 @@
// OpenPF2 for UE Game Logic, Copyright 2023, Guy Elsmore-Paddock. All Rights Reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not
// distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.

// ReSharper disable CppRedundantQualifier

#include "Libraries/PF2K2Node_MapInvert.h"

#include <BlueprintActionDatabaseRegistrar.h>
#include <BlueprintNodeSpawner.h>
#include <EdGraphSchema_K2.h>
#include <K2Node_CallFunction.h>
#include <KismetCompiler.h>

#include <Framework/Notifications/NotificationManager.h>

#include <Kismet2/BlueprintEditorUtils.h>

#include <Widgets/Notifications/SNotificationList.h>

#include "Libraries/PF2MapLibrary.h"

#include "Utilities/PF2BlueprintUtilities.h"

#define LOCTEXT_NAMESPACE "K2Node_MapInvert"

const FName UPF2K2Node_MapInvert::InputPinName = TEXT("Map");
const FName UPF2K2Node_MapInvert::OutputPinName = TEXT("InvertedMap");

FText UPF2K2Node_MapInvert::GetMenuCategory() const
{
return LOCTEXT("MapMenuCategory", "OpenPF2|Utility|Map");
}

void UPF2K2Node_MapInvert::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const
{
UClass* Action = this->GetClass();

if (ActionRegistrar.IsOpenForRegistration(Action))
{
UBlueprintNodeSpawner* Spawner = UBlueprintNodeSpawner::Create(Action);
check(Spawner != nullptr);

ActionRegistrar.AddBlueprintAction(Action, Spawner);
}
}

FText UPF2K2Node_MapInvert::GetNodeTitle(const ENodeTitleType::Type TitleType) const
{
if (TitleType == ENodeTitleType::MenuTitle)
{
return LOCTEXT("InvertMapTitle", "Invert");
}
else
{
return LOCTEXT("InvertMapTitle", "INVERT");
}
}

FText UPF2K2Node_MapInvert::GetTooltipText() const
{
return LOCTEXT(
"InvertMapTooltip",
"Inverts the keys and values of a map, so that for each pair the key becomes the value and vice-versa."
);
}

void UPF2K2Node_MapInvert::AllocateDefaultPins()
{
UEdGraphNode::FCreatePinParams InputParams,
OutputParams;

// Input pin.
InputParams.ContainerType = EPinContainerType::Map;
CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Wildcard, UPF2K2Node_MapInvert::InputPinName, InputParams);

// Output pin.
OutputParams.ContainerType = EPinContainerType::Map;
CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Wildcard, UPF2K2Node_MapInvert::OutputPinName, OutputParams);
}

void UPF2K2Node_MapInvert::NotifyPinConnectionListChanged(UEdGraphPin* Pin)
{
Super::NotifyPinConnectionListChanged(Pin);

this->PropagateLinkedPinType(Pin);
}

void UPF2K2Node_MapInvert::PostReconstructNode()
{
Super::PostReconstructNode();

// We only propagate type changes that originate from the input pin (see PropagateLinkedPinType() for why).
this->PropagateLinkedPinType(this->GetInputPin());
}

void UPF2K2Node_MapInvert::ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
{
UEdGraphPin *InputPin = this->GetInputPin(),
*OutputPin = this->GetOutputPin();
UK2Node_CallFunction *PinCallFunction;
const UFunction *Function;
UEdGraphPin *InvertInputMap,
*InvertOutputMap;

Super::ExpandNode(CompilerContext, SourceGraph);

PinCallFunction = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, SourceGraph);

Function =
UPF2MapLibrary::StaticClass()->FindFunctionByName(GET_FUNCTION_NAME_CHECKED(UPF2MapLibrary, InvertMap));

PinCallFunction->SetFromFunction(Function);
PinCallFunction->AllocateDefaultPins();

InvertInputMap = PinCallFunction->FindPinChecked(TEXT("InputMap"));
InvertOutputMap = PinCallFunction->FindPinChecked(TEXT("OutputMap"));

check((InvertInputMap != nullptr) && (InvertOutputMap != nullptr));

InvertInputMap->PinType = InputPin->PinType;
InvertOutputMap->PinType = OutputPin->PinType;

UE_LOG(
LogPf2CoreBlueprintNodes,
VeryVerbose,
TEXT("[%s] Populated nested input pin (%s) as \"%s\" and nested output pin (%s) to \"%s\"."),
*(this->GetIdForLogs()),
*(InvertInputMap->GetName()),
*(PF2BlueprintUtilities::GetTypeDescription(InvertInputMap->PinType).ToString()),
*(InvertOutputMap->GetName()),
*(PF2BlueprintUtilities::GetTypeDescription(InvertOutputMap->PinType).ToString())
);

CompilerContext.MovePinLinksToIntermediate(*InputPin, *InvertInputMap);
CompilerContext.MovePinLinksToIntermediate(*OutputPin, *InvertOutputMap);

// Break any links to the expanded node, now that we've replaced it with a call to the real thing.
this->BreakAllNodeLinks();
}

FString UPF2K2Node_MapInvert::GetIdForLogs() const
{
return this->GetFullName();
}

UEdGraphPin* UPF2K2Node_MapInvert::GetInputPin() const
{
return this->FindPinChecked(UPF2K2Node_MapInvert::InputPinName, EGPD_Input);
}

UEdGraphPin* UPF2K2Node_MapInvert::GetOutputPin() const
{
return this->FindPinChecked(UPF2K2Node_MapInvert::OutputPinName, EGPD_Output);
}

void UPF2K2Node_MapInvert::PropagateLinkedPinType(UEdGraphPin* LocalPin)
{
UEdGraphPin* InputPin = this->GetInputPin();
UEdGraphPin* OutputPin = this->GetOutputPin();

if ((LocalPin == InputPin) || (LocalPin == OutputPin))
{
const UEdGraphPin* ConnectedToPin = (LocalPin->LinkedTo.Num() > 0) ? LocalPin->LinkedTo[0] : nullptr;
const bool IsInputConnected = (InputPin->LinkedTo.Num() != 0);
const bool IsOutputConnected = (OutputPin->LinkedTo.Num() != 0);

if (ConnectedToPin == nullptr)
{
// If both input and output pins are unlinked, then reset the types of both to wildcard.
if (!IsInputConnected && !IsOutputConnected)
{
this->ResetPinToWildcard(InputPin);
this->ResetPinToWildcard(OutputPin);
}
}
else
{
// We only propagate type changes that originate from the input pin.
//
// In an earlier draft, we propagated type changes from the output pins the same way that we do for the
// input pins, but this created a "constness" conflict if the output pin of this node was connected to a
// const input pin in another node, since that would force the input pin of this node to be const when it
// didn't need to be.
if (LocalPin == InputPin)
{
this->PropagatePinType(ConnectedToPin, LocalPin);
}
}
}
}

// ReSharper disable once CppMemberFunctionMayBeConst
void UPF2K2Node_MapInvert::PropagatePinType(const UEdGraphPin* OtherPin, const UEdGraphPin* LocalPin)
{
UEdGraphPin* InputPin = this->GetInputPin();
UEdGraphPin* OutputPin = this->GetOutputPin();
const FEdGraphPinType ConnectedPinType = OtherPin->PinType;

if ((ConnectedPinType.PinCategory != UEdGraphSchema_K2::PC_Wildcard) &&
(ConnectedPinType.PinValueType.TerminalCategory != UEdGraphSchema_K2::PC_Wildcard))
{
check(OtherPin != InputPin);

if (LocalPin == InputPin)
{
this->PropagatePinType(ConnectedPinType, InputPin, OutputPin);
}

if (LocalPin == OutputPin)
{
this->PropagatePinType(ConnectedPinType, OutputPin, InputPin);
}

this->ValidateKeyType();
}
}

// ReSharper disable once CppMemberFunctionMayBeConst
void UPF2K2Node_MapInvert::PropagatePinType(const FEdGraphPinType& PinType,
UEdGraphPin* RegularTargetPin,
UEdGraphPin* InverseTargetPin)
{
RegularTargetPin->PinType = PinType;
InverseTargetPin->PinType = PF2BlueprintUtilities::InvertMapPinType(PinType);

UE_LOG(
LogPf2CoreBlueprintNodes,
VeryVerbose,
TEXT("[%s] Changed local pin (%s) to \"%s\" and local pin (%s) to \"%s\"."),
*(this->GetIdForLogs()),
*(RegularTargetPin->GetName()),
*(PF2BlueprintUtilities::GetTypeDescription(RegularTargetPin->PinType).ToString()),
*(InverseTargetPin->GetName()),
*(PF2BlueprintUtilities::GetTypeDescription(InverseTargetPin->PinType).ToString())
);
}

// ReSharper disable once CppMemberFunctionMayBeConst
void UPF2K2Node_MapInvert::ValidateKeyType()
{
UEdGraphPin* InputPin = this->GetInputPin();
UEdGraphPin* OutputPin = this->GetOutputPin();
const FEdGraphPinType PinType = OutputPin->PinType;

if (!FBlueprintEditorUtils::HasGetTypeHash(PinType))
{
// Inform user via toast why the type change was exceptional and clear the pin because the key type
// cannot be hashed.
const FText NotificationText =
FText::Format(
LOCTEXT(
"TypeCannotBeHashed",
"A map of type '{0}' cannot be inverted because the value type does not have a GetTypeHash function. Maps require a hash function to insert and find elements"
),
UEdGraphSchema_K2::TypeToText(PinType)
);

FNotificationInfo Info(NotificationText);

Info.FadeInDuration = 0.0f;
Info.FadeOutDuration = 0.0f;
Info.ExpireDuration = 10.0f;

InputPin->BreakAllPinLinks();
OutputPin->BreakAllPinLinks();
this->NotifyPinConnectionListChanged(OutputPin);

FSlateNotificationManager::Get().AddNotification(Info);
}
}

// ReSharper disable once CppMemberFunctionMayBeConst
void UPF2K2Node_MapInvert::ResetPinToWildcard(UEdGraphPin* TargetPin)
{
UE_LOG(
LogPf2CoreBlueprintNodes,
VeryVerbose,
TEXT("[%s] Resetting pin (%s) on Invert node to being a wildcard."),
*(this->GetIdForLogs()),
*(TargetPin->GetName())
);

TargetPin->BreakAllPinLinks();

TargetPin->PinType.ResetToDefaults();
TargetPin->PinType.ContainerType = EPinContainerType::Map;
TargetPin->PinType.PinCategory = UEdGraphSchema_K2::PC_Wildcard;
}

#undef LOCTEXT_NAMESPACE
58 changes: 58 additions & 0 deletions Source/OpenPF2Core/Private/Libraries/PF2MapLibrary.cpp
@@ -0,0 +1,58 @@
// OpenPF2 for UE Game Logic, Copyright 2023, Guy Elsmore-Paddock. All Rights Reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not
// distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.

// ReSharper disable CppRedundantQualifier

#include "Libraries/PF2MapLibrary.h"

DEFINE_FUNCTION(UPF2MapLibrary::execInvertMap)
{
P_GET_TMAP_REF_UNCHECKED(InputMap);
P_GET_TMAP_REF_UNCHECKED(OutputMap);
P_FINISH;

P_NATIVE_BEGIN;
UPF2MapLibrary::GenericMap_Invert(InputMapAddr, InputMapProperty, OutputMapAddr, OutputMapProperty);
P_NATIVE_END;
}

// ReSharper disable IdentifierTypo
void UPF2MapLibrary::GenericMap_Invert(const void* InputMapAddr,
const FMapProperty* InputMapProperty,
const void* OutputMapAddr,
const FMapProperty* OutputMapProperty)
{
FScriptMapHelper InputMapHelper = FScriptMapHelper(InputMapProperty, InputMapAddr);
FScriptMapHelper OutputMapHelper = FScriptMapHelper(OutputMapProperty, OutputMapAddr);

OutputMapHelper.EmptyValues(InputMapHelper.Num());

for (FScriptMapHelper::FIterator InputMapIt = InputMapHelper.CreateIterator(); InputMapIt; ++InputMapIt)
{
const uint8* InputMapKeyPtr = InputMapHelper.GetKeyPtr(*InputMapIt);
const uint8* InputMapValuePtr = InputMapHelper.GetValuePtr(*InputMapIt);

// Search for a _key_ in the output map that has a hash equal to that of the _value_ from the input map.
if (OutputMapHelper.FindValueFromHash(InputMapValuePtr))
{
FString ValueString;
const FProperty* OutputKeyProperty = OutputMapHelper.GetKeyProperty();

OutputKeyProperty->ExportTextItem_Direct(ValueString, InputMapValuePtr, nullptr, nullptr, PPF_None);

UE_LOG(
LogPf2CoreBlueprintNodes,
Warning,
TEXT("GenericMap_Invert: Key (%s) already exists in output map (%s)."),
*ValueString,
*(OutputMapProperty->GetFullName())
);
}

// Now, invert the key and value as a new pair in the output map.
OutputMapHelper.AddPair(InputMapValuePtr, InputMapKeyPtr);
}
}
// ReSharper restore IdentifierTypo
1 change: 1 addition & 0 deletions Source/OpenPF2Core/Private/OpenPF2Core.cpp
Expand Up @@ -28,6 +28,7 @@ IMPLEMENT_MODULE(FOpenPF2CoreModule, OpenPF2Core)
// =====================================================================================================================
DEFINE_LOG_CATEGORY(LogPf2Core);
DEFINE_LOG_CATEGORY(LogPf2CoreAbilities);
DEFINE_LOG_CATEGORY(LogPf2CoreBlueprintNodes);
DEFINE_LOG_CATEGORY(LogPf2CoreEncounters);
DEFINE_LOG_CATEGORY(LogPf2CoreInitiative);
DEFINE_LOG_CATEGORY(LogPf2CoreStats);
Expand Down

0 comments on commit 002c64e

Please sign in to comment.