#include "FMODAssetBuilder.h" #include "AssetRegistry/AssetRegistryModule.h" #include "FMODAssetLookup.h" #include "FMODAssetTable.h" #include "FMODBank.h" #include "FMODBankLookup.h" #include "FMODBus.h" #include "FMODEvent.h" #include "FMODSettings.h" #include "FMODSnapshot.h" #include "FMODSnapshotReverb.h" #include "FMODPort.h" #include "FMODUtils.h" #include "FMODVCA.h" #include "FileHelpers.h" #include "ObjectTools.h" #include "SourceControlHelpers.h" #include "HAL/FileManager.h" #include "Misc/MessageDialog.h" #include "fmod_studio.hpp" #define LOCTEXT_NAMESPACE "FMODAssetBuilder" FFMODAssetBuilder::~FFMODAssetBuilder() { if (StudioSystem) { StudioSystem->release(); } } void FFMODAssetBuilder::Create() { verifyfmod(FMOD::Studio::System::create(&StudioSystem)); FMOD::System *lowLevelSystem = nullptr; verifyfmod(StudioSystem->getCoreSystem(&lowLevelSystem)); verifyfmod(lowLevelSystem->setOutput(FMOD_OUTPUTTYPE_NOSOUND_NRT)); verifyfmod(StudioSystem->initialize(1, FMOD_STUDIO_INIT_ALLOW_MISSING_PLUGINS | FMOD_STUDIO_INIT_SYNCHRONOUS_UPDATE, FMOD_INIT_MIX_FROM_UPDATE, nullptr)); } void FFMODAssetBuilder::ProcessBanks() { TArray AssetsToSave; TArray AssetsToDelete; const UFMODSettings& Settings = *GetDefault(); FString PackagePath = Settings.GetFullContentPath() / FFMODAssetTable::PrivateDataPath(); BuildBankLookup(FFMODAssetTable::BankLookupName(), PackagePath, Settings, AssetsToSave); BuildAssets(Settings, FFMODAssetTable::AssetLookupName(), PackagePath, AssetsToSave, AssetsToDelete); SaveAssets(AssetsToSave); DeleteAssets(AssetsToDelete); } FString FFMODAssetBuilder::GetMasterStringsBankPath() { return BankLookup ? BankLookup->MasterStringsBankPath : FString(); } void FFMODAssetBuilder::BuildAssets(const UFMODSettings& InSettings, const FString &AssetLookupName, const FString &AssetLookupPath, TArray& AssetsToSave, TArray& AssetsToDelete) { if (!BankLookup->MasterStringsBankPath.IsEmpty()) { FString StringPath = InSettings.GetFullBankPath() / BankLookup->MasterStringsBankPath; UE_LOG(LogFMOD, Log, TEXT("Loading strings bank: %s"), *StringPath); FMOD::Studio::Bank *StudioStringBank; FMOD_RESULT StringResult = StudioSystem->loadBankFile(TCHAR_TO_UTF8(*StringPath), FMOD_STUDIO_LOAD_BANK_NORMAL, &StudioStringBank); if (StringResult == FMOD_OK) { TArray RawBuffer; RawBuffer.SetNum(256); // Initial capacity int Count = 0; verifyfmod(StudioStringBank->getStringCount(&Count)); // Enumerate all of the names in the strings bank and gather the information required to create the UE4 assets for each object TArray AssetCreateInfos; AssetCreateInfos.Reserve(Count); for (int StringIdx = 0; StringIdx < Count; ++StringIdx) { FMOD::Studio::ID Guid = { 0 }; while (true) { int ActualSize = 0; FMOD_RESULT Result = StudioStringBank->getStringInfo(StringIdx, &Guid, RawBuffer.GetData(), RawBuffer.Num(), &ActualSize); if (Result == FMOD_ERR_TRUNCATED) { RawBuffer.SetNum(ActualSize); } else { verifyfmod(Result); break; } } FString AssetName(UTF8_TO_TCHAR(RawBuffer.GetData())); FGuid AssetGuid = FMODUtils::ConvertGuid(Guid); if (!AssetName.IsEmpty()) { AssetCreateInfo CreateInfo = {}; if (MakeAssetCreateInfo(AssetGuid, AssetName, &CreateInfo)) { AssetCreateInfos.Add(CreateInfo); } } } verifyfmod(StudioStringBank->unload()); verifyfmod(StudioSystem->update()); // Load or create asset lookup FString AssetLookupPackageName = AssetLookupPath + AssetLookupName; UPackage *AssetLookupPackage = CreatePackage(*AssetLookupPackageName); AssetLookupPackage->FullyLoad(); bool bAssetLookupCreated = false; bool bAssetLookupModified = false; UDataTable *AssetLookup = FindObject(AssetLookupPackage, *AssetLookupName, true); if (!AssetLookup) { AssetLookup = NewObject(AssetLookupPackage, *AssetLookupName, RF_Public | RF_Standalone | RF_MarkAsRootSet); AssetLookup->RowStruct = FFMODAssetLookupRow::StaticStruct(); bAssetLookupCreated = true; } // Create a list of existing assets in the lookup - we'll use this to delete stale assets TMap StaleAssets{}; AssetLookup->ForeachRow(FString(), [&StaleAssets](const FName& Key, const FFMODAssetLookupRow& Value) { StaleAssets.Add(Key, Value); }); for (const AssetCreateInfo &CreateInfo : AssetCreateInfos) { UFMODAsset *Asset = CreateAsset(CreateInfo, AssetsToSave); if (Asset) { UPackage *AssetPackage = Asset->GetPackage(); FString AssetPackageName = AssetPackage->GetPathName(); FString AssetName = Asset->GetPathName(AssetPackage); FName LookupRowName = FName(*CreateInfo.StudioPath); FFMODAssetLookupRow* LookupRow = AssetLookup->FindRow(LookupRowName, FString(), false); if (LookupRow) { if (LookupRow->PackageName != AssetPackageName || LookupRow->AssetName != AssetName) { LookupRow->PackageName = AssetPackageName; LookupRow->AssetName = AssetName; bAssetLookupModified = true; } } else { FFMODAssetLookupRow NewRow{}; NewRow.PackageName = AssetPackageName; NewRow.AssetName = AssetName; AssetLookup->AddRow(LookupRowName, NewRow); bAssetLookupModified = true; } StaleAssets.Remove(LookupRowName); } } // Delete stale assets if (StaleAssets.Num() > 0) { for (auto& Entry : StaleAssets) { UPackage *Package = CreatePackage(*Entry.Value.PackageName); Package->FullyLoad(); UFMODAsset *Asset = Package ? FindObject(Package, *Entry.Value.AssetName) : nullptr; if (Asset) { UE_LOG(LogFMOD, Log, TEXT("Deleting stale asset %s/%s."), *Entry.Value.PackageName, *Entry.Value.AssetName); AssetsToDelete.Add(Asset); } AssetLookup->RemoveRow(Entry.Key); } bAssetLookupModified = true; } if (bAssetLookupCreated || bAssetLookupModified) { AssetsToSave.Add(AssetLookup); } } else { UE_LOG(LogFMOD, Warning, TEXT("Failed to load strings bank: %s"), *StringPath); } } } void FFMODAssetBuilder::BuildBankLookup(const FString &AssetName, const FString &PackagePath, const UFMODSettings &InSettings, TArray& AssetsToSave) { FString PackageName = PackagePath + AssetName; UPackage *Package = CreatePackage(*PackageName); Package->FullyLoad(); bool bCreated = false; bool bModified = false; BankLookup = FindObject(Package, *AssetName, true); if (!BankLookup) { BankLookup = NewObject(Package, *AssetName, RF_Public | RF_Standalone | RF_MarkAsRootSet); BankLookup->DataTable = NewObject(BankLookup, "DataTable", RF_NoFlags); BankLookup->DataTable->RowStruct = FFMODLocalizedBankTable::StaticStruct(); bCreated = true; } // Get a list of all bank GUIDs already in the lookup - this will be used to remove stale GUIDs after processing // the current banks on disk. TArray StaleBanks(BankLookup->DataTable->GetRowNames()); // Process all banks on disk TArray BankPaths; FString SearchDir = InSettings.GetFullBankPath(); IFileManager::Get().FindFilesRecursive(BankPaths, *SearchDir, TEXT("*.bank"), true, false, false); if (BankPaths.Num() <= 0) { return; } TArray LocalizedEntriesVisited; for (FString BankPath : BankPaths) { FMOD::Studio::Bank* Bank; FMOD_GUID BankID; FMOD_RESULT result = StudioSystem->loadBankFile(TCHAR_TO_UTF8(*BankPath), FMOD_STUDIO_LOAD_BANK_NORMAL, &Bank); if (result == FMOD_OK) { result = Bank->getID(&BankID); Bank->unload(); } if (result != FMOD_OK) { UE_LOG(LogFMOD, Error, TEXT("Failed to add bank %s to lookup."), *BankPath); continue; } FString GUID = FMODUtils::ConvertGuid(BankID).ToString(EGuidFormats::DigitsWithHyphensInBraces); FName OuterRowName(*GUID); FFMODLocalizedBankTable* Row = BankLookup->DataTable->FindRow(OuterRowName, nullptr, false); if (Row) { StaleBanks.RemoveSingle(OuterRowName); } else { FFMODLocalizedBankTable NewRow{}; NewRow.Banks = NewObject(BankLookup->DataTable, *BankPath, RF_NoFlags); NewRow.Banks->RowStruct = FFMODLocalizedBankRow::StaticStruct(); BankLookup->DataTable->AddRow(OuterRowName, NewRow); Row = BankLookup->DataTable->FindRow(OuterRowName, nullptr, false); bModified = true; } // Set InnerRowName to either "" or a locale code based on the BankPath e.g. "JP" FName InnerRowName(""); for (const FFMODProjectLocale& Locale : InSettings.Locales) { // Remove all expected extensions from end of filename before checking for locale code. // Note, we may encounter multiple extensions e.g. "Dialogue.assets.bank" const FString BankExtensions[] = { TEXT(".assets"), TEXT(".streams"), TEXT(".strings") }; FString Filename = FPaths::GetCleanFilename(BankPath); Filename.RemoveFromEnd(TEXT(".bank")); for (const FString& extension : BankExtensions) { Filename.RemoveFromEnd(extension); } if (Filename.EndsWith(FString("_") + Locale.LocaleCode)) { InnerRowName = FName(*Locale.LocaleCode); break; } } // See if we've visited this OuterRowName + InnerRowName already and skip it if so. This is mainly to // avoid setting "" multiple times (and causing BankLookup to be modified) when no // locales are set up in Unreal. FString LocalizedEntryKey = OuterRowName.ToString() + InnerRowName.ToString(); if (LocalizedEntriesVisited.Find(LocalizedEntryKey) != INDEX_NONE) { UE_LOG(LogFMOD, Warning, TEXT("Ignoring bank %s as another bank with the same GUID is already being used.\n" "Bank %s does not match any locales in the FMOD Studio plugin settings."), *BankPath, *BankPath); continue; } LocalizedEntriesVisited.Add(LocalizedEntryKey); FFMODLocalizedBankRow* InnerRow = Row->Banks->FindRow(InnerRowName, nullptr, false); FString RelativeBankPath = BankPath.RightChop(InSettings.GetFullBankPath().Len() + 1); if (InnerRow) { if (InnerRow->Path != RelativeBankPath) { InnerRow->Path = RelativeBankPath; bModified = true; } } else { FFMODLocalizedBankRow NewRow{}; NewRow.Path = RelativeBankPath; Row->Banks->AddRow(InnerRowName, NewRow); bModified = true; } FString CurFilename = FPaths::GetCleanFilename(BankPath); if (CurFilename == InSettings.GetMasterBankFilename() && BankLookup->MasterBankPath != RelativeBankPath) { BankLookup->MasterBankPath = RelativeBankPath; bModified = true; } else if (CurFilename == InSettings.GetMasterStringsBankFilename() && BankLookup->MasterStringsBankPath != RelativeBankPath) { BankLookup->MasterStringsBankPath = RelativeBankPath; bModified = true; } else if (CurFilename == InSettings.GetMasterAssetsBankFilename() && BankLookup->MasterAssetsBankPath != RelativeBankPath) { BankLookup->MasterAssetsBankPath = RelativeBankPath; bModified = true; } } StudioSystem->flushCommands(); // Remove stale banks from lookup if (StaleBanks.Num() > 0) { for (const auto& RowName : StaleBanks) { BankLookup->DataTable->RemoveRow(RowName); } bModified = true; } // Remove stale localized bank entries from lookup for (auto& outer : BankLookup->DataTable->GetRowMap()) { FName outerrowname = outer.Key; FFMODLocalizedBankTable* outerrow = reinterpret_cast(outer.Value); TArray RowsToRemove; for (auto& inner : outerrow->Banks->GetRowMap()) { FName innerrowname = inner.Key; FFMODLocalizedBankRow* innerrow = reinterpret_cast(inner.Value); FString LocalizedEntryKey = outerrowname.ToString() + innerrowname.ToString(); if (LocalizedEntriesVisited.Find(LocalizedEntryKey) == INDEX_NONE) { RowsToRemove.Add(innerrowname); } } for (auto& rowname : RowsToRemove) { outerrow->Banks->RemoveRow(rowname); bModified = true; } } if (bCreated) { UE_LOG(LogFMOD, Log, TEXT("BankLookup created.\n")); FAssetRegistryModule::AssetCreated(BankLookup); } if (bCreated || bModified) { UE_LOG(LogFMOD, Log, TEXT("BankLookup modified.\n")); AssetsToSave.Add(BankLookup); } UE_LOG(LogFMOD, Log, TEXT("===== BankLookup =====\n")); for (auto& outer : BankLookup->DataTable->GetRowMap()) { FName outerrowname = outer.Key; FFMODLocalizedBankTable* outerrow = reinterpret_cast(outer.Value); UE_LOG(LogFMOD, Log, TEXT("GUID: %s\n"), *(outerrowname.ToString())); for (auto& inner : outerrow->Banks->GetRowMap()) { FName innerrowname = inner.Key; FFMODLocalizedBankRow* innerrow = reinterpret_cast(inner.Value); UE_LOG(LogFMOD, Log, TEXT(" Locale: %s Path: %s\n"), *(innerrowname.ToString()), *innerrow->Path); } } } FString FFMODAssetBuilder::GetAssetClassName(UClass* AssetClass) { FString ClassName(""); if (AssetClass == UFMODEvent::StaticClass()) { ClassName = TEXT("Events"); } else if (AssetClass == UFMODSnapshot::StaticClass()) { ClassName = TEXT("Snapshots"); } else if (AssetClass == UFMODBank::StaticClass()) { ClassName = TEXT("Banks"); } else if (AssetClass == UFMODBus::StaticClass()) { ClassName = TEXT("Buses"); } else if (AssetClass == UFMODVCA::StaticClass()) { ClassName = TEXT("VCAs"); } else if (AssetClass == UFMODSnapshotReverb::StaticClass()) { ClassName = TEXT("Reverbs"); } else if (AssetClass == UFMODPort::StaticClass()) { ClassName = TEXT("Ports"); } return ClassName; } bool FFMODAssetBuilder::MakeAssetCreateInfo(const FGuid &AssetGuid, const FString &StudioPath, AssetCreateInfo *CreateInfo) { CreateInfo->StudioPath = StudioPath; CreateInfo->Guid = AssetGuid; FString AssetType; FString AssetPath; StudioPath.Split(TEXT(":"), &AssetType, &AssetPath, ESearchCase::CaseSensitive, ESearchDir::FromStart); if (AssetType.Equals(TEXT("event"))) { CreateInfo->Class = UFMODEvent::StaticClass(); } else if (AssetType.Equals(TEXT("snapshot"))) { CreateInfo->Class = UFMODSnapshot::StaticClass(); } else if (AssetType.Equals(TEXT("bank"))) { CreateInfo->Class = UFMODBank::StaticClass(); } else if (AssetType.Equals(TEXT("bus"))) { CreateInfo->Class = UFMODBus::StaticClass(); } else if (AssetType.Equals(TEXT("vca"))) { CreateInfo->Class = UFMODVCA::StaticClass(); } else if (AssetType.Equals(TEXT("port"))) { CreateInfo->Class = UFMODPort::StaticClass(); } else if (AssetType.Equals(TEXT("parameter"))) { return false; } else { UE_LOG(LogFMOD, Warning, TEXT("Unknown asset type: %s"), *AssetType); CreateInfo->Class = UFMODAsset::StaticClass(); } AssetPath.Split(TEXT("/"), &(CreateInfo->Path), &(CreateInfo->AssetName), ESearchCase::CaseSensitive, ESearchDir::FromEnd); if (CreateInfo->AssetName.IsEmpty() || CreateInfo->AssetName.Contains(TEXT(".strings"))) { return false; } return true; } UFMODAsset *FFMODAssetBuilder::CreateAsset(const AssetCreateInfo& CreateInfo, TArray& AssetsToSave) { FString SanitizedAssetName; FText OutReason; if (FName::IsValidXName(CreateInfo.AssetName, INVALID_OBJECTNAME_CHARACTERS, &OutReason)) { SanitizedAssetName = CreateInfo.AssetName; } else { SanitizedAssetName = ObjectTools::SanitizeObjectName(CreateInfo.AssetName); UE_LOG(LogFMOD, Log, TEXT("'%s' cannot be used as a UE4 asset name. %s. Using '%s' instead."), *CreateInfo.AssetName, *OutReason.ToString(), *SanitizedAssetName); } const UFMODSettings &Settings = *GetDefault(); FString Folder = Settings.GetFullContentPath() / GetAssetClassName(CreateInfo.Class) + CreateInfo.Path; FString PackagePath = FString::Printf(TEXT("%s/%s"), *Folder, *SanitizedAssetName); FString SanitizedPackagePath; if (FName::IsValidXName(PackagePath, INVALID_LONGPACKAGE_CHARACTERS, &OutReason)) { SanitizedPackagePath = PackagePath; } else { SanitizedPackagePath = ObjectTools::SanitizeInvalidChars(PackagePath, INVALID_LONGPACKAGE_CHARACTERS); UE_LOG(LogFMOD, Log, TEXT("'%s' cannot be used as a UE4 asset path. %s. Using '%s' instead."), *PackagePath, *OutReason.ToString(), *SanitizedPackagePath); } UPackage *Package = CreatePackage(*SanitizedPackagePath); Package->FullyLoad(); UFMODAsset *Asset = FindObject(Package, *SanitizedAssetName); bool bCreated = false; bool bModified = false; if (Asset && Asset->GetClass() == CreateInfo.Class) { if (Asset->AssetGuid != CreateInfo.Guid) { UE_LOG(LogFMOD, Log, TEXT("Updating asset: %s"), *SanitizedPackagePath); Asset->AssetGuid = CreateInfo.Guid; bModified = true; } } else { UE_LOG(LogFMOD, Log, TEXT("Adding asset: %s"), *SanitizedPackagePath); Asset = NewObject(Package, CreateInfo.Class, FName(*SanitizedAssetName), RF_Standalone | RF_Public | RF_MarkAsRootSet); Asset->AssetGuid = CreateInfo.Guid; bCreated = true; } if (bCreated) { FAssetRegistryModule::AssetCreated(Asset); } if (bCreated || bModified) { AssetsToSave.Add(Asset); } if (!IsValid(Asset)) { UE_LOG(LogFMOD, Error, TEXT("Failed to construct asset: %s"), *SanitizedPackagePath); } if (CreateInfo.Class == UFMODSnapshot::StaticClass()) { FString OldPrefix = Settings.ContentBrowserPrefix + GetAssetClassName(Asset->GetClass()); FString NewPrefix = Settings.ContentBrowserPrefix + GetAssetClassName(UFMODSnapshotReverb::StaticClass()); UObject *Outer = Asset->GetOuter() ? Asset->GetOuter() : Asset; FString ReverbPackagePath = Outer->GetPathName().Replace(*OldPrefix, *NewPrefix); UPackage *ReverbPackage = CreatePackage(*ReverbPackagePath); ReverbPackage->FullyLoad(); UFMODSnapshotReverb *AssetReverb = FindObject(ReverbPackage, *SanitizedAssetName, true); bCreated = false; bModified = false; if (AssetReverb) { if (AssetReverb->AssetGuid != CreateInfo.Guid) { UE_LOG(LogFMOD, Log, TEXT("Updating snapshot reverb asset: %s"), *ReverbPackagePath); AssetReverb->AssetGuid = CreateInfo.Guid; bModified = true; } } else { UE_LOG(LogFMOD, Log, TEXT("Constructing snapshot reverb asset: %s"), *ReverbPackagePath); AssetReverb = NewObject(ReverbPackage, UFMODSnapshotReverb::StaticClass(), FName(*SanitizedAssetName), RF_Standalone | RF_Public | RF_MarkAsRootSet); AssetReverb->AssetGuid = CreateInfo.Guid; bCreated = true; } if (bCreated) { FAssetRegistryModule::AssetCreated(AssetReverb); } if (bCreated || bModified) { AssetsToSave.Add(AssetReverb); } if (!IsValid(AssetReverb)) { UE_LOG(LogFMOD, Error, TEXT("Failed to construct snapshot reverb asset: %s"), *ReverbPackagePath); } } return Asset; } void FFMODAssetBuilder::SaveAssets(TArray& AssetsToSave) { if (AssetsToSave.Num() == 0) { return; } TArray PackagesToSave; for (auto& Asset : AssetsToSave) { UPackage* Package = Asset->GetPackage(); if (Package) { Package->MarkPackageDirty(); PackagesToSave.Add(Package); } } UEditorLoadingAndSavingUtils::SavePackages(PackagesToSave, true); } void FFMODAssetBuilder::DeleteAssets(TArray& AssetsToDelete) { if (AssetsToDelete.Num() == 0) { return; } TArray ObjectsToDelete; for (auto& Asset : AssetsToDelete) { ObjectsToDelete.Add(Asset); if (Asset->GetClass() == UFMODSnapshot::StaticClass()) { // Also delete the reverb asset const UFMODSettings &Settings = *GetDefault(); FString OldPrefix = Settings.ContentBrowserPrefix + GetAssetClassName(Asset->GetClass()); FString NewPrefix = Settings.ContentBrowserPrefix + GetAssetClassName(UFMODSnapshotReverb::StaticClass()); FString ReverbName = Asset->GetPathName().Replace(*OldPrefix, *NewPrefix); UObject *Reverb = StaticFindObject(UFMODSnapshotReverb::StaticClass(), nullptr, *ReverbName); if (Reverb) { ObjectsToDelete.Add(Reverb); } } } // Use ObjectTools to delete assets - ObjectTools::DeleteObjects handles confirmation, source control, and making read only files writables ObjectTools::DeleteObjects(ObjectsToDelete, !IsRunningCommandlet()); } #undef LOCTEXT_NAMESPACE