// Copyright Epic Games, Inc. All Rights Reserved. #include "ConversationCompiler.h" #include "ConversationGraph.h" #include "ConversationGraphSchema.h" #include "ConversationDatabase.h" #include "ConversationSubNode.h" #include "ConversationGraphNode_Task.h" #include "ConversationEntryPointNode.h" #include "ConversationGraphNode_EntryPoint.h" #include "ConversationGraphNode_Knot.h" #include "Kismet2/BlueprintEditorUtils.h" #include "Stats/StatsMisc.h" #include "ConversationLinkNode.h" #include "Engine/AssetManager.h" #include "Misc/MessageDialog.h" #include "FileHelpers.h" ////////////////////////////////////////////////////////////////////// #define LOCTEXT_NAMESPACE "FConversationCompiler" enum class EConversationCompilerVersion { Initial, AddedSupportForLazyloadMetadata, ChangedTheEntryTagsStructureToIncludeGuid, // ------------------------------------------------------ VersionPlusOne, LatestVersion = VersionPlusOne - 1 }; //@TODO: CONVERSATION: Push this into the engine, and update UAssetToolsImpl::CreateUniqueAssetName, FBlueprintEditorUtils::FindUniqueKismetName, FNiagaraUtilities::GetUniqueName, etc... to use it struct FUniqueNameGenerator { private: FString TrimmedBaseName; int32 IntSuffix = 0; int32 TrailingIntegerLength = 0; public: FUniqueNameGenerator(const FString& InSeedName) { int32 CharIndex = InSeedName.Len() - 1; while (CharIndex >= 0 && InSeedName[CharIndex] >= TEXT('0') && InSeedName[CharIndex] <= TEXT('9')) { --CharIndex; } if ((InSeedName.Len() > 0) && (CharIndex == -1)) { // This is the all numeric name, in this case we'd like to append _number, because just adding a number isn't great TrimmedBaseName = InSeedName + TEXT("_"); IntSuffix = 2; } else if ((CharIndex >= 0) && (CharIndex < InSeedName.Len() - 1)) { const FString TrailingInteger = InSeedName.RightChop(CharIndex + 1); TrailingIntegerLength = TrailingInteger.Len(); TrimmedBaseName = InSeedName.Left(CharIndex + 1); IntSuffix = FCString::Atoi(*TrailingInteger); } else { TrimmedBaseName = InSeedName; } } // Generates the next name (does not test for uniqueness, that's up to the caller) FString GenerateName() { FString Result; if (IntSuffix < 1) { Result = TrimmedBaseName; } else { FString Suffix = FString::Printf(TEXT("%d"), IntSuffix); while (Suffix.Len() < TrailingIntegerLength) { Suffix = TEXT("0") + Suffix; } Result = FString::Printf(TEXT("%s%s"), *TrimmedBaseName, *Suffix); } IntSuffix++; return Result; } // Generates a name that will be unique within the specified outer FName GenerateUniqueNameWithinOuter(UObject* Outer) { while (true) { FName TestName(*GenerateName()); if (FindObjectWithOuter(Outer, nullptr, TestName) == nullptr) { return TestName; } } } }; ////////////////////////////////////////////////////////////////////// // FConversationCompiler /** Struct that contains all important identifier that define an entry point. */ struct FConversationEntryInfo { FConversationEntryInfo(FGameplayTag InEntryTag, FString InEntryIdentifier) { EntryTag = InEntryTag; EntryIdentifier = InEntryIdentifier; } bool operator==(const FConversationEntryInfo& Other) const { return EntryTag == Other.EntryTag && EntryIdentifier == Other.EntryIdentifier; } bool operator!=(const FConversationEntryInfo& Other) const { return EntryTag != Other.EntryTag || EntryIdentifier != Other.EntryIdentifier; } FGameplayTag EntryTag; FString EntryIdentifier; }; /** Used so we can have a TMap of this struct */ FORCEINLINE uint32 GetTypeHash(const FConversationEntryInfo& EntryIdentifiersStruct) { return HashCombineFast(GetTypeHash(EntryIdentifiersStruct.EntryTag), GetTypeHash(EntryIdentifiersStruct.EntryIdentifier)); } int32 FConversationCompiler::GetCompilerVersion() { return (int32)EConversationCompilerVersion::LatestVersion; } UConversationGraph* FConversationCompiler::CreateNewGraph(UConversationDatabase* ConversationAsset, FName GraphName) { UConversationGraph* NewGraph = CastChecked(FBlueprintEditorUtils::CreateNewGraph(ConversationAsset, GraphName, UConversationGraph::StaticClass(), UConversationGraphSchema::StaticClass())); const UEdGraphSchema* Schema = NewGraph->GetSchema(); Schema->CreateDefaultNodesForGraph(*NewGraph); NewGraph->OnCreated(); return NewGraph; } UConversationGraph* FConversationCompiler::AddNewGraph(UConversationDatabase* ConversationAsset, const FString& DesiredName) { // Find a unique default name for the duplicated asset FUniqueNameGenerator NameGenerator(DesiredName); const FName GraphName = NameGenerator.GenerateUniqueNameWithinOuter(ConversationAsset); check(ConversationAsset); UConversationGraph* NewGraph = CreateNewGraph(ConversationAsset, GraphName); ConversationAsset->SourceGraphs.Add(NewGraph); return NewGraph; } int32 FConversationCompiler::GetNumGraphs(UConversationDatabase* ConversationAsset) { check(ConversationAsset); return ConversationAsset->SourceGraphs.Num(); } UConversationGraph* FConversationCompiler::GetGraphFromBank(UConversationDatabase* ConversationAsset, int32 Index) { check(ConversationAsset); return ConversationAsset->SourceGraphs.IsValidIndex(Index) ? CastChecked(ConversationAsset->SourceGraphs[Index]) : nullptr; } void FConversationCompiler::RebuildBank(UConversationDatabase* ConversationAsset) { SCOPE_LOG_TIME_IN_SECONDS(TEXT("FConversationCompiler::RebuildBank"), nullptr); check(ConversationAsset); ConversationAsset->CompilerVersion = GetCompilerVersion(); // Merge all the graphs TArray AllGraphNodes; for (UEdGraph* Graph : ConversationAsset->SourceGraphs) { Graph->GetNodesOfClass(/*inout*/ AllGraphNodes); } // Clear all error messages and add to the full nodes map (used for the editor only) ConversationAsset->FullNodeMap.Reset(); for (UConversationGraphNode* EdNode : AllGraphNodes) { EdNode->ErrorMsg.Reset(); if (EdNode->NodeInstance == nullptr) { EdNode->ErrorMsg = TEXT("Unknown Node"); continue; } check(EdNode->GetRuntimeNode()); check(EdNode->NodeGuid.IsValid()); // Add to the editor full nodes map if (ConversationAsset->FullNodeMap.Contains(EdNode->NodeGuid)) { EdNode->ErrorMsg = TEXT("Duplicate GUID"); } else { ConversationAsset->FullNodeMap.Add(EdNode->NodeGuid, EdNode->GetRuntimeNode()); } // Wire up subnodes if (UConversationGraphNode_Task* EdTaskNode = Cast(EdNode)) { UConversationTaskNode* TaskNode = EdTaskNode->GetRuntimeNode(); TaskNode->SubNodes.Reset(); for (UAIGraphNode* SubNode : EdTaskNode->SubNodes) { UConversationGraphNode* TypedSubNode = Cast(SubNode); if (ensure(TypedSubNode)) { UConversationSubNode* TaskSubNode = TypedSubNode->GetRuntimeNode(); if (ensure(TaskSubNode)) { TaskNode->SubNodes.Add(TaskSubNode); } } } } // Wire up links if (UConversationNodeWithLinks* RuntimeNodeWithLinks = Cast(EdNode->NodeInstance)) { RuntimeNodeWithLinks->OutputConnections.Reset(); UEdGraphPin* OutputPin = EdNode->GetOutputPin(); check(OutputPin); ForeachConnectedOutgoingConversationNode(OutputPin, [RuntimeNodeWithLinks](UConversationGraphNode* RemoteNode) { RuntimeNodeWithLinks->OutputConnections.Add(RemoteNode->NodeGuid); if (UConversationNodeWithLinks* ChildNodeWithLinks = Cast(RemoteNode->NodeInstance)) { // Tell child about its parent so calls to 'GetParentNode' work as expected // Could alternatively pass as FGuid and store similarly to 'output connections' (from RuntimeNodeWithLinks->GetNodeGuid()), // Users would need to call UConversationRegistry::GetRuntimeNodeFromGUID when they want the parent node in that case ChildNodeWithLinks->InitializeNode(RuntimeNodeWithLinks); } }); } } // Gather all entry points ConversationAsset->EntryTags.Reset(); TMap> EntryMap; TArray EntryGraphNodes; for (UConversationGraphNode* EdNode : AllGraphNodes) { if (UConversationGraphNode_EntryPoint* EdEntryNode = Cast(EdNode)) { UConversationEntryPointNode* EntryNode = EdEntryNode->GetRuntimeNode(); if (ensure(EntryNode)) { if (EntryNode->EntryTag.IsValid()) { EntryMap.FindOrAdd(FConversationEntryInfo(EntryNode->EntryTag, EntryNode->GetIdentifier())).Add(EdEntryNode->NodeGuid); EntryGraphNodes.Add(EdEntryNode); } else { EdEntryNode->ErrorMsg = TEXT("No EntryTag set"); } } } } // Add the resulting entry nodes to the entry list. for (const auto& KVP : EntryMap) { FConversationEntryList Entry; Entry.EntryTag = KVP.Key.EntryTag; Entry.EntryIdentifier = KVP.Key.EntryIdentifier; Entry.DestinationList.Append(KVP.Value); ConversationAsset->EntryTags.Add(Entry); } // Determine reachability of the rest of the nodes TSet ReachableNodeSet; { TArray ReachableStack; ReachableStack.Append(EntryGraphNodes); while (ReachableStack.Num() > 0) { UConversationGraphNode* Candidate = ReachableStack.Pop(); if (!ReachableNodeSet.Contains(Candidate) && !Candidate->HasErrors()) { ReachableNodeSet.Add(Candidate); if (UEdGraphPin* OutputPin = Candidate->GetOutputPin()) { ForeachConnectedOutgoingConversationNode(OutputPin, [&ReachableStack](UConversationGraphNode* RemoteNode) { ReachableStack.Add(RemoteNode); }); } } } } // Add the reachable nodes to the map ConversationAsset->ReachableNodeMap.Reset(); ConversationAsset->InternalNodeIds.Reset(); ConversationAsset->ExitTags.Reset(); for (UConversationGraphNode* EdNode : ReachableNodeSet) { UConversationNode* NodeInstance = CastChecked(EdNode->NodeInstance); NodeInstance->Compiled_NodeGUID = EdNode->NodeGuid; // (no need to check for uniqueness, we already did that for all nodes above) ConversationAsset->ReachableNodeMap.Add(EdNode->NodeGuid, NodeInstance); ConversationAsset->InternalNodeIds.Add(EdNode->NodeGuid); if (UConversationLinkNode* ExitTagNode = Cast(NodeInstance)) { ConversationAsset->ExitTags.AddTag(ExitTagNode->GetRemoteEntryTag()); } } // TMap EntryMap; // TMap NodeMap; // TArray Speakers; // TMap FullNodeMap; //@TODO: CONVERSATION: Do stuff here for runtime use // See UBehaviorTreeComponent::RequestExecution //UPROPERTY(AssetRegistrySearchable) //TArray LinkedToNodeIds; } void FConversationCompiler::ForeachConnectedOutgoingConversationNode(UEdGraphPin* Pin, TFunctionRef Predicate) { for (UEdGraphPin* RemotePin : Pin->LinkedTo) { if (UConversationGraphNode_Knot* Knot = Cast(RemotePin->GetOwningNode())) { ForeachConnectedOutgoingConversationNode(Knot->GetOutputPin(), Predicate); } else if (UConversationGraphNode* RemoteNode = Cast(RemotePin->GetOwningNode())) { Predicate(RemoteNode); } } } void FConversationCompiler::ScanAndRecompileOutOfDateCompiledConversations() { TArray AllConversations; UAssetManager::Get().GetPrimaryAssetDataList(FPrimaryAssetType(UConversationDatabase::StaticClass()->GetFName()), AllConversations); TArray OutOfDateConversationPackages; for (FAssetData& ConversationAsset : AllConversations) { const int32 ConversationCompilerVersion = ConversationAsset.GetTagValueRef(GET_MEMBER_NAME_CHECKED(UConversationDatabase, CompilerVersion)); if (ConversationCompilerVersion < GetCompilerVersion()) { if (UConversationDatabase* ConversationDB = Cast(ConversationAsset.GetAsset())) { FConversationCompiler::RebuildBank(ConversationDB); OutOfDateConversationPackages.Add(ConversationDB->GetOutermost()); } } } if (OutOfDateConversationPackages.Num() > 0) { EAppReturnType::Type SaveConversations = FMessageDialog::Open(EAppMsgType::YesNo, FText::Format(LOCTEXT("ResaveConversations", "We found {0} conversations on an old version of the compiler that need to be resaved.\n\nSave?"), OutOfDateConversationPackages.Num()) ); if (SaveConversations == EAppReturnType::Yes) { FEditorFileUtils::PromptForCheckoutAndSave(OutOfDateConversationPackages, /*bCheckDirty*/false, /*bPromptToSave*/false); } } } #undef LOCTEXT_NAMESPACE