A Demo Project for the UnrealEngineSDK
Loading...
Searching...
No Matches
DialogueEditorUtilities.cpp
Go to the documentation of this file.
1// Copyright Csaba Molnar, Daniel Butum. All Rights Reserved.
3
4#include "Toolkits/IToolkit.h"
5#include "Toolkits/ToolkitManager.h"
6#include "Templates/Casts.h"
7#include "Containers/Queue.h"
8#include "EdGraphNode_Comment.h"
9#include "FileHelpers.h"
10#include "Kismet2/BlueprintEditorUtils.h"
11#include "Kismet2/SClassPickerDialog.h"
12
17#include "DlgHelper.h"
18#include "DlgManager.h"
20#include "Kismet2/KismetEditorUtilities.h"
21#include "K2Node_Event.h"
22
25{
27 NodeWithParentPosition(UDialogueGraphNode* InNode, const int32 InParentNodeX, const int32 InParentNodeY) :
28 Node(InNode), ParentNodeX(InParentNodeX), ParentNodeY(InParentNodeY) {}
29
31 int32 ParentNodeX = 0;
32 int32 ParentNodeY = 0;
33};
34
36// FDialogueEditorUtilities
38{
39 //const int32 NumDialoguesBefore = UDlgManager::GetAllDialoguesFromMemory().Num();
40 const int32 NumLoadedDialogues = UDlgManager::LoadAllDialoguesIntoMemory(false);
41 //const int32 NumDialoguesAfter = UDlgManager::GetAllDialoguesFromMemory().Num();
42 //check(NumDialoguesBefore == NumDialoguesAfter);
43 UE_LOG(LogDlgSystemEditor, Log, TEXT("UDlgManager::LoadAllDialoguesIntoMemory loaded %d Dialogues into Memory"), NumLoadedDialogues);
44
45 // Try to fix duplicate GUID
46 // Can happen for one of the following reasons:
47 // - duplicated files outside of UE
48 // - somehow loaded from text files?
49 // - the universe hates us? +_+
51 {
52 UE_LOG(
53 LogDlgSystemEditor,
54 Warning,
55 TEXT("Dialogue = `%s`, GUID = `%s` has a Duplicate GUID. Regenerating."),
56 *Dialogue->GetPathName(), *Dialogue->GetGUID().ToString()
57 )
58 Dialogue->RegenerateGUID();
59 Dialogue->MarkPackageDirty();
60 }
61
62 // Give it another try, Give up :((
63 // May the math Gods have mercy on us!
65 {
66 // GUID already exists (╯°□°)╯︵ ┻━┻
67 // Does this break the universe?
68 UE_LOG(
69 LogDlgSystemEditor,
70 Error,
71 TEXT("Dialogue = `%s`, GUID = `%s`"),
72 *Dialogue->GetPathName(), *Dialogue->GetGUID().ToString()
73 )
74
75 UE_LOG(
76 LogDlgSystemEditor,
77 Fatal,
78 TEXT("(╯°□°)╯︵ ┻━┻ Congrats, you just broke the universe, are you even human? Now please go and proove an NP complete problem."
79 "The chance of generating two equal random FGuid (picking 4, uint32 numbers) is p = 9.3132257 * 10^(-10) % (or something like this)")
80 )
81 }
82}
83
84const TSet<UObject*> FDialogueEditorUtilities::GetSelectedNodes(const UEdGraph* Graph)
85{
86 TSharedPtr<IDialogueEditor> DialogueEditor = GetDialogueEditorForGraph(Graph);
87 if (DialogueEditor.IsValid())
88 {
89 return DialogueEditor->GetSelectedNodes();
90 }
91
92 return {};
93}
94
95bool FDialogueEditorUtilities::GetBoundsForSelectedNodes(const UEdGraph* Graph, class FSlateRect& Rect, float Padding)
96{
97 TSharedPtr<IDialogueEditor> DialogueEditor = GetDialogueEditorForGraph(Graph);
98 if (DialogueEditor.IsValid())
99 {
100 return DialogueEditor->GetBoundsForSelectedNodes(Rect, Padding);
101 }
102
103 return false;
104}
105
106void FDialogueEditorUtilities::RefreshDetailsView(const UEdGraph* Graph, bool bRestorePreviousSelection)
107{
108 TSharedPtr<IDialogueEditor> DialogueEditor = GetDialogueEditorForGraph(Graph);
109 if (DialogueEditor.IsValid())
110 {
111 DialogueEditor->RefreshDetailsView(bRestorePreviousSelection);
112 }
113}
114
115void FDialogueEditorUtilities::Refresh(const UEdGraph* Graph, bool bRestorePreviousSelection)
116{
117 TSharedPtr<IDialogueEditor> DialogueEditor = GetDialogueEditorForGraph(Graph);
118 if (DialogueEditor.IsValid())
119 {
120 DialogueEditor->Refresh(bRestorePreviousSelection);
121 }
122}
123
125{
126 TSharedPtr<IDialogueEditor> DialogueEditor = GetDialogueEditorForGraph(Graph);
127 if (DialogueEditor.IsValid())
128 {
129 return DialogueEditor->GetLastTargetGraphEdgeBeforeDrag();
130 }
131
132 return nullptr;
133}
134
136{
137 TSharedPtr<IDialogueEditor> DialogueEditor = GetDialogueEditorForGraph(Graph);
138 if (DialogueEditor.IsValid())
139 {
140 DialogueEditor->SetLastTargetGraphEdgeBeforeDrag(InEdge);
141 }
142}
143
144TSharedPtr<class IDialogueEditor> FDialogueEditorUtilities::GetDialogueEditorForGraph(const UEdGraph* Graph)
145{
146 // Find the associated Dialogue
148 TSharedPtr<IDialogueEditor> DialogueEditor;
149
150 // This Dialogue has already an asset editor opened
151 TSharedPtr<IToolkit> FoundAssetEditor = FToolkitManager::Get().FindEditorForAsset(Dialogue);
152 if (FoundAssetEditor.IsValid())
153 {
154 DialogueEditor = StaticCastSharedPtr<IDialogueEditor>(FoundAssetEditor);
155 }
156
157 return DialogueEditor;
158}
159
161{
162 if (!IsValid(NodeToRemove))
163 {
164 return false;
165 }
166
167 UDialogueGraph* Graph = CastChecked<UDialogueGraph>(NodeToRemove->GetGraph());
168 if (!IsValid(Graph))
169 {
170 return false;
171 }
172
173 // Transactions should be declared in the code that calls this method
174 if (!Graph->Modify())
175 {
176 UE_LOG(LogDlgSystemEditor, Fatal, TEXT("FDialogueEditorUtilities::RemoveNode No transaction was declared before calling this method, aborting!"));
177 return false;
178 }
179 if (!NodeToRemove->Modify())
180 {
181 UE_LOG(LogDlgSystemEditor, Fatal, TEXT("FDialogueEditorUtilities::RemoveNode No transaction was declared before calling this method, aborting!"));
182 return false;
183 }
184
185 return Graph->RemoveGraphNode(NodeToRemove);
186}
187
189 UObject* ParentScope,
190 FName GraphName,
191 TSubclassOf<UEdGraph> GraphClass,
192 TSubclassOf<UEdGraphSchema> SchemaClass
193)
194{
195 // Mostly copied from FBlueprintEditorUtils::CreateNewGraph
196 UEdGraph* NewGraph;
197 bool bRename = false;
198
199 // Ensure this name isn't already being used for a graph
200 if (GraphName != NAME_None)
201 {
202 UEdGraph* ExistingGraph = FindObject<UEdGraph>(ParentScope, *(GraphName.ToString()));
203 ensureMsgf(!ExistingGraph, TEXT("Graph %s already exists: %s"), *GraphName.ToString(), *ExistingGraph->GetFullName());
204
205 // Rename the old graph out of the way; but we have already failed at this point
206 if (ExistingGraph)
207 {
208 ExistingGraph->Rename(nullptr, ExistingGraph->GetOuter(), REN_DoNotDirty | REN_ForceNoResetLoaders);
209 }
210
211 // Construct new graph with the supplied name
212 NewGraph = NewObject<UEdGraph>(ParentScope, GraphClass, NAME_None, RF_Transactional);
213 bRename = true;
214 }
215 else
216 {
217 // Construct a new graph with a default name
218 NewGraph = NewObject<UEdGraph>(ParentScope, GraphClass, NAME_None, RF_Transactional);
219 }
220
221 NewGraph->Schema = SchemaClass;
222
223 // Now move to where we want it to. Workaround to ensure transaction buffer is correctly utilized
224 if (bRename)
225 {
226 NewGraph->Rename(*GraphName.ToString(), ParentScope, REN_DoNotDirty | REN_ForceNoResetLoaders);
227 }
228
229 return NewGraph;
230}
231
233{
234 bool bIsDataValid = true;
235#if DO_CHECK
236 const TArray<UDlgNode*>& DialogueNodes = Dialogue->GetNodes();
237 // Do some additional checks to ensure the data is safe, useful in development
238 auto checkIfMultipleEdgesToSameNode = [DialogueNodes, bDisplayWarning](UDlgNode* Node)
239 {
240 if (!IsValid(Node))
241 {
242 return true;
243 }
244
245 TSet<int32> NodeEdgesFound;
246 TSet<int32> EdgesToRemove;
247 // Find the duplicate edges
248 const TArray<FDlgEdge>& NodeChildren = Node->GetNodeChildren();
249 for (int32 EdgeIndex = 0, EdgesNum = NodeChildren.Num(); EdgeIndex < EdgesNum; EdgeIndex++)
250 {
251 const FDlgEdge& Edge = NodeChildren[EdgeIndex];
252 if (Edge.TargetIndex == INDEX_NONE)
253 {
254 continue;
255 }
256
257 if (NodeEdgesFound.Contains(Edge.TargetIndex))
258 {
259 // Mark for deletion
260 EdgesToRemove.Add(EdgeIndex);
261
262 if (!bDisplayWarning)
263 {
264 continue;
265 }
266
267 // Find source and destination
268 const int32 IndexToNode = Edge.TargetIndex;
269 int32 IndexFromNode = DialogueNodes.Find(Node);
270 if (IndexFromNode == INDEX_NONE) // start node
271 {
272 IndexFromNode = -1;
273 }
274
275 const FString Message = FString::Printf(
276 TEXT("Node with index = `%d` connects multiple times to destination Node with index = `%d`. One of the Edges will be removed."),
277 IndexFromNode, IndexToNode);
278 ShowMessageBox(EAppMsgType::Ok, Message, TEXT("Invalid Dialogue data"));
279 }
280 else
281 {
282 NodeEdgesFound.Add(Edge.TargetIndex);
283 }
284 }
285
286 // Remove if any duplicate edges
287 for (int32 EdgeIndex : EdgesToRemove)
288 {
289 Node->RemoveChildAt(EdgeIndex);
290 }
291
292 return EdgesToRemove.Num() == 0;
293 };
294
295 bIsDataValid = bIsDataValid && checkIfMultipleEdgesToSameNode(Dialogue->GetMutableStartNode());
296 for (UDlgNode* Node : DialogueNodes)
297 {
298 bIsDataValid = bIsDataValid && checkIfMultipleEdgesToSameNode(Node);
299 }
300
301#endif
302
303 return bIsDataValid;
304}
305
307{
308 // Clear the graph if the number of nodes differ
310 {
311 return;
312 }
313
314 // Simply do the operations without any consent
315 if (!bPrompt)
316 {
317 // Always keep in sync with the .dlg (text file).
318 Dialogue->InitialSyncWithTextFile();
320 Dialogue->ClearGraph();
321 return;
322 }
323
324 // Prompt to the user to initial sync with the text file
325 {
326 const EAppReturnType::Type Response = ShowMessageBox(EAppMsgType::YesNo,
327 FString::Printf(TEXT("Initial sync the Dialogues nodes of `%s` from the text file with the same name?"), *Dialogue->GetName()),
328 TEXT("Get Dialogue nodes from the text file"));
329
330 if (Response == EAppReturnType::Yes)
331 {
332 Dialogue->InitialSyncWithTextFile();
333 }
334 }
336
337 // Prompt the user and only if he answers yes we clear the graph
338 {
339 const int32 NumGraphNodes = CastChecked<UDialogueGraph>(Dialogue->GetGraph())->GetAllDialogueGraphNodes().Num();
340 const int32 NumDialogueNodes = Dialogue->GetNodes().Num() + 1; // (plus the start node)
341 const FString Message = FString::Printf(TEXT("Dialogue with name = `%s` has number of graph nodes (%d) != number dialogue nodes (%d)."),
342 *Dialogue->GetName(), NumGraphNodes, NumDialogueNodes);
343 const EAppReturnType::Type Response = ShowMessageBox(EAppMsgType::YesNo,
344 FString::Printf(TEXT("%s%s"), *Message, TEXT("\nWould you like to autogenerate the graph nodes from the dialogue nodes?\n WARNING: Graph nodes will be lost")),
345 TEXT("Autogenerate graph nodes from dialogue nodes?"));
346
347 // This will trigger the CreateDefaultNodesForGraph in the the GraphSchema
348 if (Response == EAppReturnType::Yes)
349 {
350 Dialogue->ClearGraph();
351 }
352 }
353}
354
356{
357 const int32 NumGraphNodes = CastChecked<UDialogueGraph>(Dialogue->GetGraph())->GetAllDialogueGraphNodes().Num();
358 const int32 NumDialogueNodes = Dialogue->GetNodes().Num() + 1; // (plus the start node)
359 if (NumGraphNodes == NumDialogueNodes)
360 {
361 return true;
362 }
363
364 return false;
365}
366
368{
369 const UDialogueGraphNode_Base* BaseNode = Cast<UDialogueGraphNode_Base>(GraphNode);
370 if (!BaseNode)
371 {
372 return nullptr;
373 }
374
375 // Node
376 if (const UDialogueGraphNode* Node = Cast<UDialogueGraphNode>(BaseNode))
377 {
378 return Node->GetMutableDialogueNode();
379 }
380
381 // Edge
382 if (const UDialogueGraphNode_Edge* EdgeNode = Cast<UDialogueGraphNode_Edge>(BaseNode))
383 {
384 if (EdgeNode->HasParentNode())
385 return EdgeNode->GetParentNode()->GetMutableDialogueNode();
386 if (EdgeNode->HasChildNode())
387 return EdgeNode->GetChildNode()->GetMutableDialogueNode();
388 }
389
390 return nullptr;
391}
392
394 UDialogueGraphNode* RootNode,
395 const TArray<UDialogueGraphNode*>& GraphNodes,
396 int32 OffsetBetweenColumnsX,
397 int32 OffsetBetweenRowsY,
398 bool bIsDirectionVertical
399)
400{
401 TSet<UDialogueGraphNode*> VisitedNodes;
402 VisitedNodes.Add(RootNode);
403 TQueue<NodeWithParentPosition> Queue;
404 verify(Queue.Enqueue(NodeWithParentPosition(RootNode, 0, 0)));
405
406 // Find first node with children so that we do not get all the graph with orphan nodes
407 {
408 UDialogueGraphNode* Node = RootNode;
409 int32 Index = 0;
410 while (Index < GraphNodes.Num() && Node->GetOutputPin()->LinkedTo.Num() == 0)
411 {
412 Node = GraphNodes[Index];
413 Index++;
414 }
415 if (Node != RootNode)
416 {
417 NodeWithParentPosition ParentPosition;
418 if (bIsDirectionVertical)
419 {
420 ParentPosition = NodeWithParentPosition(Node, 0, OffsetBetweenRowsY);
421 }
422 else
423 {
424 ParentPosition = NodeWithParentPosition(Node, OffsetBetweenColumnsX, 0);
425 }
426
427 verify(Queue.Enqueue(ParentPosition));
428 }
429 }
430
431 // Just some BFS
432 while (!Queue.IsEmpty())
433 {
434 NodeWithParentPosition NodeWithPosition;
435 verify(Queue.Dequeue(NodeWithPosition));
436 UDialogueGraphNode* Node = NodeWithPosition.Node;
437
438 if (bIsDirectionVertical)
439 {
440 // Position this node at the same level only one row further (down)
441 Node->SetPosition(
442 NodeWithPosition.ParentNodeX,
443 NodeWithPosition.ParentNodeY + OffsetBetweenRowsY
444 );
445 }
446 else
447 {
448 // Position this node at the same level only one column further (to the right)
449 Node->SetPosition(
450 NodeWithPosition.ParentNodeX + OffsetBetweenColumnsX,
451 NodeWithPosition.ParentNodeY
452 );
453 }
454
455 // Gather the list of unvisited child nodes, useful for not drawing weird children
456 TArray<UDialogueGraphNode*> ChildNodesUnvisited;
457 for (UDialogueGraphNode* ChildNode : Node->GetChildNodes())
458 {
459 // Prevent double visiting
460 if (!VisitedNodes.Contains(ChildNode))
461 {
462 ChildNodesUnvisited.Add(ChildNode);
463 }
464 }
465
466 // Adjust
467 int32 ChildOffsetPos;
468 if (bIsDirectionVertical)
469 {
470 // Adjust for the number of nodes, so that we are left, down by half
471 int32 ChildOffsetPosX = Node->NodePosX;
472 if (ChildNodesUnvisited.Num() > 1) // only adjust X position if we have more than one child
473 {
474 ChildOffsetPosX -= OffsetBetweenColumnsX * ChildNodesUnvisited.Num() / 2;
475 }
476
477 ChildOffsetPos = ChildOffsetPosX;
478 }
479 else
480 {
481 // Adjust for the number of nodes, so that we are right above (top) by half
482 int32 ChildOffsetPosY = Node->NodePosY;
483 if (ChildNodesUnvisited.Num() > 1) // only adjust Y position if we have more than one child
484 {
485 ChildOffsetPosY -= OffsetBetweenRowsY * ChildNodesUnvisited.Num() / 2;
486 }
487
488 ChildOffsetPos = ChildOffsetPosY;
489 }
490
491 // Position children
492 for (int32 ChildIndex = 0, ChildNum = ChildNodesUnvisited.Num(); ChildIndex < ChildNum; ChildIndex++)
493 {
494 UDialogueGraphNode* ChildNode = ChildNodesUnvisited[ChildIndex];
495 if (bIsDirectionVertical)
496 {
497 ChildNode->SetPosition(ChildOffsetPos, Node->NodePosY);
498 }
499 else
500 {
501 ChildNode->SetPosition(Node->NodePosX, ChildOffsetPos);
502 }
503
504 VisitedNodes.Add(ChildNode);
505
506 NodeWithParentPosition ParentPosition;
507 if (bIsDirectionVertical)
508 {
509 ParentPosition = NodeWithParentPosition(ChildNode, ChildOffsetPos, Node->NodePosY);
510 ChildOffsetPos += OffsetBetweenColumnsX + ChildNode->EstimateNodeWidth();
511 }
512 else
513 {
514 // Next child on this level will set X aka columns to Node->NodePosX + OffsetBetweenColumnsX
515 // And Y aka row will be the same as this parent node, so it will be ChildOffsetPosY
516 ParentPosition = NodeWithParentPosition(ChildNode, Node->NodePosX + ChildNode->EstimateNodeWidth() * 1.5, ChildOffsetPos);
517 ChildOffsetPos += OffsetBetweenRowsY;
518 }
519
520 Queue.Enqueue(ParentPosition);
521 }
522 }
523
524 // Fix position of orphans (nodes/node group with no parents)
525 if (GraphNodes.Num() != VisitedNodes.Num())
526 {
527 TSet<UDialogueGraphNode*> NodesSet(GraphNodes);
528 // Nodes that are in the graph but not in the visited nodes set
529 TSet<UDialogueGraphNode*> OrphanedNodes = NodesSet.Difference(VisitedNodes);
530 for (UDialogueGraphNode* Node : OrphanedNodes)
531 {
532 // Finds the highest bottom left point
533 const FVector2D NodePos = Node->GetGraph()->GetGoodPlaceForNewNode();
534 if (bIsDirectionVertical)
535 {
536 Node->SetPosition(
537 NodePos.X,
538 NodePos.Y + OffsetBetweenRowsY
539 );
540 }
541 else
542 {
543 Node->SetPosition(
544 NodePos.X + OffsetBetweenColumnsX,
545 NodePos.Y
546 );
547 }
548 }
549 }
550}
551
553 const TSet<UObject*>& SelectedNodes,
554 TArray<UDialogueGraphNode*>& OutSelectedGraphNodes
555)
556{
557 OutSelectedGraphNodes.Empty();
558 if (SelectedNodes.Num() == 0)
559 {
560 return false;
561 }
562
563 // Helper to return false :(
564 const auto returnFailure = [&OutSelectedGraphNodes]() -> bool
565 {
566 OutSelectedGraphNodes.Empty();
567 return false;
568 };
569
570 // We must make sure that nodes are valid and are in a linear order
571 // Step 1. Check if selected nodes are valid
572 for (UObject* Node : SelectedNodes)
573 {
574 // Ignore edges
575 if (Node->IsA(UDialogueGraphNode_Edge::StaticClass()))
576 {
577 continue;
578 }
579
580 // Not a graph node, can't convert
581 UDialogueGraphNode* GraphNode = Cast<UDialogueGraphNode>(Node);
582 if (!IsValid(GraphNode))
583 {
584 return returnFailure();
585 }
586
587 // Selected the root node, can't convert
588 if (GraphNode->IsRootNode())
589 {
590 return returnFailure();
591 }
592
593 // Not a speech node, can't convert
594 if (!GraphNode->IsSpeechNode())
595 {
596 return returnFailure();
597 }
598
599 OutSelectedGraphNodes.Add(GraphNode);
600 }
601 if (OutSelectedGraphNodes.Num() == 0)
602 {
603 return returnFailure();
604 }
605
606 // Step 2. Sort in increasing order by the dialogue index.
607 // This will make sure that the left/top (first) most selected node will be at index 0
608 OutSelectedGraphNodes.Sort([](const UDialogueGraphNode& LHS, const UDialogueGraphNode& RHS) -> bool
609 {
610 return LHS.GetDialogueNodeIndex() < RHS.GetDialogueNodeIndex();
611 });
612
613 // Step 3. Check that every node in the sequence is ONLY connected to the next
614 for (int32 NodeIndex = 0, NodesNum = OutSelectedGraphNodes.Num(); NodeIndex < NodesNum; NodeIndex++)
615 {
616 const bool bIsFirstNode = NodeIndex == 0;
617 const bool bIsLastNode = NodeIndex == NodesNum - 1;
618 const UDialogueGraphNode* CurrentGraphNode = OutSelectedGraphNodes[NodeIndex];
619
620 // The first node can have any number of input connections
621 if (!bIsFirstNode)
622 {
623 // Check input connections
624 const TArray<UDialogueGraphNode*> ParentNodes = CurrentGraphNode->GetParentNodes();
625 if (ParentNodes.Num() > 1)
626 {
627 return returnFailure();
628 }
629
630 // Is not connected to the previous node in the selection
631 if (ParentNodes.Num() == 1 &&
632 ParentNodes[0] != OutSelectedGraphNodes[NodeIndex - 1])
633 {
634 return returnFailure();
635 }
636 // if ParentNodes.Num() == 0, it is valid
637 }
638
639 // The last node can have any number of output pins
640 if (!bIsLastNode)
641 {
642 // Check output connections
643 const TArray<UDialogueGraphNode*> ChildNodes = CurrentGraphNode->GetChildNodes();
644 if (ChildNodes.Num() > 1)
645 {
646 return returnFailure();
647 }
648
649 // Is not connected to the next node in the selection
650 if (ChildNodes.Num() == 1 &&
651 ChildNodes[0] != OutSelectedGraphNodes[NodeIndex + 1])
652 {
653 return returnFailure();
654 }
655 // if ChildNodes.Num() == 0, it is valid
656 }
657 // We do not care if the node does not have any input/output connections, it is simply an orphan, lets adopt it :)
658 }
659
660 return true;
661}
662
664{
665 // Is the node a speech sequence? and has at least one speech sequence inside it
666 if (SelectedNodes.Num() == 1)
667 {
668 if (UDialogueGraphNode* SelectedNode = Cast<UDialogueGraphNode>(*FDlgHelper::GetFirstSetElement(SelectedNodes)))
669 {
670 return SelectedNode->IsSpeechSequenceNode() &&
671 SelectedNode->GetDialogueNode<UDlgNode_SpeechSequence>().HasSpeechSequences();
672 }
673 }
674
675 return false;
676}
677
678void FDialogueEditorUtilities::CloseOtherEditors(UObject* Asset, IAssetEditorInstance* OnlyEditor)
679{
680 if (!IsValid(Asset) || !GEditor)
681 {
682 return;
683 }
684
685#if ENGINE_MINOR_VERSION >= 24
686 GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->CloseOtherEditors(Asset, OnlyEditor);
687#else
688 FAssetEditorManager::Get().CloseOtherEditors(Asset, OnlyEditor);
689#endif
690}
691
693{
694 if (!IsValid(Asset) || !GEditor)
695 {
696 return false;
697 }
698
699#if ENGINE_MINOR_VERSION >= 24
700 return GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->OpenEditorForAsset(const_cast<UObject*>(Asset));
701#else
702 return FAssetEditorManager::Get().OpenEditorForAsset(const_cast<UObject*>(Asset));
703#endif
704}
705
706IAssetEditorInstance* FDialogueEditorUtilities::FindEditorForAsset(UObject* Asset, bool bFocusIfOpen)
707{
708 if (!IsValid(Asset) || !GEditor)
709 {
710 return nullptr;
711 }
712
713#if ENGINE_MINOR_VERSION >= 24
714 return GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->FindEditorForAsset(Asset, bFocusIfOpen);
715#else
716 return FAssetEditorManager::Get().FindEditorForAsset(Asset, bFocusIfOpen);
717#endif
718}
719
720bool FDialogueEditorUtilities::OpenEditorAndJumpToGraphNode(const UEdGraphNode* GraphNode, bool bFocusIfOpen /*= false*/)
721{
722 if (!IsValid(GraphNode))
723 {
724 return false;
725 }
726
727 // Open if not already.
730 {
731 return false;
732 }
733
734 // Could still fail focus on the graph node
735 if (IAssetEditorInstance* EditorInstance = FindEditorForAsset(Dialogue, bFocusIfOpen))
736 {
737 EditorInstance->FocusWindow(const_cast<UEdGraphNode*>(GraphNode));
738 return true;
739 }
740
741 return false;
742}
743
745{
746 if (!IsValid(GraphNode))
747 {
748 return false;
749 }
750
751 TSharedPtr<IDialogueEditor> DialogueEditor = GetDialogueEditorForGraph(GraphNode->GetGraph());
752 if (DialogueEditor.IsValid())
753 {
754 DialogueEditor->JumpToObject(GraphNode);
755 return true;
756 }
757
758 return false;
759}
760
762{
763 if (!Dialogue)
764 {
765 return false;
766 }
767
768 if (UDlgNode* Node = Dialogue->GetMutableNodeFromIndex(NodeIndex))
769 {
770 return JumpToGraphNode(Node->GetGraphNode());
771 }
772
773 return false;
774}
775
776
778{
779 check(FromNode != ToNode);
780 const UEdGraphSchema* GraphSchema = FromNode->GetSchema();
781 const TArray<UDialogueGraphNode*> ChildNodes = FromNode->GetChildNodes();
782 UEdGraphPin* ToNodeOutputPin = ToNode->GetOutputPin();
783
784 // Remake connections to children for the ToNode
785 // (input pin) FromNode (output pin) -> (input pin) ChildEdgeConnection (output pin) -> (input pin) ChildNode (output pin)
786 for (UDialogueGraphNode* ChildNode : ChildNodes)
787 {
788 verify(GraphSchema->TryCreateConnection(ToNodeOutputPin, ChildNode->GetInputPin()));
789 }
790
791 // Copy the dialogue Data
792 ToNode->SetEdges(FromNode->GetDialogueNode().GetNodeChildren());
793}
794
796{
797 check(OldNode != NewNode);
798 const UEdGraphSchema* GraphSchema = OldNode->GetSchema();
799 const TArray<UDialogueGraphNode_Edge*> ParentEdgeNodes = OldNode->GetParentEdgeNodes();
800 UEdGraphPin* NewNodeInputPin = NewNode->GetInputPin();
801
802 // (input pin) ParentNode (output pin) -> (input pin) ParentEdgeConnection (output pin) -> (input pin) NewNode (output pin)
803 for (UDialogueGraphNode_Edge* ParentEdgeConnection : ParentEdgeNodes)
804 {
805 // Replace connection from the edge output pin to the new node
806 // Reparenting logic handled by UDialogueGraphNode_Edge::PinConnectionListChanged
807 verify(GraphSchema->TryCreateConnection(ParentEdgeConnection->GetOutputPin(), NewNodeInputPin));
808 }
809}
810
811EAppReturnType::Type FDialogueEditorUtilities::ShowMessageBox(EAppMsgType::Type MsgType, const FString& Text, const FString& Caption)
812{
813 UE_LOG(LogDlgSystemEditor, Warning, TEXT("%s\n%s"), *Caption, *Text);
814 return FPlatformMisc::MessageBoxExt(MsgType, *Text, *Caption);
815}
816
818 const TArray<UDialogueGraphNode*>& GraphNodes,
819 const TMap<int32, int32>& OldToNewIndexMap
820)
821{
822 if (GraphNodes.Num() == 0)
823 {
824 return;
825 }
826
827 const UDlgDialogue* Dialogue = GraphNodes[0]->GetDialogue();
828 const TArray<UDlgNode*>& Nodes = Dialogue->GetNodes();
829
830 // helper function to set the new IntValue on the condition if it exists in the history and it is different
831 auto UpdateConditionIndex = [&OldToNewIndexMap](FDlgCondition* ModifiedCondition) -> bool
832 {
833 const int32* NewIndex = OldToNewIndexMap.Find(ModifiedCondition->IntValue);
834 // Ignore invalid node indices any case
835 if (NewIndex == nullptr)
836 {
837 return false;
838 }
839
840 if (ModifiedCondition->IntValue != *NewIndex)
841 {
842 ModifiedCondition->IntValue = *NewIndex;
843 return true;
844 }
845
846 return false;
847 };
848
849 // Fix the weak references in the FDlgCondition::IntValue if it is of type WasNodeVisited
850 for (UDialogueGraphNode* GraphNode : GraphNodes)
851 {
852 UDlgNode* DialogueNode = GraphNode->GetMutableDialogueNode();
853 const TArray<UDialogueGraphNode_Edge*> ChildEdgeNodes = GraphNode->GetChildEdgeNodes();
854
855 // Update Enter condition
856 for (int32 ConditionIndex = 0, ConditionNum = DialogueNode->GetNodeEnterConditions().Num(); ConditionIndex < ConditionNum; ConditionIndex++)
857 {
858 FDlgCondition* EnterCondition = DialogueNode->GetMutableEnterConditionAt(ConditionIndex);
859 if (FDlgCondition::HasNodeIndex(EnterCondition->ConditionType))
860 {
861 UpdateConditionIndex(EnterCondition);
862 EnterCondition->GUID = Nodes[EnterCondition->IntValue]->GetGUID();
863 }
864 }
865
866 // Update Edges
867 for (int32 EdgeIndex = 0, EdgesNum = DialogueNode->GetNodeChildren().Num(); EdgeIndex < EdgesNum; EdgeIndex++)
868 {
869 FDlgEdge* DialogueEdge = DialogueNode->GetSafeMutableNodeChildAt(EdgeIndex);
870
871 for (FDlgCondition& Condition : DialogueEdge->Conditions)
872 {
873 if (FDlgCondition::HasNodeIndex(Condition.ConditionType))
874 {
875 UpdateConditionIndex(&Condition);
876 Condition.GUID = Nodes[Condition.IntValue]->GetGUID();
877 }
878 }
879
880 // Update graph node edge
881 ChildEdgeNodes[EdgeIndex]->SetDialogueEdge(*DialogueEdge);
882 }
883
884 GraphNode->CheckDialogueNodeSyncWithGraphNode(true);
885 }
886}
887
889{
890 if (const UDialogueGraphNode_Base* DialogueBaseNode = Cast<UDialogueGraphNode_Base>(GraphNode))
891 {
892 return DialogueBaseNode->GetDialogue();
893 }
894
895 // Last change
896 if (const UDialogueGraph* DialogueGraph = Cast<UDialogueGraph>(GraphNode->GetGraph()))
897 {
898 return DialogueGraph->GetDialogue();
899 }
900
901 return nullptr;
902}
903
905{
906 const TArray<UDlgDialogue*> Dialogues = UDlgManager::GetAllDialoguesFromMemory();
907 TArray<UPackage*> PackagesToSave;
908 const bool bBatchOnlyInGameDialogues = GetDefault<UDlgSystemSettings>()->bBatchOnlyInGameDialogues;
909
910 for (UDlgDialogue* Dialogue : Dialogues)
911 {
912 // Ignore, not in game directory
913 if (bBatchOnlyInGameDialogues && !Dialogue->IsInProjectDirectory())
914 {
915 continue;
916 }
917
918 Dialogue->MarkPackageDirty();
919 PackagesToSave.Add(Dialogue->GetOutermost());
920 }
921
922 static constexpr bool bCheckDirty = false;
923 static constexpr bool bPromptToSave = false;
924 return FEditorFileUtils::PromptForCheckoutAndSave(PackagesToSave, bCheckDirty, bPromptToSave) == FEditorFileUtils::EPromptReturnCode::PR_Success;
925}
926
928{
929 const TArray<UDlgDialogue*> Dialogues = UDlgManager::GetAllDialoguesFromMemory();
930 const bool bBatchOnlyInGameDialogues = GetDefault<UDlgSystemSettings>()->bBatchOnlyInGameDialogues;
931 for (const UDlgDialogue* Dialogue : Dialogues)
932 {
933 // Ignore, not in game directory
934 if (bBatchOnlyInGameDialogues && !Dialogue->IsInProjectDirectory())
935 {
936 continue;
937 }
938
939 Dialogue->DeleteAllTextFiles();
940 }
941
942 return true;
943}
944
945bool FDialogueEditorUtilities::PickChildrenOfClass(const FText& TitleText, UClass*& OutChosenClass, UClass* Class)
946{
947 // Create filter
948 TSharedPtr<FDialogueChildrenOfClassFilterViewer> Filter = MakeShareable(new FDialogueChildrenOfClassFilterViewer);
949 Filter->AllowedChildrenOfClasses.Add(Class);
950
951 // Fill in options
952 FClassViewerInitializationOptions Options;
953 Options.Mode = EClassViewerMode::ClassPicker;
954
955 const UDlgSystemSettings* Settings = GetDefault<UDlgSystemSettings>();
956 Options.DisplayMode = Settings->GetUnrealClassPickerDisplayMode();
957 Options.ClassFilter = Filter;
958 Options.bShowUnloadedBlueprints = true;
959 Options.bExpandRootNodes = true;
960 Options.NameTypeToDisplay = EClassViewerNameTypeToDisplay::Dynamic;
961
962 return SClassPickerDialog::PickClass(TitleText, Options, OutChosenClass, Class);
963}
964
966 UBlueprint* Blueprint,
968 FName FunctionNameToOpen,
969 bool bForceFullEditor,
970 bool bAddBlueprintFunctionIfItDoesNotExist
971)
972{
973 if (!Blueprint)
974 {
975 return false;
976 }
977
978 Blueprint->bForceFullEditor = bForceFullEditor;
979
980 // Find Function Graph
981 UObject* ObjectToFocusOn = nullptr;
982 if (OpenType != EDialogueBlueprintOpenType::None && FunctionNameToOpen != NAME_None)
983 {
984 UClass* Class = Blueprint->GeneratedClass;
985 check(Class);
986
988 {
989 ObjectToFocusOn = bAddBlueprintFunctionIfItDoesNotExist
990 ? BlueprintGetOrAddFunction(Blueprint, FunctionNameToOpen, Class)
991 : BlueprintGetFunction(Blueprint, FunctionNameToOpen, Class);
992 }
993 else if (OpenType == EDialogueBlueprintOpenType::Event)
994 {
995 ObjectToFocusOn = bAddBlueprintFunctionIfItDoesNotExist
996 ? BlueprintGetOrAddEvent(Blueprint, FunctionNameToOpen, Class)
997 : BlueprintGetEvent(Blueprint, FunctionNameToOpen, Class);
998 }
999 }
1000
1001 // Default to the last uber graph
1002 if (ObjectToFocusOn == nullptr)
1003 {
1004 ObjectToFocusOn = Blueprint->GetLastEditedUberGraph();
1005 }
1006 if (ObjectToFocusOn)
1007 {
1008 FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(ObjectToFocusOn);
1009 return true;
1010 }
1011
1012 return OpenEditorForAsset(Blueprint);
1013}
1014
1015UEdGraph* FDialogueEditorUtilities::BlueprintGetOrAddFunction(UBlueprint* Blueprint, FName FunctionName, UClass* FunctionClassSignature)
1016{
1017 if (!Blueprint || Blueprint->BlueprintType != BPTYPE_Normal)
1018 {
1019 return nullptr;
1020 }
1021
1022 // Find existing function
1023 if (UEdGraph* GraphFunction = BlueprintGetFunction(Blueprint, FunctionName, FunctionClassSignature))
1024 {
1025 return GraphFunction;
1026 }
1027
1028 // Create a new function
1029 UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(Blueprint, FunctionName, UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
1030 FBlueprintEditorUtils::AddFunctionGraph(Blueprint, NewGraph, /*bIsUserCreated=*/ false, FunctionClassSignature);
1031 Blueprint->LastEditedDocuments.Add(NewGraph);
1032 return NewGraph;
1033}
1034
1035UEdGraph* FDialogueEditorUtilities::BlueprintGetFunction(UBlueprint* Blueprint, FName FunctionName, UClass* FunctionClassSignature)
1036{
1037 if (!Blueprint || Blueprint->BlueprintType != BPTYPE_Normal)
1038 {
1039 return nullptr;
1040 }
1041
1042 // Find existing function
1043 for (UEdGraph* GraphFunction : Blueprint->FunctionGraphs)
1044 {
1045 if (FunctionName == GraphFunction->GetFName())
1046 {
1047 return GraphFunction;
1048 }
1049 }
1050
1051 // Find in the implemented Interfaces Graphs
1052 for (const FBPInterfaceDescription& Interface : Blueprint->ImplementedInterfaces)
1053 {
1054 for (UEdGraph* GraphFunction : Interface.Graphs)
1055 {
1056 if (FunctionName == GraphFunction->GetFName())
1057 {
1058 return GraphFunction;
1059 }
1060 }
1061 }
1062
1063 return nullptr;
1064}
1065
1066UK2Node_Event* FDialogueEditorUtilities::BlueprintGetOrAddEvent(UBlueprint* Blueprint, FName EventName, UClass* EventClassSignature)
1067{
1068 if (!Blueprint || Blueprint->BlueprintType != BPTYPE_Normal)
1069 {
1070 return nullptr;
1071 }
1072
1073 // Find existing event
1074 if (UK2Node_Event* EventNode = BlueprintGetEvent(Blueprint, EventName, EventClassSignature))
1075 {
1076 return EventNode;
1077 }
1078
1079 // Create a New Event
1080 if (Blueprint->UbergraphPages.Num())
1081 {
1082 int32 NodePositionY = 0;
1083 UK2Node_Event* NodeEvent = FKismetEditorUtilities::AddDefaultEventNode(
1084 Blueprint,
1085 Blueprint->UbergraphPages[0],
1086 EventName,
1087 EventClassSignature,
1088 NodePositionY
1089 );
1090 NodeEvent->SetEnabledState(ENodeEnabledState::Enabled);
1091 NodeEvent->NodeComment = "";
1092 NodeEvent->bCommentBubbleVisible = false;
1093 return NodeEvent;
1094 }
1095
1096 return nullptr;
1097}
1098
1099UK2Node_Event* FDialogueEditorUtilities::BlueprintGetEvent(UBlueprint* Blueprint, FName EventName, UClass* EventClassSignature)
1100{
1101 if (!Blueprint || Blueprint->BlueprintType != BPTYPE_Normal)
1102 {
1103 return nullptr;
1104 }
1105
1106 TArray<UK2Node_Event*> AllEvents;
1107 FBlueprintEditorUtils::GetAllNodesOfClass<UK2Node_Event>(Blueprint, AllEvents);
1108 for (UK2Node_Event* EventNode : AllEvents)
1109 {
1110 if (EventNode->bOverrideFunction && EventNode->EventReference.GetMemberName() == EventName)
1111 {
1112 return EventNode;
1113 }
1114 }
1115
1116 return nullptr;
1117}
1118
1119UEdGraphNode_Comment* FDialogueEditorUtilities::BlueprintAddComment(UBlueprint* Blueprint, const FString& CommentString, FVector2D Location)
1120{
1121 if (!Blueprint || Blueprint->BlueprintType != BPTYPE_Normal || Blueprint->UbergraphPages.Num() == 0)
1122 {
1123 return nullptr;
1124 }
1125
1126 UEdGraph* Graph = Blueprint->UbergraphPages[0];
1127 TSharedPtr<FEdGraphSchemaAction> Action = Graph->GetSchema()->GetCreateCommentAction();
1128 if (!Action.IsValid())
1129 {
1130 return nullptr;
1131 }
1132
1133 UEdGraphNode* GraphNode = Action->PerformAction(Graph, nullptr, Location);
1134 if (UEdGraphNode_Comment* CommentNode = Cast<UEdGraphNode_Comment>(GraphNode))
1135 {
1136 CommentNode->NodeComment = CommentString;
1137 return CommentNode;
1138 }
1139
1140 return nullptr;
1141}
EDialogueBlueprintOpenType
static bool OpenEditorAndJumpToGraphNode(const UEdGraphNode *GraphNode, bool bFocusIfOpen=false)
static void CopyNodeChildren(const UDialogueGraphNode *FromNode, UDialogueGraphNode *ToNode)
static IAssetEditorInstance * FindEditorForAsset(UObject *Asset, bool bFocusIfOpen)
static void RemapOldIndicesWithNewAndUpdateGUID(const TArray< UDialogueGraphNode * > &GraphNodes, const TMap< int32, int32 > &OldToNewIndexMap)
static bool AreDialogueNodesInSyncWithGraphNodes(const UDlgDialogue *Dialogue)
static bool JumpToGraphNodeIndex(const UDlgDialogue *Dialogue, int32 NodeIndex)
static UDialogueGraphNode_Edge * GetLastTargetGraphEdgeBeforeDrag(const UEdGraph *Graph)
static UK2Node_Event * BlueprintGetOrAddEvent(UBlueprint *Blueprint, FName EventName, UClass *EventClassSignature)
static UEdGraph * BlueprintGetFunction(UBlueprint *Blueprint, FName FunctionName, UClass *FunctionClassSignature)
static void TryToCreateDefaultGraph(UDlgDialogue *Dialogue, bool bPrompt=true)
static bool PickChildrenOfClass(const FText &TitleText, UClass *&OutChosenClass, UClass *Class)
static UDlgDialogue * GetDialogueFromGraphNode(const UEdGraphNode *GraphNode)
static UEdGraph * CreateNewGraph(UObject *ParentScope, FName GraphName, TSubclassOf< UEdGraph > GraphClass, TSubclassOf< UEdGraphSchema > SchemaClass)
static TSharedPtr< class IDialogueEditor > GetDialogueEditorForGraph(const UEdGraph *Graph)
static void SetLastTargetGraphEdgeBeforeDrag(const UEdGraph *Graph, UDialogueGraphNode_Edge *InEdge)
static void CloseOtherEditors(UObject *Asset, IAssetEditorInstance *OnlyEditor)
static bool CanConvertSpeechNodesToSpeechSequence(const TSet< UObject * > &SelectedNodes, TArray< UDialogueGraphNode * > &OutSelectedGraphNodes)
static void ReplaceParentConnectionsToNode(const UDialogueGraphNode *OldNode, const UDialogueGraphNode *NewNode)
static bool CheckAndTryToFixDialogue(UDlgDialogue *Dialogue, bool bDisplayWarning=true)
static UEdGraphNode_Comment * BlueprintAddComment(UBlueprint *Blueprint, const FString &CommentString, FVector2D Location=FVector2D::ZeroVector)
static bool OpenBlueprintEditor(UBlueprint *Blueprint, EDialogueBlueprintOpenType OpenType=EDialogueBlueprintOpenType::None, FName FunctionNameToOpen=NAME_None, bool bForceFullEditor=true, bool bAddBlueprintFunctionIfItDoesNotExist=false)
static UK2Node_Event * BlueprintGetEvent(UBlueprint *Blueprint, FName EventName, UClass *EventClassSignature)
static bool OpenEditorForAsset(const UObject *Asset)
static void AutoPositionGraphNodes(UDialogueGraphNode *RootNode, const TArray< UDialogueGraphNode * > &GraphNodes, int32 OffsetBetweenColumnsX, int32 OffsetBetweenRowsY, bool bIsDirectionVertical)
static bool GetBoundsForSelectedNodes(const UEdGraph *Graph, FSlateRect &Rect, float Padding=0.0f)
static bool RemoveNode(UEdGraphNode *NodeToRemove)
static const TSet< UObject * > GetSelectedNodes(const UEdGraph *Graph)
static bool CanConvertSpeechSequenceNodeToSpeechNodes(const TSet< UObject * > &SelectedNodes)
static EAppReturnType::Type ShowMessageBox(EAppMsgType::Type MsgType, const FString &Text, const FString &Caption)
static void Refresh(const UEdGraph *Graph, bool bRestorePreviousSelection)
static UEdGraph * BlueprintGetOrAddFunction(UBlueprint *Blueprint, FName FunctionName, UClass *FunctionClassSignature)
static UDlgDialogue * GetDialogueForGraph(const UEdGraph *Graph)
static UDlgNode * GetClosestNodeFromGraphNode(UEdGraphNode *GraphNode)
static void RefreshDetailsView(const UEdGraph *Graph, bool bRestorePreviousSelection)
static bool JumpToGraphNode(const UEdGraphNode *GraphNode)
static TCopyQualifiersFromTo< SetType, typenameSetType::ElementType >::Type * GetFirstSetElement(SetType &Set)
Definition DlgHelper.h:373
bool RemoveGraphNode(UEdGraphNode *NodeToRemove)
bool Modify(bool bAlwaysMarkDirty=true) override
UEdGraphPin * GetOutputPin() const
virtual void SetPosition(int32 X, int32 Y)
UEdGraphPin * GetInputPin() const
int32 EstimateNodeWidth() const
virtual int32 GetDialogueNodeIndex() const
TArray< UDialogueGraphNode * > GetChildNodes() const
TArray< UDialogueGraphNode * > GetParentNodes() const
const DlgNodeType & GetDialogueNode() const
virtual bool IsRootNode() const
TArray< UDialogueGraphNode_Edge * > GetParentEdgeNodes(bool bCheckChild=true) const
void SetEdges(const TArray< FDlgEdge > &InEdges)
UCLASS(BlueprintType, Meta = (DisplayThumbnail = "true"))
Definition DlgDialogue.h:85
const TArray< UDlgNode * > & GetNodes() const
UFUNCTION(BlueprintPure, Category = "Dialogue")
static int32 LoadAllDialoguesIntoMemory(bool bAsync=false)
static TArray< UDlgDialogue * > GetDialoguesWithDuplicateGUIDs()
static TArray< UDlgDialogue * > GetAllDialoguesFromMemory()
UCLASS(BlueprintType, ClassGroup = "Dialogue")
UCLASS(BlueprintType, Abstract, EditInlineNew, ClassGroup = "Dialogue")
Definition DlgNode.h:40
virtual FDlgEdge * GetSafeMutableNodeChildAt(int32 EdgeIndex)
Definition DlgNode.h:213
virtual const TArray< FDlgEdge > & GetNodeChildren() const
Gets this nodes children (edges) as a const/mutable array.
Definition DlgNode.h:184
virtual FDlgCondition * GetMutableEnterConditionAt(int32 EnterConditionIndex)
Definition DlgNode.h:150
virtual const TArray< FDlgCondition > & GetNodeEnterConditions() const
UFUNCTION(BlueprintPure, Category = "Dialogue|Node")
Definition DlgNode.h:145
UCLASS(Config = Engine, DefaultConfig, meta = (DisplayName = "Dialogue System Settings"))
USTRUCT(Blueprintable)
static bool HasNodeIndex(EDlgConditionType ConditionType)
FGuid GUID
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Dialogue|Condition")
int32 IntValue
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dialogue|Condition")
EDlgConditionType ConditionType
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dialogue|Condition")
USTRUCT(BlueprintType)
Definition DlgEdge.h:25
TArray< FDlgCondition > Conditions
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dialogue|Edge")
Definition DlgEdge.h:134
int32 TargetIndex
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Dialogue|Edge", Meta = (ClampMin = -1))
Definition DlgEdge.h:126
NodeWithParentPosition(UDialogueGraphNode *InNode, const int32 InParentNodeX, const int32 InParentNodeY)