Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[#8] Implement a Custom, Generic "Invert" Blueprint Node for TMaps
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
1 parent
f964fc6
commit 002c64e
Showing
9 changed files
with
793 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
292 changes: 292 additions & 0 deletions
292
Source/OpenPF2Core/Private/Libraries/PF2K2Node_MapInvert.cpp
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.