You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
This change consists of multiple changes: Core: - Deprecation of ANY_PACKAGE macro. Added ANY_PACKAGE_DEPRECATED macro which can still be used for backwards compatibility purposes (only used in CoreUObject) - Deprecation of StaticFindObjectFast* functions that take bAnyPackage parameter - Added UStruct::GetStructPathName function that returns FTopLevelAssetPath representing the path name (package + object FName, super quick compared to UObject::GetPathName) + wrapper UClass::GetClassPathName to make it look better when used with UClasses - Added (Static)FindFirstObject* functions that find a first object given its Name (no Outer). These functions are used in places I consider valid to do global UObject (UClass) lookups like parsing command line parameters / checking for unique object names - Added static UClass::TryFindType function which serves a similar purpose as FindFirstObject however it's going to throw a warning (with a callstack / maybe ensure in the future?) if short class name is provided. This function is used in places that used to use short class names but now should have been converted to use path names to catch any potential regressions and or edge cases I missed. - Added static UClass::TryConvertShortNameToPathName utility function - Added static UClass::TryFixShortClassNameExportPath utility function - Object text export paths will now also include class path (Texture2D'/Game/Textures/Grass.Grass' -> /Script/Engine.Texture2D'/Game/Textures/Grass.Grass') - All places that manually generated object export paths for objects will now use FObjectPropertyBase::GetExportPath - Added a new startup test that checks for short type names in UClass/FProperty MetaData values AssetRegistry: - Deprecated any member variables (FAssetData / FARFilter) or functions that use FNames to represent class names and replaced them with FTopLevelAssetPath - Added new member variables and new function overloads that use FTopLevelAssetPath to represent class names - This also applies to a few other modules' APIs to match AssetRegistry changes Everything else: - Updated code that used ANY_PACKAGE (depending on the use case) to use FindObject(nullptr, PathToObject), UClass::TryFindType (used when path name is expected, warns if it's a short name) or FindFirstObject (usually for finding types based on user input but there's been a few legitimate use cases not related to user input) - Updated code that used AssetRegistry API to use FTopLevelAssetPaths and USomeClass::StaticClass()->GetClassPathName() instead of GetFName() - Updated meta data and hardcoded FindObject(ANY_PACKAGE, "EEnumNameOrClassName") calls to use path names #jira UE-99463 #rb many.people [FYI] Marcus.Wassmer #preflight 629248ec2256738f75de9b32 #codereviewnumbers 20320742, 20320791, 20320799, 20320756, 20320809, 20320830, 20320840, 20320846, 20320851, 20320863, 20320780, 20320765, 20320876, 20320786 #ROBOMERGE-OWNER: robert.manuszewski #ROBOMERGE-AUTHOR: robert.manuszewski #ROBOMERGE-SOURCE: CL 20430220 via CL 20433854 via CL 20435474 via CL 20435484 #ROBOMERGE-BOT: UE5 (Release-Engine-Staging -> Main) (v949-20362246) [CL 20448496 by robert manuszewski in ue5-main branch]
533 lines
23 KiB
C++
533 lines
23 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "Commandlets/ImportLocalizedDialogueCommandlet.h"
|
|
#include "Modules/ModuleManager.h"
|
|
#include "Misc/PackageName.h"
|
|
#include "AssetRegistry/AssetData.h"
|
|
#include "Sound/SoundWave.h"
|
|
#include "Misc/Paths.h"
|
|
#include "Misc/App.h"
|
|
#include "UObject/MetaData.h"
|
|
#include "EditorFramework/AssetImportData.h"
|
|
#include "Sound/DialogueWave.h"
|
|
#include "Utils.h"
|
|
#include "AssetRegistry/AssetRegistryModule.h"
|
|
#include "Interfaces/ITargetPlatform.h"
|
|
#include "Interfaces/ITargetPlatformManagerModule.h"
|
|
#include "AudioEditorModule.h"
|
|
#include "AudioCompressionSettingsUtils.h"
|
|
|
|
DEFINE_LOG_CATEGORY_STATIC(LogImportLocalizedDialogueCommandlet, Log, All);
|
|
|
|
namespace
|
|
{
|
|
const FString GeneratedByCommandletMetaDataKey = TEXT("GeneratedByCommandlet");
|
|
const FString GeneratedByCommandletMetaDataValue = TEXT("ImportLocalizedDialogueCommandlet");
|
|
}
|
|
|
|
UImportLocalizedDialogueCommandlet::UImportLocalizedDialogueCommandlet(const FObjectInitializer& ObjectInitializer)
|
|
: Super(ObjectInitializer)
|
|
{
|
|
}
|
|
|
|
int32 UImportLocalizedDialogueCommandlet::Main(const FString& Params)
|
|
{
|
|
// Parse command line
|
|
TArray<FString> Tokens;
|
|
TArray<FString> Switches;
|
|
TMap<FString, FString> ParamVals;
|
|
UCommandlet::ParseCommandLine(*Params, Tokens, Switches, ParamVals);
|
|
|
|
// Set config path
|
|
FString ConfigPath;
|
|
{
|
|
const FString* ConfigPathParamVal = ParamVals.Find(FString(TEXT("Config")));
|
|
if (!ConfigPathParamVal)
|
|
{
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Error, TEXT("No config specified."));
|
|
return -1;
|
|
}
|
|
ConfigPath = *ConfigPathParamVal;
|
|
}
|
|
|
|
// Set config section
|
|
FString SectionName;
|
|
{
|
|
const FString* SectionNameParamVal = ParamVals.Find(FString(TEXT("Section")));
|
|
if (!SectionNameParamVal)
|
|
{
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Error, TEXT("No config section specified."));
|
|
return -1;
|
|
}
|
|
SectionName = *SectionNameParamVal;
|
|
}
|
|
|
|
// Source path to the root folder that manifest/archive files live in
|
|
FString SourcePath;
|
|
if (!GetPathFromConfig(*SectionName, TEXT("SourcePath"), SourcePath, ConfigPath))
|
|
{
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Error, TEXT("No source path specified."));
|
|
return -1;
|
|
}
|
|
|
|
// Get the native culture
|
|
FString NativeCulture;
|
|
if (!GetStringFromConfig(*SectionName, TEXT("NativeCulture"), NativeCulture, ConfigPath))
|
|
{
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Error, TEXT("No native culture specified."));
|
|
return -1;
|
|
}
|
|
|
|
// Get cultures to generate
|
|
TArray<FString> CulturesToGenerate;
|
|
if (GetStringArrayFromConfig(*SectionName, TEXT("CulturesToGenerate"), CulturesToGenerate, ConfigPath) == 0)
|
|
{
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Error, TEXT("No cultures specified for import."));
|
|
return -1;
|
|
}
|
|
|
|
// Get the manifest name
|
|
FString ManifestName;
|
|
if (!GetStringFromConfig(*SectionName, TEXT("ManifestName"), ManifestName, ConfigPath))
|
|
{
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Error, TEXT("No manifest name specified."));
|
|
return -1;
|
|
}
|
|
|
|
// Get the archive name
|
|
FString ArchiveName;
|
|
if (!GetStringFromConfig(*SectionName, TEXT("ArchiveName"), ArchiveName, ConfigPath))
|
|
{
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Error, TEXT("No archive name specified."));
|
|
return -1;
|
|
}
|
|
|
|
// Should we import the native audio as source audio?
|
|
bool bImportNativeAsSource = false;
|
|
if (!GetBoolFromConfig(*SectionName, TEXT("bImportNativeAsSource"), bImportNativeAsSource, ConfigPath))
|
|
{
|
|
bImportNativeAsSource = false;
|
|
}
|
|
|
|
// Source path to the raw audio files that we're going to import
|
|
FString RawAudioPath;
|
|
if (!GetPathFromConfig(*SectionName, TEXT("RawAudioPath"), RawAudioPath, ConfigPath))
|
|
{
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Error, TEXT("No raw audio path specified."));
|
|
return -1;
|
|
}
|
|
if (!FPaths::DirectoryExists(RawAudioPath))
|
|
{
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Error, TEXT("Invalid raw audio path specified: %s."), *RawAudioPath);
|
|
return -1;
|
|
}
|
|
|
|
// Folder in which to place automatically imported sound wave assets
|
|
FString ImportedDialogueFolder;
|
|
if (!GetStringFromConfig(*SectionName, TEXT("ImportedDialogueFolder"), ImportedDialogueFolder, ConfigPath))
|
|
{
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Error, TEXT("No imported dialogue folder specified."));
|
|
return -1;
|
|
}
|
|
if (ImportedDialogueFolder.IsEmpty())
|
|
{
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Error, TEXT("Imported dialogue folder cannot be empty."));
|
|
return -1;
|
|
}
|
|
|
|
// Load the manifest and all archives
|
|
FLocTextHelper LocTextHelper(SourcePath, ManifestName, ArchiveName, NativeCulture, CulturesToGenerate, MakeShareable(new FLocFileSCCNotifies(SourceControlInfo)));
|
|
{
|
|
FText LoadError;
|
|
if (!LocTextHelper.LoadAll(ELocTextHelperLoadFlags::LoadOrCreate, &LoadError))
|
|
{
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Error, TEXT("%s"), *LoadError.ToString());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const FString RootAssetPath = FApp::HasProjectName() ? TEXT("/Game") : TEXT("/Engine");
|
|
const FString RootContentDir = FApp::HasProjectName() ? FPaths::ProjectContentDir() : FPaths::EngineContentDir();
|
|
|
|
// Prepare the asset registry
|
|
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
|
|
IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
|
|
AssetRegistry.SearchAllAssets(true);
|
|
|
|
// We want all the non-localized project specific dialogue waves
|
|
TArray<FAssetData> AssetDataArrayForDialogueWaves;
|
|
if (!FLocalizedAssetUtil::GetAssetsByPathAndClass(AssetRegistry, *RootAssetPath, UDialogueWave::StaticClass()->GetClassPathName(), /*bIncludeLocalizedAssets*/false, AssetDataArrayForDialogueWaves))
|
|
{
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Error, TEXT("Unable to get dialogue wave asset data from asset registry."));
|
|
return -1;
|
|
}
|
|
|
|
// Build up the culture specific import info
|
|
TMap<FString, FCultureImportInfo> CultureImportInfoMap;
|
|
for (const FString& CultureName : CulturesToGenerate)
|
|
{
|
|
FCultureImportInfo& CultureImportInfo = CultureImportInfoMap.Add(CultureName);
|
|
CultureImportInfo.Name = CultureName;
|
|
CultureImportInfo.AudioPath = RawAudioPath / CultureName;
|
|
CultureImportInfo.ArchiveFileName = SourcePath / CultureName / ArchiveName;
|
|
CultureImportInfo.LocalizedRootContentPath = RootContentDir / TEXT("L10N") / CultureName;
|
|
CultureImportInfo.LocalizedRootPackagePath = RootAssetPath / TEXT("L10N") / CultureName;
|
|
CultureImportInfo.LocalizedImportedDialoguePackagePath = CultureImportInfo.LocalizedRootPackagePath / ImportedDialogueFolder;
|
|
CultureImportInfo.bIsNativeCulture = CultureName == NativeCulture;
|
|
}
|
|
|
|
// Find all of the existing localized dialogue and sound waves - we'll keep track of which ones we process so we can delete any that are no longer needed
|
|
TArray<FAssetData> LocalizedAssetsToPotentiallyDelete;
|
|
{
|
|
TArray<FName> LocalizedDialogueWavePathsToSearch;
|
|
TArray<FName> LocalizedSoundWavePathsToSearch;
|
|
|
|
// We always add the source imported dialogue folder to ensure we clean it up correctly if we change the "import native as source" option
|
|
// This is also why we always add the native culture, even though only one will be in use at any one time
|
|
LocalizedSoundWavePathsToSearch.Add(*(RootAssetPath / ImportedDialogueFolder));
|
|
|
|
for (const auto& CultureImportInfoPair : CultureImportInfoMap)
|
|
{
|
|
const FCultureImportInfo& CultureImportInfo = CultureImportInfoPair.Value;
|
|
LocalizedDialogueWavePathsToSearch.Add(*CultureImportInfo.LocalizedRootPackagePath);
|
|
LocalizedSoundWavePathsToSearch.Add(*CultureImportInfo.LocalizedImportedDialoguePackagePath);
|
|
}
|
|
|
|
FLocalizedAssetUtil::GetAssetsByPathAndClass(AssetRegistry, LocalizedDialogueWavePathsToSearch, UDialogueWave::StaticClass()->GetClassPathName(), /*bIncludeLocalizedAssets*/true, LocalizedAssetsToPotentiallyDelete);
|
|
FLocalizedAssetUtil::GetAssetsByPathAndClass(AssetRegistry, LocalizedSoundWavePathsToSearch, USoundWave::StaticClass()->GetClassPathName(), /*bIncludeLocalizedAssets*/true, LocalizedAssetsToPotentiallyDelete);
|
|
}
|
|
|
|
// We're going to walk every context from every dialogue wave asset looking to see whether there's new audio to import for each culture we generate for
|
|
// We filter these dialogue waves against the current manifest so that we only attempt to update assets that we gather text from
|
|
for (const FAssetData& AssetData : AssetDataArrayForDialogueWaves)
|
|
{
|
|
// Verify that the found asset is a dialogue wave
|
|
if (AssetData.GetClass() != UDialogueWave::StaticClass())
|
|
{
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Error, TEXT("Asset registry found asset '%s', but the asset with this name is not actually a dialogue wave."), *AssetData.AssetName.ToString());
|
|
continue;
|
|
}
|
|
|
|
// Get the dialogue wave
|
|
UDialogueWave* const DialogueWave = Cast<UDialogueWave>(AssetData.GetAsset());
|
|
|
|
// Verify that the dialogue wave was loaded
|
|
if (!DialogueWave)
|
|
{
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Error, TEXT("Asset registry found asset '%s', but the dialogue wave could not be accessed."), *AssetData.AssetName.ToString());
|
|
continue;
|
|
}
|
|
|
|
FString _Unused_DialogueWaveRoot, DialogueWaveSubPath, _Unused_DialogueWaveAssetName;
|
|
if (!FPackageName::SplitLongPackageName(AssetData.PackageName.ToString(), _Unused_DialogueWaveRoot, DialogueWaveSubPath, _Unused_DialogueWaveAssetName))
|
|
{
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Error, TEXT("Failed to split dialogue wave package name '%s'."), *AssetData.PackageName.ToString());
|
|
continue;
|
|
}
|
|
|
|
// If we're importing native as source, then we import using a special culture import info
|
|
if (bImportNativeAsSource && CultureImportInfoMap.Contains(NativeCulture))
|
|
{
|
|
FCultureImportInfo SourceCultureImportInfo = CultureImportInfoMap.FindRef(NativeCulture);
|
|
SourceCultureImportInfo.LocalizedRootContentPath = RootContentDir;
|
|
SourceCultureImportInfo.LocalizedRootPackagePath = RootAssetPath;
|
|
SourceCultureImportInfo.LocalizedImportedDialoguePackagePath = SourceCultureImportInfo.LocalizedRootPackagePath / ImportedDialogueFolder;
|
|
|
|
ImportDialogueForCulture(LocTextHelper, DialogueWave, DialogueWaveSubPath, SourceCultureImportInfo, /*bImportAsSource*/true);
|
|
}
|
|
|
|
// Iterate over each context looking for new audio to import
|
|
for (const auto& CultureImportInfoPair : CultureImportInfoMap)
|
|
{
|
|
const FCultureImportInfo& CultureImportInfo = CultureImportInfoPair.Value;
|
|
|
|
// Skip the native culture if importing native as source, as we'll have imported it above
|
|
if (bImportNativeAsSource && CultureImportInfo.bIsNativeCulture)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
ImportDialogueForCulture(LocTextHelper, DialogueWave, DialogueWaveSubPath, CultureImportInfo, /*bImportAsSource*/false);
|
|
}
|
|
}
|
|
|
|
// Remove any left over assets that we no longer need
|
|
for (const FAssetData& LocalizedAssetData : LocalizedAssetsToPotentiallyDelete)
|
|
{
|
|
// Has this asset already keen marked as "keep"?
|
|
if (AssetsToKeep.Contains(LocalizedAssetData.ObjectPath))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Check the package meta-data to make sure that we only delete packages that we own
|
|
UObject* LocalizedAsset = LocalizedAssetData.GetAsset();
|
|
const FString AssetGeneratedByCommandletMetaDataValue = LocalizedAsset->GetOutermost()->GetMetaData()->GetValue(LocalizedAsset, *GeneratedByCommandletMetaDataKey);
|
|
if (AssetGeneratedByCommandletMetaDataValue != GeneratedByCommandletMetaDataValue)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FLocalizedAssetSCCUtil::DeleteAssetWithSCC(SourceControlInfo, LocalizedAsset);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool UImportLocalizedDialogueCommandlet::ImportDialogueForCulture(FLocTextHelper& InLocTextHelper, UDialogueWave* const DialogueWave, const FString& DialogueWaveSubPath, const FCultureImportInfo& InCultureImportInfo, const bool bImportAsSource)
|
|
{
|
|
UDialogueWave* LocalizedDialogueWave = nullptr;
|
|
FString LocalizedDialogueWaveFileName;
|
|
|
|
if (bImportAsSource)
|
|
{
|
|
LocalizedDialogueWave = DialogueWave;
|
|
LocalizedDialogueWaveFileName = FPackageName::LongPackageNameToFilename(DialogueWave->GetOutermost()->GetPathName(), FPackageName::GetAssetPackageExtension());
|
|
}
|
|
else
|
|
{
|
|
LocalizedDialogueWaveFileName = (InCultureImportInfo.LocalizedRootContentPath / DialogueWaveSubPath / DialogueWave->GetName()) + FPackageName::GetAssetPackageExtension();
|
|
|
|
// Clone the source dialogue wave into the localized folder, replacing any existing asset to ensure that we're up-to-date with the source data
|
|
{
|
|
if (!FLocalizedAssetSCCUtil::SaveAssetWithSCC(SourceControlInfo, DialogueWave, LocalizedDialogueWaveFileName))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Load up the newly saved asset
|
|
const FString LocalizedDialogueWavePackagePath = (InCultureImportInfo.LocalizedRootPackagePath / DialogueWaveSubPath / TEXT("")) + DialogueWave->GetName();
|
|
const FString LocalizedDialogueWaveAssetPath = FString::Printf(TEXT("%s.%s"), *LocalizedDialogueWavePackagePath, *DialogueWave->GetName());
|
|
LocalizedDialogueWave = LoadObject<UDialogueWave>(nullptr, *LocalizedDialogueWaveAssetPath);
|
|
}
|
|
|
|
if (!LocalizedDialogueWave)
|
|
{
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Error, TEXT("Failed to create a localized dialogue wave '%s' for culture '%s'. No dialogue will be imported for this culture."), *DialogueWave->GetName(), *InCultureImportInfo.Name);
|
|
return false;
|
|
}
|
|
|
|
// Mark this localized dialogue wave as used so it doesn't get deleted later
|
|
AssetsToKeep.Add(*LocalizedDialogueWave->GetPathName());
|
|
}
|
|
|
|
// First pass, handle any contexts that have an exact mapping to their audio file
|
|
TArray<FDialogueContextMapping*> ContextMappingsWithMissingAudio;
|
|
for (FDialogueContextMapping& ContextMapping : LocalizedDialogueWave->ContextMappings)
|
|
{
|
|
const FString ContextLocalizationKey = LocalizedDialogueWave->GetContextLocalizationKey(ContextMapping);
|
|
|
|
// Check that this entry exists in the manifest file, as we want to skip over dialogue that we aren't gathering
|
|
TSharedPtr<FManifestEntry> ContextManifestEntry = InLocTextHelper.FindSourceText(FDialogueConstants::DialogueNamespace, ContextLocalizationKey, &LocalizedDialogueWave->SpokenText);
|
|
if (!ContextManifestEntry.IsValid())
|
|
{
|
|
// We're skipping this context entry due to our manifest, but we don't want the sound wave it's using to be deleted
|
|
if (ContextMapping.SoundWave)
|
|
{
|
|
AssetsToKeep.Add(*ContextMapping.SoundWave->GetPathName());
|
|
}
|
|
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Log, TEXT("No internationalization manifest entry was found for context '%s' in culture '%s'. This context will be skipped."), *ContextLocalizationKey, *InCultureImportInfo.Name);
|
|
continue;
|
|
}
|
|
|
|
const FString ContextAudioFilename = InCultureImportInfo.AudioPath / LocalizedDialogueWave->GetContextRecordedAudioFilename(ContextMapping);
|
|
if (!FPaths::FileExists(ContextAudioFilename))
|
|
{
|
|
// No specific audio file exists for this context, however that means we may use a different audio file if we have another context with the same speaker (to share sound waves where possible)
|
|
// Flag this context as needing a second pass
|
|
ContextMappingsWithMissingAudio.Add(&ContextMapping);
|
|
continue;
|
|
}
|
|
|
|
// Import the WAV file as a sound wave asset, potentially overwriting any existing asset
|
|
// The WAV file will only be imported if it has been changed since the last time it was imported
|
|
USoundWave* const SoundWave = ConditionalImportSoundWave(InCultureImportInfo.LocalizedImportedDialoguePackagePath / ContextLocalizationKey, ContextLocalizationKey, ContextAudioFilename);
|
|
if (SoundWave)
|
|
{
|
|
// Set this context to use the newly imported sound wave
|
|
ContextMapping.SoundWave = SoundWave;
|
|
}
|
|
|
|
// This sound wave is in use, so shouldn't be deleted
|
|
if (ContextMapping.SoundWave)
|
|
{
|
|
AssetsToKeep.Add(*ContextMapping.SoundWave->GetPathName());
|
|
}
|
|
}
|
|
|
|
// Second pass, handle any contexts that should share sound data with another context
|
|
for (FDialogueContextMapping* ContextMappingPtr : ContextMappingsWithMissingAudio)
|
|
{
|
|
auto GetTranslatedTextForContext = [&](const FDialogueContextMapping& InContextMapping) -> FString
|
|
{
|
|
const FString ContextLocalizationKey = LocalizedDialogueWave->GetContextLocalizationKey(*ContextMappingPtr);
|
|
|
|
// Find the manifest entry for our context
|
|
TSharedPtr<FManifestEntry> ContextManifestEntry = InLocTextHelper.FindSourceText(FDialogueConstants::DialogueNamespace, ContextLocalizationKey, &LocalizedDialogueWave->SpokenText);
|
|
if (!ContextManifestEntry.IsValid())
|
|
{
|
|
return FString();
|
|
}
|
|
|
|
// Find the correct entry for our context
|
|
const FManifestContext* ContextManifestEntryContext = ContextManifestEntry->FindContextByKey(ContextLocalizationKey);
|
|
check(ContextManifestEntryContext); // This should never fail as we pass in the key to FindSourceText
|
|
|
|
// Get the localized text to export
|
|
FLocItem ExportedSource;
|
|
FLocItem ExportedTranslation;
|
|
InLocTextHelper.GetExportText(InCultureImportInfo.Name, FDialogueConstants::DialogueNamespace, ContextManifestEntryContext->Key, ContextManifestEntryContext->KeyMetadataObj, ELocTextExportSourceMethod::NativeText, ContextManifestEntry->Source, ExportedSource, ExportedTranslation);
|
|
|
|
return ExportedTranslation.Text;
|
|
};
|
|
|
|
// Find the correct localized dialogue for this context
|
|
const FString ContextLocalizedDialogue = GetTranslatedTextForContext(*ContextMappingPtr);
|
|
if (ContextLocalizedDialogue.IsEmpty())
|
|
{
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Warning, TEXT("No dialogue was imported for context '%s' in culture '%s' as it has an empty translation."), *LocalizedDialogueWave->GetContextLocalizationKey(*ContextMappingPtr), *InCultureImportInfo.Name);
|
|
}
|
|
else
|
|
{
|
|
// Try and find another context using the same speaking voice and localized dialogue that does have audio to import - we'll share its sound wave
|
|
const FDialogueContextMapping* FoundContextMapping = LocalizedDialogueWave->ContextMappings.FindByPredicate([&](const FDialogueContextMapping& PotentialContextMapping) -> bool
|
|
{
|
|
// Can't match ourself
|
|
if (ContextMappingPtr == &PotentialContextMapping)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Need to be using the same speaking voice
|
|
if (ContextMappingPtr->Context.Speaker != PotentialContextMapping.Context.Speaker)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Need to be saying the same dialogue
|
|
const FString PotentialContextLocalizedDialogue = GetTranslatedTextForContext(PotentialContextMapping);
|
|
if (!ContextLocalizedDialogue.Equals(PotentialContextLocalizedDialogue, ESearchCase::CaseSensitive))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Needs to actually have a valid audio file to import
|
|
const FString PotentialContextAudioFilename = InCultureImportInfo.AudioPath / LocalizedDialogueWave->GetContextRecordedAudioFilename(PotentialContextMapping);
|
|
return FPaths::FileExists(PotentialContextAudioFilename);
|
|
});
|
|
|
|
if (FoundContextMapping)
|
|
{
|
|
// Set this context to use the same sound wave as the found context
|
|
ContextMappingPtr->SoundWave = FoundContextMapping->SoundWave;
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Warning, TEXT("No dialogue was imported for context '%s' in culture '%s' as no suitable audio file could be found to import."), *LocalizedDialogueWave->GetContextLocalizationKey(*ContextMappingPtr), *InCultureImportInfo.Name);
|
|
}
|
|
}
|
|
|
|
// This sound wave is in use, so shouldn't be deleted
|
|
if (ContextMappingPtr->SoundWave)
|
|
{
|
|
AssetsToKeep.Add(*ContextMappingPtr->SoundWave->GetPathName());
|
|
}
|
|
}
|
|
|
|
LocalizedDialogueWave->MarkPackageDirty();
|
|
|
|
// Add meta-data stating that this asset is owned by this commandlet
|
|
LocalizedDialogueWave->GetOutermost()->GetMetaData()->SetValue(LocalizedDialogueWave, *GeneratedByCommandletMetaDataKey, *GeneratedByCommandletMetaDataValue);
|
|
|
|
return FLocalizedAssetSCCUtil::SaveAssetWithSCC(SourceControlInfo, LocalizedDialogueWave, LocalizedDialogueWaveFileName);
|
|
}
|
|
|
|
USoundWave* UImportLocalizedDialogueCommandlet::ConditionalImportSoundWave(const FString& InSoundWavePackageName, const FString& InSoundWaveAssetName, const FString& InWavFilename) const
|
|
{
|
|
FString PackageFileName;
|
|
if (!FPackageName::TryConvertLongPackageNameToFilename(InSoundWavePackageName, PackageFileName, FPackageName::GetAssetPackageExtension()) || !FPaths::FileExists(PackageFileName))
|
|
{
|
|
// No existing asset, we need to perform the import
|
|
return ImportSoundWave(InSoundWavePackageName, InSoundWaveAssetName, InWavFilename);
|
|
}
|
|
|
|
USoundWave* const ExistingSoundWave = LoadObject<USoundWave>(nullptr, *FString::Printf(TEXT("%s.%s"), *InSoundWavePackageName, *InSoundWaveAssetName));
|
|
if (!ExistingSoundWave)
|
|
{
|
|
// No existing asset, we need to perform the import
|
|
return ImportSoundWave(InSoundWavePackageName, InSoundWaveAssetName, InWavFilename);
|
|
}
|
|
|
|
// Find the import data that matches the file we're going to import
|
|
FMD5Hash OldFileHash;
|
|
{
|
|
const FString WavLeafname = FPaths::GetCleanFilename(InWavFilename);
|
|
const FAssetImportInfo::FSourceFile* const FoundSourceFile = ExistingSoundWave->AssetImportData->SourceData.SourceFiles.FindByPredicate([&](const FAssetImportInfo::FSourceFile& InSourceFile) -> bool
|
|
{
|
|
const FString SourceFileLeafname = FPaths::GetCleanFilename(InSourceFile.RelativeFilename);
|
|
return SourceFileLeafname == WavLeafname;
|
|
});
|
|
|
|
if (FoundSourceFile)
|
|
{
|
|
OldFileHash = FoundSourceFile->FileHash;
|
|
}
|
|
}
|
|
|
|
// We only need to import the sound wave if the file hash has changed, or the source hash is invalid
|
|
const FMD5Hash CurrentFileHash = FMD5Hash::HashFile(*InWavFilename);
|
|
if (!OldFileHash.IsValid() || CurrentFileHash != OldFileHash)
|
|
{
|
|
return ImportSoundWave(InSoundWavePackageName, InSoundWaveAssetName, InWavFilename);
|
|
}
|
|
|
|
return ExistingSoundWave;
|
|
}
|
|
|
|
USoundWave* UImportLocalizedDialogueCommandlet::ImportSoundWave(const FString& InSoundWavePackageName, const FString& InSoundWaveAssetName, const FString& InWavFilename) const
|
|
{
|
|
// Find or create the package to host the sound wave
|
|
UPackage* const SoundWavePackage = CreatePackage( *InSoundWavePackageName);
|
|
if (!SoundWavePackage)
|
|
{
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Error, TEXT("Failed to create a sound wave package '%s'."), *InSoundWavePackageName);
|
|
return nullptr;
|
|
}
|
|
|
|
// Make sure the destination package is loaded
|
|
SoundWavePackage->FullyLoad();
|
|
|
|
IAudioEditorModule* AudioEditorModule = &FModuleManager::LoadModuleChecked<IAudioEditorModule>("AudioEditor");
|
|
USoundWave* const SoundWave = AudioEditorModule->ImportSoundWave(SoundWavePackage, *InSoundWaveAssetName, *InWavFilename);
|
|
if (!SoundWave)
|
|
{
|
|
UE_LOG(LogImportLocalizedDialogueCommandlet, Error, TEXT("Failed to import the sound wave asset '%s.%s' from '%s'"), *InSoundWavePackageName, *InSoundWaveAssetName, *InWavFilename);
|
|
return nullptr;
|
|
}
|
|
|
|
// Compress to whatever formats the active target platforms want prior to saving the asset
|
|
{
|
|
ITargetPlatformManagerModule* TPM = GetTargetPlatformManager();
|
|
if (TPM)
|
|
{
|
|
const TArray<ITargetPlatform*>& Platforms = TPM->GetActiveTargetPlatforms();
|
|
for (ITargetPlatform* Platform : Platforms)
|
|
{
|
|
SoundWave->GetCompressedData(Platform->GetWaveFormat(SoundWave), FPlatformCompressionUtilities::GetCookOverrides(*Platform->IniPlatformName()));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add meta-data stating that this asset is owned by this commandlet
|
|
SoundWavePackage->GetMetaData()->SetValue(SoundWave, *GeneratedByCommandletMetaDataKey, *GeneratedByCommandletMetaDataValue);
|
|
|
|
// Write out the updated sound wave asset
|
|
if (!FLocalizedAssetSCCUtil::SavePackageWithSCC(SourceControlInfo, SoundWavePackage))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
return SoundWave;
|
|
}
|