A Demo Project for the UnrealEngineSDK
Loading...
Searching...
No Matches
DlgHumanReadableTextCommandlet.cpp
Go to the documentation of this file.
1// Copyright Csaba Molnar, Daniel Butum. All Rights Reserved.
3
4#include "Misc/Paths.h"
5#include "HAL/PlatformFilemanager.h"
6#include "GenericPlatform/GenericPlatformFile.h"
7#include "UObject/Package.h"
8#include "FileHelpers.h"
9
10#include "DlgManager.h"
12#include "IO/DlgJsonWriter.h"
15#include "IO/DlgJsonParser.h"
16#include "DlgCommandletHelper.h"
17#include "DlgHelper.h"
18
19
20DEFINE_LOG_CATEGORY(LogDlgHumanReadableTextCommandlet);
21
22const TCHAR* UDlgHumanReadableTextCommandlet::FileExtension = TEXT(".dlg_human.json");
23
25{
26 IsClient = false;
27 IsEditor = true;
28 IsServer = false;
29 LogToConsole = true;
30 ShowErrorCount = true;
31}
32
33int32 UDlgHumanReadableTextCommandlet::Main(const FString& Params)
34{
35 UE_LOG(LogDlgHumanReadableTextCommandlet, Display, TEXT("Starting"));
36 Settings = GetDefault<UDlgSystemSettings>();
37
38 // Parse command line - we're interested in the param vals
39 TArray<FString> Tokens;
40 TArray<FString> Switches;
41 TMap<FString, FString> ParamVals;
42 UCommandlet::ParseCommandLine(*Params, Tokens, Switches, ParamVals);
43
44 // Set the output directory
45 const FString* OutputInputDirectoryVal = ParamVals.Find(FString(TEXT("OutputInputDirectory")));
46 if (OutputInputDirectoryVal == nullptr)
47 {
48 UE_LOG(LogDlgHumanReadableTextCommandlet, Error, TEXT("Did not provide argument -OutputInputDirectory=<Path>"));
49 return -1;
50 }
51 OutputInputDirectory = *OutputInputDirectoryVal;
52
53 if (OutputInputDirectory.IsEmpty())
54 {
55 UE_LOG(LogDlgHumanReadableTextCommandlet, Error, TEXT("OutputInputDirectory is empty, please provide a non empty one with -OutputInputDirectory=<Path>"));
56 return -1;
57 }
58
59 // Make it absolute
60 if (FPaths::IsRelative(OutputInputDirectory))
61 {
62 OutputInputDirectory = FPaths::Combine(FPaths::ProjectDir(), OutputInputDirectory);
63 }
64
65 if (Switches.Contains(TEXT("SaveAllDialogues")))
66 {
67 bSaveAllDialogues = true;
68 }
69 else if (Switches.Contains(TEXT("NoSaveAllDialogues")))
70 {
71 bSaveAllDialogues = false;
72 }
73
74 if (Switches.Contains(TEXT("Export")))
75 {
76 bExport = true;
77 }
78 else if (Switches.Contains("Import"))
79 {
80 bImport = true;
81 }
82 if (!bExport && !bImport)
83 {
84 UE_LOG(LogDlgHumanReadableTextCommandlet, Error, TEXT("Did not choose any operationg. Either -export OR -import"));
85 return -1;
86 }
87
88 // Create destination directory
89 IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
90 if (!PlatformFile.DirectoryExists(*OutputInputDirectory) && PlatformFile.CreateDirectoryTree(*OutputInputDirectory))
91 {
92 UE_LOG(LogDlgHumanReadableTextCommandlet, Display, TEXT("Creating OutputInputDirectory = `%s`"), *OutputInputDirectory);
93 }
94
96
97 if (bExport)
98 return Export();
99 if (bImport)
100 return Import();
101
102 return 0;
103}
104
106{
107 UE_LOG(LogDlgHumanReadableTextCommandlet, Display, TEXT("Exporting to = `%s`"), *OutputInputDirectory);
108
109 // Some Dialogues may be unclean?
111 {
113 }
114
115 IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
116 const TArray<UDlgDialogue*> AllDialogues = UDlgManager::GetAllDialoguesFromMemory();
117 for (const UDlgDialogue* Dialogue : AllDialogues)
118 {
119 UPackage* Package = Dialogue->GetOutermost();
120 check(Package);
121 const FString OriginalDialoguePath = Package->GetPathName();
122 FString DialoguePath = OriginalDialoguePath;
123
124 // Only export game dialogues
125 if (!FDlgHelper::IsPathInProjectDirectory(DialoguePath))
126 {
127 UE_LOG(LogDlgHumanReadableTextCommandlet, Warning, TEXT("Dialogue = `%s` is not in the game directory, ignoring"), *DialoguePath);
128 continue;
129 }
130
131 verify(DialoguePath.RemoveFromStart(TEXT("/Game")));
132 const FString FileName = FPaths::GetBaseFilename(DialoguePath);
133 const FString Directory = FPaths::GetPath(DialoguePath);
134
135 // Ensure directory tree
136 const FString FileSystemDirectoryPath = OutputInputDirectory / Directory;
137 if (!PlatformFile.DirectoryExists(*FileSystemDirectoryPath) && PlatformFile.CreateDirectoryTree(*FileSystemDirectoryPath))
138 {
139 UE_LOG(LogDlgHumanReadableTextCommandlet, Display, TEXT("Creating directory = `%s`"), *FileSystemDirectoryPath);
140 }
141
142 // Export file
143 FDlgJsonWriter JsonWriter;
146 {
147 continue;
148 }
149 JsonWriter.Write(FDlgDialogue_FormatHumanReadable::StaticStruct(), &ExportFormat);
150
151 const FString FileSystemFilePath = FileSystemDirectoryPath / FileName + FileExtension;
152 if (JsonWriter.ExportToFile(FileSystemFilePath))
153 {
154 UE_LOG(LogDlgHumanReadableTextCommandlet, Display, TEXT("Writing file = `%s` for Dialogue = `%s` "), *FileSystemFilePath, *OriginalDialoguePath);
155 }
156 else
157 {
158 UE_LOG(LogDlgHumanReadableTextCommandlet, Error, TEXT("FAILED to write file = `%s` for Dialogue = `%s`"), *FileSystemFilePath, *OriginalDialoguePath);
159 }
160 }
161
162 return 0;
163}
164
166{
167 UE_LOG(LogDlgHumanReadableTextCommandlet, Display, TEXT("Importing from = `%s`"), *OutputInputDirectory);
168
169 PackagesToSave.Empty();
170 TMap<FGuid, UDlgDialogue*> DialoguesMap = UDlgManager::GetAllDialoguesGUIDsMap();
171 IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
172
173 // Find all files
174 TArray<FString> FoundFiles;
175 PlatformFile.FindFilesRecursively(FoundFiles, *OutputInputDirectory, FileExtension);
176 if (FoundFiles.Num() == 0)
177 {
178 UE_LOG(LogDlgHumanReadableTextCommandlet, Error, TEXT("FAILED import, could not find any files with the extension = `%s` inside the directory = `%s`"), FileExtension, *OutputInputDirectory);
179 return -1;
180 }
181
182 for (const FString& File : FoundFiles)
183 {
184 UE_LOG(LogDlgHumanReadableTextCommandlet, Display, TEXT("Reading file = `%s` "), *File);
185
186 FDlgJsonParser JsonParser;
187 JsonParser.InitializeParser(File);
188 if (!JsonParser.IsValidFile())
189 {
190 UE_LOG(LogDlgHumanReadableTextCommandlet, Error, TEXT("FAILED to read file = `%s`"), *File);
191 continue;
192 }
193
195 JsonParser.ReadAllProperty(FDlgDialogue_FormatHumanReadable::StaticStruct(), &HumanFormat);
196 if (!JsonParser.IsValidFile())
197 {
198 UE_LOG(LogDlgHumanReadableTextCommandlet, Error, TEXT("File = `%s` is not a valid JSON file"), *File);
199 continue;
200 }
201
202
203 // Find Dialogue
204 UDlgDialogue** DialoguePtr = DialoguesMap.Find(HumanFormat.DialogueGUID);
205 if (DialoguePtr == nullptr)
206 {
207 UE_LOG(LogDlgHumanReadableTextCommandlet,
208 Error,
209 TEXT("Can't find Dialogue for GUID = `%s`, DialogueName = `%s` from File = `%s`"),
210 *HumanFormat.DialogueGUID.ToString(), *HumanFormat.DialogueName.ToString(), *File);
211 continue;
212 }
213
214 // Import
215 UDlgDialogue* Dialogue = *DialoguePtr;
217 {
218 PackagesToSave.Add(Dialogue->GetOutermost());
219 }
220 }
221
222 return UEditorLoadingAndSavingUtils::SavePackages(PackagesToSave, false) == true ? 0 : -1;
223}
224
226{
227 OutFormat.DialogueName = Dialogue.GetDialogueFName();
228 OutFormat.DialogueGUID = Dialogue.GetGUID();
229
230 // Root Node
231 {
233 RootNode.NodeIndex = RootNodeIndex;
234 ExportNodeEdgesToHumanReadableFormat(Dialogue.GetStartNode().GetNodeChildren(), RootNode.Edges);
235 OutFormat.SpeechNodes.Add(RootNode);
236 }
237
238 const TArray<UDlgNode*>& Nodes = Dialogue.GetNodes();
239 for (int32 NodeIndex = 0; NodeIndex < Nodes.Num(); NodeIndex++)
240 {
241 const UDlgNode* Node = Nodes[NodeIndex];
242 if (const UDlgNode_Speech* NodeSpeech = Cast<UDlgNode_Speech>(Node))
243 {
244 // Fill Nodes
246 ExportNode.NodeIndex = NodeIndex;
247 ExportNode.Speaker = NodeSpeech->GetNodeParticipantName();
248 ExportNode.Text = NodeSpeech->GetNodeUnformattedText();
249
250 // Fill Edges
252 OutFormat.SpeechNodes.Add(ExportNode);
253 }
254 else if (const UDlgNode_SpeechSequence* NodeSpeechSequence = Cast<UDlgNode_SpeechSequence>(Node))
255 {
256 // Speech Sequence
257
259 ExportNode.NodeIndex = NodeIndex;
260 ExportNode.Speaker = NodeSpeechSequence->GetNodeParticipantName();
261
262 // Fill sequence
263 for (const FDlgSpeechSequenceEntry& Entry : NodeSpeechSequence->GetNodeSpeechSequence())
264 {
266 ExportEntry.EdgeText = Entry.EdgeText;
267 ExportEntry.Text = Entry.Text;
268 ExportEntry.Speaker = Entry.Speaker;
269 ExportNode.Sequence.Add(ExportEntry);
270 }
271
272 // Fill Edges
274 OutFormat.SpeechSequenceNodes.Add(ExportNode);
275 }
276 else
277 {
278 // not supported
279 }
280
281 // Sanity check
282 if (const UDialogueGraphNode_Base* GraphNode = Cast<UDialogueGraphNode_Base>(Node->GetGraphNode()))
283 {
284 GraphNode->CheckAll();
285 }
286 }
287
288 return true;
289}
290
292{
293 if (Node == nullptr)
294 {
295 return false;
296 }
297
298 const UEdGraphNode* GraphNode = Node->GetGraphNode();
299 if (GraphNode == nullptr)
300 {
301 return false;
302 }
303
304 const UDialogueGraphNode* DialogueGraphNode = Cast<UDialogueGraphNode>(GraphNode);
305 if (DialogueGraphNode == nullptr)
306 {
307 return false;
308 }
309
310 for (const UDialogueGraphNode* ParentNode : DialogueGraphNode->GetParentNodes())
311 {
312 OutContext.ParentNodeIndices.Add(ParentNode->GetDialogueNodeIndex());
313 }
314 for (const UDialogueGraphNode* ChildNode : DialogueGraphNode->GetChildNodes())
315 {
316 OutContext.ChildNodeIndices.Add(ChildNode->GetDialogueNodeIndex());
317 }
318
319 return true;
320}
321
322void UDlgHumanReadableTextCommandlet::ExportNodeEdgesToHumanReadableFormat(const TArray<FDlgEdge>& Edges, TArray<FDlgEdge_FormatHumanReadable>& OutEdges)
323{
324 // Fill Edges
325 for (const FDlgEdge& Edge : Edges)
326 {
327 if (!Edge.IsValid())
328 {
329 continue;
330 }
331
333 ExportEdge.TargetNodeIndex = Edge.TargetIndex;
334 ExportEdge.Text = Edge.GetUnformattedText();
335 OutEdges.Add(ExportEdge);
336 }
337}
338
340{
341 verify(Dialogue);
342
343 bool bModified = false;
344 if (Format.SpeechNodes.Num() == 0 && Format.SpeechSequenceNodes.Num() == 0)
345 {
346 UE_LOG(LogDlgHumanReadableTextCommandlet, Warning, TEXT("ImportHumanReadableFormatIntoDialogue: No data to import for Dialogue = `%s`"), *Dialogue->GetPathName());
347 return false;
348 }
349
350 // Speech nodes
351 for (const FDlgNodeSpeech_FormatHumanReadable& HumanNode : Format.SpeechNodes)
352 {
353 if (!HumanNode.IsValid())
354 {
355 continue;
356 }
357
358 // Handle root Node
359 const bool bIsRootNode = HumanNode.NodeIndex == RootNodeIndex;
360
361 // Node
362 UDlgNode* Node = bIsRootNode ? Dialogue->GetMutableStartNode() : Dialogue->GetMutableNodeFromIndex(HumanNode.NodeIndex);
363 if (Node == nullptr)
364 {
365 UE_LOG(LogDlgHumanReadableTextCommandlet, Warning, TEXT("Invalid node index = %d, in Dialogue = `%s`. Ignoring."), HumanNode.NodeIndex, *Dialogue->GetPathName());
366 continue;
367 }
368
369 if (!bIsRootNode)
370 {
371 UDlgNode_Speech* NodeSpeech = Cast<UDlgNode_Speech>(Node);
372 if (NodeSpeech == nullptr)
373 {
374 UE_LOG(LogDlgHumanReadableTextCommandlet, Warning, TEXT("Node index = %d is not a UDlgNode_Speech, in Dialogue = `%s`. Ignoring."), HumanNode.NodeIndex, *Dialogue->GetPathName());
375 continue;
376 }
377
378 // Node Text changed
379 if (!NodeSpeech->GetNodeUnformattedText().EqualTo(HumanNode.Text))
380 {
381 NodeSpeech->SetNodeText(HumanNode.Text);
382 bModified = true;
383 }
384
385 // Node speaker changed
386 if (!NodeSpeech->GetNodeParticipantName().IsEqual(HumanNode.Speaker, ENameCase::CaseSensitive))
387 {
388 NodeSpeech->SetNodeParticipantName(HumanNode.Speaker);
389 bModified = true;
390 }
391 }
392
393 UDialogueGraphNode* GraphNode = Cast<UDialogueGraphNode>(Node->GetGraphNode());
394 if (GraphNode == nullptr)
395 {
396 UE_LOG(LogDlgHumanReadableTextCommandlet, Warning, TEXT("Invalid UDialogueGraphNode for Node index = %d in Dialogue = `%s`. Ignoring."), HumanNode.NodeIndex, *Dialogue->GetPathName());
397 continue;
398 }
399
400 // Edges
401 if (SetGraphNodesNewEdgesText(GraphNode, HumanNode.Edges, HumanNode.NodeIndex, Dialogue))
402 {
403 bModified = true;
404 }
405 GraphNode->CheckAll();
406 }
407
408 // Speech sequence nodes
409 for (const FDlgNodeSpeechSequence_FormatHumanReadable& HumanSpeechSequence : Format.SpeechSequenceNodes)
410 {
411 if (!HumanSpeechSequence.IsValid())
412 {
413 continue;
414 }
415
416 // Node
417 UDlgNode* Node = Dialogue->GetMutableNodeFromIndex(HumanSpeechSequence.NodeIndex);
418 if (Node == nullptr)
419 {
420 UE_LOG(LogDlgHumanReadableTextCommandlet,
421 Warning,
422 TEXT("Invalid node speech sequence index = %d, in Dialogue = `%s`. Ignoring."),
423 HumanSpeechSequence.NodeIndex, *Dialogue->GetPathName());
424 continue;
425 }
426
427 UDlgNode_SpeechSequence* NodeSpeechSequence = Cast<UDlgNode_SpeechSequence>(Node);
428 if (NodeSpeechSequence == nullptr)
429 {
430 UE_LOG(LogDlgHumanReadableTextCommandlet,
431 Warning,
432 TEXT("Node node speech sequence index = %d is not a UDlgNode_SpeechSequence, in Dialogue = `%s`. Ignoring."),
433 HumanSpeechSequence.NodeIndex, *Dialogue->GetPathName());
434 continue;
435 }
436
437 // Node speaker changed
438 if (!NodeSpeechSequence->GetNodeParticipantName().IsEqual(HumanSpeechSequence.Speaker, ENameCase::CaseSensitive))
439 {
440 NodeSpeechSequence->SetNodeParticipantName(HumanSpeechSequence.Speaker);
441 bModified = true;
442 }
443
444 // Sequence nodes
445 TArray<FDlgSpeechSequenceEntry>& SequenceArray = *NodeSpeechSequence->GetMutableNodeSpeechSequence();
446 for (int32 SequenceIndex = 0; SequenceIndex < SequenceArray.Num() && SequenceIndex < HumanSpeechSequence.Sequence.Num(); SequenceIndex++)
447 {
448 const FDlgSpeechSequenceEntry_FormatHumanReadable& HumanSequence = HumanSpeechSequence.Sequence[SequenceIndex];
449
450 // Edge changed
451 if (!SequenceArray[SequenceIndex].EdgeText.EqualTo(HumanSequence.EdgeText))
452 {
453 SequenceArray[SequenceIndex].EdgeText = HumanSequence.EdgeText;
454 bModified = true;
455 }
456
457 // Speaker Changed
458 if (!SequenceArray[SequenceIndex].Speaker.IsEqual(HumanSequence.Speaker, ENameCase::CaseSensitive))
459 {
460 SequenceArray[SequenceIndex].Speaker = HumanSequence.Speaker;
461 bModified = true;
462 }
463
464 // Text changed
465 if (!SequenceArray[SequenceIndex].Text.EqualTo(HumanSequence.Text))
466 {
467 SequenceArray[SequenceIndex].Text = HumanSequence.Text;
468 bModified = true;
469 }
470 }
471
472 UDialogueGraphNode* GraphNode = Cast<UDialogueGraphNode>(Node->GetGraphNode());
473 if (GraphNode == nullptr)
474 {
475 UE_LOG(LogDlgHumanReadableTextCommandlet, Warning, TEXT("Invalid UDialogueGraphNode for Node index = %d in Dialogue = `%s`. Ignoring."), HumanSpeechSequence.NodeIndex, *Dialogue->GetPathName());
476 continue;
477 }
478
479 // Edges from big node
480 if (SetGraphNodesNewEdgesText(GraphNode, HumanSpeechSequence.Edges, HumanSpeechSequence.NodeIndex, Dialogue))
481 {
482 bModified = true;
483 }
484 GraphNode->CheckAll();
485 }
486
487 if (bModified)
488 {
489 Dialogue->Modify();
490 Dialogue->MarkPackageDirty();
491 }
492
493 return bModified;
494}
495
496bool UDlgHumanReadableTextCommandlet::SetGraphNodesNewEdgesText(UDialogueGraphNode* GraphNode, const TArray<FDlgEdge_FormatHumanReadable>& Edges, int32 NodeIndex, const UDlgDialogue* Dialogue)
497{
498 bool bModified = false;
499
500 for (const FDlgEdge_FormatHumanReadable& HumanEdge : Edges)
501 {
502 const int32 EdgeIndex = GraphNode->GetChildEdgeIndexForChildNodeIndex(HumanEdge.TargetNodeIndex);
503 if (EdgeIndex < 0)
504 {
505 UE_LOG(LogDlgHumanReadableTextCommandlet,
506 Warning,
507 TEXT("Invalid EdgeIndex = %d for Node index = %d in Dialogue = `%s`. Ignoring."),
508 HumanEdge.TargetNodeIndex, NodeIndex, *Dialogue->GetPathName());
509 continue;
510 }
511
512 // Edge Changed
513 if (!GraphNode->GetDialogueNode().GetNodeChildren()[EdgeIndex].GetUnformattedText().EqualTo(HumanEdge.Text))
514 {
515 GraphNode->SetEdgeTextAt(EdgeIndex, HumanEdge.Text);
516 bModified = true;
517 }
518 }
519
520 return bModified;
521}
522
524{
525 checkNoEntry();
526 return false;
527 // if (Settings->bSetDefaultEdgeTexts)
528 // {
529
530 // }
531
532 // return UDlgSystemSettings::EdgeTextFinish.EqualToCaseIgnored(EdgeText) || UDlgSystemSettings::EdgeTextNext.EqualToCaseIgnored(EdgeText);
533}
DEFINE_LOG_CATEGORY(LogDlgHumanReadableTextCommandlet)
static FORCEINLINE bool IsPathInProjectDirectory(const FString &Path)
Definition DlgHelper.h:224
The DlgJsonParser class mostly adapted for Dialogues, copied from FJsonObjectConverter See IDlgParser...
bool IsValidFile() const override
void ReadAllProperty(const UStruct *ReferenceClass, void *TargetObject, UObject *DefaultObjectOuter=nullptr) override
void InitializeParser(const FString &FilePath) override
The DlgJsonWriter class mostly adapted for Dialogues, copied from FJsonObjectConverter See IDlgWriter...
bool ExportToFile(const FString &FileName) override
void Write(const UStruct *StructDefinition, const void *ContainerPtr) override
int32 GetChildEdgeIndexForChildNodeIndex(int32 ChildNodeIndex) const
void SetEdgeTextAt(int32 EdgeIndex, const FText &NewText)
TArray< UDialogueGraphNode * > GetChildNodes() const
TArray< UDialogueGraphNode * > GetParentNodes() const
int32 NodeIndex
UPROPERTY(VisibleAnywhere, Category = DialogueGraphNode)
const DlgNodeType & GetDialogueNode() const
void CheckAll() const override
UCLASS(BlueprintType, Meta = (DisplayThumbnail = "true"))
Definition DlgDialogue.h:85
int32 Main(const FString &Params) override
static bool ExportNodeToContext(const UDlgNode *Node, FDlgNodeContext_FormatHumanReadable &OutContext)
static void ExportNodeEdgesToHumanReadableFormat(const TArray< FDlgEdge > &Edges, TArray< FDlgEdge_FormatHumanReadable > &OutEdges)
bool ImportHumanReadableFormatIntoDialogue(const FDlgDialogue_FormatHumanReadable &Format, UDlgDialogue *Dialogue)
static bool SetGraphNodesNewEdgesText(UDialogueGraphNode *GraphNode, const TArray< FDlgEdge_FormatHumanReadable > &Edges, int32 NodeIndex, const UDlgDialogue *Dialogue)
bool ExportDialogueToHumanReadableFormat(const UDlgDialogue &Dialogue, FDlgDialogue_FormatHumanReadable &OutFormat)
static TMap< FGuid, UDlgDialogue * > GetAllDialoguesGUIDsMap()
static int32 LoadAllDialoguesIntoMemory(bool bAsync=false)
static TArray< UDlgDialogue * > GetAllDialoguesFromMemory()
UCLASS(BlueprintType, ClassGroup = "Dialogue")
virtual void SetNodeText(const FText &InText)
const FText & GetNodeUnformattedText() const override
UFUNCTION(BlueprintPure, Category = "Dialogue|Node")
UCLASS(BlueprintType, ClassGroup = "Dialogue")
FName GetNodeParticipantName() const override
UFUNCTION(BlueprintPure, Category = "Dialogue|Node")
TArray< FDlgSpeechSequenceEntry > * GetMutableNodeSpeechSequence()
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
virtual void SetNodeParticipantName(FName InName)
Definition DlgNode.h:129
TArray< FDlgNodeSpeechSequence_FormatHumanReadable > SpeechSequenceNodes
UPROPERTY()
TArray< FDlgNodeSpeech_FormatHumanReadable > SpeechNodes
UPROPERTY()
USTRUCT(BlueprintType)
Definition DlgEdge.h:25
TArray< FDlgEdge_FormatHumanReadable > Edges
UPROPERTY()
TArray< FDlgEdge_FormatHumanReadable > Edges
UPROPERTY()
TArray< FDlgSpeechSequenceEntry_FormatHumanReadable > Sequence
UPROPERTY()
USTRUCT()
FText Text
UPROPERTY()
FName Speaker
UPROPERTY()
FText EdgeText
UPROPERTY()
USTRUCT(BlueprintType)