// Copyright Epic Games, Inc. All Rights Reserved. #include "MetasoundFrontendTransform.h" #include "Algo/Transform.h" #include "Interfaces/MetasoundFrontendInterface.h" #include "Interfaces/MetasoundFrontendInterfaceRegistry.h" #include "NodeTemplates/MetasoundFrontendNodeTemplateInput.h" #include "MetasoundAccessPtr.h" #include "MetasoundAssetBase.h" #include "MetasoundDocumentInterface.h" #include "MetasoundFrontendDocument.h" #include "MetasoundFrontendDocumentBuilder.h" #include "MetasoundFrontendDocumentController.h" #include "MetasoundFrontendDocumentIdGenerator.h" #include "MetasoundFrontendRegistries.h" #include "MetasoundFrontendSearchEngine.h" #include "MetasoundLog.h" #include "MetasoundTrace.h" #include "Misc/App.h" namespace Metasound { namespace Frontend { #define METASOUND_VERSIONING_LOG(Verbosity, Format, ...) if (DocumentTransform::bVersioningLoggingEnabled) { UE_LOG(LogMetaSound, Verbosity, Format, ##__VA_ARGS__); } namespace DocumentTransform { bool bVersioningLoggingEnabled = true; void LogAutoUpdateWarning(const FString& LogMessage) { // These should eventually move back to warning on cook // but are temporarily downgraded to prevent // warnings on things like unused test content from // blocking code checkins if (IsRunningCookCommandlet()) { METASOUND_VERSIONING_LOG(Display, TEXT("%s"), *LogMessage); } else { METASOUND_VERSIONING_LOG(Warning, TEXT("%s"), *LogMessage); } } #if WITH_EDITOR FGetNodeDisplayNameProjection NodeDisplayNameProjection = [] (const FNodeHandle&) { return FText(); }; bool GetVersioningLoggingEnabled() { return bVersioningLoggingEnabled; } void SetVersioningLoggingEnabled(bool bEnabled) { bVersioningLoggingEnabled = bEnabled; } void RegisterNodeDisplayNameProjection(FGetNodeDisplayNameProjection&& InNameProjection) { NodeDisplayNameProjection = MoveTemp(InNameProjection); } FGetNodeDisplayNameProjectionRef GetNodeDisplayNameProjection() { return NodeDisplayNameProjection; } #endif // WITH_EDITOR } // namespace DocumentTransform bool IDocumentTransform::Transform(FMetasoundFrontendDocument& InOutDocument) const { FDocumentAccessPtr DocAccessPtr = MakeAccessPtr(InOutDocument.AccessPoint, InOutDocument); return Transform(FDocumentController::CreateDocumentHandle(DocAccessPtr)); } bool INodeTransform::Transform(const FGuid& InNodeID, FMetaSoundFrontendDocumentBuilder& OutBuilder) const { return false; } FModifyRootGraphInterfaces::FModifyRootGraphInterfaces(const TArray& InInterfacesToRemove, const TArray& InInterfacesToAdd) : InterfacesToRemove(InInterfacesToRemove) , InterfacesToAdd(InInterfacesToAdd) { Init(); } FModifyRootGraphInterfaces::FModifyRootGraphInterfaces(const TArray& InInterfaceVersionsToRemove, const TArray& InInterfaceVersionsToAdd) { Algo::Transform(InInterfaceVersionsToRemove, InterfacesToRemove, [](const FMetasoundFrontendVersion& Version) { FMetasoundFrontendInterface Interface; const bool bFromInterfaceFound = IInterfaceRegistry::Get().FindInterface(GetInterfaceRegistryKey(Version), Interface); if (!ensureAlways(bFromInterfaceFound)) { METASOUND_VERSIONING_LOG(Error, TEXT("Failed to find interface '%s' to remove"), *Version.ToString()); } return Interface; }); Algo::Transform(InInterfaceVersionsToAdd, InterfacesToAdd, [](const FMetasoundFrontendVersion& Version) { FMetasoundFrontendInterface Interface; const bool bToInterfaceFound = IInterfaceRegistry::Get().FindInterface(GetInterfaceRegistryKey(Version), Interface); if (!ensureAlways(bToInterfaceFound)) { METASOUND_VERSIONING_LOG(Error, TEXT("Failed to find interface '%s' to add"), *Version.ToString()); } return Interface; }); Init(); } #if WITH_EDITOR void FModifyRootGraphInterfaces::SetDefaultNodeLocations(bool bInSetDefaultNodeLocations) { bSetDefaultNodeLocations = bInSetDefaultNodeLocations; } #endif // WITH_EDITOR void FModifyRootGraphInterfaces::SetNamePairingFunction(const TFunction& InNamePairingFunction) { // Reinit required to rebuild list of pairs Init(&InNamePairingFunction); } bool FModifyRootGraphInterfaces::AddMissingVertices(FGraphHandle GraphHandle) const { for (const FInputData& InputData : InputsToAdd) { const FMetasoundFrontendClassInput& InputToAdd = InputData.Input; GraphHandle->AddInputVertex(InputToAdd); } for (const FOutputData& OutputData : OutputsToAdd) { const FMetasoundFrontendClassOutput& OutputToAdd = OutputData.Output; GraphHandle->AddOutputVertex(OutputToAdd); } return !InputsToAdd.IsEmpty() || !OutputsToAdd.IsEmpty(); } void FModifyRootGraphInterfaces::Init(const TFunction* InNamePairingFunction) { InputsToRemove.Reset(); InputsToAdd.Reset(); OutputsToRemove.Reset(); OutputsToAdd.Reset(); PairedInputs.Reset(); PairedOutputs.Reset(); for (const FMetasoundFrontendInterface& FromInterface : InterfacesToRemove) { InputsToRemove.Append(FromInterface.Inputs); OutputsToRemove.Append(FromInterface.Outputs); } // This function combines all the inputs of all interfaces into one input list and ptrs to their originating interfaces. // The interface ptr will be used to query the interface for required validations on inputs. Interfaces define required inputs (and possibly other validation requirements). for (const FMetasoundFrontendInterface& ToInterface : InterfacesToAdd) { TArray NewInputDataArray; for (const FMetasoundFrontendClassInput& Input : ToInterface.Inputs) { FInputData NewData; NewData.Input = Input; NewData.InputInterface = &ToInterface; NewInputDataArray.Add(NewData); } InputsToAdd.Append(NewInputDataArray); TArray NewOutputDataArray; for (const FMetasoundFrontendClassOutput& Output : ToInterface.Outputs) { FOutputData NewData; NewData.Output = Output; NewData.OutputInterface = &ToInterface; NewOutputDataArray.Add(NewData); } OutputsToAdd.Append(NewOutputDataArray); } // Iterate in reverse to allow removal from `InputsToAdd` for (int32 AddIndex = InputsToAdd.Num() - 1; AddIndex >= 0; AddIndex--) { const FMetasoundFrontendClassVertex& VertexToAdd = InputsToAdd[AddIndex].Input; const int32 RemoveIndex = InputsToRemove.IndexOfByPredicate([&](const FMetasoundFrontendClassVertex& VertexToRemove) { if (VertexToAdd.TypeName != VertexToRemove.TypeName) { return false; } if (InNamePairingFunction && *InNamePairingFunction) { return (*InNamePairingFunction)(VertexToAdd.Name, VertexToRemove.Name); } FName ParamA; FName ParamB; FName Namespace; VertexToAdd.SplitName(Namespace, ParamA); VertexToRemove.SplitName(Namespace, ParamB); return ParamA == ParamB; }); if (INDEX_NONE != RemoveIndex) { PairedInputs.Add(FVertexPair{InputsToRemove[RemoveIndex], InputsToAdd[AddIndex].Input}); InputsToRemove.RemoveAtSwap(RemoveIndex); InputsToAdd.RemoveAtSwap(AddIndex); } } // Iterate in reverse to allow removal from `OutputsToAdd` for (int32 AddIndex = OutputsToAdd.Num() - 1; AddIndex >= 0; AddIndex--) { const FMetasoundFrontendClassVertex& VertexToAdd = OutputsToAdd[AddIndex].Output; const int32 RemoveIndex = OutputsToRemove.IndexOfByPredicate([&](const FMetasoundFrontendClassVertex& VertexToRemove) { if (VertexToAdd.TypeName != VertexToRemove.TypeName) { return false; } if (InNamePairingFunction && *InNamePairingFunction) { return (*InNamePairingFunction)(VertexToAdd.Name, VertexToRemove.Name); } FName ParamA; FName ParamB; FName Namespace; VertexToAdd.SplitName(Namespace, ParamA); VertexToRemove.SplitName(Namespace, ParamB); return ParamA == ParamB; }); if (INDEX_NONE != RemoveIndex) { PairedOutputs.Add(FVertexPair { OutputsToRemove[RemoveIndex], OutputsToAdd[AddIndex].Output }); OutputsToRemove.RemoveAtSwap(RemoveIndex); OutputsToAdd.RemoveAtSwap(AddIndex); } } } bool FModifyRootGraphInterfaces::RemoveUnsupportedVertices(FGraphHandle GraphHandle) const { // Remove unsupported inputs for (const FMetasoundFrontendClassVertex& InputToRemove : InputsToRemove) { if (const FMetasoundFrontendClassInput* ClassInput = GraphHandle->FindClassInputWithName(InputToRemove.Name).Get()) { if (FMetasoundFrontendClassInput::IsFunctionalEquivalent(*ClassInput, InputToRemove)) { GraphHandle->RemoveInputVertex(InputToRemove.Name); } } } // Remove unsupported outputs for (const FMetasoundFrontendClassVertex& OutputToRemove : OutputsToRemove) { if (const FMetasoundFrontendClassOutput* ClassOutput = GraphHandle->FindClassOutputWithName(OutputToRemove.Name).Get()) { if (FMetasoundFrontendClassOutput::IsFunctionalEquivalent(*ClassOutput, OutputToRemove)) { GraphHandle->RemoveOutputVertex(OutputToRemove.Name); } } } return !InputsToRemove.IsEmpty() || !OutputsToRemove.IsEmpty(); } bool FModifyRootGraphInterfaces::SwapPairedVertices(FGraphHandle GraphHandle) const { for (const FVertexPair& InputPair : PairedInputs) { const FMetasoundFrontendClassVertex& OriginalVertex = InputPair.Get<0>(); FMetasoundFrontendClassInput NewVertex = InputPair.Get<1>(); // Cache off node locations and connections to push to new node TMap Locations; TArray ConnectedInputs; if (const FMetasoundFrontendClassInput* ClassInput = GraphHandle->FindClassInputWithName(OriginalVertex.Name).Get()) { if (FMetasoundFrontendVertex::IsFunctionalEquivalent(*ClassInput, OriginalVertex)) { const FMetasoundFrontendLiteral& DefaultLiteral = ClassInput->FindConstDefaultChecked(Frontend::DefaultPageID); NewVertex.FindDefaultChecked(Frontend::DefaultPageID) = DefaultLiteral; NewVertex.NodeID = ClassInput->NodeID; FNodeHandle OriginalInputNode = GraphHandle->GetInputNodeWithName(OriginalVertex.Name); #if WITH_EDITOR Locations = OriginalInputNode->GetNodeStyle().Display.Locations; #endif // WITH_EDITOR FOutputHandle OriginalInputNodeOutput = OriginalInputNode->GetOutputWithVertexName(OriginalVertex.Name); ConnectedInputs = OriginalInputNodeOutput->GetConnectedInputs(); GraphHandle->RemoveInputVertex(OriginalVertex.Name); } } FNodeHandle NewInputNode = GraphHandle->AddInputVertex(NewVertex); #if WITH_EDITOR // Copy prior node locations if (!Locations.IsEmpty()) { FMetasoundFrontendNodeStyle Style = NewInputNode->GetNodeStyle(); Style.Display.Locations = Locations; NewInputNode->SetNodeStyle(Style); } #endif // WITH_EDITOR // Copy prior node connections FOutputHandle OutputHandle = NewInputNode->GetOutputWithVertexName(NewVertex.Name); for (FInputHandle& ConnectedInput : ConnectedInputs) { OutputHandle->Connect(*ConnectedInput); } } // Swap paired outputs. for (const FVertexPair& OutputPair : PairedOutputs) { const FMetasoundFrontendClassVertex& OriginalVertex = OutputPair.Get<0>(); FMetasoundFrontendClassVertex NewVertex = OutputPair.Get<1>(); #if WITH_EDITOR // Cache off node locations to push to new node // Default add output node to origin. TMap Locations; Locations.Add(FGuid(), FVector2D { 0.f, 0.f }); #endif // WITH_EDITOR FOutputHandle ConnectedOutput = IOutputController::GetInvalidHandle(); if (const FMetasoundFrontendClassOutput* ClassOutput = GraphHandle->FindClassOutputWithName(OriginalVertex.Name).Get()) { if (FMetasoundFrontendVertex::IsFunctionalEquivalent(*ClassOutput, OriginalVertex)) { NewVertex.NodeID = ClassOutput->NodeID; #if WITH_EDITOR // Interface members do not serialize text to avoid localization // mismatches between assets and interfaces defined in code. NewVertex.Metadata.SetSerializeText(false); #endif // WITH_EDITOR FNodeHandle OriginalOutputNode = GraphHandle->GetOutputNodeWithName(OriginalVertex.Name); #if WITH_EDITOR Locations = OriginalOutputNode->GetNodeStyle().Display.Locations; #endif // WITH_EDITOR FInputHandle Input = OriginalOutputNode->GetInputWithVertexName(OriginalVertex.Name); ConnectedOutput = Input->GetConnectedOutput(); GraphHandle->RemoveOutputVertex(OriginalVertex.Name); } } FNodeHandle NewOutputNode = GraphHandle->AddOutputVertex(NewVertex); #if WITH_EDITOR if (Locations.Num() > 0) { FMetasoundFrontendNodeStyle Style = NewOutputNode->GetNodeStyle(); Style.Display.Locations = Locations; NewOutputNode->SetNodeStyle(Style); } #endif // WITH_EDITOR // Copy prior node connections FInputHandle InputHandle = NewOutputNode->GetInputWithVertexName(NewVertex.Name); ConnectedOutput->Connect(*InputHandle); } return !PairedInputs.IsEmpty() || !PairedOutputs.IsEmpty(); } bool FModifyRootGraphInterfaces::Transform(FDocumentHandle InDocument) const { bool bDidEdit = false; FGraphHandle GraphHandle = InDocument->GetRootGraph(); if (ensure(GraphHandle->IsValid())) { bDidEdit |= UpdateInterfacesInternal(InDocument); const bool bAddedVertices = AddMissingVertices(GraphHandle); bDidEdit |= bAddedVertices; bDidEdit |= SwapPairedVertices(GraphHandle); bDidEdit |= RemoveUnsupportedVertices(GraphHandle); #if WITH_EDITORONLY_DATA if (bAddedVertices && bSetDefaultNodeLocations) { UpdateAddedVertexNodePositions(GraphHandle); } #endif // WITH_EDITORONLY_DATA } return bDidEdit; } bool FModifyRootGraphInterfaces::Transform(FMetasoundFrontendDocument& InOutDocument) const { FDocumentAccessPtr DocAccessPtr = MakeAccessPtr(InOutDocument.AccessPoint, InOutDocument); return Transform(FDocumentController::CreateDocumentHandle(DocAccessPtr)); } bool FModifyRootGraphInterfaces::UpdateInterfacesInternal(FDocumentHandle DocumentHandle) const { for (const FMetasoundFrontendInterface& Interface : InterfacesToRemove) { DocumentHandle->RemoveInterfaceVersion(Interface.Metadata.Version); } for (const FMetasoundFrontendInterface& Interface : InterfacesToAdd) { DocumentHandle->AddInterfaceVersion(Interface.Metadata.Version); } return !InterfacesToRemove.IsEmpty() || !InterfacesToAdd.IsEmpty(); } #if WITH_EDITORONLY_DATA void FModifyRootGraphInterfaces::UpdateAddedVertexNodePositions(FGraphHandle GraphHandle) const { auto SortAndPlaceMemberNodes = [&GraphHandle](EMetasoundFrontendClassType ClassType, TSet& AddedNames, TFunctionRef InGetSortOrder) { // Add graph member nodes by sort order TSortedMap SortOrderToName; GraphHandle->IterateNodes([&GraphHandle, &SortOrderToName, &InGetSortOrder](FNodeHandle NodeHandle) { const int32 Index = InGetSortOrder(NodeHandle->GetNodeName()); SortOrderToName.Add(Index, NodeHandle); }, ClassType); // Prime the first location as an offset prior to an existing location (as provided by a swapped member) // to avoid placing away from user's active area if possible. FVector2D NextLocation = { 0.0f, 0.0f }; { int32 NumBeforeDefined = 1; for (const TPair& Pair : SortOrderToName) //-V1078 { const FConstNodeHandle& NodeHandle = Pair.Value; const FName NodeName = NodeHandle->GetNodeName(); if (AddedNames.Contains(NodeName)) { NumBeforeDefined++; } else { const TMap& Locations = NodeHandle->GetNodeStyle().Display.Locations; if (!Locations.IsEmpty()) { auto It = Locations.CreateConstIterator(); const TPair& Location = *It; NextLocation = Location.Value - (NumBeforeDefined * DisplayStyle::NodeLayout::DefaultOffsetY); break; } } } } // Iterate through sorted map in sequence, slotting in new locations after existing swapped nodes with predefined locations. for (TPair& Pair : SortOrderToName) //-V1078 { FNodeHandle& NodeHandle = Pair.Value; const FName NodeName = NodeHandle->GetNodeName(); if (AddedNames.Contains(NodeName)) { FMetasoundFrontendNodeStyle NewStyle = NodeHandle->GetNodeStyle(); NewStyle.Display.Locations.Add(FGuid(), NextLocation); NextLocation += DisplayStyle::NodeLayout::DefaultOffsetY; NodeHandle->SetNodeStyle(NewStyle); } else { for (const TPair& Location : NodeHandle->GetNodeStyle().Display.Locations) { NextLocation = Location.Value + DisplayStyle::NodeLayout::DefaultOffsetY; } } } }; // Sort/Place Inputs { TSet AddedNames; Algo::Transform(InputsToAdd, AddedNames, [](const FInputData& InputData) { return InputData.Input.Name; }); auto GetInputSortOrder = [&GraphHandle](const FVertexName& InVertexName) { return GraphHandle->GetSortOrderIndexForInput(InVertexName); }; SortAndPlaceMemberNodes(EMetasoundFrontendClassType::Input, AddedNames, GetInputSortOrder); } // Sort/Place Outputs { TSet AddedNames; Algo::Transform(OutputsToAdd, AddedNames, [](const FOutputData& OutputData) { return OutputData.Output.Name; }); auto GetOutputSortOrder = [&GraphHandle](const FVertexName& InVertexName) { return GraphHandle->GetSortOrderIndexForOutput(InVertexName); }; SortAndPlaceMemberNodes(EMetasoundFrontendClassType::Output, AddedNames, GetOutputSortOrder); } } bool FAutoUpdateRootGraph::Transform(FMetaSoundFrontendDocumentBuilder& InOutBuilderToTransform) { METASOUND_TRACE_CPUPROFILER_EVENT_SCOPE(FAutoUpdateRootGraph::Transform); bool bDidEdit = false; const bool bIsPreset = InOutBuilderToTransform.IsPreset(); const FMetasoundFrontendGraphClass& RootGraph = InOutBuilderToTransform.GetConstDocumentChecked().RootGraph; // If preset, rebuild root graph if (bIsPreset) { FMetasoundAssetBase* PresetReferencedMetaSoundAsset = InOutBuilderToTransform.GetReferencedPresetAsset(); if (!PresetReferencedMetaSoundAsset) { METASOUND_VERSIONING_LOG(Error, TEXT("Auto-Updating preset '%s' failed: Referenced class missing."), *DebugAssetPath); return false; } TScriptInterface RefDocInterface = PresetReferencedMetaSoundAsset->GetOwningAsset(); const FMetaSoundFrontendDocumentBuilder& ReferenceBuilder = IDocumentBuilderRegistry::GetChecked().FindOrBeginBuilding(RefDocInterface); bDidEdit |= FRebuildPresetRootGraph(ReferenceBuilder).Transform(InOutBuilderToTransform); if (bDidEdit) { FMetasoundFrontendClassMetadata PresetMetadata = InOutBuilderToTransform.GetConstDocumentChecked().RootGraph.Metadata; PresetMetadata.SetType(EMetasoundFrontendClassType::External); const FNodeRegistryKey RegistryKey(PresetMetadata); FMetasoundAssetBase* PresetMetaSoundAsset = IMetaSoundAssetManager::GetChecked().TryLoadAssetFromKey(RegistryKey); if (ensure(PresetMetaSoundAsset)) { TScriptInterface PresetInterface = PresetMetaSoundAsset->GetOwningAsset(); check(PresetInterface); PresetInterface->ConformObjectToDocument(); } InOutBuilderToTransform.RemoveUnusedDependencies(); InOutBuilderToTransform.SynchronizeDependencyMetadata(); } } // Non preset else { // Update external node dependencies until there are no more updates // (A node could need a minor version update, then an update transform, // and then updated to another minor version which would require multiple passes) bool bUpdated = true; while (bUpdated) { bUpdated = false; bUpdated |= UpdateExternalDependencies(InOutBuilderToTransform); bDidEdit |= bUpdated; } } return bDidEdit; } bool FAutoUpdateRootGraph::UpdateExternalDependencies(FMetaSoundFrontendDocumentBuilder& InOutBuilderToTransform) { bool bDidEdit = false; // using FDependencyPair = TPair; TSet DependenciesToUpdate; TSet DependenciesToTransform; // Collect updates const FMetasoundFrontendGraphClass& RootGraph = InOutBuilderToTransform.GetConstDocumentChecked().RootGraph; RootGraph.IterateGraphPages([&](const FMetasoundFrontendGraph& Graph) { const FGuid PageID = Graph.PageID; InOutBuilderToTransform.IterateNodesByClassType([&](const FMetasoundFrontendClass& Class, const FMetasoundFrontendNode& Node) { const FNodeClassRegistryKey& NodeClassKey = FNodeClassRegistryKey(Class.Metadata); const bool bHasUpdated = UpdatedClasses.Contains(Class.ID); const EAutoUpdateEligibility AutoUpdateReason = InOutBuilderToTransform.CanAutoUpdate(Node.GetID(), &PageID); if (bHasUpdated || AutoUpdateReason == EAutoUpdateEligibility::Ineligible) { return; } UpdatedClasses.Add(Class.ID); // Check if a updated minor version exists. FMetasoundFrontendClass ClassWithHighestMinorVersion; const bool bFoundClassInSearchEngine = Frontend::ISearchEngine::Get().FindClassWithHighestMinorVersion(NodeClassKey.ClassName, NodeClassKey.Version.Major, ClassWithHighestMinorVersion); if (bFoundClassInSearchEngine && (ClassWithHighestMinorVersion.Metadata.GetVersion() > NodeClassKey.Version)) { const FMetasoundFrontendVersionNumber UpdateVersion = ClassWithHighestMinorVersion.Metadata.GetVersion(); METASOUND_VERSIONING_LOG(Display, TEXT("Auto-Updating '%s' node class '%s': Newer version '%s' found."), *DebugAssetPath, *NodeClassKey.ClassName.ToString(), *UpdateVersion.ToString()); DependenciesToUpdate.Add(FDependencyPair(NodeClassKey, FNodeClassRegistryKey(ClassWithHighestMinorVersion.Metadata))); bDidEdit |= true; } else if (AutoUpdateReason == EAutoUpdateEligibility::Eligible_InterfaceChange) { METASOUND_VERSIONING_LOG(Display, TEXT("Auto-Updating '%s' node class '%s (%s)': Interface change detected."), *DebugAssetPath, *NodeClassKey.ClassName.ToString(), *NodeClassKey.Version.ToString()); DependenciesToUpdate.Add(FDependencyPair(NodeClassKey, NodeClassKey)); bDidEdit |= true; } // Order is intentional; node must be updated to highest minor version before node update transform can be applied else if (AutoUpdateReason == EAutoUpdateEligibility::Eligible_NodeUpdateTransform) { METASOUND_VERSIONING_LOG(Display, TEXT("Auto-Updating '%s' node class '%s (%s)': Node update transform found."), *DebugAssetPath, *NodeClassKey.ClassName.ToString(), *NodeClassKey.Version.ToString()); DependenciesToTransform.Add(NodeClassKey); bDidEdit |= true; } else { DependenciesToUpdate.Add(FDependencyPair(NodeClassKey, NodeClassKey)); } }, EMetasoundFrontendClassType::External, &PageID); }); // Apply dependency updates and log disconnections for (const FDependencyPair& DependencyPair : DependenciesToUpdate) { using FVertexNameAndType = FMetaSoundFrontendDocumentBuilder::FVertexNameAndType; TArray DisconnectedInputs; TArray DisconnectedOutputs; InOutBuilderToTransform.ReplaceDependency(DependencyPair.Key, DependencyPair.Value, &DisconnectedInputs, &DisconnectedOutputs); // Log warnings for any disconnections if (bLogWarningOnDroppedConnection) { if ((DisconnectedInputs.Num() > 0) || (DisconnectedOutputs.Num() > 0)) { const FString NodeClassName = DependencyPair.Key.ClassName.ToString(); const FString NewClassVersion = DependencyPair.Value.Version.ToString(); for (const FVertexNameAndType& InputPin : DisconnectedInputs) { DocumentTransform::LogAutoUpdateWarning(FString::Printf(TEXT("Auto-Updating '%s' node class '%s (%s)': Previously connected input '%s' with data type '%s' no longer exists."), *DebugAssetPath, *NodeClassName, *NewClassVersion, *InputPin.Get<0>().ToString(), *InputPin.Get<1>().ToString())); } for (const FVertexNameAndType& OutputPin : DisconnectedOutputs) { DocumentTransform::LogAutoUpdateWarning(FString::Printf(TEXT("Auto-Updating '%s' node class '%s (%s)': Previously connected output '%s' with data type '%s' no longer exists."), *DebugAssetPath, *NodeClassName, *NewClassVersion, *OutputPin.Get<0>().ToString(), *OutputPin.Get<1>().ToString())); } } } } // Apply dependency transforms for (const FNodeClassRegistryKey& NodeClassKey : DependenciesToTransform) { InOutBuilderToTransform.ApplyDependencyUpdateTransform(NodeClassKey); } InOutBuilderToTransform.RemoveUnusedDependencies(); InOutBuilderToTransform.SynchronizeDependencyMetadata(); return bDidEdit; } bool FAutoUpdateRootGraph::Transform(FDocumentHandle InDocument) { return false; } #endif // WITH_EDITORONLY_DATA bool FRebuildPresetRootGraph::Transform(FDocumentHandle InDocument) const { return false; } FRebuildPresetRootGraph::FRebuildPresetRootGraph(const FMetasoundFrontendDocument& InReferencedDocument) { } bool FRebuildPresetRootGraph::Transform(FMetasoundFrontendDocument& InDocument) const { return false; } bool FRebuildPresetRootGraph::Transform(FMetaSoundFrontendDocumentBuilder& InOutBuilderToTransform) const { METASOUND_TRACE_CPUPROFILER_EVENT_SCOPE(Metasound::Frontend::FRebuildPresetRootGraph::Transform); // Callers of this transform should check that the graph is supposed to // be managed externally and the parent builder is valid before calling this transform. If a scenario // arises where this transform is used outside of AutoUpdate, then this // early exist should be removed as it's mostly here to protect against // accidental manipulation of metasound graphs. if (!InOutBuilderToTransform.IsPreset() || !ParentBuilder) { return false; } const FMetasoundFrontendDocument& DocumentToTransform = InOutBuilderToTransform.GetConstDocumentChecked(); const FMetasoundFrontendDocument& ParentDocument = ParentBuilder->GetConstDocumentChecked(); // Determine the inputs and outputs needed in the wrapping graph. Also // cache any exiting literals that have been set on the wrapping graph. TSet InputsInheritingDefault; TArray ClassInputs = GenerateRequiredClassInputs(InOutBuilderToTransform, InputsInheritingDefault); TArray ClassOutputs = GenerateRequiredClassOutputs(InOutBuilderToTransform); #if WITH_EDITORONLY_DATA // Cache off member metadata so it be can be readded if necessary after the graph is cleared FMemberIDToMetadataMap CachedMemberMetadata; CacheMemberMetadata(InOutBuilderToTransform, InputsInheritingDefault, CachedMemberMetadata); #endif // WITH_EDITORONLY_DATA FGuid PresetNodeID; InOutBuilderToTransform.IterateNodesByClassType([&](const FMetasoundFrontendClass&, const FMetasoundFrontendNode& Node) { PresetNodeID = Node.GetID(); }, EMetasoundFrontendClassType::External); if (!PresetNodeID.IsValid()) { // This ID was originally being set to FGuid::NewGuid. // If you were reliant on that ID, please resave the asset so it is serialized with a valid ID PresetNodeID = DocumentToTransform.RootGraph.ID; } // Clear the root graph so it can be rebuilt. TSharedRef ModifyDelegates = MakeShared(InOutBuilderToTransform.GetDocumentDelegates()); InOutBuilderToTransform.ClearDocument(ModifyDelegates); InOutBuilderToTransform.SetPresetFlags(); // Ensure preset interfaces match those found in referenced graph. Referenced graph is assumed to be // well-formed (i.e. all inputs/outputs/environment variables declared by interfaces are present, and // of proper name & data type). Pass in parent builder to copy member ids so added member ids can be consistent const TSet& RefInterfaceVersions = ParentDocument.Interfaces; for (const FMetasoundFrontendVersion& Version : RefInterfaceVersions) { InOutBuilderToTransform.AddInterface(Version.Name, /*bAddUserModifiableInterfaceOnly = */false, ParentBuilder); } // Add referenced node const FMetasoundFrontendClassMetadata& ParentClassMetadata = ParentDocument.RootGraph.Metadata; const FMetasoundFrontendNode* ReferencedNode = InOutBuilderToTransform.AddNodeByClassName(ParentClassMetadata.GetClassName(), ParentClassMetadata.GetVersion().Major, PresetNodeID); check(ReferencedNode); #if WITH_EDITOR // Set node location, offset to be to the right of input nodes const FGuid EdNodeGuid = FGuid::NewGuid(); // EdNodes are now never serialized and are transient, so just assign here InOutBuilderToTransform.SetNodeLocation(PresetNodeID, DisplayStyle::NodeLayout::DefaultOffsetX, &EdNodeGuid); #endif // WITH_EDITOR // Connect parent graph to referenced graph InOutBuilderToTransform.SetGraphInputsInheritingDefault(MoveTemp(InputsInheritingDefault)); AddAndConnectInputs(ClassInputs, InOutBuilderToTransform, PresetNodeID); AddAndConnectOutputs(ClassOutputs, InOutBuilderToTransform, PresetNodeID); #if WITH_EDITORONLY_DATA AddMemberMetadata(CachedMemberMetadata, InOutBuilderToTransform); #endif // WITH_EDITORONLY_DATA return true; } #if WITH_EDITORONLY_DATA void FRebuildPresetRootGraph::AddMemberMetadata(const FMemberIDToMetadataMap& InCachedMemberMetadata, FMetaSoundFrontendDocumentBuilder& InOutBuilderToTransform) const { // Add member metadata if a member with the corresponding node ID exists in the preset graph if (!InCachedMemberMetadata.IsEmpty()) { for (const TPair>& MemberMetadataPair : InCachedMemberMetadata) { if (InOutBuilderToTransform.FindNode(MemberMetadataPair.Key)) { InOutBuilderToTransform.SetMemberMetadata(*MemberMetadataPair.Value); } } } } void FRebuildPresetRootGraph::CacheMemberMetadata(FMetaSoundFrontendDocumentBuilder& InOutBuilderToTransform, const TSet& InInputsInheritingDefault, FMemberIDToMetadataMap& InOutCachedMemberMetadata) const { const FMetasoundFrontendDocument& ParentDocument = ParentBuilder->GetConstDocumentChecked(); auto CreateNewLiteral = [&](UMetaSoundFrontendMemberMetadata* TemplateObject, FGuid MemberID) -> UMetaSoundFrontendMemberMetadata* { if (TemplateObject) { UMetaSoundFrontendMemberMetadata* NewMetadata = NewObject(&InOutBuilderToTransform.CastDocumentObjectChecked(), TemplateObject->GetClass(), FName(), RF_Transactional, TemplateObject); check(NewMetadata); NewMetadata->MemberID = MemberID; return NewMetadata; } return nullptr; }; for (const FMetasoundFrontendClassInput& ParentClassInput : ParentDocument.RootGraph.GetDefaultInterface().Inputs) { UMetaSoundFrontendMemberMetadata* MemberMetadata = nullptr; // Member id is id in InOutBuilder, which may not be the same as the node id of the class input of ParentBuilder FGuid MemberID; const FMetasoundFrontendClassInput* GraphInput = InOutBuilderToTransform.FindGraphInput(ParentClassInput.Name); if (GraphInput && GraphInput->TypeName == ParentClassInput.TypeName) { MemberID = GraphInput->NodeID; // If the input vertex already exists in the parent graph, // check if parent should be used or not from set of managed // input names. if (InInputsInheritingDefault.Contains(ParentClassInput.Name)) { UMetaSoundFrontendMemberMetadata* ParentMetadata = ParentBuilder->FindMemberMetadata(ParentClassInput.NodeID); MemberMetadata = CreateNewLiteral(ParentMetadata, MemberID); } else { // Use existing defaults MemberMetadata = InOutBuilderToTransform.FindMemberMetadata(GraphInput->NodeID); } } else { UMetaSoundFrontendMemberMetadata* ParentMetadata = ParentBuilder->FindMemberMetadata(ParentClassInput.NodeID); MemberID = ParentClassInput.NodeID; MemberMetadata = CreateNewLiteral(ParentMetadata, MemberID); } if (MemberMetadata) { InOutCachedMemberMetadata.Emplace(MemberID, MemberMetadata); } } } #endif // WITH_EDITORONLY_DATA void FRebuildPresetRootGraph::AddAndConnectInputs(const TArray& InClassInputs, FMetaSoundFrontendDocumentBuilder& InOutBuilderToTransform, const FGuid& InReferencedNodeID) const { // Add inputs and space appropriately const INodeTemplate* InputTemplate = INodeTemplateRegistry::Get().FindTemplate(FInputNodeTemplate::ClassName); check(InputTemplate); TArray ReferencedNodeInputVertices; TArray InputTemplateNodes; const TSet* InputsInheritingDefault = InOutBuilderToTransform.GetGraphInputsInheritingDefault(); check(InputsInheritingDefault); for (const FMetasoundFrontendClassInput& ClassInput : InClassInputs) { const FName InputName = ClassInput.Name; // Input may have already been added if interface member, but defaults need to be overridden const FMetasoundFrontendNode* InputNode = InOutBuilderToTransform.FindGraphInputNode(InputName); if (!InputNode) { InputNode = InOutBuilderToTransform.AddGraphInput(ClassInput); } else { // Defaults must be set as either the parent default or existing default may be different than the registered interface default // Setting defaults will set the input as non inheriting even if we're just copying the value // so cache off bool and set back if necessary const bool bInheritsDefault = InputsInheritingDefault->Contains(InputName); InOutBuilderToTransform.SetGraphInputDefaults(InputName, ClassInput.GetDefaults()); if (bInheritsDefault) { InOutBuilderToTransform.SetGraphInputInheritsDefault(InputName, /*bInputInheritsDefault=*/true); } } const FGuid InputNodeID = InputNode->GetID(); check(InputNode); const FMetasoundFrontendVertex* InputNodeOutputVertex = InOutBuilderToTransform.FindNodeOutput(InputNode->GetID(), InputName); check(InputNodeOutputVertex); const FMetasoundFrontendVertex* ReferencedNodeInputVertex = InOutBuilderToTransform.FindNodeInput(InReferencedNodeID, InputName); check(ReferencedNodeInputVertex); ReferencedNodeInputVertices.Add(ReferencedNodeInputVertex); // If not editor, just make connection directly between input and output node #if !WITH_EDITORONLY_DATA InOutBuilderToTransform.AddEdge(FMetasoundFrontendEdge { InputNodeID, InputNodeOutputVertex->VertexID, InReferencedNodeID, ReferencedNodeInputVertex->VertexID }); // If editor, add template node and connections #else // template node takes on data type of concrete input node's output type const FName DataType = InputNode->Interface.Outputs.Last().TypeName; FNodeTemplateGenerateInterfaceParams Params{ { }, { DataType } }; const FMetasoundFrontendNode* TemplateNode = InOutBuilderToTransform.AddNodeByTemplate(*InputTemplate, MoveTemp(Params)); check(TemplateNode); InputTemplateNodes.Add(TemplateNode); const FGuid TemplateNodeInputVertexID = TemplateNode->Interface.Inputs.Last().VertexID; const FGuid TemplateNodeOutputVertexID = TemplateNode->Interface.Outputs.Last().VertexID; InOutBuilderToTransform.AddEdge(FMetasoundFrontendEdge { InputNodeID, InputNodeOutputVertex->VertexID, TemplateNode->GetID(), TemplateNodeInputVertexID }); InOutBuilderToTransform.AddEdge(FMetasoundFrontendEdge { TemplateNode->GetID(), TemplateNodeOutputVertexID, InReferencedNodeID, ReferencedNodeInputVertex->VertexID }); #endif } #if WITH_EDITOR // Sort before adding nodes to graph layout & copy to preset (must be done after all // inputs/outputs are added but before setting locations to propagate effectively) const FMetasoundFrontendGraphClass& ParentRootGraph = ParentBuilder->GetConstDocumentChecked().RootGraph; FMetasoundFrontendInterfaceStyle Style = ParentRootGraph.GetDefaultInterface().GetInputStyle(); // Sort vertices on referenced node // then use that order to order connected input nodes (which share the same names) auto GetInputDisplayName = [&InOutBuilderToTransform, &InReferencedNodeID](const FMetasoundFrontendVertex& Vertex) { return InOutBuilderToTransform.GetNodeInputDisplayName(InReferencedNodeID, Vertex.Name); }; Style.SortVertices(ReferencedNodeInputVertices, GetInputDisplayName); InOutBuilderToTransform.SetInputStyle(MoveTemp(Style)); // Set editor node locations FVector2D InputNodeLocation = FVector2D::ZeroVector; for (const FMetasoundFrontendNode* TemplateNode : InputTemplateNodes) { InOutBuilderToTransform.SetNodeLocation(TemplateNode->GetID(), InputNodeLocation); InputNodeLocation += DisplayStyle::NodeLayout::DefaultOffsetY; } #endif // WITH_EDITOR } void FRebuildPresetRootGraph::AddAndConnectOutputs(const TArray& InClassOutputs, FMetaSoundFrontendDocumentBuilder& InOutBuilderToTransform, const FGuid& InReferencedNodeID) const { // Add outputs and space appropriately TArray ReferencedNodeOutputVertices; for (const FMetasoundFrontendClassOutput& ClassOutput : InClassOutputs) { // Output may have already been added if interface member const FMetasoundFrontendNode* OutputNode = InOutBuilderToTransform.FindGraphOutputNode(ClassOutput.Name); if (!OutputNode) { OutputNode = InOutBuilderToTransform.AddGraphOutput(ClassOutput); } check(OutputNode); // Connect output node input vertex to corresponding referenced node output vertex. const FMetasoundFrontendVertex* ReferencedNodeOutputVertex = InOutBuilderToTransform.FindNodeOutput(InReferencedNodeID, ClassOutput.Name); check(ReferencedNodeOutputVertex); const FMetasoundFrontendVertex* OutputNodeInputVertex = InOutBuilderToTransform.FindNodeInput(OutputNode->GetID(), ClassOutput.Name); check(OutputNodeInputVertex); InOutBuilderToTransform.AddEdge(FMetasoundFrontendEdge { InReferencedNodeID, ReferencedNodeOutputVertex->VertexID, OutputNode->GetID(), OutputNodeInputVertex->VertexID }); ReferencedNodeOutputVertices.Add(ReferencedNodeOutputVertex); } #if WITH_EDITOR // Sort before adding nodes to graph layout & copy to preset (must be done after all // inputs/outputs are added but before setting locations to propagate effectively) const FMetasoundFrontendGraphClass& ParentRootGraph = ParentBuilder->GetConstDocumentChecked().RootGraph; FMetasoundFrontendInterfaceStyle Style = ParentRootGraph.GetDefaultInterface().GetOutputStyle(); // Sort vertices on referenced node // then use that order to order connected output nodes (which share the same names) auto GetOutputDisplayName = [&InOutBuilderToTransform, &InReferencedNodeID](const FMetasoundFrontendVertex& Vertex) { return InOutBuilderToTransform.GetNodeOutputDisplayName(InReferencedNodeID, Vertex.Name); }; Style.SortVertices(ReferencedNodeOutputVertices, GetOutputDisplayName); InOutBuilderToTransform.SetOutputStyle(MoveTemp(Style)); // Set output node locations FVector2D OutputNodeLocation = (2 * DisplayStyle::NodeLayout::DefaultOffsetX); for (const FMetasoundFrontendVertex* OutputVertex : ReferencedNodeOutputVertices) { const FName NodeName = OutputVertex->Name; const FGuid OutputNodeID = InOutBuilderToTransform.FindGraphOutputNode(NodeName)->GetID(); // Set editor node locations const FGuid EdNodeGuid = FGuid::NewGuid(); // EdNodes are now never serialized and are transient, so just assign here InOutBuilderToTransform.SetNodeLocation(OutputNodeID, OutputNodeLocation); OutputNodeLocation += DisplayStyle::NodeLayout::DefaultOffsetY; } #endif // WITH_EDITOR } TArray FRebuildPresetRootGraph::GenerateRequiredClassInputs(FMetaSoundFrontendDocumentBuilder& InDocumentToTransformBuilder, TSet& OutInputsInheritingDefault) const { TArray ClassInputs; check(ParentBuilder); const FMetasoundFrontendGraphClass& ParentRootGraph = ParentBuilder->GetConstDocumentChecked().RootGraph; const FMetasoundFrontendDocument& ParentDocument = ParentBuilder->GetConstDocumentChecked(); const FMetasoundFrontendDocument& DocumentToTransform = InDocumentToTransformBuilder.GetConstDocumentChecked(); const TSet* ExistingInputsInheritingDefaultPtr = InDocumentToTransformBuilder.GetGraphInputsInheritingDefault(); check(ExistingInputsInheritingDefaultPtr); TSet InputsInheritingDefault = *ExistingInputsInheritingDefaultPtr; // Iterate through all input nodes of referenced graph for (const FMetasoundFrontendClassInput& ParentClassInput : ParentDocument.RootGraph.GetDefaultInterface().Inputs) { // Copy class input and reset defaults and id const FName& NodeName = ParentClassInput.Name; FMetasoundFrontendClassInput NewClassInput = ParentClassInput; NewClassInput.VertexID = FDocumentIDGenerator::Get().CreateVertexID(DocumentToTransform); NewClassInput.ResetDefaults(/*bInitializeDefaultPage=*/false); if (const FMetasoundFrontendClassInput* ExistingClassInput = InDocumentToTransformBuilder.FindGraphInput(ParentClassInput.Name)) { NewClassInput.NodeID = ExistingClassInput->NodeID; } auto InheritDefaultsFromGraph = [&NewClassInput, &NodeName](const FMetaSoundFrontendDocumentBuilder& InBuilder) { if (const FMetasoundFrontendClassInput* GraphClassInput = InBuilder.FindGraphInput(NodeName)) { GraphClassInput->IterateDefaults([&NewClassInput](const FGuid& PageID, const FMetasoundFrontendLiteral& Literal) { NewClassInput.AddDefault(PageID) = Literal; }); } else { NewClassInput.InitDefault(); } }; const FMetasoundFrontendClassInput* GraphInput = InDocumentToTransformBuilder.FindGraphInput(NodeName); if (GraphInput && GraphInput->TypeName == ParentClassInput.TypeName) { // If the input vertex already exists in the parent graph, // check if parent should be used or not from set of managed // input names. if (InputsInheritingDefault.Contains(NodeName)) { InheritDefaultsFromGraph(*ParentBuilder); } else { // Use existing defaults InheritDefaultsFromGraph(InDocumentToTransformBuilder); } } else { InheritDefaultsFromGraph(*ParentBuilder); // Add this to the list of inheriting as the // type no longer matches or it no longer exists InputsInheritingDefault.Add(NodeName); } ClassInputs.Add(MoveTemp(NewClassInput)); } OutInputsInheritingDefault.Reset(); Algo::TransformIf(ClassInputs, OutInputsInheritingDefault, [&InputsInheritingDefault](const FMetasoundFrontendClassInput& Input) { return InputsInheritingDefault.Contains(Input.Name); }, [](const FMetasoundFrontendClassInput& Input) { return Input.Name; }); return ClassInputs; } TArray FRebuildPresetRootGraph::GenerateRequiredClassOutputs(FMetaSoundFrontendDocumentBuilder& InDocumentToTransformBuilder) const { TArray ClassOutputs; check(ParentBuilder); const FMetasoundFrontendDocument& ParentDocument = ParentBuilder->GetConstDocumentChecked(); const FMetasoundFrontendDocument& DocumentToTransform = InDocumentToTransformBuilder.GetConstDocumentChecked(); for (const FMetasoundFrontendClassOutput& ParentClassOutput : ParentDocument.RootGraph.GetDefaultInterface().Outputs) { // Copy class output and reset defaults and id FMetasoundFrontendClassOutput NewClassOutput = ParentClassOutput; NewClassOutput.VertexID = FDocumentIDGenerator::Get().CreateVertexID(DocumentToTransform); if (const FMetasoundFrontendClassOutput* ExistingClassOutput = InDocumentToTransformBuilder.FindGraphOutput(ParentClassOutput.Name)) { NewClassOutput.NodeID = ExistingClassOutput->NodeID; } ClassOutputs.Add(MoveTemp(NewClassOutput)); } return ClassOutputs; } bool FRenameRootGraphClass::Transform(FDocumentHandle InDocument) const { return false; } bool FRenameRootGraphClass::Transform(FMetasoundFrontendDocument& InOutDocument) const { return false; } #undef METASOUND_VERSIONING_LOG } // namespace Frontend } // namespace Metasound