You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
If a node has exposed pins, it will now show up as 'fast path' when all of its pins are constant (as well as when it is actually using the fast path). If a node has no pins exposed it still does not show up as 'fast path', by design to reduce visual clutter. #jira UE-184014 #rb Jurre.deBaare [CL 26738604 by Thomas Sarkanen in 5.3 branch]
1316 lines
53 KiB
C++
1316 lines
53 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "AnimBlueprintExtension_Base.h"
|
|
#include "AnimBlueprintExtension.h"
|
|
#include "AnimBlueprintExtension_PropertyAccess.h"
|
|
#include "AnimGraphNode_Base.h"
|
|
#include "AnimationGraphSchema.h"
|
|
#include "AnimGraphNode_CustomProperty.h"
|
|
#include "K2Node_CustomEvent.h"
|
|
#include "K2Node_VariableSet.h"
|
|
#include "K2Node_StructMemberSet.h"
|
|
#include "K2Node_StructMemberGet.h"
|
|
#include "K2Node_CallArrayFunction.h"
|
|
#include "Kismet/KismetArrayLibrary.h"
|
|
#include "K2Node_Knot.h"
|
|
#include "String/ParseTokens.h"
|
|
#include "K2Node_VariableGet.h"
|
|
#include "K2Node_BreakStruct.h"
|
|
#include "K2Node_MakeStruct.h"
|
|
#include "Kismet/KismetMathLibrary.h"
|
|
#include "K2Node_TransitionRuleGetter.h"
|
|
#include "Animation/AnimNode_LinkedAnimGraph.h"
|
|
#include "Kismet2/BlueprintEditorUtils.h"
|
|
#include "PropertyAccessCompilerHandler.h"
|
|
#include "IPropertyAccessEditor.h"
|
|
#include "IPropertyAccessCompiler.h"
|
|
#include "IAnimBlueprintGeneratedClassCompiledData.h"
|
|
#include "IAnimBlueprintCompilerCreationContext.h"
|
|
#include "IAnimBlueprintCompilationContext.h"
|
|
#include "IAnimBlueprintCopyTermDefaultsContext.h"
|
|
#include "IAnimBlueprintPostExpansionStepContext.h"
|
|
#include "IAnimBlueprintCompilationBracketContext.h"
|
|
#include "Features/IModularFeatures.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "AnimBlueprintExtension_Base"
|
|
|
|
DECLARE_CYCLE_STAT(TEXT("Create Evaluation Handler"), EAnimBlueprintCompilerStats_CreateEvaluationHandler, STATGROUP_KismetCompiler )
|
|
DECLARE_CYCLE_STAT(TEXT("Create Evaluation Handler - Node Properties"), EAnimBlueprintCompilerStats_CreateEvaluationHandler_NodeProperties, STATGROUP_KismetCompiler )
|
|
DECLARE_CYCLE_STAT(TEXT("Create Evaluation Handler - Create Assignment Node"), EAnimBlueprintCompilerStats_CreateEvaluationHandler_CreateAssignmentNode, STATGROUP_KismetCompiler )
|
|
DECLARE_CYCLE_STAT(TEXT("Create Evaluation Handler - Create Instance Assignment Node"), EAnimBlueprintCompilerStats_CreateEvaluationHandler_CreateInstanceAssignmentNode, STATGROUP_KismetCompiler )
|
|
DECLARE_CYCLE_STAT(TEXT("Create Evaluation Handler - Create Instance Assignment Node - Build Property List"), EAnimBlueprintCompilerStats_CreateEvaluationHandler_CreateInstanceAssignmentNode_BuildPropertyList, STATGROUP_KismetCompiler )
|
|
DECLARE_CYCLE_STAT(TEXT("Create Evaluation Handler - Create Instance Assignment Node - Create Visible Pins"), EAnimBlueprintCompilerStats_CreateEvaluationHandler_CreateInstanceAssignmentNode_CreateVisiblePins, STATGROUP_KismetCompiler )
|
|
|
|
void UAnimBlueprintExtension_Base::HandleCopyTermDefaultsToDefaultObject(UObject* InDefaultObject, IAnimBlueprintCopyTermDefaultsContext& InCompilationContext, IAnimBlueprintExtensionCopyTermDefaultsContext& InPerExtensionContext)
|
|
{
|
|
UAnimInstance* DefaultAnimInstance = Cast<UAnimInstance>(InDefaultObject);
|
|
|
|
if(DefaultAnimInstance)
|
|
{
|
|
// Update blueprint usage of all graph nodes that have properties exposed
|
|
for (const TPair<UAnimGraphNode_Base*, FEvaluationHandlerRecord>& EvaluationHandlerPair : PerNodeStructEvalHandlers)
|
|
{
|
|
if (EvaluationHandlerPair.Value.bHasProperties && EvaluationHandlerPair.Value.ServicedProperties.Num() == 0)
|
|
{
|
|
UAnimGraphNode_Base* Node = CastChecked<UAnimGraphNode_Base>(EvaluationHandlerPair.Key);
|
|
UAnimGraphNode_Base* TrueNode = InCompilationContext.GetMessageLog().FindSourceObjectTypeChecked<UAnimGraphNode_Base>(Node);
|
|
TrueNode->BlueprintUsage = EBlueprintUsage::DoesNotUseBlueprint;
|
|
}
|
|
}
|
|
|
|
for(const FEvaluationHandlerRecord& EvaluationHandler : ValidEvaluationHandlerList)
|
|
{
|
|
if(EvaluationHandler.AnimGraphNode)
|
|
{
|
|
UAnimGraphNode_Base* Node = CastChecked<UAnimGraphNode_Base>(EvaluationHandler.AnimGraphNode);
|
|
UAnimGraphNode_Base* TrueNode = InCompilationContext.GetMessageLog().FindSourceObjectTypeChecked<UAnimGraphNode_Base>(Node);
|
|
|
|
if(EvaluationHandler.EvaluationHandlerIdx != INDEX_NONE && EvaluationHandler.ServicedProperties.Num() > 0)
|
|
{
|
|
const FAnimNodeSinglePropertyHandler& Handler = EvaluationHandler.ServicedProperties.CreateConstIterator()->Value;
|
|
check(Handler.CopyRecords.Num() > 0);
|
|
|
|
const FExposedValueHandler& ValueHandler = Subsystem.ExposedValueHandlers[ EvaluationHandler.EvaluationHandlerIdx ];
|
|
TrueNode->BlueprintUsage = ValueHandler.BoundFunction != NAME_None ? EBlueprintUsage::UsesBlueprint : EBlueprintUsage::DoesNotUseBlueprint;
|
|
|
|
#if WITH_EDITORONLY_DATA // ANIMINST_PostCompileValidation
|
|
const bool bWarnAboutBlueprintUsage = InCompilationContext.GetAnimBlueprint()->bWarnAboutBlueprintUsage || DefaultAnimInstance->PCV_ShouldWarnAboutNodesNotUsingFastPath();
|
|
const bool bNotifyAboutBlueprintUsage = DefaultAnimInstance->PCV_ShouldNotifyAboutNodesNotUsingFastPath();
|
|
#else
|
|
const bool bWarnAboutBlueprintUsage = InCompilationContext.GetAnimBlueprint()->bWarnAboutBlueprintUsage;
|
|
const bool bNotifyAboutBlueprintUsage = false;
|
|
#endif
|
|
if ((TrueNode->BlueprintUsage == EBlueprintUsage::UsesBlueprint) && (bWarnAboutBlueprintUsage || bNotifyAboutBlueprintUsage))
|
|
{
|
|
const FString MessageString = LOCTEXT("BlueprintUsageWarning", "Node @@ uses Blueprint to update its values, access member variables directly or use a constant value for better performance.").ToString();
|
|
if (bWarnAboutBlueprintUsage)
|
|
{
|
|
InCompilationContext.GetMessageLog().Warning(*MessageString, Node);
|
|
}
|
|
else
|
|
{
|
|
InCompilationContext.GetMessageLog().Note(*MessageString, Node);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UAnimBlueprintExtension_Base::HandlePostExpansionStep(const UEdGraph* InGraph, IAnimBlueprintPostExpansionStepContext& InCompilationContext, IAnimBlueprintGeneratedClassCompiledData& OutCompiledData)
|
|
{
|
|
UEdGraph* ConsolidatedEventGraph = InCompilationContext.GetConsolidatedEventGraph();
|
|
if(InGraph == ConsolidatedEventGraph)
|
|
{
|
|
// Skip fast-path generation if the property access system is unavailable.
|
|
// Note that this wont prevent property access 'binding' copy records from running, only
|
|
// old-style 'fast-path' records that are derived from BP pure chains
|
|
if(IModularFeatures::Get().IsModularFeatureAvailable("PropertyAccessEditor"))
|
|
{
|
|
for(FEvaluationHandlerRecord& HandlerRecord : ValidEvaluationHandlerList)
|
|
{
|
|
HandlerRecord.BuildFastPathCopyRecords(InCompilationContext);
|
|
|
|
if(HandlerRecord.IsFastPath())
|
|
{
|
|
for(UEdGraphNode* CustomEventNode : HandlerRecord.CustomEventNodes)
|
|
{
|
|
// Remove custom event nodes as we dont need it any more
|
|
ConsolidatedEventGraph->RemoveNode(CustomEventNode);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cull out all anim nodes as they dont contribute to execution at all
|
|
for (int32 NodeIndex = 0; NodeIndex < ConsolidatedEventGraph->Nodes.Num(); ++NodeIndex)
|
|
{
|
|
if(UAnimGraphNode_Base* Node = Cast<UAnimGraphNode_Base>(ConsolidatedEventGraph->Nodes[NodeIndex]))
|
|
{
|
|
Node->BreakAllNodeLinks();
|
|
ConsolidatedEventGraph->Nodes.RemoveAtSwap(NodeIndex);
|
|
--NodeIndex;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UAnimBlueprintExtension_Base::PatchEvaluationHandlers(IAnimBlueprintCompilationBracketContext& InCompilationContext, IAnimBlueprintGeneratedClassCompiledData& OutCompiledData)
|
|
{
|
|
TArray<FExposedValueHandler>& ExposedValueHandlers = Subsystem.ExposedValueHandlers;
|
|
|
|
// Exposed value handlers indices must match the index of anim node properties,
|
|
// so we iterate over anim node properties here when patching up
|
|
const int32 NumAllocatedNodes = InCompilationContext.GetAllocatedAnimNodeIndices().Num();
|
|
ExposedValueHandlers.SetNum(NumAllocatedNodes);
|
|
for(const TPair<UAnimGraphNode_Base*, int32>& GraphNodePair : InCompilationContext.GetAllocatedAnimNodeIndices())
|
|
{
|
|
if(int32* EvaluationHandlerIndexPtr = ValidEvaluationHandlerMap.Find(GraphNodePair.Key))
|
|
{
|
|
// Indices here are in reverse order with respect to iterated properties as properties are prepended to the linked list when they are added
|
|
const int32 NodePropertyIndex = NumAllocatedNodes - 1 - GraphNodePair.Value;
|
|
|
|
FEvaluationHandlerRecord& EvaluationHandlerRecord = ValidEvaluationHandlerList[*EvaluationHandlerIndexPtr];
|
|
EvaluationHandlerRecord.EvaluationHandlerIdx = NodePropertyIndex;
|
|
EvaluationHandlerRecord.PatchFunctionNameAndCopyRecordsInto(ExposedValueHandlers[NodePropertyIndex]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void UAnimBlueprintExtension_Base::HandleStartCompilingClass(const UClass* InClass, IAnimBlueprintCompilationBracketContext& InCompilationContext, IAnimBlueprintGeneratedClassCompiledData& OutCompiledData)
|
|
{
|
|
PerNodeStructEvalHandlers.Empty();
|
|
ValidEvaluationHandlerList.Empty();
|
|
ValidEvaluationHandlerMap.Empty();
|
|
HandlerFunctionNames.Empty();
|
|
PreLibraryCompiledDelegateHandle.Reset();
|
|
PostLibraryCompiledDelegateHandle.Reset();
|
|
|
|
UAnimBlueprintExtension_PropertyAccess* PropertyAccessExtension = UAnimBlueprintExtension::GetExtension<UAnimBlueprintExtension_PropertyAccess>(GetAnimBlueprint());
|
|
if(PropertyAccessExtension)
|
|
{
|
|
PreLibraryCompiledDelegateHandle = PropertyAccessExtension->OnPreLibraryCompiled().AddLambda([this, PropertyAccessExtension, InClass]()
|
|
{
|
|
if(IModularFeatures::Get().IsModularFeatureAvailable("PropertyAccessEditor"))
|
|
{
|
|
IPropertyAccessEditor& PropertyAccessEditor = IModularFeatures::Get().GetModularFeature<IPropertyAccessEditor>("PropertyAccessEditor");
|
|
|
|
// Build the classes property access library before the library is compiled
|
|
for(FEvaluationHandlerRecord& HandlerRecord : ValidEvaluationHandlerList)
|
|
{
|
|
for(TPair<FName, FAnimNodeSinglePropertyHandler>& PropertyHandler : HandlerRecord.ServicedProperties)
|
|
{
|
|
for(FPropertyCopyRecord& Record : PropertyHandler.Value.CopyRecords)
|
|
{
|
|
if(Record.IsFastPath())
|
|
{
|
|
Record.LibraryHandle = PropertyAccessExtension->AddCopy(Record.SourcePropertyPath, Record.DestPropertyPath, Record.BindingContextId, HandlerRecord.AnimGraphNode);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
PropertyAccessExtension->OnPreLibraryCompiled().Remove(PreLibraryCompiledDelegateHandle);
|
|
});
|
|
|
|
PostLibraryCompiledDelegateHandle = PropertyAccessExtension->OnPostLibraryCompiled().AddLambda([this, PropertyAccessExtension](IAnimBlueprintCompilationBracketContext& InCompilationContext, IAnimBlueprintGeneratedClassCompiledData& OutCompiledData)
|
|
{
|
|
for(FEvaluationHandlerRecord& HandlerRecord : ValidEvaluationHandlerList)
|
|
{
|
|
UAnimGraphNode_Base* OriginalNode = Cast<UAnimGraphNode_Base>(InCompilationContext.GetMessageLog().FindSourceObject(HandlerRecord.AnimGraphNode));
|
|
|
|
// Map global copy index to batched indices
|
|
for(TPair<FName, FAnimNodeSinglePropertyHandler>& PropertyHandler : HandlerRecord.ServicedProperties)
|
|
{
|
|
for(FPropertyCopyRecord& CopyRecord : PropertyHandler.Value.CopyRecords)
|
|
{
|
|
if(CopyRecord.IsFastPath())
|
|
{
|
|
CopyRecord.LibraryCompiledHandle = PropertyAccessExtension->GetCompiledHandle(CopyRecord.LibraryHandle);
|
|
|
|
// Push compiled desc back to original node for feedback
|
|
FName BindingName = CopyRecord.DestProperty->GetFName();
|
|
if(CopyRecord.DestArrayIndex != INDEX_NONE)
|
|
{
|
|
BindingName.SetNumber(CopyRecord.DestArrayIndex + 1);
|
|
}
|
|
|
|
if(FAnimGraphNodePropertyBinding* Binding = OriginalNode->PropertyBindings.Find(BindingName))
|
|
{
|
|
if(CopyRecord.LibraryCompiledHandle.IsValid())
|
|
{
|
|
Binding->CompiledContext = UAnimBlueprintExtension_PropertyAccess::GetCompiledHandleContext(CopyRecord.LibraryCompiledHandle);
|
|
Binding->CompiledContextDesc = UAnimBlueprintExtension_PropertyAccess::GetCompiledHandleContextDesc(CopyRecord.LibraryCompiledHandle);
|
|
}
|
|
else
|
|
{
|
|
Binding->CompiledContext = FText::GetEmpty();
|
|
Binding->CompiledContextDesc = FText::GetEmpty();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
PatchEvaluationHandlers(InCompilationContext, OutCompiledData);
|
|
|
|
PropertyAccessExtension->OnPostLibraryCompiled().Remove(PostLibraryCompiledDelegateHandle);
|
|
});
|
|
}
|
|
}
|
|
|
|
void UAnimBlueprintExtension_Base::HandleFinishCompilingClass(const UClass* InClass, IAnimBlueprintCompilationBracketContext& InCompilationContext, IAnimBlueprintGeneratedClassCompiledData& OutCompiledData)
|
|
{
|
|
UAnimBlueprintExtension_PropertyAccess* PropertyAccessExtension = UAnimBlueprintExtension::GetExtension<UAnimBlueprintExtension_PropertyAccess>(GetAnimBlueprint());
|
|
if(PropertyAccessExtension == nullptr)
|
|
{
|
|
// Without the property access system we need to patch generated function names here
|
|
PatchEvaluationHandlers(InCompilationContext, OutCompiledData);
|
|
}
|
|
}
|
|
|
|
void UAnimBlueprintExtension_Base::ProcessNodePins(UAnimGraphNode_Base* InNode, IAnimBlueprintCompilationContext& InCompilationContext, IAnimBlueprintGeneratedClassCompiledData& OutCompiledData)
|
|
{
|
|
const UAnimationGraphSchema* AnimGraphDefaultSchema = GetDefault<UAnimationGraphSchema>();
|
|
|
|
FStructProperty* NodeProperty = CastFieldChecked<FStructProperty>(InCompilationContext.GetAllocatedPropertiesByNode().FindChecked(InNode));
|
|
|
|
for (auto SourcePinIt = InNode->Pins.CreateIterator(); SourcePinIt; ++SourcePinIt)
|
|
{
|
|
UEdGraphPin* SourcePin = *SourcePinIt;
|
|
bool bConsumed = false;
|
|
|
|
// Register pose links for future use
|
|
if ((SourcePin->Direction == EGPD_Input) && (AnimGraphDefaultSchema->IsPosePin(SourcePin->PinType)))
|
|
{
|
|
// Input pose pin, going to need to be linked up
|
|
FPoseLinkMappingRecord LinkRecord = InNode->GetLinkIDLocation(NodeProperty->Struct, SourcePin);
|
|
if (LinkRecord.IsValid())
|
|
{
|
|
InCompilationContext.AddPoseLinkMappingRecord(LinkRecord);
|
|
bConsumed = true;
|
|
}
|
|
}
|
|
else if(!InNode->ShouldCreateStructEvalHandlers())
|
|
{
|
|
bConsumed = true;
|
|
}
|
|
else
|
|
{
|
|
FEvaluationHandlerRecord& EvalHandler = PerNodeStructEvalHandlers.FindOrAdd(InNode);
|
|
|
|
// The property source for our data, either the struct property for an anim node, or the
|
|
// owning anim instance if using a linked instance node.
|
|
FProperty* SourcePinProperty = nullptr;
|
|
int32 SourceArrayIndex = INDEX_NONE;
|
|
bool bInstancePropertyExists = false;
|
|
|
|
// We have special handling below if we're targeting a linked instance instead of our own instance properties
|
|
UAnimGraphNode_CustomProperty* CustomPropertyNode = Cast<UAnimGraphNode_CustomProperty>(InNode);
|
|
|
|
InNode->GetPinAssociatedProperty(NodeProperty->Struct, SourcePin, /*out*/ SourcePinProperty, /*out*/ SourceArrayIndex);
|
|
|
|
// Does this pin have an associated evaluation handler?
|
|
if(!SourcePinProperty && CustomPropertyNode)
|
|
{
|
|
// Custom property nodes use instance properties not node properties as they aren't UObjects
|
|
// and we can't store non-native properties there
|
|
CustomPropertyNode->GetInstancePinProperty(InCompilationContext, SourcePin, SourcePinProperty);
|
|
bInstancePropertyExists = true;
|
|
}
|
|
|
|
if (SourcePinProperty != NULL)
|
|
{
|
|
EvalHandler.bHasProperties = true;
|
|
|
|
if (SourcePin->LinkedTo.Num() == 0)
|
|
{
|
|
// Literal that can be pushed into the CDO instead of re-evaluated every frame
|
|
bConsumed = true;
|
|
}
|
|
else
|
|
{
|
|
// Dynamic value that needs to be wired up and evaluated each frame
|
|
const FString& EvaluationHandlerStr = SourcePinProperty->GetMetaData(AnimGraphDefaultSchema->NAME_OnEvaluate);
|
|
FName EvaluationHandlerName(*EvaluationHandlerStr);
|
|
if (EvaluationHandlerName != NAME_None)
|
|
{
|
|
// warn that NAME_OnEvaluate is deprecated:
|
|
InCompilationContext.GetMessageLog().Warning(*LOCTEXT("OnEvaluateDeprecated", "OnEvaluate meta data is deprecated, found on @@").ToString(), SourcePinProperty);
|
|
}
|
|
|
|
ensure(EvalHandler.NodeVariableProperty == nullptr || EvalHandler.NodeVariableProperty == NodeProperty);
|
|
EvalHandler.AnimGraphNode = InNode;
|
|
EvalHandler.NodeVariableProperty = NodeProperty;
|
|
EvalHandler.RegisterPin(SourcePin, SourcePinProperty, SourceArrayIndex);
|
|
// if it's not instance property, ensure we mark it
|
|
EvalHandler.bServicesNodeProperties = EvalHandler.bServicesNodeProperties | !bInstancePropertyExists;
|
|
|
|
if (CustomPropertyNode)
|
|
{
|
|
EvalHandler.bServicesInstanceProperties = EvalHandler.bServicesInstanceProperties | bInstancePropertyExists;
|
|
|
|
FAnimNodeSinglePropertyHandler* SinglePropHandler = EvalHandler.ServicedProperties.Find(SourcePinProperty->GetFName());
|
|
check(SinglePropHandler); // Should have been added in RegisterPin
|
|
|
|
// Flag that the target property is actually on the instance class and not the node
|
|
SinglePropHandler->bInstanceIsTarget = bInstancePropertyExists;
|
|
}
|
|
|
|
bConsumed = true;
|
|
}
|
|
|
|
UEdGraphPin* TrueSourcePin = InCompilationContext.GetMessageLog().FindSourcePin(SourcePin);
|
|
if (TrueSourcePin)
|
|
{
|
|
OutCompiledData.GetBlueprintDebugData().RegisterClassPropertyAssociation(TrueSourcePin, SourcePinProperty);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!bConsumed && (SourcePin->Direction == EGPD_Input))
|
|
{
|
|
//@TODO: ANIMREFACTOR: It's probably OK to have certain pins ignored eventually, but this is very helpful during development
|
|
InCompilationContext.GetMessageLog().Note(TEXT("@@ was visible but ignored"), SourcePin);
|
|
}
|
|
}
|
|
|
|
// Add any property bindings
|
|
for(const TPair<FName, FAnimGraphNodePropertyBinding>& PropertyBinding : InNode->PropertyBindings)
|
|
{
|
|
if(PropertyBinding.Value.bIsBound)
|
|
{
|
|
FEvaluationHandlerRecord& EvalHandler = PerNodeStructEvalHandlers.FindOrAdd(InNode);
|
|
EvalHandler.AnimGraphNode = InNode;
|
|
|
|
// for array properties we need to account for the extra FName number
|
|
FName ComparisonName = PropertyBinding.Key;
|
|
ComparisonName.SetNumber(0);
|
|
|
|
if (FProperty* Property = FindFProperty<FProperty>(NodeProperty->Struct, ComparisonName))
|
|
{
|
|
EvalHandler.NodeVariableProperty = NodeProperty;
|
|
EvalHandler.bServicesNodeProperties = true;
|
|
EvalHandler.RegisterPropertyBinding(Property, PropertyBinding.Value);
|
|
}
|
|
else if(FProperty* ClassProperty = FindFProperty<FProperty>(InCompilationContext.GetBlueprint()->SkeletonGeneratedClass, PropertyBinding.Value.PropertyName))
|
|
{
|
|
EvalHandler.NodeVariableProperty = NodeProperty;
|
|
EvalHandler.bServicesInstanceProperties = true;
|
|
EvalHandler.RegisterPropertyBinding(ClassProperty, PropertyBinding.Value);
|
|
}
|
|
else
|
|
{
|
|
InCompilationContext.GetMessageLog().Warning(*FString::Printf(TEXT("ICE: @@ Failed to find a property '%s'"), *ComparisonName.ToString()), InNode);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UAnimBlueprintExtension_Base::CreateEvaluationHandlerForNode(IAnimBlueprintCompilationContext& InCompilationContext, UAnimGraphNode_Base* InNode)
|
|
{
|
|
if(FEvaluationHandlerRecord* RecordPtr = PerNodeStructEvalHandlers.Find(InNode))
|
|
{
|
|
// Generate a new event to update the value of these properties
|
|
FEvaluationHandlerRecord& Record = *RecordPtr;
|
|
|
|
if (Record.NodeVariableProperty)
|
|
{
|
|
CreateEvaluationHandler(InCompilationContext, InNode, Record);
|
|
|
|
RedirectPropertyAccesses(InCompilationContext, InNode, Record);
|
|
|
|
int32 NewIndex = ValidEvaluationHandlerList.Add(Record);
|
|
ValidEvaluationHandlerMap.Add(InNode, NewIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Optional pin manager used to optimize the creation of internal struct member set nodes
|
|
struct FInternalOptionalPinManager : public FOptionalPinManager
|
|
{
|
|
FInternalOptionalPinManager(UAnimGraphNode_Base* InNode, FStructProperty* InNodeProperty, IAnimBlueprintCompilationContext& InCompilationContext)
|
|
: Node(InNode)
|
|
, NodeProperty(InNodeProperty)
|
|
, CompilationContext(InCompilationContext)
|
|
{}
|
|
|
|
void BuildPropertyList(TArray<FOptionalPinFromProperty>& Properties, UStruct* SourceStruct)
|
|
{
|
|
// Build optional pins for all properties
|
|
for(TFieldIterator<FProperty> It(SourceStruct); It; ++It)
|
|
{
|
|
FOptionalPinFromProperty& OptionalPin = Properties.AddDefaulted_GetRef();
|
|
OptionalPin.PropertyName = It->GetFName();
|
|
}
|
|
|
|
// Then expose only those that have records for this node
|
|
for(TFieldIterator<FProperty> It(NodeProperty->Struct); It; ++It)
|
|
{
|
|
if(const IAnimBlueprintCompilationContext::FFoldedPropertyRecord* FoldedPropertyRecord = CompilationContext.GetFoldedPropertyRecord(Node, It->GetFName()))
|
|
{
|
|
// Dont expose array properties here - they are handled by a struct member get-by-ref
|
|
if(!FoldedPropertyRecord->bIsOnClass && !FoldedPropertyRecord->GeneratedProperty->IsA<FArrayProperty>())
|
|
{
|
|
FOptionalPinFromProperty& OptionalPin = Properties[Properties.Num() - 1 - FoldedPropertyRecord->PropertyIndex];
|
|
|
|
check(OptionalPin.PropertyName == FoldedPropertyRecord->GeneratedProperty->GetFName());
|
|
OptionalPin.bShowPin = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Duplicated & re-worked from base class (because we are never re-creating) to optimize our case
|
|
void CreateVisiblePinsEx(TArray<FOptionalPinFromProperty>& Properties, UStruct* SourceStruct, EEdGraphPinDirection Direction, UK2Node* TargetNode)
|
|
{
|
|
const UEdGraphSchema_K2* Schema = GetDefault<UEdGraphSchema_K2>();
|
|
|
|
for (FOptionalPinFromProperty& PropertyEntry : Properties)
|
|
{
|
|
if (PropertyEntry.bShowPin)
|
|
{
|
|
if (FProperty* OuterProperty = FindFieldChecked<FProperty>(SourceStruct, PropertyEntry.PropertyName))
|
|
{
|
|
// Not an array property
|
|
FEdGraphPinType PinType;
|
|
if (Schema->ConvertPropertyToPinType(OuterProperty, /*out*/ PinType))
|
|
{
|
|
// Create the pin
|
|
const FName PinName = PropertyEntry.PropertyName;
|
|
UEdGraphPin* NewPin = TargetNode->CreatePin(Direction, PinType, PinName);
|
|
NewPin->PinFriendlyName = FText::FromString(PropertyEntry.PropertyFriendlyName.IsEmpty() ? PinName.ToString() : PropertyEntry.PropertyFriendlyName);
|
|
NewPin->bNotConnectable = !PropertyEntry.bIsSetValuePinVisible;
|
|
NewPin->bDefaultValueIsIgnored = !PropertyEntry.bIsSetValuePinVisible;
|
|
Schema->ConstructBasicPinTooltip(*NewPin, PropertyEntry.PropertyTooltip, NewPin->PinToolTip);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
UAnimGraphNode_Base* Node;
|
|
FStructProperty* NodeProperty;
|
|
IAnimBlueprintCompilationContext& CompilationContext;
|
|
};
|
|
|
|
void UAnimBlueprintExtension_Base::CreateEvaluationHandler(IAnimBlueprintCompilationContext& InCompilationContext, UAnimGraphNode_Base* InNode, FEvaluationHandlerRecord& Record)
|
|
{
|
|
BP_SCOPED_COMPILER_EVENT_STAT(EAnimBlueprintCompilerStats_CreateEvaluationHandler);
|
|
|
|
// Shouldn't create a handler if there is nothing to work with
|
|
check(Record.ServicedProperties.Num() > 0);
|
|
check(Record.NodeVariableProperty != NULL);
|
|
|
|
const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();
|
|
const UAnimationGraphSchema* AnimGraphDefaultSchema = GetDefault<UAnimationGraphSchema>();
|
|
|
|
// Use the node GUID for a stable name across compiles
|
|
FString FunctionName = FString::Printf(TEXT("%s_%s_%s_%s"), *AnimGraphDefaultSchema->DefaultEvaluationHandlerName.ToString(), *InNode->GetOuter()->GetName(), *InNode->GetClass()->GetName(), *InNode->NodeGuid.ToString());
|
|
Record.HandlerFunctionName = FName(*FunctionName);
|
|
|
|
// check function name isnt already used (data exists that can contain duplicate GUIDs) and apply a numeric extension until it is unique
|
|
int32 ExtensionIndex = 0;
|
|
FName* ExistingName = HandlerFunctionNames.Find(Record.HandlerFunctionName);
|
|
while (ExistingName != nullptr)
|
|
{
|
|
FunctionName = FString::Printf(TEXT("%s_%s_%s_%s_%d"), *AnimGraphDefaultSchema->DefaultEvaluationHandlerName.ToString(), *InNode->GetOuter()->GetName(), *InNode->GetClass()->GetName(), *InNode->NodeGuid.ToString(), ExtensionIndex);
|
|
Record.HandlerFunctionName = FName(*FunctionName);
|
|
ExistingName = HandlerFunctionNames.Find(Record.HandlerFunctionName);
|
|
ExtensionIndex++;
|
|
}
|
|
|
|
HandlerFunctionNames.Add(Record.HandlerFunctionName);
|
|
|
|
// Add a custom event in the graph
|
|
UK2Node_CustomEvent* CustomEventNode = InCompilationContext.SpawnIntermediateEventNode<UK2Node_CustomEvent>(InNode, nullptr, InCompilationContext.GetConsolidatedEventGraph());
|
|
CustomEventNode->bInternalEvent = true;
|
|
CustomEventNode->CustomFunctionName = Record.HandlerFunctionName;
|
|
CustomEventNode->AllocateDefaultPins();
|
|
Record.CustomEventNodes.Add(CustomEventNode);
|
|
|
|
// The ExecChain is the current exec output pin in the linear chain
|
|
UEdGraphPin* ExecChain = K2Schema->FindExecutionPin(*CustomEventNode, EGPD_Output);
|
|
if (Record.bServicesInstanceProperties)
|
|
{
|
|
// Need to create a variable set call for each serviced property in the handler
|
|
for (TPair<FName, FAnimNodeSinglePropertyHandler>& PropHandlerPair : Record.ServicedProperties)
|
|
{
|
|
FAnimNodeSinglePropertyHandler& PropHandler = PropHandlerPair.Value;
|
|
FName PropertyName = PropHandlerPair.Key;
|
|
|
|
// We only want to deal with instance targets in here
|
|
if (PropHandler.bInstanceIsTarget)
|
|
{
|
|
for (FPropertyCopyRecord& CopyRecord : PropHandler.CopyRecords)
|
|
{
|
|
if(CopyRecord.DestPin)
|
|
{
|
|
// New set node for the property
|
|
UK2Node_VariableSet* VarAssignNode = InCompilationContext.SpawnIntermediateNode<UK2Node_VariableSet>(InNode, InCompilationContext.GetConsolidatedEventGraph());
|
|
VarAssignNode->VariableReference.SetSelfMember(CopyRecord.DestProperty->GetFName());
|
|
VarAssignNode->AllocateDefaultPins();
|
|
Record.CustomEventNodes.Add(VarAssignNode);
|
|
|
|
// Wire up the exec line, and update the end of the chain
|
|
UEdGraphPin* ExecVariablesIn = K2Schema->FindExecutionPin(*VarAssignNode, EGPD_Input);
|
|
ExecChain->MakeLinkTo(ExecVariablesIn);
|
|
ExecChain = K2Schema->FindExecutionPin(*VarAssignNode, EGPD_Output);
|
|
|
|
// Find the property pin on the set node and configure
|
|
for (UEdGraphPin* TargetPin : VarAssignNode->Pins)
|
|
{
|
|
FName PinPropertyName(TargetPin->PinName);
|
|
|
|
if (PinPropertyName == PropertyName)
|
|
{
|
|
// This is us, wire up the variable
|
|
UEdGraphPin* DestPin = CopyRecord.DestPin;
|
|
|
|
// Copy the data (link up to the source nodes)
|
|
TargetPin->CopyPersistentDataFromOldPin(*DestPin);
|
|
InCompilationContext.GetMessageLog().NotifyIntermediatePinCreation(TargetPin, DestPin);
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Record.bServicesNodeProperties)
|
|
{
|
|
BP_SCOPED_COMPILER_EVENT_STAT(EAnimBlueprintCompilerStats_CreateEvaluationHandler_NodeProperties);
|
|
|
|
UK2Node_StructMemberSet* AssignmentNode;
|
|
{
|
|
BP_SCOPED_COMPILER_EVENT_STAT(EAnimBlueprintCompilerStats_CreateEvaluationHandler_CreateAssignmentNode);
|
|
|
|
// Create a struct member write node to store the parameters into the animation node
|
|
AssignmentNode = InCompilationContext.SpawnIntermediateNode<UK2Node_StructMemberSet>(InNode, InCompilationContext.GetConsolidatedEventGraph());
|
|
AssignmentNode->VariableReference.SetSelfMember(Record.NodeVariableProperty->GetFName());
|
|
AssignmentNode->StructType = Record.NodeVariableProperty->Struct;
|
|
AssignmentNode->AllocateExecPins();
|
|
|
|
// Simple FOptionalPinManager that exposes all pins. The default used in UK2Node_StructMemberSet::AllocateDefaultPins will hide any that
|
|
// are PinHiddenByDefault, so for this instance we dont want that as users may have exposed some pins
|
|
struct FAssignmentNodeOptionalPinManager : public FOptionalPinManager
|
|
{
|
|
// FOptionalPinManager Interface
|
|
virtual void GetRecordDefaults(FProperty* TestProperty, FOptionalPinFromProperty& Record) const override
|
|
{
|
|
// Pins are always visible
|
|
Record.bCanToggleVisibility = true;
|
|
Record.bShowPin = true;
|
|
}
|
|
};
|
|
|
|
FAssignmentNodeOptionalPinManager OptionalPinManager;
|
|
OptionalPinManager.RebuildPropertyList(AssignmentNode->ShowPinForProperties, AssignmentNode->StructType);
|
|
OptionalPinManager.CreateVisiblePins(AssignmentNode->ShowPinForProperties, AssignmentNode->StructType, EGPD_Input, AssignmentNode);
|
|
|
|
Record.CustomEventNodes.Add(AssignmentNode);
|
|
}
|
|
|
|
// If we have folded properties we will need to set members on the classes generated mutable data block
|
|
const FStructProperty* MutableDataProperty = InCompilationContext.GetMutableDataProperty();
|
|
UK2Node_StructMemberSet* InstanceAssignmentNode = nullptr;
|
|
if(InCompilationContext.IsAnimGraphNodeFolded(InNode) && MutableDataProperty != nullptr)
|
|
{
|
|
BP_SCOPED_COMPILER_EVENT_STAT(EAnimBlueprintCompilerStats_CreateEvaluationHandler_CreateInstanceAssignmentNode);
|
|
|
|
InstanceAssignmentNode = InCompilationContext.SpawnIntermediateNode<UK2Node_StructMemberSet>(InNode, InCompilationContext.GetConsolidatedEventGraph());
|
|
InstanceAssignmentNode->VariableReference.SetSelfMember(MutableDataProperty->GetFName());
|
|
InstanceAssignmentNode->StructType = MutableDataProperty->Struct;
|
|
|
|
// We build this struct member set node using specialized logic to optimize its creation
|
|
// as it can have 1000's of properties harvested from animation nodes
|
|
InstanceAssignmentNode->AllocateExecPins();
|
|
|
|
{
|
|
FInternalOptionalPinManager OptionalPinManager(InNode, InNode->GetFNodeProperty(), InCompilationContext);
|
|
{
|
|
BP_SCOPED_COMPILER_EVENT_STAT(EAnimBlueprintCompilerStats_CreateEvaluationHandler_CreateInstanceAssignmentNode_BuildPropertyList);
|
|
OptionalPinManager.BuildPropertyList(InstanceAssignmentNode->ShowPinForProperties, MutableDataProperty->Struct);
|
|
}
|
|
{
|
|
BP_SCOPED_COMPILER_EVENT_STAT(EAnimBlueprintCompilerStats_CreateEvaluationHandler_CreateInstanceAssignmentNode_CreateVisiblePins);
|
|
OptionalPinManager.CreateVisiblePinsEx(InstanceAssignmentNode->ShowPinForProperties, MutableDataProperty->Struct, EGPD_Input, InstanceAssignmentNode);
|
|
}
|
|
}
|
|
|
|
Record.CustomEventNodes.Add(InstanceAssignmentNode);
|
|
}
|
|
|
|
// Run thru each property
|
|
TSet<FName> PropertiesBeingSet;
|
|
|
|
for (auto TargetPinIt = AssignmentNode->Pins.CreateIterator(); TargetPinIt; ++TargetPinIt)
|
|
{
|
|
UEdGraphPin* TargetPin = *TargetPinIt;
|
|
FName PropertyName(TargetPin->PinName);
|
|
|
|
// Does it get serviced by this handler?
|
|
if (FAnimNodeSinglePropertyHandler* SourceInfo = Record.ServicedProperties.Find(PropertyName))
|
|
{
|
|
// Skip if the property is folded, we should have handled it above
|
|
const IAnimBlueprintCompilationContext::FFoldedPropertyRecord* FoldedPropertyRecord = InCompilationContext.GetFoldedPropertyRecord(InNode, PropertyName);
|
|
|
|
if(FoldedPropertyRecord != nullptr)
|
|
{
|
|
// We only support per-instance members here.
|
|
if(!FoldedPropertyRecord->bIsOnClass)
|
|
{
|
|
// We must have created an assignment node for the mutable data block by now
|
|
check(InstanceAssignmentNode != nullptr);
|
|
|
|
// Redirect to the instance's mutable data area assignment node
|
|
PropertyName = FoldedPropertyRecord->GeneratedProperty->GetFName();
|
|
|
|
// We dont need to handle arrays with a member set, as they use a member get-by-ref
|
|
if(!TargetPin->PinType.IsArray())
|
|
{
|
|
TargetPin = InstanceAssignmentNode->FindPinChecked(FoldedPropertyRecord->GeneratedProperty->GetFName());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (TargetPin->PinType.IsArray())
|
|
{
|
|
// Grab the array that we need to set members for
|
|
UK2Node_StructMemberGet* FetchArrayNode = InCompilationContext.SpawnIntermediateNode<UK2Node_StructMemberGet>(InNode, InCompilationContext.GetConsolidatedEventGraph());
|
|
FetchArrayNode->VariableReference.SetSelfMember(FoldedPropertyRecord ? MutableDataProperty->GetFName() : Record.NodeVariableProperty->GetFName());
|
|
FetchArrayNode->StructType = FoldedPropertyRecord ? MutableDataProperty->Struct : Record.NodeVariableProperty->Struct;
|
|
FetchArrayNode->AllocatePinsForSingleMemberGet(PropertyName);
|
|
Record.CustomEventNodes.Add(FetchArrayNode);
|
|
|
|
UEdGraphPin* ArrayVariableNode = FetchArrayNode->FindPin(PropertyName);
|
|
|
|
if (SourceInfo->CopyRecords.Num() > 0)
|
|
{
|
|
// Set each element in the array
|
|
for (FPropertyCopyRecord& CopyRecord : SourceInfo->CopyRecords)
|
|
{
|
|
int32 ArrayIndex = CopyRecord.DestArrayIndex;
|
|
if(UEdGraphPin* DestPin = CopyRecord.DestPin)
|
|
{
|
|
// Create an array element set node
|
|
UK2Node_CallArrayFunction* ArrayNode = InCompilationContext.SpawnIntermediateNode<UK2Node_CallArrayFunction>(InNode, InCompilationContext.GetConsolidatedEventGraph());
|
|
ArrayNode->FunctionReference.SetExternalMember(GET_FUNCTION_NAME_CHECKED(UKismetArrayLibrary, Array_Set), UKismetArrayLibrary::StaticClass());
|
|
ArrayNode->AllocateDefaultPins();
|
|
Record.CustomEventNodes.Add(ArrayNode);
|
|
|
|
// Connect the execution chain
|
|
ExecChain->MakeLinkTo(ArrayNode->GetExecPin());
|
|
ExecChain = ArrayNode->GetThenPin();
|
|
|
|
// Connect the input array
|
|
UEdGraphPin* TargetArrayPin = ArrayNode->FindPinChecked(TEXT("TargetArray"));
|
|
TargetArrayPin->MakeLinkTo(ArrayVariableNode);
|
|
ArrayNode->PinConnectionListChanged(TargetArrayPin);
|
|
|
|
// Set the array index
|
|
UEdGraphPin* TargetIndexPin = ArrayNode->FindPinChecked(TEXT("Index"));
|
|
TargetIndexPin->DefaultValue = FString::FromInt(ArrayIndex);
|
|
|
|
// Wire up the data input
|
|
UEdGraphPin* TargetItemPin = ArrayNode->FindPinChecked(TEXT("Item"));
|
|
TargetItemPin->CopyPersistentDataFromOldPin(*DestPin);
|
|
InCompilationContext.GetMessageLog().NotifyIntermediatePinCreation(TargetItemPin, DestPin);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Single property
|
|
if (SourceInfo->CopyRecords.Num() > 0 && SourceInfo->CopyRecords[0].DestPin != nullptr)
|
|
{
|
|
UEdGraphPin* DestPin = SourceInfo->CopyRecords[0].DestPin;
|
|
|
|
PropertiesBeingSet.Add(PropertyName);
|
|
TargetPin->CopyPersistentDataFromOldPin(*DestPin);
|
|
InCompilationContext.GetMessageLog().NotifyIntermediatePinCreation(TargetPin, DestPin);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove any unused pins from the assignment nodes to avoid smashing constant values
|
|
bool bAnyNodePropertiesSet = false;
|
|
for (int32 PinIndex = 0; PinIndex < AssignmentNode->ShowPinForProperties.Num(); ++PinIndex)
|
|
{
|
|
FOptionalPinFromProperty& TestProperty = AssignmentNode->ShowPinForProperties[PinIndex];
|
|
TestProperty.bShowPin = PropertiesBeingSet.Contains(TestProperty.PropertyName);
|
|
bAnyNodePropertiesSet |= TestProperty.bShowPin;
|
|
}
|
|
|
|
if(bAnyNodePropertiesSet)
|
|
{
|
|
AssignmentNode->ReconstructNode();
|
|
|
|
UEdGraphPin* ExecVariablesIn = K2Schema->FindExecutionPin(*AssignmentNode, EGPD_Input);
|
|
ExecChain->MakeLinkTo(ExecVariablesIn);
|
|
ExecChain = K2Schema->FindExecutionPin(*AssignmentNode, EGPD_Output);
|
|
}
|
|
|
|
if(InstanceAssignmentNode != nullptr)
|
|
{
|
|
for (int32 PinIndex = 0; PinIndex < InstanceAssignmentNode->ShowPinForProperties.Num(); ++PinIndex)
|
|
{
|
|
FOptionalPinFromProperty& TestProperty = InstanceAssignmentNode->ShowPinForProperties[PinIndex];
|
|
if(TestProperty.bShowPin)
|
|
{
|
|
UEdGraphPin* ExecVariablesIn = K2Schema->FindExecutionPin(*InstanceAssignmentNode, EGPD_Input);
|
|
ExecChain->MakeLinkTo(ExecVariablesIn);
|
|
ExecChain = K2Schema->FindExecutionPin(*InstanceAssignmentNode, EGPD_Output);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UAnimBlueprintExtension_Base::RedirectPropertyAccesses(IAnimBlueprintCompilationContext& InCompilationContext, UAnimGraphNode_Base* InNode, FEvaluationHandlerRecord& InRecord)
|
|
{
|
|
const FStructProperty* MutableDataProperty = InCompilationContext.GetMutableDataProperty();
|
|
if(InCompilationContext.IsAnimGraphNodeFolded(InNode) && MutableDataProperty != nullptr)
|
|
{
|
|
for(TPair<FName, FAnimNodeSinglePropertyHandler>& NamePropertyPair : InRecord.ServicedProperties)
|
|
{
|
|
if(const IAnimBlueprintCompilationContext::FFoldedPropertyRecord* FoldedPropertyRecord = InCompilationContext.GetFoldedPropertyRecord(InNode, NamePropertyPair.Key))
|
|
{
|
|
for(FPropertyCopyRecord& CopyRecord : NamePropertyPair.Value.CopyRecords)
|
|
{
|
|
if(CopyRecord.DestPropertyPath.Num() > 1)
|
|
{
|
|
// If this record writes to the node, switch it to the mutable data's property instead
|
|
if(CopyRecord.DestPropertyPath[0] == InRecord.NodeVariableProperty->GetName())
|
|
{
|
|
CopyRecord.DestPropertyPath[0] = MutableDataProperty->GetName();
|
|
|
|
FString DestPropertyPathTail = CopyRecord.DestPropertyPath[1];
|
|
FString DestPropertyPathWithoutArray = DestPropertyPathTail;
|
|
FString ArrayIndex;
|
|
int32 ArrayDelim = INDEX_NONE;
|
|
if(DestPropertyPathTail.FindChar(TEXT('['), ArrayDelim))
|
|
{
|
|
DestPropertyPathWithoutArray = DestPropertyPathTail.Left(ArrayDelim);
|
|
ArrayIndex = DestPropertyPathTail.RightChop(ArrayDelim);
|
|
}
|
|
|
|
// Switch the destination property from the node's property to the generated one
|
|
if(DestPropertyPathWithoutArray == FoldedPropertyRecord->Property->GetName())
|
|
{
|
|
CopyRecord.DestPropertyPath[1] = FoldedPropertyRecord->GeneratedProperty->GetName() + ArrayIndex;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UAnimBlueprintExtension_Base::FEvaluationHandlerRecord::PatchFunctionNameAndCopyRecordsInto(FExposedValueHandler& Handler) const
|
|
{
|
|
Handler.CopyRecords.Empty();
|
|
|
|
for (const TPair<FName, FAnimNodeSinglePropertyHandler>& ServicedPropPair : ServicedProperties)
|
|
{
|
|
const FName& PropertyName = ServicedPropPair.Key;
|
|
const FAnimNodeSinglePropertyHandler& PropertyHandler = ServicedPropPair.Value;
|
|
|
|
for (const FPropertyCopyRecord& PropertyCopyRecord : PropertyHandler.CopyRecords)
|
|
{
|
|
// Only unbatched copies can be processed on a per-node basis
|
|
// Skip invalid copy indices as these are usually the result of BP errors/warnings
|
|
if(PropertyCopyRecord.LibraryCompiledHandle.IsValid() && PropertyCopyRecord.LibraryCompiledHandle.GetBatchId() == (int32)EAnimPropertyAccessCallSite::WorkerThread_Unbatched)
|
|
{
|
|
Handler.CopyRecords.Emplace(PropertyCopyRecord.LibraryCompiledHandle.GetId(), PropertyCopyRecord.Operation);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!IsFastPath())
|
|
{
|
|
// not all of our pins use copy records so we will need to call our exposed value handler
|
|
Handler.BoundFunction = HandlerFunctionName;
|
|
}
|
|
}
|
|
|
|
static UEdGraphPin* FindFirstInputPin(UEdGraphNode* InNode)
|
|
{
|
|
const UAnimationGraphSchema* Schema = GetDefault<UAnimationGraphSchema>();
|
|
|
|
for(UEdGraphPin* Pin : InNode->Pins)
|
|
{
|
|
if(Pin && Pin->Direction == EGPD_Input && !Schema->IsExecPin(*Pin) && !Schema->IsSelfPin(*Pin))
|
|
{
|
|
return Pin;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
static bool ForEachInputPin(UEdGraphNode* InNode, TFunctionRef<bool(UEdGraphPin*)> InFunction)
|
|
{
|
|
const UAnimationGraphSchema* Schema = GetDefault<UAnimationGraphSchema>();
|
|
bool bResult = false;
|
|
|
|
for(UEdGraphPin* Pin : InNode->Pins)
|
|
{
|
|
if(Pin && Pin->Direction == EGPD_Input && !Schema->IsExecPin(*Pin) && !Schema->IsSelfPin(*Pin))
|
|
{
|
|
bResult |= InFunction(Pin);
|
|
}
|
|
}
|
|
|
|
return bResult;
|
|
}
|
|
|
|
static UEdGraphNode* FollowKnots(UEdGraphPin* FromPin, UEdGraphPin*& ToPin)
|
|
{
|
|
if (FromPin->LinkedTo.Num() == 0)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
UEdGraphPin* LinkedPin = FromPin->LinkedTo[0];
|
|
ToPin = LinkedPin;
|
|
if(LinkedPin)
|
|
{
|
|
UEdGraphNode* LinkedNode = LinkedPin->GetOwningNode();
|
|
UK2Node_Knot* KnotNode = Cast<UK2Node_Knot>(LinkedNode);
|
|
while(KnotNode)
|
|
{
|
|
if(UEdGraphPin* InputPin = FindFirstInputPin(KnotNode))
|
|
{
|
|
if (InputPin->LinkedTo.Num() > 0 && InputPin->LinkedTo[0])
|
|
{
|
|
ToPin = InputPin->LinkedTo[0];
|
|
LinkedNode = InputPin->LinkedTo[0]->GetOwningNode();
|
|
KnotNode = Cast<UK2Node_Knot>(LinkedNode);
|
|
}
|
|
else
|
|
{
|
|
KnotNode = nullptr;
|
|
}
|
|
}
|
|
}
|
|
return LinkedNode;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void UAnimBlueprintExtension_Base::FEvaluationHandlerRecord::RegisterPin(UEdGraphPin* DestPin, FProperty* AssociatedProperty, int32 AssociatedPropertyArrayIndex)
|
|
{
|
|
FAnimNodeSinglePropertyHandler& Handler = ServicedProperties.FindOrAdd(AssociatedProperty->GetFName());
|
|
|
|
TArray<FString> DestPropertyPath;
|
|
|
|
// Prepend the destination property with the node's member property if the property is not on a UClass
|
|
if(Cast<UClass>(AssociatedProperty->Owner.ToUObject()) == nullptr)
|
|
{
|
|
DestPropertyPath.Add(NodeVariableProperty->GetName());
|
|
}
|
|
|
|
if(AssociatedPropertyArrayIndex != INDEX_NONE)
|
|
{
|
|
DestPropertyPath.Add(FString::Printf(TEXT("%s[%d]"), *AssociatedProperty->GetName(), AssociatedPropertyArrayIndex));
|
|
}
|
|
else
|
|
{
|
|
DestPropertyPath.Add(AssociatedProperty->GetName());
|
|
}
|
|
|
|
Handler.CopyRecords.Emplace(DestPin, AssociatedProperty, AssociatedPropertyArrayIndex, MoveTemp(DestPropertyPath));
|
|
}
|
|
|
|
void UAnimBlueprintExtension_Base::FEvaluationHandlerRecord::RegisterPropertyBinding(FProperty* InProperty, const FAnimGraphNodePropertyBinding& InBinding)
|
|
{
|
|
FAnimNodeSinglePropertyHandler& Handler = ServicedProperties.FindOrAdd(InProperty->GetFName());
|
|
|
|
TArray<FString> DestPropertyPath;
|
|
|
|
// Prepend the destination property with the node's member property if the property is not on a UClass
|
|
if(Cast<UClass>(InProperty->Owner.ToUObject()) == nullptr)
|
|
{
|
|
Handler.bInstanceIsTarget = false;
|
|
DestPropertyPath.Add(NodeVariableProperty->GetName());
|
|
}
|
|
else
|
|
{
|
|
Handler.bInstanceIsTarget = true;
|
|
}
|
|
|
|
if(InBinding.ArrayIndex != INDEX_NONE)
|
|
{
|
|
DestPropertyPath.Add(FString::Printf(TEXT("%s[%d]"), *InProperty->GetName(), InBinding.ArrayIndex));
|
|
}
|
|
else
|
|
{
|
|
DestPropertyPath.Add(InProperty->GetName());
|
|
}
|
|
|
|
FPropertyCopyRecord& CopyRecord = Handler.CopyRecords.Emplace_GetRef(InBinding.PropertyPath, DestPropertyPath);
|
|
CopyRecord.DestProperty = InProperty;
|
|
CopyRecord.DestArrayIndex = InBinding.ArrayIndex;
|
|
CopyRecord.BindingContextId = InBinding.ContextId;
|
|
}
|
|
|
|
void UAnimBlueprintExtension_Base::FEvaluationHandlerRecord::BuildFastPathCopyRecords(IAnimBlueprintPostExpansionStepContext& InCompilationContext)
|
|
{
|
|
typedef bool (UAnimBlueprintExtension_Base::FEvaluationHandlerRecord::*GraphCheckerFunc)(FCopyRecordGraphCheckContext&, UEdGraphPin*);
|
|
|
|
GraphCheckerFunc GraphCheckerFuncs[] =
|
|
{
|
|
&UAnimBlueprintExtension_Base::FEvaluationHandlerRecord::CheckForSplitPinAccess,
|
|
&UAnimBlueprintExtension_Base::FEvaluationHandlerRecord::CheckForVariableGet,
|
|
&UAnimBlueprintExtension_Base::FEvaluationHandlerRecord::CheckForLogicalNot,
|
|
&UAnimBlueprintExtension_Base::FEvaluationHandlerRecord::CheckForStructMemberAccess,
|
|
&UAnimBlueprintExtension_Base::FEvaluationHandlerRecord::CheckForArrayAccess,
|
|
};
|
|
|
|
if (GetDefault<UEngine>()->bOptimizeAnimBlueprintMemberVariableAccess)
|
|
{
|
|
for (TPair<FName, FAnimNodeSinglePropertyHandler>& ServicedPropPair : ServicedProperties)
|
|
{
|
|
TArray<FPropertyCopyRecord> AllAdditionalCopyRecords;
|
|
|
|
for (FPropertyCopyRecord& CopyRecord : ServicedPropPair.Value.CopyRecords)
|
|
{
|
|
if(CopyRecord.SourcePropertyPath.Num() == 0)
|
|
{
|
|
TArray<FPropertyCopyRecord> AdditionalCopyRecords;
|
|
|
|
FCopyRecordGraphCheckContext Context(CopyRecord, AdditionalCopyRecords, InCompilationContext.GetMessageLog());
|
|
|
|
for (GraphCheckerFunc& CheckFunc : GraphCheckerFuncs)
|
|
{
|
|
if ((this->*CheckFunc)(Context, CopyRecord.DestPin))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(AdditionalCopyRecords.Num() > 0)
|
|
{
|
|
for(FPropertyCopyRecord& AdditionalCopyRecord : AdditionalCopyRecords)
|
|
{
|
|
CheckForMemberOnlyAccess(AdditionalCopyRecord, AdditionalCopyRecord.DestPin);
|
|
}
|
|
|
|
CopyRecord = AdditionalCopyRecords[0];
|
|
|
|
for(int32 AdditionalRecordIndex = 1; AdditionalRecordIndex < AdditionalCopyRecords.Num(); ++AdditionalRecordIndex)
|
|
{
|
|
AllAdditionalCopyRecords.Add(AdditionalCopyRecords[AdditionalRecordIndex]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CheckForMemberOnlyAccess(CopyRecord, CopyRecord.DestPin);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Append any additional copy records
|
|
ServicedPropPair.Value.CopyRecords.Append(AllAdditionalCopyRecords);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void GetFullyQualifiedPathFromPin(const UEdGraphPin* Pin, TArray<FString>& OutPath)
|
|
{
|
|
FString PinName = Pin->PinName.ToString();
|
|
while (Pin->ParentPin != nullptr)
|
|
{
|
|
PinName[Pin->ParentPin->PinName.GetStringLength()] = TEXT('.');
|
|
Pin = Pin->ParentPin;
|
|
}
|
|
|
|
UE::String::ParseTokens(PinName, TEXT('.'), [&OutPath](FStringView InStringView)
|
|
{
|
|
OutPath.Add(FString(InStringView));
|
|
});
|
|
}
|
|
|
|
bool UAnimBlueprintExtension_Base::FEvaluationHandlerRecord::CheckForVariableGet(FCopyRecordGraphCheckContext& Context, UEdGraphPin* DestPin)
|
|
{
|
|
if(DestPin)
|
|
{
|
|
UEdGraphPin* SourcePin = nullptr;
|
|
if(UK2Node_VariableGet* VariableGetNode = Cast<UK2Node_VariableGet>(FollowKnots(DestPin, SourcePin)))
|
|
{
|
|
if(VariableGetNode && VariableGetNode->IsNodePure() && VariableGetNode->VariableReference.IsSelfContext())
|
|
{
|
|
if(SourcePin)
|
|
{
|
|
GetFullyQualifiedPathFromPin(SourcePin, Context.CopyRecord->SourcePropertyPath);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool UAnimBlueprintExtension_Base::FEvaluationHandlerRecord::CheckForLogicalNot(FCopyRecordGraphCheckContext& Context, UEdGraphPin* DestPin)
|
|
{
|
|
if(DestPin)
|
|
{
|
|
UEdGraphPin* SourcePin = nullptr;
|
|
UK2Node_CallFunction* CallFunctionNode = Cast<UK2Node_CallFunction>(FollowKnots(DestPin, SourcePin));
|
|
if(CallFunctionNode && CallFunctionNode->FunctionReference.GetMemberName() == FName(TEXT("Not_PreBool")))
|
|
{
|
|
// find and follow input pin
|
|
if(UEdGraphPin* InputPin = FindFirstInputPin(CallFunctionNode))
|
|
{
|
|
check(InputPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Boolean);
|
|
if(CheckForVariableGet(Context, InputPin) || CheckForStructMemberAccess(Context, InputPin) || CheckForArrayAccess(Context, InputPin))
|
|
{
|
|
check(Context.CopyRecord->SourcePropertyPath.Num() > 0); // this should have been filled in by CheckForVariableGet() or CheckForStructMemberAccess() above
|
|
Context.CopyRecord->Operation = EPostCopyOperation::LogicalNegateBool;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/** The functions that we can safely native-break */
|
|
static const FName NativeBreakFunctionNameAllowList[] =
|
|
{
|
|
FName(TEXT("BreakVector")),
|
|
FName(TEXT("BreakVector2D")),
|
|
FName(TEXT("BreakRotator")),
|
|
};
|
|
|
|
/** Check whether a native break function can be safely used in the fast-path copy system (ie. source and dest data will be the same) */
|
|
static bool IsNativeBreakAllowed(const FName& InFunctionName)
|
|
{
|
|
for(const FName& FunctionName : NativeBreakFunctionNameAllowList)
|
|
{
|
|
if(InFunctionName == FunctionName)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/** The functions that we can safely native-make */
|
|
static const FName NativeMakeFunctionNameAllowList[] =
|
|
{
|
|
FName(TEXT("MakeVector")),
|
|
FName(TEXT("MakeVector2D")),
|
|
FName(TEXT("MakeRotator")),
|
|
};
|
|
|
|
/** Check whether a native break function can be safely used in the fast-path copy system (ie. source and dest data will be the same) */
|
|
static bool IsNativeMakeAllowed(const FName& InFunctionName)
|
|
{
|
|
for(const FName& FunctionName : NativeMakeFunctionNameAllowList)
|
|
{
|
|
if(InFunctionName == FunctionName)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool UAnimBlueprintExtension_Base::FEvaluationHandlerRecord::CheckForStructMemberAccess(FCopyRecordGraphCheckContext& Context, UEdGraphPin* DestPin)
|
|
{
|
|
if(DestPin)
|
|
{
|
|
UEdGraphPin* SourcePin = nullptr;
|
|
if(UK2Node_BreakStruct* BreakStructNode = Cast<UK2Node_BreakStruct>(FollowKnots(DestPin, SourcePin)))
|
|
{
|
|
if(UEdGraphPin* InputPin = FindFirstInputPin(BreakStructNode))
|
|
{
|
|
if(CheckForStructMemberAccess(Context, InputPin) || CheckForVariableGet(Context, InputPin) || CheckForArrayAccess(Context, InputPin))
|
|
{
|
|
check(Context.CopyRecord->SourcePropertyPath.Num() > 0); // this should have been filled in by CheckForVariableGet() above
|
|
Context.CopyRecord->SourcePropertyPath.Add(SourcePin->PinName.ToString());
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
// could be a native break
|
|
else if(UK2Node_CallFunction* NativeBreakNode = Cast<UK2Node_CallFunction>(FollowKnots(DestPin, SourcePin)))
|
|
{
|
|
UFunction* Function = NativeBreakNode->FunctionReference.ResolveMember<UFunction>(UKismetMathLibrary::StaticClass());
|
|
if(Function && Function->HasMetaData(TEXT("NativeBreakFunc")) && IsNativeBreakAllowed(Function->GetFName()))
|
|
{
|
|
if(UEdGraphPin* InputPin = FindFirstInputPin(NativeBreakNode))
|
|
{
|
|
if(CheckForStructMemberAccess(Context, InputPin) || CheckForVariableGet(Context, InputPin) || CheckForArrayAccess(Context, InputPin))
|
|
{
|
|
check(Context.CopyRecord->SourcePropertyPath.Num() > 0); // this should have been filled in by CheckForVariableGet() above
|
|
Context.CopyRecord->SourcePropertyPath.Add(SourcePin->PinName.ToString());
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool UAnimBlueprintExtension_Base::FEvaluationHandlerRecord::CheckForSplitPinAccess(FCopyRecordGraphCheckContext& Context, UEdGraphPin* DestPin)
|
|
{
|
|
if(DestPin)
|
|
{
|
|
FPropertyCopyRecord OriginalRecord = *Context.CopyRecord;
|
|
|
|
UEdGraphPin* SourcePin = nullptr;
|
|
if(UK2Node_MakeStruct* MakeStructNode = Cast<UK2Node_MakeStruct>(FollowKnots(DestPin, SourcePin)))
|
|
{
|
|
// Idea here is to account for split pins, so we want to narrow the scope to not also include user-placed makes
|
|
UObject* SourceObject = Context.MessageLog.FindSourceObject(MakeStructNode);
|
|
if(SourceObject && SourceObject->IsA<UAnimGraphNode_Base>())
|
|
{
|
|
return ForEachInputPin(MakeStructNode, [this, &Context, &OriginalRecord](UEdGraphPin* InputPin)
|
|
{
|
|
Context.CopyRecord->SourcePropertyPath = OriginalRecord.SourcePropertyPath;
|
|
if(CheckForStructMemberAccess(Context, InputPin) || CheckForVariableGet(Context, InputPin) || CheckForArrayAccess(Context, InputPin))
|
|
{
|
|
check(Context.CopyRecord->DestPropertyPath.Num() > 0);
|
|
FPropertyCopyRecord RecordCopy = *Context.CopyRecord;
|
|
FPropertyCopyRecord& NewRecord = Context.AdditionalCopyRecords.Add_GetRef(MoveTemp(RecordCopy));
|
|
|
|
NewRecord.DestPropertyPath = OriginalRecord.DestPropertyPath;
|
|
NewRecord.DestPropertyPath.Add(InputPin->PinName.ToString());
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
}
|
|
}
|
|
else if(UK2Node_CallFunction* NativeMakeNode = Cast<UK2Node_CallFunction>(FollowKnots(DestPin, SourcePin)))
|
|
{
|
|
UFunction* Function = NativeMakeNode->FunctionReference.ResolveMember<UFunction>(UKismetMathLibrary::StaticClass());
|
|
if(Function && Function->HasMetaData(TEXT("NativeMakeFunc")) && IsNativeMakeAllowed(Function->GetFName()))
|
|
{
|
|
// Idea here is to account for split pins, so we want to narrow the scope to not also include user-placed makes
|
|
UObject* SourceObject = Context.MessageLog.FindSourceObject(NativeMakeNode);
|
|
if(SourceObject && SourceObject->IsA<UAnimGraphNode_Base>())
|
|
{
|
|
return ForEachInputPin(NativeMakeNode, [this, &Context, &OriginalRecord](UEdGraphPin* InputPin)
|
|
{
|
|
Context.CopyRecord->SourcePropertyPath = OriginalRecord.SourcePropertyPath;
|
|
if(CheckForStructMemberAccess(Context, InputPin) || CheckForVariableGet(Context, InputPin) || CheckForArrayAccess(Context, InputPin))
|
|
{
|
|
check(Context.CopyRecord->DestPropertyPath.Num() > 0);
|
|
FPropertyCopyRecord RecordCopy = *Context.CopyRecord;
|
|
FPropertyCopyRecord& NewRecord = Context.AdditionalCopyRecords.Add_GetRef(MoveTemp(RecordCopy));
|
|
|
|
NewRecord.DestPropertyPath = OriginalRecord.DestPropertyPath;
|
|
NewRecord.DestPropertyPath.Add(InputPin->PinName.ToString());
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool UAnimBlueprintExtension_Base::FEvaluationHandlerRecord::CheckForArrayAccess(FCopyRecordGraphCheckContext& Context, UEdGraphPin* DestPin)
|
|
{
|
|
if(DestPin)
|
|
{
|
|
UEdGraphPin* SourcePin = nullptr;
|
|
if(UK2Node_CallArrayFunction* CallArrayFunctionNode = Cast<UK2Node_CallArrayFunction>(FollowKnots(DestPin, SourcePin)))
|
|
{
|
|
if(CallArrayFunctionNode->GetTargetFunction() == UKismetArrayLibrary::StaticClass()->FindFunctionByName(GET_FUNCTION_NAME_CHECKED(UKismetArrayLibrary, Array_Get)))
|
|
{
|
|
// Check array index is constant
|
|
int32 ArrayIndex = INDEX_NONE;
|
|
if(UEdGraphPin* IndexPin = CallArrayFunctionNode->FindPin(TEXT("Index")))
|
|
{
|
|
if(IndexPin->LinkedTo.Num() > 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ArrayIndex = FCString::Atoi(*IndexPin->DefaultValue);
|
|
}
|
|
|
|
if(UEdGraphPin* TargetArrayPin = CallArrayFunctionNode->FindPin(TEXT("TargetArray")))
|
|
{
|
|
if(CheckForVariableGet(Context, TargetArrayPin) || CheckForStructMemberAccess(Context, TargetArrayPin))
|
|
{
|
|
check(Context.CopyRecord->SourcePropertyPath.Num() > 0); // this should have been filled in by CheckForVariableGet() or CheckForStructMemberAccess() above
|
|
Context.CopyRecord->SourcePropertyPath.Last().Append(FString::Printf(TEXT("[%d]"), ArrayIndex));
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool UAnimBlueprintExtension_Base::FEvaluationHandlerRecord::CheckForMemberOnlyAccess(FPropertyCopyRecord& CopyRecord, UEdGraphPin* DestPin)
|
|
{
|
|
const UAnimationGraphSchema* AnimGraphDefaultSchema = GetDefault<UAnimationGraphSchema>();
|
|
|
|
if(DestPin)
|
|
{
|
|
// traverse pins to leaf nodes and check for member access/pure only
|
|
TArray<UEdGraphPin*> PinStack;
|
|
PinStack.Add(DestPin);
|
|
while(PinStack.Num() > 0)
|
|
{
|
|
UEdGraphPin* CurrentPin = PinStack.Pop(false);
|
|
for(auto& LinkedPin : CurrentPin->LinkedTo)
|
|
{
|
|
UEdGraphNode* LinkedNode = LinkedPin->GetOwningNode();
|
|
if(LinkedNode)
|
|
{
|
|
bool bLeafNode = true;
|
|
for(auto& Pin : LinkedNode->Pins)
|
|
{
|
|
if(Pin != LinkedPin && Pin->Direction == EGPD_Input && !AnimGraphDefaultSchema->IsPosePin(Pin->PinType))
|
|
{
|
|
bLeafNode = false;
|
|
PinStack.Add(Pin);
|
|
}
|
|
}
|
|
|
|
if(bLeafNode)
|
|
{
|
|
if(UK2Node_VariableGet* LinkedVariableGetNode = Cast<UK2Node_VariableGet>(LinkedNode))
|
|
{
|
|
if(!LinkedVariableGetNode->IsNodePure() || !LinkedVariableGetNode->VariableReference.IsSelfContext())
|
|
{
|
|
// only local variable access is allowed for leaf nodes
|
|
CopyRecord.InvalidateFastPath();
|
|
}
|
|
}
|
|
else if(UK2Node_CallFunction* CallFunctionNode = Cast<UK2Node_CallFunction>(LinkedNode))
|
|
{
|
|
if(!CallFunctionNode->IsNodePure())
|
|
{
|
|
// only allow pure function calls
|
|
CopyRecord.InvalidateFastPath();
|
|
}
|
|
}
|
|
else if(!LinkedNode->IsA<UK2Node_TransitionRuleGetter>())
|
|
{
|
|
CopyRecord.InvalidateFastPath();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return CopyRecord.IsFastPath();
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE |