Files
UnrealEngineUWP/Engine/Source/Editor/UnrealEd/Private/Commandlets/ExportDialogueScriptCommandlet.cpp
Andrew Grant 505e2440b1 Copying //UE4/Orion-Staging to //UE4/Main (Origin: //Orion/Dev-General @ 2904087)
==========================
MAJOR FEATURES + CHANGES
==========================

#lockdown Nick.Penwarden

Change 2903938 on 2016/03/10 by Frank.Gigliotti

	Added an instance ID to FAnimMontageInstance

	#CodeReview Laurent.Delayen
	#RB Laurent.Delayen
	#Tests PIE

Change 2903745 on 2016/03/10 by Wes.Hunt

	Update Oodle TPS
	#rb none
	#tests none
	#codereview:john.pollard

Change 2903689 on 2016/03/10 by Uriel.Doyon

	New "LogHeroMaterials" console command, displaying the current state of materials and  textures on the character hero.
	#rb marcus.wasmer
	#codereview marcus.wassmer
	#tests editor, playing PC games, trying the new command

Change 2903669 on 2016/03/10 by Aaron.McLeran

	OR-17180 Make stat soundcues and stat soundwaves NOT display zero volume sounds

	- Change only effects debug stat commands for audio guys

	#rb none
	#tests played paragon with new debug stat commands, confirms doesn't show zero-volume sounds

Change 2903625 on 2016/03/10 by John.Pollard

	XB1 Oodle SDK

	#rb none
	#tests none
	#codereview Jeff.Campeau

Change 2903577 on 2016/03/10 by Ben.Marsh

	Remaking latest build scripts from //UE4/Main @ 2900980.

