A Demo Project for the UnrealEngineSDK
Loading...
Searching...
No Matches
DlgDialogue.cpp
Go to the documentation of this file.
1// Copyright Csaba Molnar, Daniel Butum. All Rights Reserved.
2#include "DlgDialogue.h"
3
4#include "UObject/DevObjectVersion.h"
5#include "HAL/FileManager.h"
6#include "Misc/Paths.h"
7
8#if WITH_EDITOR
9#include "EdGraph/EdGraph.h"
10#include "EdGraph/EdGraphSchema.h"
11#endif
12
13#include "DlgSystemModule.h"
14#include "IO/DlgConfigParser.h"
15#include "IO/DlgConfigWriter.h"
16#include "IO/DlgJsonWriter.h"
17#include "IO/DlgJsonParser.h"
19#include "Nodes/DlgNode_End.h"
20#include "DlgManager.h"
21#include "Logging/DlgLogger.h"
22#include "DlgHelper.h"
23
24#define LOCTEXT_NAMESPACE "DlgDialogue"
25
26// Unique DlgDialogue Object version id, generated with random
27const FGuid FDlgDialogueObjectVersion::GUID(0x2B8E5105, 0x6F66348F, 0x2A8A0B25, 0x9047A071);
28// Register Dialogue custom version with Core
30 FDlgDialogueObjectVersion::LatestVersion, TEXT("Dev-DlgDialogue"));
31
32
33// Update dialogue up to the ConvertedNodesToUObject version
35{
36 // No Longer supported, get data from text file, and reconstruct everything
37 Dialogue->InitialSyncWithTextFile();
38#if WITH_EDITOR
39 // Force clear the old graph
40 Dialogue->ClearGraph();
41#endif
42}
43
44// Update dialogue up to the UseOnlyOneOutputAndInputPin version
46{
47#if WITH_EDITOR
48 Dialogue->GetDialogueEditorAccess()->UpdateDialogueToVersion_UseOnlyOneOutputAndInputPin(Dialogue);
49#endif
50}
51
52
54// Begin UObject interface
55void UDlgDialogue::PreSave(const class ITargetPlatform* TargetPlatform)
56{
57 Super::PreSave(TargetPlatform);
59 bWasLoaded = true;
61}
62
63void UDlgDialogue::Serialize(FArchive& Ar)
64{
65 Ar.UsingCustomVersion(FDlgDialogueObjectVersion::GUID);
66 Super::Serialize(Ar);
67 const int32 DialogueVersion = Ar.CustomVer(FDlgDialogueObjectVersion::GUID);
69 {
70 // No Longer supported
71 return;
72 }
73}
74
76{
77 Super::PostLoad();
78 const int32 DialogueVersion = GetLinkerCustomVersion(FDlgDialogueObjectVersion::GUID);
79 // Old files, UDlgNode used to be a FDlgNode
81 {
83 }
84
85 // Simplified and reduced the number of pins (only one input/output pin), used for the new visualization
87 {
89 }
90
91 // Simply the number of nodes, VirtualParent Node is merged into Speech Node and SelectRandom and SelectorFirst are merged into one Selector Node
93 {
95 TEXT("Dialogue = `%s` with Version MergeVirtualParentAndSelectorTypes will not be converted. See https://gitlab.com/snippets/1691704 for manual conversion"),
97 );
98 }
99
100 // Refresh the data, so that it is valid after loading.
103 {
105 }
106
107 // Create thew new GUID
108 if (!HasGUID())
109 {
112 TEXT("Creating new GUID = `%s` for Dialogue = `%s` because of of invalid GUID."),
113 *GUID.ToString(), *GetPathName()
114 );
115 }
116
117#if WITH_EDITOR
118 const bool bHasDialogueEditorModule = GetDialogueEditorAccess().IsValid();
119 // If this is false it means the graph nodes are not even created? Check for old files that were saved
120 // before graph editor was even implemented. The editor will popup a prompt from FDialogueEditorUtilities::TryToCreateDefaultGraph
121 if (bHasDialogueEditorModule && !GetDialogueEditorAccess()->AreDialogueNodesInSyncWithGraphNodes(this))
122 {
123 return;
124 }
125#endif
126
127 // Check Nodes for validity
128 const int32 NodesNum = Nodes.Num();
129 for (int32 NodeIndex = 0; NodeIndex < NodesNum; NodeIndex++)
130 {
131 UDlgNode* Node = Nodes[NodeIndex];
132#if WITH_EDITOR
133 if (bHasDialogueEditorModule)
134 {
135 checkf(Node->GetGraphNode(), TEXT("Expected DialogueVersion = %d to have a valid GraphNode for Node index = %d :("), DialogueVersion, NodeIndex);
136 }
137#endif
138 // Check children point to the right Node
139 const TArray<FDlgEdge>& NodeEdges = Node->GetNodeChildren();
140 const int32 EdgesNum = NodeEdges.Num();
141 for (int32 EdgeIndex = 0; EdgeIndex < EdgesNum; EdgeIndex++)
142 {
143 const FDlgEdge& Edge = NodeEdges[EdgeIndex];
144 if (!Edge.IsValid())
145 {
146 continue;
147 }
148
149 if (!Nodes.IsValidIndex(Edge.TargetIndex))
150 {
151 UE_LOG(
152 LogDlgSystem,
153 Fatal,
154 TEXT("Node with index = %d does not have a valid Edge index = %d with TargetIndex = %d"),
155 NodeIndex, EdgeIndex, Edge.TargetIndex
156 );
157 }
158 }
159 }
160
161 bWasLoaded = true;
162}
163
165{
166 Super::PostInitProperties();
167
168 // Ignore these cases
169 if (HasAnyFlags(RF_ClassDefaultObject | RF_NeedLoad))
170 {
171 return;
172 }
173
174 const int32 DialogueVersion = GetLinkerCustomVersion(FDlgDialogueObjectVersion::GUID);
175
176#if WITH_EDITOR
177 // Wait for the editor module to be set by the editor in UDialogueGraph constructor
178 if (GetDialogueEditorAccess().IsValid())
179 {
180 CreateGraph();
181 }
182#endif // #if WITH_EDITOR
183
184 // Keep Name in sync with the file name
186
187 // Used when creating new Dialogues
188 // Initialize with a valid GUID
189 if (DialogueVersion >= FDlgDialogueObjectVersion::AddGUID && !HasGUID())
190 {
193 TEXT("Creating new GUID = `%s` for Dialogue = `%s` because of new created Dialogue."),
194 *GUID.ToString(), *GetPathName()
195 );
196 }
197}
198
199void UDlgDialogue::PostRename(UObject* OldOuter, const FName OldName)
200{
201 Super::PostRename(OldOuter, OldName);
203}
204
205void UDlgDialogue::PostDuplicate(bool bDuplicateForPIE)
206{
207 Super::PostDuplicate(bDuplicateForPIE);
208
209 // Used when duplicating dialogues.
210 // Make new guid for this copied Dialogue.
213 TEXT("Creating new GUID = `%s` for Dialogue = `%s` because Dialogue was copied."),
214 *GUID.ToString(), *GetPathName()
215 );
216}
217
219{
220 Super::PostEditImport();
221
222 // Used when duplicating dialogues.
223 // Make new guid for this copied Dialogue
226 TEXT("Creating new GUID = `%s` for Dialogue = `%s` because Dialogue was copied."),
227 *GUID.ToString(), *GetPathName()
228 );
229}
230
231#if WITH_EDITOR
232TSharedPtr<IDlgDialogueEditorAccess> UDlgDialogue::DialogueEditorAccess = nullptr;
233
234bool UDlgDialogue::Modify(bool bAlwaysMarkDirty)
235{
236 if (!CanModify())
237 {
238 return false;
239 }
240
241 const bool bWasSaved = Super::Modify(bAlwaysMarkDirty);
242 // if (StartNode)
243 // {
244 // bWasSaved = bWasSaved && StartNode->Modify(bAlwaysMarkDirty);
245 // }
246
247 // for (UDlgNode* Node : Nodes)
248 // {
249 // bWasSaved = bWasSaved && Node->Modify(bAlwaysMarkDirty);
250 // }
251
252 return bWasSaved;
253}
254
255void UDlgDialogue::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
256{
257 Super::PostEditChangeProperty(PropertyChangedEvent);
258
259 // Signal to the listeners
260 check(OnDialoguePropertyChanged.IsBound());
261 OnDialoguePropertyChanged.Broadcast(PropertyChangedEvent);
262}
263
264void UDlgDialogue::PostEditChangeChainProperty(struct FPropertyChangedChainEvent& PropertyChangedEvent)
265{
267
268 const auto* ActiveMemberNode = PropertyChangedEvent.PropertyChain.GetActiveMemberNode();
269 const auto* ActivePropertyNode = PropertyChangedEvent.PropertyChain.GetActiveNode();
270 const FName MemberPropertyName = ActiveMemberNode && ActiveMemberNode->GetValue() ? ActiveMemberNode->GetValue()->GetFName() : NAME_None;
271 const FName PropertyName = ActivePropertyNode && ActivePropertyNode->GetValue() ? ActivePropertyNode->GetValue()->GetFName() : NAME_None;
272
273 // Check if the participant UClass implements our interface
274 if (MemberPropertyName == GET_MEMBER_NAME_CHECKED(ThisClass, ParticipantsClasses))
275 {
276 if (PropertyName == GET_MEMBER_NAME_CHECKED(FDlgParticipantClass, ParticipantClass))
277 {
278 //const int32 ArrayIndex = PropertyChangedEvent.GetArrayIndex(MemberPropertyName.ToString());
280 {
281 if (!IsValid(Participant.ParticipantClass))
282 {
283 continue;
284 }
285
286 if (!Participant.ParticipantClass->ImplementsInterface(UDlgDialogueParticipant::StaticClass()))
287 {
288 Participant.ParticipantClass = nullptr;
289 }
290 }
291 }
292 }
293
294 Super::PostEditChangeChainProperty(PropertyChangedEvent);
295}
296
297void UDlgDialogue::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector)
298{
299 // Add the graph to the list of referenced objects
300 UDlgDialogue* This = CastChecked<UDlgDialogue>(InThis);
301 Collector.AddReferencedObject(This->DlgGraph, This);
302 Super::AddReferencedObjects(InThis, Collector);
303}
304// End UObject interface
306
308// Begin own functions
309void UDlgDialogue::CreateGraph()
310{
311 // The Graph will only be null if this is the first time we are creating the graph for the Dialogue.
312 // After the Dialogue asset is saved, the Dialogue will get the dialogue from the serialized uasset.
313 if (DlgGraph != nullptr)
314 {
315 return;
316 }
317
318 if (!IsValid(StartNode))
319 {
320 StartNode = ConstructDialogueNode<UDlgNode_Speech>();
321 }
322
323 FDlgLogger::Get().Debugf(TEXT("Creating graph for Dialogue = `%s`"), *GetPathName());
324 DlgGraph = GetDialogueEditorAccess()->CreateNewDialogueGraph(this);
325
326 // Give the schema a chance to fill out any required nodes
327 DlgGraph->GetSchema()->CreateDefaultNodesForGraph(*DlgGraph);
328 MarkPackageDirty();
329}
330
331void UDlgDialogue::ClearGraph()
332{
333 if (!IsValid(DlgGraph))
334 {
335 return;
336 }
337
338 FDlgLogger::Get().Debugf(TEXT("Clearing graph for Dialogue = `%s`"), *GetPathName());
339 GetDialogueEditorAccess()->RemoveAllGraphNodes(this);
340
341 // Give the schema a chance to fill out any required nodes
342 DlgGraph->GetSchema()->CreateDefaultNodesForGraph(*DlgGraph);
343 MarkPackageDirty();
344}
345
346void UDlgDialogue::CompileDialogueNodesFromGraphNodes()
347{
348 if (!bCompileDialogue)
349 {
350 return;
351 }
352
353 FDlgLogger::Get().Infof(TEXT("Compiling Dialogue = `%s` (Graph data -> Dialogue data)`"), *GetPathName());
354 GetDialogueEditorAccess()->CompileDialogueNodesFromGraphNodes(this);
355}
356#endif // #if WITH_EDITOR
357
359{
360 // Simply ignore reloading
361 const EDlgDialogueTextFormat TextFormat = GetDefault<UDlgSystemSettings>()->DialogueTextFormat;
362 if (TextFormat == EDlgDialogueTextFormat::None)
363 {
365 return;
366 }
367
368 ImportFromFileFormat(TextFormat);
369}
370
372{
373 const bool bHasExtension = UDlgSystemSettings::HasTextFileExtension(TextFormat);
374 const FString& TextFileName = GetTextFilePathName(TextFormat);
375
376 // Nothing to do
377 IFileManager& FileManager = IFileManager::Get();
378 if (!bHasExtension)
379 {
380 // Useful For debugging
381 if (TextFormat == EDlgDialogueTextFormat::All)
382 {
383 // Import from all
384 const int32 TextFormatsNum = static_cast<int32>(EDlgDialogueTextFormat::NumTextFormats);
385 for (int32 TextFormatIndex = static_cast<int32>(EDlgDialogueTextFormat::StartTextFormats);
386 TextFormatIndex < TextFormatsNum; TextFormatIndex++)
387 {
388 const EDlgDialogueTextFormat CurrentTextFormat = static_cast<EDlgDialogueTextFormat>(TextFormatIndex);
389 const FString& CurrentTextFileName = GetTextFilePathName(CurrentTextFormat);
390 if (FileManager.FileExists(*CurrentTextFileName))
391 {
392 ImportFromFileFormat(CurrentTextFormat);
393 }
394 }
395 }
396 return;
397 }
398
399 // File does not exist abort
400 if (!FileManager.FileExists(*TextFileName))
401 {
402 FDlgLogger::Get().Errorf(TEXT("Reloading data for Dialogue = `%s` FROM file = `%s` FAILED, because the file does not exist"), *GetPathName(), *TextFileName);
403 return;
404 }
405
406 // Clear data first
407 StartNode = nullptr;
408 Nodes.Empty();
409
410 // TODO handle Name == NAME_None or invalid filename
411 FDlgLogger::Get().Infof(TEXT("Reloading data for Dialogue = `%s` FROM file = `%s`"), *GetPathName(), *TextFileName);
412
413 // TODO(vampy): Check for errors
414 check(TextFormat != EDlgDialogueTextFormat::None);
415 switch (TextFormat)
416 {
418 {
419 FDlgJsonParser JsonParser;
420 JsonParser.InitializeParser(TextFileName);
421 JsonParser.ReadAllProperty(GetClass(), this, this);
422 break;
423 }
425 {
426 FDlgConfigParser Parser(TEXT("Dlg"));
427 Parser.InitializeParser(TextFileName);
428 Parser.ReadAllProperty(GetClass(), this, this);
429 break;
430 }
431 default:
432 checkNoEntry();
433 break;
434
435 }
436
437 if (!IsValid(StartNode))
438 {
439 StartNode = ConstructDialogueNode<UDlgNode_Speech>();
440 }
441
442 // TODO(vampy): validate if data is legit, indicies exist and that sort.
443 // Check if Guid is not a duplicate
444 const TArray<UDlgDialogue*> DuplicateDialogues = UDlgManager::GetDialoguesWithDuplicateGUIDs();
445 if (DuplicateDialogues.Num() > 0)
446 {
447 if (DuplicateDialogues.Contains(this))
448 {
449 // found duplicate of this Dialogue
452 TEXT("Creating new GUID = `%s` for Dialogue = `%s` because the input file contained a duplicate GUID."),
453 *GUID.ToString(), *GetPathName()
454 );
455 }
456 else
457 {
458 // We have bigger problems on our hands
460 TEXT("Found Duplicate Dialogue that does not belong to this Dialogue = `%s`, DuplicateDialogues.Num = %d"),
461 *GetPathName(), DuplicateDialogues.Num()
462 );
463 }
464 }
465
467 AutoFixGraph();
469}
470
472{
473#if WITH_EDITOR
474 // Compile, graph data -> dialogue data
475 CompileDialogueNodesFromGraphNodes();
476#endif
477
478 // Save file, dialogue data -> text file (.dlg)
480 ExportToFile();
481}
482
484{
485 const EDlgDialogueTextFormat TextFormat = GetDefault<UDlgSystemSettings>()->DialogueTextFormat;
486 if (TextFormat == EDlgDialogueTextFormat::None)
487 {
488 // Simply ignore saving
489 return;
490 }
491
492 ExportToFileFormat(TextFormat);
493}
494
496{
497 // TODO(vampy): Check for errors
498 const bool bHasExtension = UDlgSystemSettings::HasTextFileExtension(TextFormat);
499 const FString& TextFileName = GetTextFilePathName(TextFormat);
500 if (bHasExtension)
501 {
502 FDlgLogger::Get().Infof(TEXT("Exporting data for Dialogue = `%s` TO file = `%s`"), *GetPathName(), *TextFileName);
503 }
504
505 switch (TextFormat)
506 {
508 {
509 FDlgJsonWriter JsonWriter;
510 JsonWriter.Write(GetClass(), this);
511 JsonWriter.ExportToFile(TextFileName);
512 break;
513 }
515 {
516 FDlgConfigWriter DlgWriter(TEXT("Dlg"));
517 DlgWriter.Write(GetClass(), this);
518 DlgWriter.ExportToFile(TextFileName);
519 break;
520 }
522 {
523 // Useful for debugging
524 // Export to all formats
525 const int32 TextFormatsNum = static_cast<int32>(EDlgDialogueTextFormat::NumTextFormats);
526 for (int32 TextFormatIndex = static_cast<int32>(EDlgDialogueTextFormat::StartTextFormats);
527 TextFormatIndex < TextFormatsNum; TextFormatIndex++)
528 {
529 const EDlgDialogueTextFormat CurrentTextFormat = static_cast<EDlgDialogueTextFormat>(TextFormatIndex);
530 ExportToFileFormat(CurrentTextFormat);
531 }
532 break;
533 }
534 default:
535 // It Should not have any extension
536 check(!bHasExtension);
537 break;
538 }
539}
540
541FDlgParticipantData& UDlgDialogue::GetParticipantDataEntry(FName ParticipantName, FName FallbackParticipantName, bool bCheckNone, const FString& ContextMessage)
542{
543 // Used to ignore some participants
544 static FDlgParticipantData BlackHoleParticipant;
545
546 // If the Participant Name is not set, it adopts the Node Owner Name
547 const FName& ValidParticipantName = ParticipantName == NAME_None ? FallbackParticipantName : ParticipantName;
548
549 // Parent/child is not valid, simply do nothing
550 if (bCheckNone && ValidParticipantName == NAME_None)
551 {
553 TEXT("Ignoring ParticipantName = None, Context = `%s`. Either your node participant name is None or your participant name is None."),
554 *ContextMessage
555 );
556 return BlackHoleParticipant;
557 }
558
559 return ParticipantsData.FindOrAdd(ValidParticipantName);
560}
561
563{
564 const FString NodeContext = FString::Printf(TEXT("Node %s"), NodeIndex > INDEX_NONE ? *FString::FromInt(NodeIndex) : TEXT("Start") );
565 const FName FallbackParticipantName = Node->GetNodeParticipantName();
566
567 for (const FDlgEdge& Edge : Node->GetNodeChildren())
568 {
569 const int32 TargetIndex = Edge.TargetIndex;
570
571 for (const FDlgCondition& Condition : Edge.Conditions)
572 {
573 if (Condition.IsParticipantInvolved())
574 {
575 const FString ContextMessage = FString::Printf(TEXT("Adding Edge primary condition data from %s to Node %d"), *NodeContext, TargetIndex);
576 GetParticipantDataEntry(Condition.ParticipantName, FallbackParticipantName, true, ContextMessage)
578 }
579 if (Condition.IsSecondParticipantInvolved())
580 {
581 const FString ContextMessage = FString::Printf(TEXT("Adding Edge secondary condition data from %s to Node %d"), *NodeContext, TargetIndex);
582 GetParticipantDataEntry(Condition.OtherParticipantName, FallbackParticipantName, true, ContextMessage)
584 }
585 }
586 }
587}
588
589void UDlgDialogue::RebuildAndUpdateNode(UDlgNode* Node, const UDlgSystemSettings& Settings, bool bUpdateTextsNamespacesAndKeys)
590{
591 static constexpr bool bEdges = true;
592 static constexpr bool bUpdateGraphNode = false;
593
594 // Rebuild & Update
595 // NOTE: this can do a dialogue data -> graph node data update
596 Node->RebuildTextArguments(bEdges, bUpdateGraphNode);
597 Node->UpdateTextsValuesFromDefaultsAndRemappings(Settings, bEdges, bUpdateGraphNode);
598 if (bUpdateTextsNamespacesAndKeys)
599 {
600 Node->UpdateTextsNamespacesAndKeys(Settings, bEdges, bUpdateGraphNode);
601 }
602
603 // Sync with the editor aka bUpdateGraphNode = true
604 Node->UpdateGraphNode();
605}
606
607void UDlgDialogue::UpdateAndRefreshData(bool bUpdateTextsNamespacesAndKeys)
608{
609 FDlgLogger::Get().Infof(TEXT("Refreshing data for Dialogue = `%s`"), *GetPathName());
610
611 const UDlgSystemSettings* Settings = GetDefault<UDlgSystemSettings>();
612 ParticipantsData.Empty();
613 AllSpeakerStates.Empty();
614
615 // do not forget about the edges of the Root/Start Node
616 if (IsValid(StartNode))
617 {
619 RebuildAndUpdateNode(StartNode, *Settings, bUpdateTextsNamespacesAndKeys);
620 }
621
622 // Regular Nodes
623 const int32 NodesNum = Nodes.Num();
624 for (int32 NodeIndex = 0; NodeIndex < NodesNum; NodeIndex++)
625 {
626 const FString NodeContext = FString::Printf(TEXT("Node %d"), NodeIndex);
627 UDlgNode* Node = Nodes[NodeIndex];
628 const FName NodeParticipantName = Node->GetNodeParticipantName();
629
630 // Rebuild & Update
631 RebuildAndUpdateNode(Node, *Settings, bUpdateTextsNamespacesAndKeys);
632
633 // participant names
634 TArray<FName> Participants;
635 Node->GetAssociatedParticipants(Participants);
636 for (const FName& Participant : Participants)
637 {
638 if (!ParticipantsData.Contains(Participant))
639 {
641 }
642 }
643
644 // gather SpeakerStates
646
647 // Conditions from nodes
648 for (const FDlgCondition& Condition : Node->GetNodeEnterConditions())
649 {
650 if (Condition.IsParticipantInvolved())
651 {
652 const FString ContextMessage = FString::Printf(TEXT("Adding primary condition data for %s"), *NodeContext);
653 GetParticipantDataEntry(Condition.ParticipantName, NodeParticipantName, true, ContextMessage)
655 }
656 if (Condition.IsSecondParticipantInvolved())
657 {
658 const FString ContextMessage = FString::Printf(TEXT("Adding secondary condition data for %s"), *NodeContext);
659 GetParticipantDataEntry(Condition.OtherParticipantName, NodeParticipantName, true, ContextMessage)
661 }
662 }
663
664 // Gather Edge Data
665 AddConditionsDataFromNodeEdges(Node, NodeIndex);
666
667 // Walk over edges of speaker nodes
668 // NOTE: for speaker sequence nodes, the inner edges are handled by AddAllSpeakerStatesIntoSet
669 // so no need to special case handle it
670 const int32 NumNodeChildren = Node->GetNumNodeChildren();
671 for (int32 EdgeIndex = 0; EdgeIndex < NumNodeChildren; EdgeIndex++)
672 {
673 const FDlgEdge& Edge = Node->GetNodeChildAt(EdgeIndex);
674 const int32 TargetIndex = Edge.TargetIndex;
675
676 // Speaker states
678
679 // Text arguments are rebuild from the Node
680 for (const FDlgTextArgument& TextArgument : Edge.GetTextArguments())
681 {
682 const FString ContextMessage = FString::Printf(TEXT("Adding Edge text arguments data from %s, to Node %d"), *NodeContext, TargetIndex);
683 GetParticipantDataEntry(TextArgument.ParticipantName, NodeParticipantName, true, ContextMessage)
684 .AddTextArgumentData(TextArgument);
685 }
686 }
687
688 // Events
689 for (const FDlgEvent& Event : Node->GetNodeEnterEvents())
690 {
691 const FString ContextMessage = FString::Printf(TEXT("Adding events data for %s"), *NodeContext);
692 GetParticipantDataEntry(Event.ParticipantName, NodeParticipantName, true, ContextMessage)
694 }
695
696 // Text arguments
697 for (const FDlgTextArgument& TextArgument : Node->GetTextArguments())
698 {
699 const FString ContextMessage = FString::Printf(TEXT("Adding text arguments data for %s"), *NodeContext);
700 GetParticipantDataEntry(TextArgument.ParticipantName, NodeParticipantName, true, ContextMessage)
701 .AddTextArgumentData(TextArgument);
702 }
703 }
704
705 // Remove default values
706 AllSpeakerStates.Remove(FName(NAME_None));
707
708 //
709 // Fill ParticipantClasses
710 //
711 TSet<FName> Participants;
712 GetAllParticipantNames(Participants);
713
714 // 1. remove outdated entries
715 for (int32 Index = ParticipantsClasses.Num() - 1; Index >= 0; --Index)
716 {
717 const FName ExaminedName = ParticipantsClasses[Index].ParticipantName;
718 if (!Participants.Contains(ExaminedName) || ExaminedName.IsNone())
719 {
720 ParticipantsClasses.RemoveAtSwap(Index);
721 }
722
723 Participants.Remove(ExaminedName);
724 }
725
726 // 2. add new entries
727 for (const FName Participant : Participants)
728 {
729 if (Participant != NAME_None)
730 {
731 ParticipantsClasses.Add({ Participant, nullptr });
732 }
733 else
734 {
735 FDlgLogger::Get().Warning(TEXT("Trying to fill ParticipantsClasses, got a Participant name = None. Ignoring!"));
736 }
737 }
738
739 // 3. Set auto default participant classes
741 {
742 TArray<UClass*> NativeClasses;
743 TArray<UClass*> BlueprintClasses;
744 FDlgHelper::GetAllClassesImplementingInterface(UDlgDialogueParticipant::StaticClass(), NativeClasses, BlueprintClasses);
745
746 const TMap<FName, TArray<FDlgClassAndObject>> NativeClassesMap = FDlgHelper::ConvertDialogueParticipantsClassesIntoMap(NativeClasses);
747 const TMap<FName, TArray<FDlgClassAndObject>> BlueprintClassesMap = FDlgHelper::ConvertDialogueParticipantsClassesIntoMap(BlueprintClasses);
748
750 {
751 // Participant Name is not set or Class is set, ignore
752 if (Struct.ParticipantName == NAME_None || Struct.ParticipantClass != nullptr)
753 {
754 continue;
755 }
756
757 // Blueprint
758 if (BlueprintClassesMap.Contains(Struct.ParticipantName))
759 {
760 const TArray<FDlgClassAndObject>& Array = BlueprintClassesMap.FindChecked(Struct.ParticipantName);
761 if (Array.Num() == 1)
762 {
763 Struct.ParticipantClass = Array[0].Class;
764 }
765 }
766
767 // Native last resort
768 if (Struct.ParticipantClass == nullptr && NativeClassesMap.Contains(Struct.ParticipantName))
769 {
770 const TArray<FDlgClassAndObject>& Array = NativeClassesMap.FindChecked(Struct.ParticipantName);
771 if (Array.Num() == 1)
772 {
773 Struct.ParticipantClass = Array[0].Class;
774 }
775 }
776 }
777 }
778}
779
780FGuid UDlgDialogue::GetNodeGUIDForIndex(int32 NodeIndex) const
781{
782 if (IsValidNodeIndex(NodeIndex))
783 {
784 return Nodes[NodeIndex]->GetGUID();
785 }
786
787 // Invalid GUID
788 return FGuid{};
789}
790
791int32 UDlgDialogue::GetNodeIndexForGUID(const FGuid& NodeGUID) const
792{
793 if (const int32* NodeIndexPtr = NodesGUIDToIndexMap.Find(NodeGUID))
794 {
795 return *NodeIndexPtr;
796 }
797
798 return INDEX_NONE;
799}
800
802{
803 if (!InStartNode)
804 {
805 return;
806 }
807
808 StartNode = InStartNode;
809 // UpdateGUIDToIndexMap(StartNode, INDEX_NONE);
810}
811
812void UDlgDialogue::SetNodes(const TArray<UDlgNode*>& InNodes)
813{
814 Nodes = InNodes;
815 for (int32 NodeIndex = 0; NodeIndex < Nodes.Num(); NodeIndex++)
816 {
817 UpdateGUIDToIndexMap(Nodes[NodeIndex], NodeIndex);
818 }
819}
820
821void UDlgDialogue::SetNode(int32 NodeIndex, UDlgNode* InNode)
822{
823 if (!IsValidNodeIndex(NodeIndex) || !InNode)
824 {
825 return;
826 }
827
828 Nodes[NodeIndex] = InNode;
829 UpdateGUIDToIndexMap(InNode, NodeIndex);
830}
831
832void UDlgDialogue::UpdateGUIDToIndexMap(const UDlgNode* Node, int32 NodeIndex)
833{
834 if (!Node || !IsValidNodeIndex(NodeIndex) || !Node->HasGUID())
835 {
836 return;
837 }
838
839 NodesGUIDToIndexMap.Add(Node->GetGUID(), NodeIndex);
840}
841
842bool UDlgDialogue::IsEndNode(int32 NodeIndex) const
843{
844 if (!Nodes.IsValidIndex(NodeIndex))
845 {
846 return false;
847 }
848
849 return Nodes[NodeIndex]->IsA<UDlgNode_End>();
850}
851
853{
854 check(StartNode);
855 // syntax correction 1: if there is no start node, we create one pointing to the first node
856 if (StartNode->GetNodeChildren().Num() == 0 && Nodes.Num() > 0)
857 {
858 StartNode->AddNodeChild({ 0 });
859 }
860 StartNode->SetFlags(RF_Transactional);
861
862 // syntax correction 2: if there is no end node, we add one
863 bool bHasEndNode = false;
864 // check if the end node is already there
865 for (UDlgNode* Node : Nodes)
866 {
867 check(Node);
868 Node->SetFlags(RF_Transactional);
869 if (Node->IsA<UDlgNode_End>())
870 {
871 bHasEndNode = true;
872 break;
873 }
874 }
875 // add it if not
876 if (!bHasEndNode && Nodes.Num() > 0)
877 {
878 auto* EndNode = ConstructDialogueNode<UDlgNode_End>();
879 EndNode->SetNodeParticipantName(Nodes[0]->GetNodeParticipantName());
880 Nodes.Add(EndNode);
881 }
882
883 // syntax correction 3: if a node is not an end node but has no children it will "adopt" the next node
884 const UDlgSystemSettings* Settings = GetDefault<UDlgSystemSettings>();
885 for (int32 i = 0; i < Nodes.Num() - 1; ++i)
886 {
887 UDlgNode* Node = Nodes[i];
888 const TArray<FDlgEdge>& NodeChildren = Node->GetNodeChildren();
889
890 if (!Node->IsA<UDlgNode_End>() && NodeChildren.Num() == 0)
891 {
892 Node->AddNodeChild({ i + 1 });
893 }
894
895 // Add some text to the edges.
896 Node->UpdateTextsValuesFromDefaultsAndRemappings(*Settings, true, true);
897 }
898}
899
900FString UDlgDialogue::GetTextFilePathName(bool bAddExtension/* = true*/) const
901{
902 return GetTextFilePathName(GetDefault<UDlgSystemSettings>()->DialogueTextFormat, bAddExtension);
903}
904
905FString UDlgDialogue::GetTextFilePathName(EDlgDialogueTextFormat TextFormat, bool bAddExtension/* = true*/) const
906{
907 // Extract filename from path
908 // NOTE: this is not a filesystem path, it is an unreal path 'Outermost.[Outer:]Name'
909 // Usually GetPathName works, but the path name might be weird.
910 // FSoftObjectPath(this).ToString(); which does call this function GetPathName() but it returns a legit clean path
911 // if it is in the wrong format
912 FString TextFileName = GetTextFilePathNameFromAssetPathName(FSoftObjectPath(this).ToString());
913 if (bAddExtension)
914 {
915 // Modify the extension of the base text file depending on the extension
916 TextFileName += UDlgSystemSettings::GetTextFileExtension(TextFormat);
917 }
918
919 return TextFileName;
920}
921
926
927bool UDlgDialogue::DeleteTextFileForExtension(const FString& FileExtension) const
928{
929 const FString TextFilePathName = GetTextFilePathName(false);
930 if (TextFilePathName.IsEmpty())
931 {
932 // Memory corruption? tread carefully here
934 TEXT("Can't delete text file for Dialogue = `%s` because the file path name is empty :O"),
935 *GetPathName()
936 );
937 return false;
938 }
939
940 const FString FullPathName = TextFilePathName + FileExtension;
941 return FDlgHelper::DeleteFile(FullPathName);
942}
943
945{
946 bool bStatus = true;
947 for (const FString& FileExtension : GetDefault<UDlgSystemSettings>()->GetAllTextFileExtensions())
948 {
949 bStatus &= DeleteTextFileForExtension(FileExtension);
950 }
951 return bStatus;
952}
953
955{
956 return FDlgHelper::IsPathInProjectDirectory(GetPathName());
957}
958
959FString UDlgDialogue::GetTextFilePathNameFromAssetPathName(const FString& AssetPathName)
960{
961 static const TCHAR* Separator = TEXT("/");
962
963 // Get rid of the extension from `filename.extension` from the end of the path
964 FString PathName = FPaths::GetBaseFilename(AssetPathName, false);
965
966 // Get rid of the first folder, Game/ or Name/ (if in the plugins dir) from the beginning of the path.
967 // Are we in the game directory?
968 FString ContentDir = FPaths::ProjectContentDir();
969 if (!PathName.RemoveFromStart(TEXT("/Game/")))
970 {
971 // We are in the plugins dir
972 TArray<FString> PathParts;
973 PathName.ParseIntoArray(PathParts, Separator);
974 if (PathParts.Num() > 0)
975 {
976 const FString PluginName = PathParts[0];
977 const FString PluginDir = FPaths::ProjectPluginsDir() / PluginName;
978
979 // Plugin exists
980 if (FPaths::DirectoryExists(PluginDir))
981 {
982 ContentDir = PluginDir / TEXT("Content/");
983 }
984
985 // remove plugin name
986 PathParts.RemoveAt(0);
987 PathName = FString::Join(PathParts, Separator);
988 }
989 }
990
991 return ContentDir + PathName;
992}
993
994
995// End own functions
997
998#undef LOCTEXT_NAMESPACE
FDevVersionRegistration GRegisterDlgDialogueObjectVersion(FDlgDialogueObjectVersion::GUID, FDlgDialogueObjectVersion::LatestVersion, TEXT("Dev-DlgDialogue"))
void UpdateDialogueToVersion_ConvertedNodesToUObject(UDlgDialogue *Dialogue)
void UpdateDialogueToVersion_UseOnlyOneOutputAndInputPin(UDlgDialogue *Dialogue)
EDlgDialogueTextFormat
UENUM()
void InitializeParser(const FString &FilePath) override
void ReadAllProperty(const UStruct *ReferenceClass, void *TargetObject, UObject *DefaultObjectOuter=nullptr) override
bool ExportToFile(const FString &FileName) override
void Write(const UStruct *StructDefinition, const void *Object) override
static FORCEINLINE bool IsPathInProjectDirectory(const FString &Path)
Definition DlgHelper.h:224
static bool DeleteFile(const FString &PathName, bool bVerbose=true)
Definition DlgHelper.cpp:14
static bool GetAllClassesImplementingInterface(const UClass *InterfaceClass, TArray< UClass * > &OutNativeClasses, TArray< UClass * > &OutBlueprintClasses)
static TMap< FName, TArray< FDlgClassAndObject > > ConvertDialogueParticipantsClassesIntoMap(const TArray< UClass * > &Classes)
The DlgJsonParser class mostly adapted for Dialogues, copied from FJsonObjectConverter See IDlgParser...
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
static FDlgLogger & Get()
Definition DlgLogger.h:24
FORCEINLINE void Warning(const FString &Message)
Definition INYLogger.h:326
void Warningf(const FmtType &Fmt, Types... Args)
Definition INYLogger.h:308
void Infof(const FmtType &Fmt, Types... Args)
Definition INYLogger.h:311
void Errorf(const FmtType &Fmt, Types... Args)
Definition INYLogger.h:305
void Debugf(const FmtType &Fmt, Types... Args)
Definition INYLogger.h:314
UCLASS(BlueprintType, Meta = (DisplayThumbnail = "true"))
Definition DlgDialogue.h:85
static FString GetTextFilePathNameFromAssetPathName(const FString &AssetPathName)
void ImportFromFileFormat(EDlgDialogueTextFormat TextFormat)
TSet< FName > AllSpeakerStates
UPROPERTY(VisibleAnywhere, AdvancedDisplay, Category = "Dialogue", Meta = (DlgNoExport))
TMap< FName, FDlgParticipantData > ParticipantsData
UPROPERTY(VisibleAnywhere, AdvancedDisplay, Category = "Dialogue", Meta = (DlgNoExport))
FDlgParticipantData & GetParticipantDataEntry(FName ParticipantName, FName FallbackParticipantName, bool bCheckNone, const FString &ContextMessage)
bool IsValidNodeIndex(int32 NodeIndex) const
UFUNCTION(BlueprintPure, Category = "Dialogue")
void GetAllParticipantNames(TSet< FName > &OutSet) const
UFUNCTION(BlueprintPure, Category = "Dialogue")
UDlgNode * StartNode
UPROPERTY(Instanced)
void ImportFromFile()
void SetNodes(const TArray< UDlgNode * > &InNodes)
void SetNode(int32 NodeIndex, UDlgNode *InNode)
FGuid GUID
UPROPERTY(VisibleAnywhere, Category = "Dialogue")
void ExportToFile() const
bool HasGUID() const
UFUNCTION(BlueprintPure, Category = "Dialogue|GUID")
FGuid GetNodeGUIDForIndex(int32 NodeIndex) const
UFUNCTION(BlueprintPure, Category = "Dialogue", DisplayName = "Get Node GUID For Index")
bool IsEndNode(int32 NodeIndex) const
void AddConditionsDataFromNodeEdges(const UDlgNode *Node, int32 NodeIndex)
bool DeleteTextFileForExtension(const FString &FileExtension) const
void SetStartNode(UDlgNode *InStartNode)
void Serialize(FArchive &Ar) override
bool DeleteTextFileForTextFormat(EDlgDialogueTextFormat TextFormat) const
void PostDuplicate(bool bDuplicateForPIE) override
TMap< FGuid, int32 > NodesGUIDToIndexMap
UPROPERTY(VisibleAnywhere, AdvancedDisplay, Category = "Dialogue", DisplayName = "Nodes GUID To Index...
void OnPreAssetSaved()
void UpdateGUIDToIndexMap(const UDlgNode *Node, int32 NodeIndex)
int32 GetNodeIndexForGUID(const FGuid &NodeGUID) const
UFUNCTION(BlueprintPure, Category = "Dialogue", DisplayName = "Get Node Index For GUID")
void PostRename(UObject *OldOuter, FName OldName) override
void ExportToFileFormat(EDlgDialogueTextFormat TextFormat) const
FName Name
UPROPERTY(VisibleAnywhere, Category = "Dialogue")
void PreSave(const class ITargetPlatform *TargetPlatform) override
bool IsInProjectDirectory() const
void AutoFixGraph()
TArray< FDlgParticipantClass > ParticipantsClasses
UPROPERTY(EditAnywhere, EditFixedSize, Category = "Dialogue")
void RebuildAndUpdateNode(UDlgNode *Node, const UDlgSystemSettings &Settings, bool bUpdateTextsNamespacesAndKeys)
void RegenerateGUID()
bool DeleteAllTextFiles() const
void PostInitProperties() override
FString GetTextFilePathName(bool bAddExtension=true) const
void UpdateAndRefreshData(bool bUpdateTextsNamespacesAndKeys=false)
void PostLoad() override
FName GetDialogueFName() const
UFUNCTION(BlueprintPure, Category = "Dialogue")
void PostEditImport() override
TArray< UDlgNode * > Nodes
UPROPERTY(AdvancedDisplay, EditFixedSize, Instanced, Meta = (DlgWriteIndex))
static TArray< UDlgDialogue * > GetDialoguesWithDuplicateGUIDs()
UCLASS(BlueprintType, ClassGroup = "Dialogue")
Definition DlgNode_End.h:21
UCLASS(BlueprintType, Abstract, EditInlineNew, ClassGroup = "Dialogue")
Definition DlgNode.h:40
void UpdateGraphNode()
Definition DlgNode.cpp:340
virtual void GetAssociatedParticipants(TArray< FName > &OutArray) const
Definition DlgNode.cpp:347
virtual const TArray< FDlgTextArgument > & GetTextArguments() const
UFUNCTION(BlueprintPure, Category = "Dialogue|Node")
Definition DlgNode.h:254
FGuid GetGUID() const
UFUNCTION(BlueprintPure, Category = "Dialogue|Node")
Definition DlgNode.h:105
virtual void RebuildTextArguments(bool bEdges, bool bUpdateGraphNode=true)
Definition DlgNode.cpp:324
virtual void AddAllSpeakerStatesIntoSet(TSet< FName > &OutStates) const
Definition DlgNode.h:310
virtual const TArray< FDlgEdge > & GetNodeChildren() const
Gets this nodes children (edges) as a const/mutable array.
Definition DlgNode.h:184
virtual int32 GetNumNodeChildren() const
UFUNCTION(BlueprintPure, Category = "Dialogue|Node")
Definition DlgNode.h:191
virtual FName GetNodeParticipantName() const
UFUNCTION(BlueprintPure, Category = "Dialogue|Node")
Definition DlgNode.h:127
virtual const TArray< FDlgEvent > & GetNodeEnterEvents() const
UFUNCTION(BlueprintPure, Category = "Dialogue|Node")
Definition DlgNode.h:170
virtual void UpdateTextsNamespacesAndKeys(const UDlgSystemSettings &Settings, bool bEdges, bool bUpdateGraphNode=true)
Definition DlgNode.cpp:307
virtual const FDlgEdge & GetNodeChildAt(int32 EdgeIndex) const
UFUNCTION(BlueprintPure, Category = "Dialogue|Node")
Definition DlgNode.h:197
bool HasGUID() const
UFUNCTION(BlueprintPure, Category = "Dialogue|Node")
Definition DlgNode.h:111
virtual void AddNodeChild(const FDlgEdge &InChild)
Definition DlgNode.h:200
virtual void UpdateTextsValuesFromDefaultsAndRemappings(const UDlgSystemSettings &Settings, bool bEdges, bool bUpdateGraphNode=true)
Definition DlgNode.cpp:271
virtual const TArray< FDlgCondition > & GetNodeEnterConditions() const
UFUNCTION(BlueprintPure, Category = "Dialogue|Node")
Definition DlgNode.h:145
UCLASS(Config = Engine, DefaultConfig, meta = (DisplayName = "Dialogue System Settings"))
static FString GetTextFileExtension(EDlgDialogueTextFormat TextFormat)
static bool HasTextFileExtension(EDlgDialogueTextFormat TextFormat)
bool bAutoSetDefaultParticipantClasses
UPROPERTY(Category = "Dialogue", Config, EditAnywhere)
USTRUCT(Blueprintable)
static const FGuid GUID
Definition DlgDialogue.h:39
USTRUCT(BlueprintType)
Definition DlgEdge.h:25
int32 TargetIndex
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Dialogue|Edge", Meta = (ClampMin = -1))
Definition DlgEdge.h:126
FName SpeakerState
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dialogue|Edge")
Definition DlgEdge.h:142
const TArray< FDlgTextArgument > & GetTextArguments() const
Definition DlgEdge.h:76
bool IsValid() const
Definition DlgEdge.h:107
USTRUCT(BlueprintType)
Definition DlgEvent.h:59
USTRUCT(BlueprintType)
Definition DlgDialogue.h:52
void AddConditionPrimaryData(const FDlgCondition &Condition)
void AddTextArgumentData(const FDlgTextArgument &TextArgument)
void AddEventData(const FDlgEvent &Event)
void AddConditionSecondaryData(const FDlgCondition &Condition)
USTRUCT(BlueprintType)