// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "Metasound.h" #include "MetasoundAssetManager.h" #include "MetasoundAssetSubsystem.h" #include "MetasoundDocumentBuilderRegistry.h" #include "MetasoundEngineModule.h" #include "MetasoundFrontendDocumentIdGenerator.h" #include "MetasoundFrontendQuery.h" #include "MetasoundFrontendRegistryKey.h" #include "MetasoundGlobals.h" #include "MetasoundSettings.h" #include "MetasoundUObjectRegistry.h" #include "Misc/App.h" #include "Modules/ModuleManager.h" #include "Serialization/Archive.h" #include "UObject/AssetRegistryTagsContext.h" #if WITH_EDITORONLY_DATA #include "Algo/Transform.h" #include "Interfaces/ITargetPlatform.h" #include "MetasoundFrontendRegistryContainer.h" #include "Misc/DataValidation.h" #include "Serialization/JsonWriter.h" #include "UObject/GarbageCollection.h" #include "UObject/ObjectMacros.h" #include "UObject/Object.h" #include "UObject/StrongObjectPtrTemplates.h" #endif // WITH_EDITORONLY_DATA #define LOCTEXT_NAMESPACE "MetasoundEngine" namespace Metasound::Engine { /** MetaSound Engine Asset helper provides routines for UObject based MetaSound assets. * Any UObject deriving from FMetaSoundAssetBase should use these helper functions * in their UObject overrides. */ struct FAssetHelper { static bool SerializationRequiresDeterminism(bool bIsCooking) { return bIsCooking || IsRunningCookCommandlet(); } #if WITH_EDITOR static void PreDuplicate(TScriptInterface MetaSound, FObjectDuplicationParameters& DupParams) { FDocumentBuilderRegistry::GetChecked().SetEventLogVerbosity(FDocumentBuilderRegistry::ELogEvent::DuplicateEntries, ELogVerbosity::NoLogging); } static void PostDuplicate(TScriptInterface MetaSound, EDuplicateMode::Type InDuplicateMode) { using namespace Engine; using namespace Frontend; if (InDuplicateMode == EDuplicateMode::Normal) { UObject* MetaSoundObject = MetaSound.GetObject(); check(MetaSoundObject); FDocumentBuilderRegistry& BuilderRegistry = FDocumentBuilderRegistry::GetChecked(); UMetaSoundBuilderBase& DuplicateBuilder = BuilderRegistry.FindOrBeginBuilding(*MetaSoundObject); FMetaSoundFrontendDocumentBuilder& DocBuilder = DuplicateBuilder.GetBuilder(); const FMetasoundFrontendClassName DuplicateName = DocBuilder.GetConstDocumentChecked().RootGraph.Metadata.GetClassName(); DocBuilder.GenerateNewClassName(); constexpr bool bForceUnregisterNodeClass = true; BuilderRegistry.FinishBuilding(DuplicateName, MetaSound->GetAssetPathChecked(), bForceUnregisterNodeClass); BuilderRegistry.SetEventLogVerbosity(FDocumentBuilderRegistry::ELogEvent::DuplicateEntries, ELogVerbosity::All); } } template static void PostEditUndo(TMetaSoundObject& InMetaSound) { InMetaSound.GetModifyContext().SetForceRefreshViews(); const FMetasoundFrontendClassName& ClassName = InMetaSound.GetConstDocument().RootGraph.Metadata.GetClassName(); Frontend::IDocumentBuilderRegistry::GetChecked().ReloadBuilder(ClassName); if (UMetasoundEditorGraphBase* Graph = Cast(InMetaSound.GetGraph())) { Graph->RegisterGraphWithFrontend(); } } template static void SetReferencedAssets(TMetaSoundObject& InMetaSound, TSet&& InAssetRefs) { using namespace Frontend; InMetaSound.ReferencedAssetClassKeys.Reset(); InMetaSound.ReferencedAssetClassObjects.Reset(); for (const IMetaSoundAssetManager::FAssetRef& AssetRef : InAssetRefs) { // Has to be serialized as node class registry key string for back compat InMetaSound.ReferencedAssetClassKeys.Add(FNodeClassRegistryKey(AssetRef.Key).ToString()); if (UObject* Object = FSoftObjectPath(AssetRef.Path).TryLoad()) { InMetaSound.ReferencedAssetClassObjects.Add(Object); } else { UE_LOG(LogMetaSound, Error, TEXT("Failed to load referenced asset %s from asset %s"), *AssetRef.Path.ToString(), *InMetaSound.GetPathName()); } } } static EDataValidationResult IsClassNameUnique(const FMetasoundFrontendDocument& Document, FDataValidationContext& InOutContext) { using namespace Metasound::Frontend; using namespace Metasound::Engine; EDataValidationResult Result = EDataValidationResult::Valid; IMetaSoundAssetManager& AssetManager = IMetaSoundAssetManager::GetChecked(); // Validation has added assets to the asset manager // and we don't remove them immediately after validation to optimize possible subsequent validation // Set this flag to prevent log spam of active assets on shutdown AssetManager.SetLogActiveAssetsOnShutdown(false); // Add error for multiple assets with the same class name const FMetaSoundAssetKey Key(Document.RootGraph.Metadata); const TArray AssetPaths = AssetManager.FindAssetPaths(Key); if (AssetPaths.Num() > 1) { Result = EDataValidationResult::Invalid; TArray PathStrings; Algo::Transform(AssetPaths, PathStrings, [](const FTopLevelAssetPath& Path) { return FText::FromString(Path.ToString()); }); InOutContext.AddError(FText::Format(LOCTEXT("UniqueClassNameValidation", "Multiple assets use the same class name which may result in unintended behavior. This may happen when an asset is moved, then the move is reverted in revision control without removing the newly created asset. Please remove the offending asset or duplicate it to automatically generate a new class name." \ "\nConflicting Asset Paths:\n{0}"), FText::Join(FText::FromString(TEXT("\n")), PathStrings))); } // Success return Result; } static EDataValidationResult IsDataValid(const UObject& MetaSound, const FMetasoundFrontendDocument& Document, FDataValidationContext& InOutContext) { using namespace Metasound; EDataValidationResult Result = EDataValidationResult::Valid; if (Engine::GetEditorAssetValidationEnabled()) { // We cannot rely on the asset registry scan being complete during the call // to IsDataValid(...) while running a cook commandlet. The IMetasoundAssetManager // will still log errors on duplicate assets which will fail cook. if (!IsRunningCookCommandlet()) { Result = IsClassNameUnique(Document, InOutContext); } } const UMetaSoundSettings* Settings = GetDefault(); check(Settings); TSet ValidPageIDs; auto ErrorIfMissing = [&](const FGuid& PageID, const FText& DataDescriptor) { if (!ValidPageIDs.Contains(PageID)) { if (const FMetaSoundPageSettings* PageSettings = Settings->FindPageSettings(PageID)) { ValidPageIDs.Add(PageSettings->UniqueId); } else { Result = EDataValidationResult::Invalid; InOutContext.AddMessage(FAssetData(&MetaSound), EMessageSeverity::Error, FText::Format( LOCTEXT("InvalidPageDataFormat", "MetaSound contains invalid {0} with page ID '{1}': page not found in Project 'MetaSound' Settings. Remove page data or migrate to existing page identifier."), DataDescriptor, FText::FromString(PageID.ToString()))); } } }; const TArray& Graphs = Document.RootGraph.GetConstGraphPages(); for (const FMetasoundFrontendGraph& Graph : Graphs) { ErrorIfMissing(Graph.PageID, LOCTEXT("GraphPageDescriptor", "graph")); } for (const FMetasoundFrontendClassInput& ClassInput : Document.RootGraph.GetDefaultInterface().Inputs) { ClassInput.IterateDefaults([&](const FGuid& PageID, const FMetasoundFrontendLiteral&) { ErrorIfMissing(PageID, FText::Format(LOCTEXT("InputPageDefaultDescriptorFormat", "input '{0}' default value"), FText::FromName(ClassInput.Name))); }); } return Result; } #endif // WITH_EDITOR static void GetAssetRegistryTags(TScriptInterface DocInterface, FAssetRegistryTagsContext& Context) { using namespace Frontend; const UObject* MetaSound = DocInterface.GetObject(); check(MetaSound); if (MetaSound->GetFlags() & (RF_Transient | RF_ClassDefaultObject)) { return; } const FMetaSoundAssetClassInfo ClassInfo(*DocInterface); ClassInfo.ExportToContext(Context); } template static FTopLevelAssetPath GetAssetPathChecked(TMetaSoundObject& InMetaSound) { FTopLevelAssetPath Path; ensureAlwaysMsgf(Path.TrySetPath(&InMetaSound), TEXT("Failed to set TopLevelAssetPath from MetaSound '%s'. MetaSound must be highest level object in package."), *InMetaSound.GetPathName()); ensureAlwaysMsgf(Path.IsValid(), TEXT("Failed to set TopLevelAssetPath from MetaSound '%s'. This may be caused by calling this function when the asset is being destroyed."), *InMetaSound.GetPathName()); return Path; } template static TArray GetReferencedAssets(TMetaSoundObject& InMetaSound) { TArray ReferencedAssets; IMetasoundUObjectRegistry& UObjectRegistry = IMetasoundUObjectRegistry::Get(); for (TObjectPtr& Object : InMetaSound.ReferencedAssetClassObjects) { if (FMetasoundAssetBase* Asset = UObjectRegistry.GetObjectAsAssetBase(Object)) { ReferencedAssets.Add(Asset); } else { UE_LOG(LogMetaSound, Error, TEXT("Referenced asset \"%s\", referenced from \"%s\", is not convertible to FMetasoundAssetBase"), *Object->GetPathName(), *InMetaSound.GetPathName()); } } return ReferencedAssets; } static void PreSaveAsset(FMetasoundAssetBase& InMetaSound, FObjectPreSaveContext InSaveContext) { #if WITH_EDITORONLY_DATA using namespace Frontend; if (IMetaSoundAssetManager* AssetManager = IMetaSoundAssetManager::Get()) { AssetManager->WaitUntilAsyncLoadReferencedAssetsComplete(InMetaSound); } const bool bIsCooking = InSaveContext.IsCooking(); const bool bCanEverExecute = Metasound::CanEverExecuteGraph(bIsCooking); if (!bCanEverExecute) { FName PlatformName; if (const ITargetPlatform* TargetPlatform = InSaveContext.GetTargetPlatform()) { PlatformName = *TargetPlatform->IniPlatformName(); } const bool bIsDeterministic = SerializationRequiresDeterminism(bIsCooking); FDocumentIDGenerator::FScopeDeterminism DeterminismScope(bIsDeterministic); InMetaSound.UpdateAndRegisterForSerialization(PlatformName); } else if (FApp::CanEverRenderAudio()) { if (UMetasoundEditorGraphBase* MetaSoundGraph = Cast(InMetaSound.GetGraph())) { // Uses graph flavor of register with frontend to update editor systems/asset editors in case editor is enabled. MetaSoundGraph->RegisterGraphWithFrontend(); InMetaSound.GetModifyContext().SetForceRefreshViews(); } } else { UE_LOG(LogMetaSound, Warning, TEXT("PreSaveAsset for MetaSound: (%s) is doing nothing because InSaveContext.IsCooking, IsRunningCommandlet, and FApp::CanEverRenderAudio were all false") , *InMetaSound.GetOwningAssetName()); } #endif // WITH_EDITORONLY_DATA } template static void SerializeToArchive(TMetaSoundObject& InMetaSound, FArchive& InArchive) { #if WITH_EDITORONLY_DATA using namespace Frontend; bool bVersionedAsset = false; if (InArchive.IsLoading()) { const bool bIsTransacting = InArchive.IsTransacting(); TStrongObjectPtr Builder; { FGCScopeGuard ScopeGuard; Builder.Reset(&FDocumentBuilderRegistry::GetChecked().FindOrBeginBuilding(InMetaSound, bIsTransacting)); } { const bool bIsCooking = InArchive.IsCooking(); const bool bIsDeterministic = SerializationRequiresDeterminism(bIsCooking); FDocumentIDGenerator::FScopeDeterminism DeterminismScope(bIsDeterministic); check(Builder.IsValid()); bVersionedAsset = InMetaSound.VersionAsset(Builder->GetBuilder()); } Builder->ClearInternalFlags(EInternalObjectFlags::Async); } if (bVersionedAsset) { InMetaSound.SetVersionedOnLoad(); } #endif // WITH_EDITORONLY_DATA } template static void PostLoad(TMetaSoundObject& InMetaSound) { using namespace Frontend; // Do not call asset manager on CDO objects which may be loaded before asset // manager is set. const bool bIsCDO = InMetaSound.HasAnyFlags(RF_ClassDefaultObject); if (!bIsCDO) { if (InMetaSound.GetAsyncReferencedAssetClassPaths().Num() > 0) { IMetaSoundAssetManager::GetChecked().RequestAsyncLoadReferencedAssets(InMetaSound); } } } template static void OnAsyncReferencedAssetsLoaded(TMetaSoundObject& InMetaSound, const TArray& InAsyncReferences) { for (FMetasoundAssetBase* AssetBase : InAsyncReferences) { if (AssetBase) { if (UObject* OwningAsset = AssetBase->GetOwningAsset()) { InMetaSound.ReferencedAssetClassObjects.Add(OwningAsset); InMetaSound.ReferenceAssetClassCache.Remove(FSoftObjectPath(OwningAsset)); } } } } }; } // namespace Metasound::Engine #undef LOCTEXT_NAMESPACE // MetasoundEngine