Change 2903560 on 2016/03/10 by Ben.Marsh

	Initial version of BuildGraph scripts - used to create build processes in UE4 which can be run locally or in parallel across a build farm (assuming synchronization and resource allocation implemented by a separate system). Intended to supersede GUBP.

	Build graphs are declared using an XML script using syntax similar to MSBuild, ANT or NAnt, and consist of the following components:

	* Tasks: Building blocks which can be executed as part of the build process. Many predefined tasks are provided (<Cook>, <Compile>, <Copy>, <Stage>, <Log>, <PakFile>, etc...), and additional tasks may be added be declaring classes derived from AutomationTool.CustomTask in other UAT modules.
	* Nodes: A named sequence of tasks which is executed to produce outputs. Nodes may have input dependencies on other nodes before they can be executed. Declared with the <Node> element in scripts.
	* Agent Groups: A set of nodes nodes which is executed on the same machine if running as part of a build system. Has no effect when building locally. Declared with the <Group> element in scripts.
	* Triggers: Container for groups which should only be executed when explicitly triggered (using the -Trigger=<Name> or -SkipTriggers command line argument). Declared with the <Trigger> element in scripts.
	* Notifiers: Specifies email recipients for failures in one or more nodes, whether they should receive notifications on warnings, and so on.

	Properties can be passed in to a script on the command line, or set procedurally with the <Property Name="Foo" Value="Bar"/> syntax. Properties referenced with the $(Property Name) notation are valid within all strings, and will be expanded as macros when the script is read. If a property name is not set explicitly, it defaults to the contents of an environment variable with the same name.

	Local properties, which only affect the scope of the containing XML element (node, group, etc...) are declared with the <Local Name="Foo" Value="Bar"/> element, and will override a similarly named global property for the local property's scope.

	Any elements can be conditionally defined via the "If" attribute, and are largely identical to MSBuild conditions. Literals in conditions may be quoted with single (') or double (") quotes, or an unquoted sequence of letters, digits and underscore characters. All literals are considered identical regardless of how they are declared, and are considered case-insensitive for comparisons (so true equals 'True', equals "TRUE"). Available operators are "==", "!=", "And", "Or", "!", "(...)", "Exists(...)" and "HasTrailingSlash(...)". A full grammar is written up in Condition.cs.

	File manipulation is done using wildcards and tags. Any attribute that accepts a list of files may consist of: a Perforce-style wildcard (matching any number of "...", "*" and "?" patterns in any location), a full path name, or a reference to a tagged collection of files, denoted by prefixing with a '#' character. Files may be added to a tag set using the <Tag> Task, which also allows performing set union/difference style operations. Each node can declare multiple outputs in the form of a list of named tags, which other nodes can then depend on.

	Build graphs may be executed in parallel as part build system. To do so, the initial graph configuration is generated by running with the -Export=<Filename> argument (producing a JSON file listing the nodes and dependencies to execute). Each participating agent should be synced to the same changelist, and UAT should be re-run with the appropriate -Node=<Name> argument. Outputs from different nodes are transferred between agents via shared storage, typically a network share, the path to which can be specified on the command line using the -SharedStorageDir=<Path> argument. Note that the allocation of machines, and coordination between them, is assumed to be managed by an external system.

	A schema for the known set of tasks can be generated by running UAT with the "-Schema=<FileName>" option. Generating a schema and referencing it from a BuildGraph script allows Visual Studio to validate and auto-complete elements as you type.

	#rb none
	#codereview Marc.Audy, Wes.Hunt, Matthew.Griffin, Richard.Fawcett
	#tests local only so far, but not part of any build process yet

Change 2903539 on 2016/03/10 by John.Pollard

	Improve replay playback debugging of character movement

	#rb none
	#tests replays

Change 2903526 on 2016/03/10 by Ben.Marsh

	Remake changes from //UE4/Main without integration history, to add support for BuildGraph tasks.

	#rb none
	#tests none

Change 2903512 on 2016/03/10 by Dan.Youhon

	Modify minimum Duration values for JumpForce and MoveToForce ability tasks so that having minimum Duration values doesn't trigger check()s

	#rb None
	#tests Compiles

Change 2903474 on 2016/03/10 by Marc.Audy

	Fix crash if ChildActor is null
	#rb None
	#tests None

Change 2903314 on 2016/03/10 by Marc.Audy

	Fix ParentComponent not being persisted and fixup content that was saved in the window it was broken
	#rb James.Golding
	#tests Selection of child actors works as expected
	#jira UE-28201

Change 2903298 on 2016/03/10 by Simon.Tovey

	Disabling the trails optimization.

	#tests none
	#rb none

	#codereview Olaf.Piesche

Change 2903124 on 2016/03/10 by Robert.Manuszewski

	Small refactor to pak signing to help with exe protection

	#rb none
	#tests none

[CL 2907678 by Andrew Grant in Main branch]
2016-03-13 18:53:13 -04:00

527 lines
19 KiB
C++

// Copyright 1998-2016 Epic Games, Inc. All Rights Reserved.
#include "UnrealEd.h"
#include "Commandlets/ExportDialogueScriptCommandlet.h"
#include "AssetRegistryModule.h"
#include "Sound/DialogueWave.h"
#include "Sound/DialogueVoice.h"
#include "Internationalization/InternationalizationManifest.h"
#include "Internationalization/InternationalizationArchive.h"
#include "JsonInternationalizationManifestSerializer.h"
#include "JsonInternationalizationArchiveSerializer.h"
DEFINE_LOG_CATEGORY_STATIC(LogExportDialogueScriptCommandlet, Log, All);
namespace
{
struct FCollapsedDialogueContextKey
{
FCollapsedDialogueContextKey(const UDialogueWave* InDialogueWave, const FDialogueContextMapping* InContext, FString InLocalizedSpokenText)
: DialogueWave(InDialogueWave)
, Context(InContext)
, LocalizedSpokenText(MoveTemp(InLocalizedSpokenText))
{
}
bool operator==(const FCollapsedDialogueContextKey& InOther) const
{
// We only care about the text that is spoken, and the voice that is speaking it
return LocalizedSpokenText.Equals(InOther.LocalizedSpokenText, ESearchCase::CaseSensitive)
&& Context->Context.Speaker == InOther.Context->Context.Speaker;
}
bool operator!=(const FCollapsedDialogueContextKey& InOther) const
{
return !(*this == InOther);
}
friend inline uint32 GetTypeHash(const FCollapsedDialogueContextKey& InKey)
{
// We only care about the text that is spoken, and the voice that is speaking it
uint32 KeyHash = 0;
KeyHash = HashCombine(KeyHash, FCrc::StrCrc32(*InKey.LocalizedSpokenText)); // Need case-sensitive hash
KeyHash = HashCombine(KeyHash, GetTypeHash(InKey.Context->Context.Speaker));
return KeyHash;
}
const UDialogueWave* DialogueWave;
const FDialogueContextMapping* Context;
FString LocalizedSpokenText;
};
}
UExportDialogueScriptCommandlet::UExportDialogueScriptCommandlet(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
int32 UExportDialogueScriptCommandlet::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(LogExportDialogueScriptCommandlet, 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(LogExportDialogueScriptCommandlet, 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(LogExportDialogueScriptCommandlet, Error, TEXT("No source path specified."));
return -1;
}
// Destination path to the root folder that dialogue script CSV files live in
FString DestinationPath;
if (!GetPathFromConfig(*SectionName, TEXT("DestinationPath"), DestinationPath, ConfigPath))
{
UE_LOG(LogExportDialogueScriptCommandlet, Error, TEXT("No destination path specified."));
return -1;
}
// Get culture directory setting, default to true if not specified (used to allow picking of export directory with windows file dialog from Translation Editor)
bool bUseCultureDirectory = true;
if (!GetBoolFromConfig(*SectionName, TEXT("bUseCultureDirectory"), bUseCultureDirectory, ConfigPath))
{
bUseCultureDirectory = true;
}
// Get the native culture
FString NativeCulture;
if (!GetStringFromConfig(*SectionName, TEXT("NativeCulture"), NativeCulture, ConfigPath))
{
UE_LOG(LogExportDialogueScriptCommandlet, 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(LogExportDialogueScriptCommandlet, Error, TEXT("No cultures specified for import."));
return -1;
}
// Get the manifest name
FString ManifestName;
if (!GetStringFromConfig(*SectionName, TEXT("ManifestName"), ManifestName, ConfigPath))
{
UE_LOG(LogExportDialogueScriptCommandlet, Error, TEXT("No manifest name specified."));
return -1;
}
// Get the archive name
FString ArchiveName;
if (!GetStringFromConfig(*SectionName, TEXT("ArchiveName"), ArchiveName, ConfigPath))
{
UE_LOG(LogExportDialogueScriptCommandlet, Error, TEXT("No archive name specified."));
return -1;
}
// Get the dialogue script name
FString DialogueScriptName;
if (!GetStringFromConfig(*SectionName, TEXT("DialogueScriptName"), DialogueScriptName, ConfigPath))
{
UE_LOG(LogExportDialogueScriptCommandlet, Error, TEXT("No dialogue script name specified."));
return -1;
}
// Prepare the manifest
TSharedPtr<FInternationalizationManifest> InternationalizationManifest;
{
const FString ManifestFileName = SourcePath / ManifestName;
if (!FPaths::FileExists(ManifestFileName))
{
UE_LOG(LogExportDialogueScriptCommandlet, Error, TEXT("Failed to find manifest '%s'."), *ManifestFileName);
return -1;
}
const TSharedPtr<FJsonObject> ManifestJsonObject = ReadJSONTextFile(ManifestFileName);
if (!ManifestJsonObject.IsValid())
{
UE_LOG(LogExportDialogueScriptCommandlet, Error, TEXT("Failed to parse manifest '%s'."), *ManifestFileName);
return -1;
}
FJsonInternationalizationManifestSerializer ManifestSerializer;
InternationalizationManifest = MakeShareable(new FInternationalizationManifest());
if (!ManifestSerializer.DeserializeManifest(ManifestJsonObject.ToSharedRef(), InternationalizationManifest.ToSharedRef()))
{
UE_LOG(LogExportDialogueScriptCommandlet, Error, TEXT("Failed to deserialize manifest '%s'."), *ManifestFileName);
return -1;
}
}
// Prepare the native archive
TSharedPtr<FInternationalizationArchive> NativeArchive;
{
const FString NativeCulturePath = SourcePath / NativeCulture;
const FString NativeArchiveFileName = NativeCulturePath / ArchiveName;
if (!FPaths::FileExists(NativeArchiveFileName))
{
UE_LOG(LogExportDialogueScriptCommandlet, Error, TEXT("Failed to find archive '%s'."), *NativeArchiveFileName);
return -1;
}
const TSharedPtr<FJsonObject> ArchiveJsonObject = ReadJSONTextFile(NativeArchiveFileName);
if (!ArchiveJsonObject.IsValid())
{
UE_LOG(LogExportDialogueScriptCommandlet, Error, TEXT("Failed to parse archive '%s'."), *NativeArchiveFileName);
return -1;
}
FJsonInternationalizationArchiveSerializer ArchiveSerializer;
NativeArchive = MakeShareable(new FInternationalizationArchive());
if (!ArchiveSerializer.DeserializeArchive(ArchiveJsonObject.ToSharedRef(), NativeArchive.ToSharedRef()))
{
UE_LOG(LogExportDialogueScriptCommandlet, Error, TEXT("Failed to deserialize archive '%s'."), *NativeArchiveFileName);
return -1;
}
}
const FString RootAssetPath = FApp::HasGameName() ? TEXT("/Game") : TEXT("/Engine");
// 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()->GetFName(), /*bIncludeLocalizedAssets*/false, AssetDataArrayForDialogueWaves))
{
UE_LOG(LogExportDialogueScriptCommandlet, Error, TEXT("Unable to get dialogue wave asset data from asset registry."));
return -1;
}
for (const FString& CultureName : CulturesToGenerate)
{
const bool bIsNativeCulture = CultureName == NativeCulture;
const FString CultureSourcePath = SourcePath / CultureName;
const FString CultureDestinationPath = DestinationPath / (bUseCultureDirectory ? CultureName : TEXT(""));
// Prepare the culture archive
TSharedPtr<FInternationalizationArchive> CultureArchive;
if (bIsNativeCulture)
{
CultureArchive = NativeArchive;
}
else
{
const FString ArchiveFileName = CultureSourcePath / ArchiveName;
if (!FPaths::FileExists(ArchiveFileName))
{
UE_LOG(LogExportDialogueScriptCommandlet, Error, TEXT("Failed to find archive '%s'."), *ArchiveFileName);
continue;
}
const TSharedPtr<FJsonObject> ArchiveJsonObject = ReadJSONTextFile(ArchiveFileName);
if (!ArchiveJsonObject.IsValid())
{
UE_LOG(LogExportDialogueScriptCommandlet, Error, TEXT("Failed to parse archive '%s'."), *ArchiveFileName);
continue;
}
FJsonInternationalizationArchiveSerializer ArchiveSerializer;
CultureArchive = MakeShareable(new FInternationalizationArchive());
if (!ArchiveSerializer.DeserializeArchive(ArchiveJsonObject.ToSharedRef(), CultureArchive.ToSharedRef()))
{
UE_LOG(LogExportDialogueScriptCommandlet, Error, TEXT("Failed to deserialize archive '%s'."), *ArchiveFileName);
continue;
}
}
TArray<TSharedPtr<FDialogueScriptEntry>> ExportedDialogueLines;
for (const FAssetData& AssetData : AssetDataArrayForDialogueWaves)
{
// Verify that the found asset is a dialogue wave
if (AssetData.GetClass() != UDialogueWave::StaticClass())
{
UE_LOG(LogExportDialogueScriptCommandlet, 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(LogExportDialogueScriptCommandlet, Error, TEXT("Asset registry found asset '%s', but the dialogue wave could not be accessed."), *AssetData.AssetName.ToString());
continue;
}
// This maps collapsed context keys to additional contexts that were collapsed into the primary context (the context within the key) - all contexts belong to the dialogue wave in the key
// If multiple contexts have the same speaking voice and use the same dialogue (because it translates to the same text), then only one of those contexts needs to be exported
// The resultant audio file will create a shared asset automatically when the dialogue is imported
TMap<FCollapsedDialogueContextKey, TArray<const FDialogueContextMapping*>> CollapsedDialogueContexts;
// Iterate over each context to build the list of unique entries
for (FDialogueContextMapping& ContextMapping : DialogueWave->ContextMappings)
{
const FString ContextLocalizationKey = DialogueWave->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 = InternationalizationManifest->FindEntryByKey(FDialogueConstants::DialogueNamespace, ContextLocalizationKey, &DialogueWave->SpokenText);
if (!ContextManifestEntry.IsValid())
{
UE_LOG(LogExportDialogueScriptCommandlet, Log, TEXT("No internationalization manifest entry was found for context '%s' in culture '%s'. This context will be skipped."), *ContextLocalizationKey, *CultureName);
continue;
}
// Find the correct entry for our context
const FContext* ContextManifestEntryContext = ContextManifestEntry->FindContextByKey(ContextLocalizationKey);
check(ContextManifestEntryContext); // This should never fail as we pass in the key to FindEntryByKey
// We might have a native translation for this text, in which case we need to export the translation for that text, rather than for the source text
FString SourceText = DialogueWave->SpokenText;
{
TSharedPtr<FArchiveEntry> NativeArchiveEntry = NativeArchive->FindEntryBySource(FDialogueConstants::DialogueNamespace, SourceText, ContextManifestEntryContext->KeyMetadataObj);
if (NativeArchiveEntry.IsValid())
{
SourceText = NativeArchiveEntry->Translation.Text;
}
}
// Find the archive entry so we can export the correct text
FString TranslatedText;
if (bIsNativeCulture)
{
TranslatedText = SourceText;
}
else
{
TSharedPtr<FArchiveEntry> ArchiveEntry = CultureArchive->FindEntryBySource(FDialogueConstants::DialogueNamespace, SourceText, ContextManifestEntryContext->KeyMetadataObj);
if (ArchiveEntry.IsValid())
{
TranslatedText = ArchiveEntry->Translation.Text;
}
}
if (TranslatedText.IsEmpty())
{
UE_LOG(LogExportDialogueScriptCommandlet, Log, TEXT("Empty translation found for context '%s' in culture '%s'. This context will be skipped."), *ContextLocalizationKey, *CultureName);
continue;
}
const auto CollapsedDialogueContextKey = FCollapsedDialogueContextKey(DialogueWave, &ContextMapping, TranslatedText);
TArray<const FDialogueContextMapping*>* MergedContextsPtr = CollapsedDialogueContexts.Find(CollapsedDialogueContextKey);
if (MergedContextsPtr)
{
MergedContextsPtr->Add(&ContextMapping);
}
else
{
CollapsedDialogueContexts.Add(CollapsedDialogueContextKey);
}
}
// Iterate over the unique contexts and generate exported data for them
for (const auto& CollapsedDialogueContextPair : CollapsedDialogueContexts)
{
TSharedRef<FDialogueScriptEntry> ExportedDialogueLine = MakeShareable(new FDialogueScriptEntry());
PopulateDialogueScriptEntry(CollapsedDialogueContextPair.Key.DialogueWave, *CollapsedDialogueContextPair.Key.Context, CollapsedDialogueContextPair.Value, CollapsedDialogueContextPair.Key.LocalizedSpokenText, *ExportedDialogueLine);
ExportedDialogueLines.Add(ExportedDialogueLine);
}
}
// Sort the exported lines to maintain a consistent order between exports
// Sort order is speaking voice name, then localized dialogue
ExportedDialogueLines.Sort([](const TSharedPtr<FDialogueScriptEntry>& InFirstEntry, const TSharedPtr<FDialogueScriptEntry>& InSecondEntry) -> bool
{
const int32 SpeakingVoiceResult = InFirstEntry->SpeakingVoice.Compare(InSecondEntry->SpeakingVoice, ESearchCase::CaseSensitive);
if (SpeakingVoiceResult < 0)
{
return true;
}
if (SpeakingVoiceResult == 0 && InFirstEntry->SpokenDialogue.Compare(InSecondEntry->SpokenDialogue, ESearchCase::CaseSensitive) < 0)
{
return true;
}
return false;
});
{
FString CSVFileData;
CSVFileData += GenerateCSVHeader() + TEXT("\n");
for (const auto& DialogueScriptEntry : ExportedDialogueLines)
{
CSVFileData += GenerateCSVRow(*DialogueScriptEntry) + TEXT("\n");
}
const FString CSVFileName = CultureDestinationPath / DialogueScriptName;
const bool bCSVFileSaved = FLocalizedAssetSCCUtil::SaveFileWithSCC(SourceControlInfo, CSVFileName, [&](const FString& InSaveFileName) -> bool
{
return FFileHelper::SaveStringToFile(CSVFileData, *InSaveFileName, FFileHelper::EEncodingOptions::ForceUTF8);
});
if (!bCSVFileSaved)
{
UE_LOG(LogExportDialogueScriptCommandlet, Error, TEXT("Failed to write CSV file for culture '%s' to '%s'."), *CultureName, *CSVFileName);
continue;
}
}
}
return 0;
}
FString UExportDialogueScriptCommandlet::GenerateCSVHeader()
{
FString CSVHeader;
for (TFieldIterator<const UProperty> PropertyIt(FDialogueScriptEntry::StaticStruct(), EFieldIteratorFlags::IncludeSuper, EFieldIteratorFlags::ExcludeDeprecated, EFieldIteratorFlags::IncludeInterfaces); PropertyIt; ++PropertyIt)
{
if (!CSVHeader.IsEmpty())
{
CSVHeader += TEXT(",");
}
const FString PropertyName = PropertyIt->GetName();
CSVHeader += TEXT("\"");
CSVHeader += PropertyName.Replace(TEXT("\""), TEXT("\"\""));
CSVHeader += TEXT("\"");
}
return CSVHeader;
}
FString UExportDialogueScriptCommandlet::GenerateCSVRow(const FDialogueScriptEntry& InDialogueScriptEntry)
{
FString CSVRow;
for (TFieldIterator<const UProperty> PropertyIt(FDialogueScriptEntry::StaticStruct(), EFieldIteratorFlags::IncludeSuper, EFieldIteratorFlags::ExcludeDeprecated, EFieldIteratorFlags::IncludeInterfaces); PropertyIt; ++PropertyIt)
{
if (!CSVRow.IsEmpty())
{
CSVRow += TEXT(",");
}
FString PropertyValue;
PropertyIt->ExportText_Direct(PropertyValue, PropertyIt->ContainerPtrToValuePtr<void>(&InDialogueScriptEntry), nullptr, nullptr, PPF_None);
CSVRow += TEXT("\"");
CSVRow += PropertyValue.Replace(TEXT("\""), TEXT("\"\""));
CSVRow += TEXT("\"");
}
return CSVRow;
}
void UExportDialogueScriptCommandlet::PopulateDialogueScriptEntry(const UDialogueWave* InDialogueWave, const FDialogueContextMapping& InPrimaryContext, const TArray<const FDialogueContextMapping*>& InAdditionalContexts, const FString& InLocalizedDialogue, FDialogueScriptEntry& OutDialogueScriptEntry)
{
auto AppendTargetVoices = [&](const FDialogueContext& InContext)
{
if (InContext.Targets.Num() > 0)
{
FString TargetVoicesText;
const bool bIsArray = InContext.Targets.Num() > 1;
if (bIsArray)
{
TargetVoicesText += TEXT("[");
}
bool bIsFirst = true;
for (const UDialogueVoice* TargetVoice : InContext.Targets)
{
if (!bIsFirst)
{
TargetVoicesText += TEXT(",");
}
bIsFirst = false;
TargetVoicesText += TargetVoice->GetName();
}
if (bIsArray)
{
TargetVoicesText += TEXT("]");
}
OutDialogueScriptEntry.TargetVoices.Add(MoveTemp(TargetVoicesText));
}
};
auto AppendTargetVoiceGUIDs = [&](const FDialogueContext& InContext)
{
if (InContext.Targets.Num() > 0)
{
FString TargetVoiceGUIDsText;
const bool bIsArray = InContext.Targets.Num() > 1;
if (bIsArray)
{
TargetVoiceGUIDsText += TEXT("[");
}
bool bIsFirst = true;
for (const UDialogueVoice* TargetVoice : InContext.Targets)
{
if (!bIsFirst)
{
TargetVoiceGUIDsText += TEXT(",");
}
bIsFirst = false;
TargetVoiceGUIDsText += TargetVoice->LocalizationGUID.ToString();
}
if (bIsArray)
{
TargetVoiceGUIDsText += TEXT("]");
}
OutDialogueScriptEntry.TargetVoiceGUIDs.Add(MoveTemp(TargetVoiceGUIDsText));
}
};
OutDialogueScriptEntry.SpokenDialogue = InLocalizedDialogue;
OutDialogueScriptEntry.VoiceActorDirection = InDialogueWave->VoiceActorDirection;
OutDialogueScriptEntry.AudioFileName = InDialogueWave->GetContextRecordedAudioFilename(InPrimaryContext);
OutDialogueScriptEntry.DialogueAsset = InDialogueWave->GetPathName();
OutDialogueScriptEntry.SpeakingVoice = InPrimaryContext.Context.Speaker->GetName();
OutDialogueScriptEntry.SpeakingVoiceGUID = InPrimaryContext.Context.Speaker->LocalizationGUID.ToString();
OutDialogueScriptEntry.DialogueAssetGUID = InDialogueWave->LocalizationGUID.ToString();
OutDialogueScriptEntry.LocalizationKeys.Add(InDialogueWave->GetContextLocalizationKey(InPrimaryContext));
AppendTargetVoices(InPrimaryContext.Context);
AppendTargetVoiceGUIDs(InPrimaryContext.Context);
for (const FDialogueContextMapping* AdditionalContext : InAdditionalContexts)
{
OutDialogueScriptEntry.LocalizationKeys.Add(InDialogueWave->GetContextLocalizationKey(*AdditionalContext));
AppendTargetVoices(AdditionalContext->Context);
AppendTargetVoiceGUIDs(AdditionalContext->Context);
}
}