A Demo Project for the UnrealEngineSDK
Loading...
Searching...
No Matches
DlgExportTwineCommandlet.cpp
Go to the documentation of this file.
1// Copyright Csaba Molnar, Daniel Butum. All Rights Reserved.
2
4
5#include "Misc/Paths.h"
6#include "Misc/FileHelper.h"
7#include "HAL/PlatformFilemanager.h"
8#include "GenericPlatform/GenericPlatformFile.h"
9#include "UObject/Package.h"
10#include "FileHelpers.h"
11
12#include "DlgManager.h"
16#include "DlgCommandletHelper.h"
17#include "DlgHelper.h"
18
19
20DEFINE_LOG_CATEGORY(LogDlgExportTwineCommandlet);
21
22
23const FString UDlgExportTwineCommandlet::TagNodeStart(TEXT("node-start"));
24const FString UDlgExportTwineCommandlet::TagNodeEnd(TEXT("node-end"));
25const FString UDlgExportTwineCommandlet::TagNodeVirtualParent(TEXT("node-virtual-parent"));
26const FString UDlgExportTwineCommandlet::TagNodeSpeech(TEXT("node-speech"));
27const FString UDlgExportTwineCommandlet::TagNodeSpeechSequence(TEXT("node-speech-sequence"));
28const FString UDlgExportTwineCommandlet::TagNodeSelectorFirst(TEXT("node-selector-first"));
29const FString UDlgExportTwineCommandlet::TagNodeSelectorRandom(TEXT("node-selector-random"));
30
31const FIntPoint UDlgExportTwineCommandlet::SizeSmall(100, 100);
32const FIntPoint UDlgExportTwineCommandlet::SizeWide(200, 100);
33const FIntPoint UDlgExportTwineCommandlet::SizeTall(100, 200);
34const FIntPoint UDlgExportTwineCommandlet::SizeLarge(200, 200);
35
37
39{
40 IsClient = false;
41 IsEditor = true;
42 IsServer = false;
43 LogToConsole = true;
44 ShowErrorCount = true;
45}
46
47
48int32 UDlgExportTwineCommandlet::Main(const FString& Params)
49{
50 UE_LOG(LogDlgExportTwineCommandlet, Display, TEXT("Starting"));
51
53
54 // Parse command line - we're interested in the param vals
55 TArray<FString> Tokens;
56 TArray<FString> Switches;
57 TMap<FString, FString> ParamVals;
58 UCommandlet::ParseCommandLine(*Params, Tokens, Switches, ParamVals);
59
60 if (Switches.Contains(TEXT("Flatten")))
61 {
62 bFlatten = true;
63 }
64
65 // Set the output directory
66 const FString* OutputDirectoryVal = ParamVals.Find(FString(TEXT("OutputDirectory")));
67 if (OutputDirectoryVal == nullptr)
68 {
69 UE_LOG(LogDlgExportTwineCommandlet, Error, TEXT("Did not provide argument -OutputDirectory=<Path>"));
70 return -1;
71 }
72 OutputDirectory = *OutputDirectoryVal;
73
74 if (OutputDirectory.IsEmpty())
75 {
76 UE_LOG(LogDlgExportTwineCommandlet, Error, TEXT("OutputDirectory is empty, please provide a non empty one with -OutputDirectory=<Path>"));
77 return -1;
78 }
79
80 // Make it absolute
81 if (FPaths::IsRelative(OutputDirectory))
82 {
83 OutputDirectory = FPaths::Combine(FPaths::ProjectDir(), OutputDirectory);
84 }
85
86 // Create destination directory
87 IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
88 if (!PlatformFile.DirectoryExists(*OutputDirectory) && PlatformFile.CreateDirectoryTree(*OutputDirectory))
89 {
90 UE_LOG(LogDlgExportTwineCommandlet, Display, TEXT("Creating OutputDirectory = `%s`"), *OutputDirectory);
91 }
92
94 UE_LOG(LogDlgExportTwineCommandlet, Display, TEXT("Exporting to = `%s`"), *OutputDirectory);
95
96 // Some Dialogues may be unclean?
97 //FDlgCommandletHelper::SaveAllDialogues();
98
99 // Keep track of all created files so that we don't have duplicates
100 TSet<FString> CreateFiles;
101
102 // Export to twine
103 const TArray<UDlgDialogue*> AllDialogues = UDlgManager::GetAllDialoguesFromMemory();
104 for (const UDlgDialogue* Dialogue : AllDialogues)
105 {
106 UPackage* Package = Dialogue->GetOutermost();
107 check(Package);
108 const FString OriginalDialoguePath = Package->GetPathName();
109 FString DialoguePath = OriginalDialoguePath;
110
111 // Only export game dialogues
112 if (!FDlgHelper::IsPathInProjectDirectory(DialoguePath))
113 {
114 UE_LOG(LogDlgExportTwineCommandlet, Warning, TEXT("Dialogue = `%s` is not in the game directory, ignoring"), *DialoguePath);
115 continue;
116 }
117
118 verify(DialoguePath.RemoveFromStart(TEXT("/Game")));
119 const FString FileName = FPaths::GetBaseFilename(DialoguePath);
120 const FString Directory = FPaths::GetPath(DialoguePath);
121
122 FString FileSystemFilePath;
123 if (bFlatten)
124 {
125 // Create in root of output directory
126
127 // Make sure file does not exist
128 FString FlattenedFileName = FileName;
129 int32 CurrentTryIndex = 1;
130 while (CreateFiles.Contains(FlattenedFileName) && CurrentTryIndex < 100)
131 {
132 FlattenedFileName = FString::Printf(TEXT("%s-%d"), *FileName, CurrentTryIndex);
133 CurrentTryIndex++;
134 }
135
136 // Give up :(
137 if (CreateFiles.Contains(FlattenedFileName))
138 {
139 UE_LOG(LogDlgExportTwineCommandlet, Warning, TEXT("Dialogue = `%s` could not generate unique flattened file, ignoring"), *DialoguePath);
140 continue;
141 }
142
143 CreateFiles.Add(FlattenedFileName);
144 FileSystemFilePath = OutputDirectory / FlattenedFileName + TEXT(".html");
145 }
146 else
147 {
148 // Ensure directory tree
149 const FString FileSystemDirectoryPath = OutputDirectory / Directory;
150 if (!PlatformFile.DirectoryExists(*FileSystemDirectoryPath) && PlatformFile.CreateDirectoryTree(*FileSystemDirectoryPath))
151 {
152 UE_LOG(LogDlgExportTwineCommandlet, Display, TEXT("Creating directory = `%s`"), *FileSystemDirectoryPath);
153 }
154 FileSystemFilePath = FileSystemDirectoryPath / FileName + TEXT(".html");
155 }
156
157 // Compute minimum graph node positions
158 const TArray<UDlgNode*>& Nodes = Dialogue->GetNodes();
159 MinimumGraphX = 0;
160 MinimumGraphY = 0;
161 if (const UDialogueGraphNode* DialogueGraphNode = Cast<UDialogueGraphNode>(Dialogue->GetStartNode().GetGraphNode()))
162 {
163 MinimumGraphX = FMath::Min(MinimumGraphX, DialogueGraphNode->NodePosX);
164 MinimumGraphY = FMath::Min(MinimumGraphY, DialogueGraphNode->NodePosY);
165 }
166 for (const UDlgNode* Node : Nodes)
167 {
168 const UDialogueGraphNode* DialogueGraphNode = Cast<UDialogueGraphNode>(Node->GetGraphNode());
169 if (DialogueGraphNode == nullptr)
170 {
171 continue;
172 }
173
174 MinimumGraphX = FMath::Min(MinimumGraphX, DialogueGraphNode->NodePosX);
175 MinimumGraphY = FMath::Min(MinimumGraphY, DialogueGraphNode->NodePosY);
176 }
177 //UE_LOG(LogDlgExportTwineCommandlet, Verbose, TEXT("MinimumGraphX = %d, MinimumGraphY = %d"), MinimumGraphX, MinimumGraphY);
178
179 // Gather passages data
180 CurrentNodesAreas.Empty();
181 FString PassagesData;
182 PassagesData += CreateTwinePassageDataFromNode(*Dialogue, Dialogue->GetStartNode(), INDEX_NONE) + TEXT("\n");
183
184 // The rest of the nodes
185 for (int32 NodeIndex = 0; NodeIndex < Nodes.Num(); NodeIndex++)
186 {
187 PassagesData += CreateTwinePassageDataFromNode(*Dialogue, *Nodes[NodeIndex], NodeIndex) + TEXT("\n");
188 }
189
190 // Export file
191 const FString TwineFileContent = CreateTwineStoryData(Dialogue->GetDialogueName(), Dialogue->GetGUID(), INDEX_NONE, PassagesData);
192 if (FFileHelper::SaveStringToFile(TwineFileContent, *FileSystemFilePath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
193 {
194 UE_LOG(LogDlgExportTwineCommandlet, Display, TEXT("Writing file = `%s` for Dialogue = `%s` "), *FileSystemFilePath, *OriginalDialoguePath);
195 }
196 else
197 {
198 UE_LOG(LogDlgExportTwineCommandlet, Error, TEXT("FAILED to write file = `%s` for Dialogue = `%s`"), *FileSystemFilePath, *OriginalDialoguePath);
199 }
200 }
201
202 return 0;
203}
204
205
206FString UDlgExportTwineCommandlet::CreateTwineStoryData(const FString& Name, const FGuid& DialogueGUID, int32 StartNodeIndex, const FString& PassagesData)
207{
208 static const FString Creator = TEXT("UE-NotYetDlgSystem");
209 static const FString CreatorVersion = TEXT("5.0"); // TODO
210 static constexpr int32 Zoom = 1;
211 static const FString Format = TEXT("Harlowe");
212 static const FString FormatVersion = TEXT("2.1.0");
213
214 //const FGuid UUID = FGuid::NewGuid();
215 return FString::Printf(
216 TEXT("<tw-storydata name=\"%s\" startnode=\"%d\" creator=\"%s\" creator-version=\"%s\"")
217 TEXT(" ifid=\"%s\" zoom=\"%d\" format=\"%s\" format-version=\"%s\" options=\"\" hidden>\n")
218
219 TEXT("<style role=\"stylesheet\" id=\"twine-user-stylesheet\" type=\"text/twine-css\">%s</style>\n")
220 TEXT("<script role=\"script\" id=\"twine-user-script\" type=\"text/twine-javascript\"></script>\n")
221
222 // tags colors data
223 TEXT("\n%s\n")
224 // Special tag to identify the dialogue id
225 //TEXT("<tw-passagedata pid=\"-1\" tags=\"\" name=\"DialogueGUID\" position=\"0,0\" size=\"10,10\">%s</tw-passagedata>\n")
226
227 TEXT("%s\n")
228
229 TEXT("</tw-storydata>"),
230 *Name, StartNodeIndex + 2, *Creator, *CreatorVersion,
231 *DialogueGUID.ToString(EGuidFormats::DigitsWithHyphens), Zoom, *Format, *FormatVersion,
234 *PassagesData
235 );
236}
237
238bool UDlgExportTwineCommandlet::GetBoxThatConflicts(const FBox2D& Box, FBox2D& OutConflict)
239{
240 for (const FBox2D& CurrentBox : CurrentNodesAreas)
241 {
242 if (CurrentBox.Intersect(Box))
243 {
244 OutConflict = CurrentBox;
245 return true;
246 }
247 }
248
249 return false;
250}
251
252FIntPoint UDlgExportTwineCommandlet::GetNonConflictingPointFor(const FIntPoint& Point, const FIntPoint& Size, const FIntPoint& Padding)
253{
254 FVector2D MinVector(Point + Padding);
255 FVector2D MaxVector(MinVector + Size);
256 FBox2D NewBox(MinVector, MaxVector);
257
258 FBox2D ConflictBox;
259 while (GetBoxThatConflicts(NewBox, ConflictBox))
260 {
261 //UE_LOG(LogDlgExportTwineCommandlet, Warning, TEXT("Found conflict in rectangle: %s for Point: %s"), *ConflictRect.ToString(), *NewPoint.ToString());
262 // Assume the curent box is a child, adjust
263 FVector2D Center, Extent;
264 ConflictBox.GetCenterAndExtents(Center, Extent);
265
266 // Update on vertical
267 MinVector.Y += Extent.Y / 2.f;
268 MaxVector = MinVector + Size;
269 NewBox = FBox2D(MinVector, MaxVector);
270 }
271
272 CurrentNodesAreas.Add(NewBox);
273 return MinVector.IntPoint();
274}
275
277{
278 const UDialogueGraphNode* DialogueGraphNode = Cast<UDialogueGraphNode>(Node.GetGraphNode());
279 if (DialogueGraphNode == nullptr)
280 {
281 UE_LOG(LogDlgExportTwineCommandlet, Warning, TEXT("Invalid UDialogueGraphNode for Node index = %d in Dialogue = `%s`. Ignoring."), NodeIndex, *Dialogue.GetPathName());
282 return "";
283 }
284 const bool bIsRootNode = DialogueGraphNode->IsRootNode();
285
286 const FString NodeName = GetNodeNameFromNode(Node, NodeIndex, bIsRootNode);
287 FString Tags;
288 FIntPoint Position = GraphNodeToTwineCanvas(DialogueGraphNode->NodePosX, DialogueGraphNode->NodePosY);
289
290 // TODO fix this
291 TSharedPtr<SGraphNode> NodeWidget = DialogueGraphNode->GetNodeWidget();
292 FIntPoint Size = SizeLarge;
293 if (NodeWidget.IsValid())
294 {
295 Size = FIntPoint(NodeWidget->GetDesiredSize().X, NodeWidget->GetDesiredSize().Y);
296 }
297
298 FString NodeContent;
299 const FIntPoint Padding(20, 20);
300 if (DialogueGraphNode->IsRootNode())
301 {
302 verify(NodeIndex == INDEX_NONE);
303 Tags += TagNodeStart;
304 Size = SizeSmall;
305 Position = GetNonConflictingPointFor(Position, Size, Padding);
306
308 return CreateTwinePassageData(NodeIndex, NodeName, Tags, Position, Size, NodeContent);
309 }
310
311 verify(NodeIndex >= 0);
312 if (DialogueGraphNode->IsVirtualParentNode())
313 {
314 // Edges from this node do not matter
315 Tags += TagNodeVirtualParent;
316 Position = GetNonConflictingPointFor(Position, Size, Padding);
317 //CurrentNodesAreas.Add(FIntRect(Position + Padding, Position + Size + Padding));
318
319 const UDlgNode_Speech& NodeSpeech = DialogueGraphNode->GetDialogueNode<UDlgNode_Speech>();
320 NodeContent += EscapeHtml(NodeSpeech.GetNodeUnformattedText().ToString());
321 NodeContent += TEXT("\n\n\n") + CreateTwinePassageDataLinksFromEdges(Dialogue, Node.GetNodeChildren(), true);
322 return CreateTwinePassageData(NodeIndex, NodeName, Tags, Position, Size, NodeContent);
323 }
324 if (DialogueGraphNode->IsSpeechNode())
325 {
326 Tags += TagNodeSpeech;
327 Position = GetNonConflictingPointFor(Position, Size, Padding);
328 //CurrentNodesAreas.Add(FIntRect(Position + Padding, Position + Size + Padding));
329
330 const UDlgNode_Speech& NodeSpeech = DialogueGraphNode->GetDialogueNode<UDlgNode_Speech>();
331 NodeContent += EscapeHtml(NodeSpeech.GetNodeUnformattedText().ToString());
332 NodeContent += TEXT("\n\n\n") + CreateTwinePassageDataLinksFromEdges(Dialogue, Node.GetNodeChildren());
333 return CreateTwinePassageData(NodeIndex, NodeName, Tags, Position, Size, NodeContent);
334 }
335 if (DialogueGraphNode->IsEndNode())
336 {
337 // Does not have any children/text
338 Tags += TagNodeEnd;
339 Size = SizeSmall;
340 Position = GetNonConflictingPointFor(Position, Size, Padding);
341 //CurrentNodesAreas.Add(FIntRect(Position + Padding, Position + Size + Padding));
342
343 NodeContent += TEXT("END");
344 return CreateTwinePassageData(NodeIndex, NodeName, Tags, Position, Size, NodeContent);
345 }
346 if (DialogueGraphNode->IsSelectorNode())
347 {
348 // Does not have any text and text for edges does not matter
349 if (DialogueGraphNode->IsSelectorFirstNode())
350 {
351 Tags += TagNodeSelectorFirst;
352 }
353 if (DialogueGraphNode->IsSelectorRandomNode())
354 {
355 Tags += TagNodeSelectorRandom;
356 }
357 Size = SizeSmall;
358 Position = GetNonConflictingPointFor(Position, Size, Padding);
359
360 NodeContent += TEXT("SELECTOR\n");
362 return CreateTwinePassageData(NodeIndex, NodeName, Tags, Position, Size, NodeContent);
363 }
364 if (DialogueGraphNode->IsSpeechSequenceNode())
365 {
366 Tags += TagNodeSpeechSequence;
367 Position = GetNonConflictingPointFor(Position, Size, Padding);
368
369 const UDlgNode_SpeechSequence& NodeSpeechSequence = DialogueGraphNode->GetDialogueNode<UDlgNode_SpeechSequence>();
370
371 // Fill sequence
372 const TArray<FDlgSpeechSequenceEntry>& Sequence = NodeSpeechSequence.GetNodeSpeechSequence();
373 for (int32 EntryIndex = 0; EntryIndex < Sequence.Num(); EntryIndex++)
374 {
375 const FDlgSpeechSequenceEntry& Entry = Sequence[EntryIndex];
376 NodeContent += FString::Printf(
377 TEXT("``Speaker:`` //%s//\n")
378 TEXT("``Text:`` //%s//\n")
379 TEXT("``EdgeText:`` //%s//\n"),
380 *EscapeHtml(Entry.Speaker.ToString()),
381 *EscapeHtml(Entry.Text.ToString()),
382 *EscapeHtml(Entry.EdgeText.ToString())
383 );
384
385 if (EntryIndex != Sequence.Num() - 1)
386 {
387 NodeContent += TEXT("---\n");
388 }
389 }
390
391 NodeContent += TEXT("\n\n\n") + CreateTwinePassageDataLinksFromEdges(Dialogue, Node.GetNodeChildren());
392 return CreateTwinePassageData(NodeIndex, NodeName, Tags, Position, Size, NodeContent);
393 }
394
395 UE_LOG(LogDlgExportTwineCommandlet, Warning, TEXT("Node index = %d not handled in Dialogue = `%s`. Ignoring."), NodeIndex, *Dialogue.GetPathName());
396 return "";
397}
398
399FString UDlgExportTwineCommandlet::GetNodeNameFromNode(const UDlgNode& Node, int32 NodeIndex, bool bIsRootNode)
400{
401 return FString::Printf(TEXT("%d. %s"), NodeIndex, bIsRootNode ? TEXT("START") : *Node.GetNodeParticipantName().ToString());
402}
403
404FString UDlgExportTwineCommandlet::CreateTwinePassageDataLinksFromEdges(const UDlgDialogue& Dialogue, const TArray<FDlgEdge>& Edges, bool bNoTextOnEdges)
405{
406 FString Links;
407 const TArray<UDlgNode*>& Nodes = Dialogue.GetNodes();
408 for (const FDlgEdge& Edge : Edges)
409 {
410 if (!Edge.IsValid())
411 {
412 continue;
413 }
414 if (!Nodes.IsValidIndex(Edge.TargetIndex))
415 {
416 UE_LOG(LogDlgExportTwineCommandlet, Warning, TEXT("Target index = %d not valid. Ignoring"), Edge.TargetIndex);
417 continue;
418 }
419
420 FString EdgeText;
421 if (bNoTextOnEdges || Edge.GetUnformattedText().IsEmpty())
422 {
423 EdgeText = FString::Printf(TEXT("~ignore~ To Node %d"), Edge.TargetIndex);
424 }
425 else
426 {
427 EdgeText = EscapeHtml(Edge.GetUnformattedText().ToString());
428 }
429 Links += FString::Printf(TEXT("[[%s|%s]]\n"), *EdgeText, *GetNodeNameFromNode(*Nodes[Edge.TargetIndex], Edge.TargetIndex, false));
430 }
431 Links.RemoveFromEnd(TEXT("\n"));
432 return Links;
433}
434
435
436FString UDlgExportTwineCommandlet::CreateTwinePassageData(int32 Pid, const FString& Name, const FString& Tags, const FIntPoint& Position, const FIntPoint& Size, const FString& Content)
437{
438 return FString::Printf(
439 TEXT("<tw-passagedata pid=\"%d\" name=\"%s\" tags=\"%s\" position=\"%d, %d\" size=\"%d, %d\">%s</tw-passagedata>"),
440 Pid + 2, *Name, *Tags, Position.X, Position.Y, Size.X, Size.Y, *Content
441 );
442}
443
445{
446 return TEXT("#storyEditView.passage.tags div.cyan { background: #19e5e6; }");
447}
448
449
451{
453
454 FString TagColorsString;
455 for (const auto& Elem : TwineTagNodesColorsMap)
456 {
457 TagColorsString += FString::Printf(
458 TEXT("<tw-tag name=\"%s\" color=\"%s\"></tw-tag>\n"),
459 *Elem.Key, *Elem.Value
460 );
461 }
462
463 return TagColorsString;
464}
465
467{
468 if (TwineTagNodesColorsMap.Num() > 0)
469 {
470 return;
471 }
472
473 TwineTagNodesColorsMap.Add(TagNodeStart, TEXT("green"));
474 TwineTagNodesColorsMap.Add(TagNodeEnd, TEXT("red"));
476 TwineTagNodesColorsMap.Add(TagNodeSpeech, TEXT("blue"));
478 TwineTagNodesColorsMap.Add(TagNodeSelectorFirst, TEXT("purple"));
480}
DEFINE_LOG_CATEGORY(LogDlgExportTwineCommandlet)
static FORCEINLINE bool IsPathInProjectDirectory(const FString &Path)
Definition DlgHelper.h:224
TSharedPtr< SGraphNode > GetNodeWidget() const
bool IsSelectorNode() const
bool IsSelectorFirstNode() const
bool IsVirtualParentNode() const
const DlgNodeType & GetDialogueNode() const
virtual bool IsRootNode() const
bool IsSelectorRandomNode() const
bool IsSpeechSequenceNode() const
UCLASS(BlueprintType, Meta = (DisplayThumbnail = "true"))
Definition DlgDialogue.h:85
FString CreateTwineStoryData(const FString &Name, const FGuid &DialogueGuid, int32 StartNodeIndex, const FString &PassagesData)
static const FString TagNodeVirtualParent
int32 Main(const FString &Params) override
static TMap< FString, FString > TwineTagNodesColorsMap
static const FString TagNodeSpeechSequence
static FORCEINLINE FString & EscapeHtml(FString &String)
FString CreateTwinePassageDataLinksFromEdges(const UDlgDialogue &Dialogue, const TArray< FDlgEdge > &Edges, bool bNoTextOnEdges=false)
FString GetNodeNameFromNode(const UDlgNode &Node, int32 NodeIndex, bool bIsRootNode=false)
FString CreateTwinePassageData(int32 Pid, const FString &Name, const FString &Tags, const FIntPoint &Position, const FIntPoint &Size, const FString &Content)
static const FString TagNodeSelectorRandom
bool GetBoxThatConflicts(const FBox2D &Box, FBox2D &OutConflict)
FIntPoint GetNonConflictingPointFor(const FIntPoint &InPoint, const FIntPoint &Size, const FIntPoint &Padding)
FORCEINLINE FIntPoint GraphNodeToTwineCanvas(int32 PositionX, int32 PositionY)
FString CreateTwinePassageDataFromNode(const UDlgDialogue &Dialogue, const UDlgNode &Node, int32 NodeIndex)
static const FString TagNodeSelectorFirst
static int32 LoadAllDialoguesIntoMemory(bool bAsync=false)
static TArray< UDlgDialogue * > GetAllDialoguesFromMemory()
UCLASS(BlueprintType, ClassGroup = "Dialogue")
const FText & GetNodeUnformattedText() const override
UFUNCTION(BlueprintPure, Category = "Dialogue|Node")
UCLASS(BlueprintType, ClassGroup = "Dialogue")
const TArray< FDlgSpeechSequenceEntry > & GetNodeSpeechSequence() const
UFUNCTION(BlueprintPure, Category = "Dialogue|Node")
UCLASS(BlueprintType, Abstract, EditInlineNew, ClassGroup = "Dialogue")
Definition DlgNode.h:40
virtual const TArray< FDlgEdge > & GetNodeChildren() const
Gets this nodes children (edges) as a const/mutable array.
Definition DlgNode.h:184
virtual FName GetNodeParticipantName() const
UFUNCTION(BlueprintPure, Category = "Dialogue|Node")
Definition DlgNode.h:127
USTRUCT(BlueprintType)
Definition DlgEdge.h:25
USTRUCT(BlueprintType)
FText Text
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dialogue|Node", Meta = (MultiLine = true))
FName Speaker
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dialogue|Node", Meta = (DisplayName = "Partic...
FText EdgeText
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dialogue|Node", Meta = (MultiLine = true))