// Copyright Epic Games, Inc. All Rights Reserved. #include "MetasoundSettings.h" #include "Algo/AnyOf.h" #include "Algo/Count.h" #include "HAL/IConsoleManager.h" #include "MetasoundFrontendDocument.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(MetasoundSettings) #define LOCTEXT_NAMESPACE "MetaSound" namespace Metasound::SettingsPrivate { #if WITH_EDITOR template TSet GetStructNames(const TArray& InSettings, int32 IgnoreIndex = INDEX_NONE) { TSet Names; for (int32 Index = 0; Index < InSettings.Num(); ++Index) { if (Index != IgnoreIndex) { Names.Add(InSettings[Index].Name); } } return Names; } /** Generate new name for the item. **/ static FName GenerateUniqueName(const TSet& Names, const TCHAR* InBaseName) { FString NewName = InBaseName; for (int32 Postfix = 1; Names.Contains(*NewName); ++Postfix) { NewName = FString::Format(TEXT("{0}_{1}"), { InBaseName, Postfix }); } return FName(*NewName); } template void OnCreateNewSettingsStruct(const TArray& InSettings, const FString& InBaseName, SettingsStructType& OutNewItem) { const TSet Names = GetStructNames(InSettings); OutNewItem.Name = GenerateUniqueName(Names, *InBaseName); OutNewItem.UniqueId = FGuid::NewGuid(); } template void OnRenameSettingsStruct(const TArray& InSettings, int32 Index, const FString& InBaseName, SettingsStructType& OutRenamed) { if (OutRenamed.Name.IsNone()) { const TSet Names = GetStructNames(InSettings); OutRenamed.Name = GenerateUniqueName(Names, *InBaseName); } else { const TSet Names = GetStructNames(InSettings, Index); if (Names.Contains(OutRenamed.Name)) { OutRenamed.Name = GenerateUniqueName(Names, *OutRenamed.Name.ToString()); } } } #endif // WITH_EDITOR template const SettingsStructType* FindSettingsStruct(const TArray& Settings, const FGuid& InUniqueID) { auto MatchesIDPredicate = [&InUniqueID](const SettingsStructType& Struct) { return Struct.UniqueId == InUniqueID; }; return Settings.FindByPredicate(MatchesIDPredicate); } template const SettingsStructType* FindSettingsStruct(const TArray& Settings, FName Name) { auto MatchesNamePredicate = [Name](const SettingsStructType& Struct) { return Struct.Name == Name; }; return Settings.FindByPredicate(MatchesNamePredicate); } #if WITH_EDITOR template void PostEditChainChangedStructMember(FPropertyChangedChainEvent& PostEditChangeChainProperty, TArray& StructSettings, FName PropertyName, const FString& NewItemName) { const int32 ItemIndex = PostEditChangeChainProperty.GetArrayIndex(PropertyName.ToString()); if (TDoubleLinkedList::TDoubleLinkedListNode* HeadNode = PostEditChangeChainProperty.PropertyChain.GetHead()) { const FProperty* Prop = HeadNode->GetValue(); if (Prop->GetName() != PropertyName) { return; } } // Item changed.. if (ItemIndex != INDEX_NONE && StructSettings.IsValidIndex(ItemIndex)) { SettingsStructType& Item = StructSettings[ItemIndex]; if (PostEditChangeChainProperty.GetPropertyName() == "Name") { OnRenameSettingsStruct(StructSettings, ItemIndex, NewItemName, Item); } else if (PostEditChangeChainProperty.GetPropertyName() == PropertyName) { // Array change add or duplicate if (PostEditChangeChainProperty.ChangeType == EPropertyChangeType::ArrayAdd || PostEditChangeChainProperty.ChangeType == EPropertyChangeType::Duplicate) { OnCreateNewSettingsStruct(StructSettings, NewItemName, Item); } } } // Handle pasting separately as we might not have a valid index in the case of pasting when array is empty. if (PostEditChangeChainProperty.GetPropertyName() == PropertyName) { // Paste... if (PostEditChangeChainProperty.ChangeType == EPropertyChangeType::ValueSet) { const int32 IndexOfPastedItem = ItemIndex != INDEX_NONE ? ItemIndex : 0; if (StructSettings.IsValidIndex(IndexOfPastedItem)) { SettingsStructType& Item = StructSettings[IndexOfPastedItem]; OnCreateNewSettingsStruct(StructSettings, NewItemName, Item); } } } } #endif // WITH_EDITOR } // namespace Metasound::SettingsPrivate #if WITH_EDITOR bool FMetaSoundPageSettings::GetExcludeFromCook(FName PlatformName) const { if (PlatformCanTargetPage(PlatformName)) { return false; } return ExcludeFromCook.GetValueForPlatform(PlatformName); } TArray FMetaSoundPageSettings::GetTargetPlatforms() const { TArray PlatformNames; Algo::TransformIf(CanTarget.PerPlatform, PlatformNames, [](const TPair& Pair) { return Pair.Value; }, [](const TPair& Pair) { return Pair.Key; }); return PlatformNames; } bool FMetaSoundPageSettings::PlatformCanTargetPage(FName PlatformName) const { const bool bIsTargeted = CanTarget.GetValueForPlatform(PlatformName); return bIsTargeted; } void UMetaSoundSettings::ConformPageSettings(bool bNotifyDefaultRenamed) { using namespace Metasound; DefaultPageSettings.UniqueId = Metasound::Frontend::DefaultPageID; DefaultPageSettings.Name = Metasound::Frontend::DefaultPageName; DefaultPageSettings.bIsDefaultPage = true; DefaultPageSettings.ExcludeFromCook = false; bool bInvalidDefaultRenamed = false; TMap PlatformHasTarget; auto GatherPlatformTargets = [&PlatformHasTarget](const FMetaSoundPageSettings& Page) { PlatformHasTarget.FindOrAdd({ }) |= Page.CanTarget.Default; for (const TPair& Pair : Page.CanTarget.PerPlatform) { PlatformHasTarget.FindOrAdd(Pair.Key) |= Pair.Value; } }; GatherPlatformTargets(DefaultPageSettings); for (FMetaSoundPageSettings& Page : PageSettings) { const bool bIsDefaultName = Page.Name == Frontend::DefaultPageName; if (bIsDefaultName) { const TSet PageNames(GetPageNames()); Page.Name = SettingsPrivate::GenerateUniqueName(PageNames, *Page.Name.ToString()); bInvalidDefaultRenamed = true; } GatherPlatformTargets(Page); Page.bIsDefaultPage = false; } // Forces each platform to target at least one page setting. for (const TPair& Pair : PlatformHasTarget) { if (!Pair.Value) { if (Pair.Key.IsNone()) { DefaultPageSettings.CanTarget.Default = true; } else { DefaultPageSettings.CanTarget.PerPlatform.FindOrAdd(Pair.Key) = true; } } } #if WITH_EDITORONLY_DATA { FScopeLock Lock(&CookPlatformTargetCritSec); CookPlatformTargetPageIDs.Reset(); CookPlatformTargetPage = { }; } #endif // WITH_EDITORONLY_DATA TargetPageNameOverride.Reset(); for (int32 Index = PageSettings.Num() - 1; Index >= 0; --Index) { FMetaSoundPageSettings& PageSetting = PageSettings[Index]; if (PageSetting.UniqueId == Metasound::Frontend::DefaultPageID || PageSetting.Name == Metasound::Frontend::DefaultPageName) { PageSettings.RemoveAt(Index); } } if (bNotifyDefaultRenamed && bInvalidDefaultRenamed) { OnDefaultRenamed.Broadcast(); } } #endif // WITH_EDITOR const FMetaSoundPageSettings* UMetaSoundSettings::FindPageSettings(FName Name) const { if (Name == Metasound::Frontend::DefaultPageName) { return &GetDefaultPageSettings(); } return Metasound::SettingsPrivate::FindSettingsStruct(PageSettings, Name); } const FMetaSoundPageSettings* UMetaSoundSettings::FindPageSettings(const FGuid& InPageID) const { if (InPageID == Metasound::Frontend::DefaultPageID) { return &GetDefaultPageSettings(); } return Metasound::SettingsPrivate::FindSettingsStruct(PageSettings, InPageID); } const FMetaSoundQualitySettings* UMetaSoundSettings::FindQualitySettings(FName Name) const { return Metasound::SettingsPrivate::FindSettingsStruct(QualitySettings, Name); } const FMetaSoundQualitySettings* UMetaSoundSettings::FindQualitySettings(const FGuid& InQualityID) const { return Metasound::SettingsPrivate::FindSettingsStruct(QualitySettings, InQualityID); } const FMetaSoundPageSettings& UMetaSoundSettings::GetDefaultPageSettings() const { return DefaultPageSettings; } #if WITH_EDITOR TArray UMetaSoundSettings::GetAllPlatformNamesImplementingTargets() const { TSet PlatformNames; IteratePageSettings([&](const FMetaSoundPageSettings& InPageSetting) { TArray PagePlatforms = InPageSetting.GetTargetPlatforms(); PlatformNames.Append(MoveTemp(PagePlatforms)); }); return PlatformNames.Array(); } #endif // WITH_EDITOR #if WITH_EDITORONLY_DATA TArray UMetaSoundSettings::GetCookedTargetPageIDs(FName PlatformName) const { FScopeLock Lock(&CookPlatformTargetCritSec); CacheCookedPageIDs(PlatformName); return CookPlatformTargetPageIDs; } TArray UMetaSoundSettings::GetCookedPageOrder(FName PlatformName) const { FScopeLock Lock(&CookPlatformTargetCritSec); CacheCookedPageIDs(PlatformName); return CookPlatformPageOrder; } void UMetaSoundSettings::CacheCookedPageIDs(FName PlatformName) const { /* Cache the CookPlatformTargetPageIDs and CookPlatformPageOrder to * avoid rebuilding these for every asset being cooked. */ if ((PlatformName != CookPlatformTargetPage) || (PlatformName.IsNone() && CookPlatformTargetPageIDs.IsEmpty() && CookPlatformPageOrder.IsEmpty())) { CookPlatformTargetPage = PlatformName; CookPlatformTargetPageIDs.Reset(); CookPlatformPageOrder.Reset(); /* Cache CookPlatformTargetPageIDs by finding all target pages for the * given platform. */ auto CanTargetPage = [&PlatformName](const FMetaSoundPageSettings& PageSetting) { #if WITH_EDITOR return PageSetting.PlatformCanTargetPage(PlatformName); #else // !WITH_EDITOR return true; #endif // !WITH_EDITOR }; auto GetID = [](const FMetaSoundPageSettings& PageSetting) { return PageSetting.UniqueId; }; if (CanTargetPage(DefaultPageSettings)) { CookPlatformTargetPageIDs.Add(DefaultPageSettings.UniqueId); } Algo::TransformIf(PageSettings, CookPlatformTargetPageIDs, CanTargetPage, GetID); if (CookPlatformTargetPageIDs.IsEmpty()) { #if WITH_EDITOR // Allow default page to be a target if no platform name is provided. // This can occur when generating reflection code for a MetaSound // in a cloud cooker. const bool bCanTargetDefault = PlatformName.IsNone() || DefaultPageSettings.CanTarget.GetValueForPlatform(PlatformName); #else // !WITH_EDITOR const bool bCanTargetDefault = DefaultPageSettings.CanTarget.GetValue(); #endif // !WITH_EDITOR if (!PageSettings.IsEmpty() && !bCanTargetDefault) { UE_LOG(LogMetaSound, Warning, TEXT("No pages set to be targeted for platform '%s', forcing 'Default' page as target"), *PlatformName.ToString()); } CookPlatformTargetPageIDs.Add(Metasound::Frontend::DefaultPageID); } /* Cache cook target page order by filtering out all pages that are * excluded from the platform and filtering out all pages that come before * any of the cook target page IDs. */ constexpr bool bReverse = true; bool bFoundAnyTargetPage = false; IteratePageSettings([&](const FMetaSoundPageSettings& InPageSettings) { #if WITH_EDITOR // Skip this page if it explicitly excluded from cooking. if (InPageSettings.GetExcludeFromCook(PlatformName)) { return; } #endif // !WITH_EDITOR // Skip any pages that occur before the any of the target pages. if (!bFoundAnyTargetPage) { // Sets flag to true if any of the CookPlatformTargetPageIDs equal PageSettings.PageID bFoundAnyTargetPage = Algo::AnyOf(CookPlatformTargetPageIDs, [&InPageSettings](const FGuid& InTargetPageID) { return InTargetPageID == InPageSettings.UniqueId; }); } if (bFoundAnyTargetPage) { CookPlatformPageOrder.Add(InPageSettings.UniqueId); } }, bReverse); if (CookPlatformPageOrder.IsEmpty()) { #if WITH_EDITOR // Allow default page to be in page order if no platform name is provided. // This can occur when generating reflection code for a MetaSound // in a cloud cooker. const bool bCanCookDefault = PlatformName.IsNone() || !DefaultPageSettings.ExcludeFromCook.GetValueForPlatform(PlatformName); #else // !WITH_EDITOR const bool bCanCookDefault = !DefaultPageSettings.ExcludeFromCook.GetValue(); #endif // !WITH_EDITOR if (!PageSettings.IsEmpty() && !bCanCookDefault) { UE_LOG(LogMetaSound, Warning, TEXT("No pages set to be cook for platform '%s', forcing 'Default' page as only cooked page"), *PlatformName.ToString()); } CookPlatformPageOrder.Add(Metasound::Frontend::DefaultPageID); } } } void UMetaSoundSettings::IterateCookedTargetPageIDs(FName PlatformName, TFunctionRef Iter) const { FScopeLock Lock(&CookPlatformTargetCritSec); CacheCookedPageIDs(PlatformName); for (const FGuid& CookPlatform : CookPlatformTargetPageIDs) { Iter(CookPlatform); } } #endif // WITH_EDITORONLY_DATA TArray UMetaSoundSettings::GeneratePageOrder() const { #if WITH_EDITOR if (PageOverrideData.IsSet()) { return GeneratePageOrderFromOverride(); } #endif // WITH_EDITOR FName PlatformName = FPlatformProperties::IniPlatformName(); return GeneratePageOrderInternal(PlatformName, GetTargetPageSettings().UniqueId); }; TArrayView UMetaSoundSettings::GetPageOrder() { // Thread access for the CachedPageOrder is a bit hairy due to the existence // of the AudioThread. // // For non-editor builds, the page order is cached during PostInitProperties // and then never altered. For these builds it is safe to access as long as // it has been initialized. // // For editor builds, access to the cached paged order is allowed from the // game thread or the audio thread. The audio thread runs in a handful of // editor builds, primarily when running StandaloneGame. In these builds, // there is a codepath where the CachedPageOrder could be altered on the // game thread and read on the audio thread, but this is generally disallowed // as StandaloneGame does not expose changes to the UMetaSoundSettings. #if WITH_EDITOR check(IsInGameThread() || IsInAudioThread()); #endif if (const UMetaSoundSettings* Settings = GetDefault()) { checkf(Settings->bIsPageOrderCached, TEXT("Attempt to get PageOrder before UMetaSoundSettings have finished initializing")); return Settings->CachedPageOrder; } return {}; } const FMetaSoundPageSettings& UMetaSoundSettings::GetTargetPageSettings() const { auto WarnIfUninitialized = [this](const FMetaSoundPageSettings& SettingsSet) { #if !NO_LOGGING if (bWarnAccessBeforeInit) { UE_LOG(LogMetaSound, Display, TEXT("Target Page Settings accessed prior to 'PostInitProperties' being called. Uninitialized PageSettings '%s' being returned."), *SettingsSet.Name.ToString()); bWarnAccessBeforeInit = false; } #endif // !NO_LOGGING }; const FName TargetPage = TargetPageNameOverride.IsSet() ? *TargetPageNameOverride : TargetPageName; #if WITH_EDITOR if (const FMetaSoundPageSettings* TargetSettings = GetTargetPageSettingsFromOverride(TargetPage)) { WarnIfUninitialized(*TargetSettings); return *TargetSettings; } #endif // WITH_EDITOR if (const FMetaSoundPageSettings* TargetSettings = FindPageSettings(TargetPage)) { WarnIfUninitialized(*TargetSettings); return *TargetSettings; } // Shouldn't hit this, but if for some reason the target page is in a bad state, // try and return any page setting set as a valid target. if (PageSettings.Num() > 0) { WarnIfUninitialized(PageSettings[0]); return PageSettings[0]; } WarnIfUninitialized(DefaultPageSettings); return DefaultPageSettings; } #if WITH_EDITOR void UMetaSoundSettings::PostEditChangeChainProperty(FPropertyChangedChainEvent& PostEditChangeChainProperty) { using namespace Metasound::SettingsPrivate; PostEditChainChangedStructMember(PostEditChangeChainProperty, PageSettings, GetPageSettingPropertyName(), TEXT("New Page")); PostEditChainChangedStructMember(PostEditChangeChainProperty, QualitySettings, GetQualitySettingPropertyName(), TEXT("New Quality")); constexpr bool bNotifyDefaultRenamed = true; ConformPageSettings(bNotifyDefaultRenamed); Super::PostEditChangeChainProperty(PostEditChangeChainProperty); } void UMetaSoundSettings::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) { Super::PostEditChangeProperty(PropertyChangedEvent); constexpr bool bNotifyDefaultRenamed = true; ConformPageSettings(bNotifyDefaultRenamed); if (PropertyChangedEvent.MemberProperty->GetName() == GetPageSettingPropertyName()) { OnPageSettingsUpdated.Broadcast(); } UpdateCachedPageOrder(); } const FMetaSoundPageSettings* UMetaSoundSettings::GetTargetPageSettingsFromOverride(const FName InTargetPage) const { const FMetaSoundPageSettings* TargetSettings = nullptr; if (PageOverrideData.IsSet()) { // The override can optionally reference a specific PageID. In that case, // the specific page settings are returned. if (PageOverrideData->PageID.IsSet()) { TargetSettings = FindPageSettings(*PageOverrideData->PageID); if (TargetSettings) { return TargetSettings; } } // If the PageID is not explicitly overridden, then search the page settings // to find a page that is supported by the platform. constexpr bool bReverse = true; bool bFoundTargetPage = false; bool bFoundPageSupportedByPlatform = false; IteratePageSettings([&](const FMetaSoundPageSettings& InPageSettings) { bFoundTargetPage |= InPageSettings.Name == InTargetPage; if (bFoundTargetPage && !bFoundPageSupportedByPlatform) { const bool bIsCooked = !InPageSettings.GetExcludeFromCook(PageOverrideData->PlatformName); if (bIsCooked) { bFoundPageSupportedByPlatform = true; TargetSettings = &InPageSettings; } } }, bReverse); } return TargetSettings; } TArray UMetaSoundSettings::GeneratePageOrderFromOverride() const { check(PageOverrideData.IsSet()); FGuid TargetPageID; if (PageOverrideData->PageID.IsSet()) { TargetPageID = *PageOverrideData->PageID; } else { TargetPageID = GetTargetPageSettings().UniqueId; } return GeneratePageOrderInternal(PageOverrideData->PlatformName, TargetPageID); } #endif // WITH_EDITOR void UMetaSoundSettings::PostInitProperties() { Super::PostInitProperties(); #if WITH_EDITOR constexpr bool bNotifyDefaultRenamed = false; ConformPageSettings(bNotifyDefaultRenamed); #endif // WITH_EDITOR #if !NO_LOGGING bWarnAccessBeforeInit = false; #endif // !NO_LOGGING if (const FMetaSoundPageSettings* Page = FindPageSettings(TargetPageName)) { UE_LOG(LogMetaSound, Display, TEXT("MetaSound Page Target Initialized to '%s'"), *GetTargetPageSettings().Name.ToString()); } else { UE_LOG(LogMetaSound, Warning, TEXT("TargetPageName '%s' at time of 'UMetaSoundSettings::PostInitProperties' did not correspond to a valid page."), *TargetPageName.ToString()); if (PageSettings.IsEmpty()) { UE_LOG(LogMetaSound, Warning, TEXT("Setting target to '%s' page settings."), *DefaultPageSettings.Name.ToString()); TargetPageName = DefaultPageSettings.Name; } else { UE_LOG(LogMetaSound, Warning, TEXT("Setting target to highest project page settings '%s'."), *PageSettings.Last().Name.ToString()); TargetPageName = PageSettings.Last().Name; } } // Initialize the cached page order CachedPageOrder = GeneratePageOrder(); bIsPageOrderCached = true; } bool UMetaSoundSettings::SetTargetPage(FName PageName) { if (const FMetaSoundPageSettings* PageSetting = FindPageSettings(PageName)) { const FName TargetPage = TargetPageNameOverride.IsSet() ? *TargetPageNameOverride : TargetPageName; if (TargetPage != PageSetting->Name) { UE_LOG(LogMetaSound, Display, TEXT("Target page override set to '%s'."), *TargetPage.ToString()); TargetPageNameOverride = PageSetting->Name; return true; } } return false; } #if WITH_EDITORONLY_DATA Metasound::Engine::FOnSettingsDefaultConformed& UMetaSoundSettings::GetOnDefaultRenamedDelegate() { return OnDefaultRenamed; } Metasound::Engine::FOnPageSettingsUpdated& UMetaSoundSettings::GetOnPageSettingsUpdatedDelegate() { return OnPageSettingsUpdated; } FName UMetaSoundSettings::GetPageSettingPropertyName() { return GET_MEMBER_NAME_CHECKED(UMetaSoundSettings, PageSettings); } FName UMetaSoundSettings::GetQualitySettingPropertyName() { return GET_MEMBER_NAME_CHECKED(UMetaSoundSettings, QualitySettings); } #endif // WITH_EDITORONLY_DATA #if WITH_EDITOR void UMetaSoundSettings::OverrideTargetPageSettings(TOptional InOverride) { PageOverrideData = MoveTemp(InOverride); UpdateCachedPageOrder(); } TArray UMetaSoundSettings::GetPageNames() { if (const UMetaSoundSettings* Settings = GetDefault()) { TArray Names; Settings->IteratePageSettings([&Names](const FMetaSoundPageSettings& InPageSetting) { Names.Add(InPageSetting.Name); }); return Names; } return { }; } TArray UMetaSoundSettings::GetQualityNames() { if (const UMetaSoundSettings* Settings = GetDefault()) { TArray Names; auto GetName = [](const FMetaSoundQualitySettings& Quality) { return Quality.Name; }; Algo::Transform(Settings->GetQualitySettings(), Names, GetName); return Names; } return { }; } void UMetaSoundSettings::UpdateCachedPageOrder() { // In editor builds, we should only be updating the CachedPageOrder on the // GameThread. check(IsInGameThread()); CachedPageOrder = GeneratePageOrder(); } #endif // WITH_EDITOR FName UMetaSoundSettings::GetCategoryName() const { return FName("Plugins"); } TArray UMetaSoundSettings::GeneratePageOrderInternal(const FName& InPlatformName, const FGuid& InTargetPageID) const { // Reverse iterate over page settings to build the priority stack. Every page // that exists before the TargetPageID is discarded. Ever page after the TargetPageID // is a fallback in case the TargetPageID does not exist for a given element. TArray PageOrder; constexpr bool bReverse = true; bool bFoundTargetPage = false; IteratePageSettings([&](const FMetaSoundPageSettings& InPageSettings) { bFoundTargetPage |= InPageSettings.UniqueId == InTargetPageID; if (bFoundTargetPage) { #if WITH_EDITOR const bool bIsCooked = !InPageSettings.GetExcludeFromCook(InPlatformName); if (bIsCooked) { PageOrder.Add(InPageSettings.UniqueId); } #else PageOrder.Add(InPageSettings.UniqueId); #endif // WITH_EDITOR } }, bReverse); if (PageOrder.Num() < 1) { PageOrder.Add(Metasound::Frontend::DefaultPageID); } // rvo return PageOrder; } void UMetaSoundSettings::IteratePageSettings(TFunctionRef Iter, bool bReverse) const { if (bReverse) { for (int32 Index = PageSettings.Num() - 1; Index >= 0; --Index) { Iter(PageSettings[Index]); } Iter(GetDefaultPageSettings()); } else { Iter(GetDefaultPageSettings()); for (const FMetaSoundPageSettings& Setting : PageSettings) { Iter(Setting); } } } #undef LOCTEXT_NAMESPACE // MetaSound