Files
UnrealEngineUWP/Engine/Source/Editor/UnrealEd/Private/Commandlets/ImportDialogueScriptCommandlet.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

373 lines
14 KiB
C++

// Copyright 1998-2016 Epic Games, Inc. All Rights Reserved.
#include "UnrealEd.h"
#include "Commandlets/ImportDialogueScriptCommandlet.h"
#include "CsvParser.h"
#include "Sound/DialogueWave.h"
#include "Internationalization/InternationalizationManifest.h"
#include "Internationalization/InternationalizationArchive.h"
#include "JsonInternationalizationManifestSerializer.h"
#include "JsonInternationalizationArchiveSerializer.h"
DEFINE_LOG_CATEGORY_STATIC(LogImportDialogueScriptCommandlet, Log, All);
UImportDialogueScriptCommandlet::UImportDialogueScriptCommandlet(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
int32 UImportDialogueScriptCommandlet::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(LogImportDialogueScriptCommandlet, 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(LogImportDialogueScriptCommandlet, Error, TEXT("No config section specified."));
return -1;
}
SectionName = *SectionNameParamVal;
}
// Source path to the root folder that dialogue script CSV files live in
FString SourcePath;
if (!GetPathFromConfig(*SectionName, TEXT("SourcePath"), SourcePath, ConfigPath))
{
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("No source path specified."));
return -1;
}
// Destination path to the root folder that manifest/archive files live in
FString DestinationPath;
if (!GetPathFromConfig(*SectionName, TEXT("DestinationPath"), DestinationPath, ConfigPath))
{
UE_LOG(LogImportDialogueScriptCommandlet, 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(LogImportDialogueScriptCommandlet, 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(LogImportDialogueScriptCommandlet, Error, TEXT("No cultures specified for import."));
return -1;
}
// Get the manifest name
FString ManifestName;
if (!GetStringFromConfig(*SectionName, TEXT("ManifestName"), ManifestName, ConfigPath))
{
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("No manifest name specified."));
return -1;
}
// Get the archive name
FString ArchiveName;
if (!GetStringFromConfig(*SectionName, TEXT("ArchiveName"), ArchiveName, ConfigPath))
{
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("No archive name specified."));
return -1;
}
// Get the dialogue script name
FString DialogueScriptName;
if (!GetStringFromConfig(*SectionName, TEXT("DialogueScriptName"), DialogueScriptName, ConfigPath))
{
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("No dialogue script name specified."));
return -1;
}
// Prepare the manifest
{
const FString ManifestFileName = DestinationPath / ManifestName;
if (!FPaths::FileExists(ManifestFileName))
{
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("Failed to find manifest '%s'."), *ManifestFileName);
return -1;
}
const TSharedPtr<FJsonObject> ManifestJsonObject = ReadJSONTextFile(ManifestFileName);
if (!ManifestJsonObject.IsValid())
{
UE_LOG(LogImportDialogueScriptCommandlet, 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(LogImportDialogueScriptCommandlet, Error, TEXT("Failed to deserialize manifest '%s'."), *ManifestFileName);
return -1;
}
}
// Prepare the native archive
{
const FString NativeCulturePath = DestinationPath / NativeCulture;
const FString NativeArchiveFileName = NativeCulturePath / ArchiveName;
if (!FPaths::FileExists(NativeArchiveFileName))
{
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("Failed to find archive '%s'."), *NativeArchiveFileName);
return -1;
}
const TSharedPtr<FJsonObject> ArchiveJsonObject = ReadJSONTextFile(NativeArchiveFileName);
if (!ArchiveJsonObject.IsValid())
{
UE_LOG(LogImportDialogueScriptCommandlet, 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(LogImportDialogueScriptCommandlet, Error, TEXT("Failed to deserialize archive '%s'."), *NativeArchiveFileName);
return -1;
}
}
// Import the native culture first as this may trigger additional translations in foreign archives
{
const FString CultureSourcePath = SourcePath / (bUseCultureDirectory ? NativeCulture : TEXT(""));
const FString CultureDestinationPath = DestinationPath / NativeCulture;
ImportDialogueScriptForCulture(CultureSourcePath / DialogueScriptName, CultureDestinationPath / ArchiveName, NativeCulture, true);
}
// Import any remaining cultures
for (const FString& CultureName : CulturesToGenerate)
{
// Skip the native culture as we already processed it above
if (CultureName == NativeCulture)
{
continue;
}
const FString CultureSourcePath = SourcePath / (bUseCultureDirectory ? CultureName : TEXT(""));
const FString CultureDestinationPath = DestinationPath / CultureName;
ImportDialogueScriptForCulture(CultureSourcePath / DialogueScriptName, CultureDestinationPath / ArchiveName, CultureName, false);
}
return 0;
}
bool UImportDialogueScriptCommandlet::ImportDialogueScriptForCulture(const FString& InDialogueScriptFileName, const FString& InCultureArchiveFileName, const FString& InCultureName, const bool bIsNativeCulture)
{
// Load dialogue script file contents to string
FString DialogScriptFileContents;
if (!FFileHelper::LoadFileToString(DialogScriptFileContents, *InDialogueScriptFileName))
{
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("Failed to load contents of dialog script file '%s' for culture '%s'."), *InDialogueScriptFileName, *InCultureName);
return false;
}
// Parse dialogue script file contents
const FCsvParser DialogScriptFileParser(DialogScriptFileContents);
const FCsvParser::FRows& Rows = DialogScriptFileParser.GetRows();
// Validate dialogue script row count
if (Rows.Num() <= 0)
{
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("Dialogue script file has insufficient rows for culture '%s'. Expected at least 1 row, got %d."), *InCultureName, Rows.Num());
return false;
}
const UProperty* SpokenDialogueProperty = FDialogueScriptEntry::StaticStruct()->FindPropertyByName(GET_MEMBER_NAME_CHECKED(FDialogueScriptEntry, SpokenDialogue));
const UProperty* LocalizationKeysProperty = FDialogueScriptEntry::StaticStruct()->FindPropertyByName(GET_MEMBER_NAME_CHECKED(FDialogueScriptEntry, LocalizationKeys));
// We need the SpokenDialogue and LocalizationKeys properties in order to perform the import, so find their respective columns in the CSV data
int32 SpokenDialogueColumnIndex = INDEX_NONE;
int32 LocalizationKeysColumnIndex = INDEX_NONE;
{
const FString SpokenDialogueColumnName = SpokenDialogueProperty->GetName();
const FString LocalizationKeysColumnName = LocalizationKeysProperty->GetName();
const auto& HeaderRowData = Rows[0];
for (int32 ColumnIndex = 0; ColumnIndex < HeaderRowData.Num(); ++ColumnIndex)
{
const TCHAR* const CellData = HeaderRowData[ColumnIndex];
if (FCString::Stricmp(CellData, *SpokenDialogueColumnName) == 0)
{
SpokenDialogueColumnIndex = ColumnIndex;
}
else if (FCString::Stricmp(CellData, *LocalizationKeysColumnName) == 0)
{
LocalizationKeysColumnIndex = ColumnIndex;
}
if (SpokenDialogueColumnIndex != INDEX_NONE && LocalizationKeysColumnIndex != INDEX_NONE)
{
break;
}
}
}
if (SpokenDialogueColumnIndex == INDEX_NONE)
{
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("Dialogue script file is missing the required column '%s' for culture '%s'."), *SpokenDialogueProperty->GetName(), *InCultureName);
return false;
}
if (LocalizationKeysColumnIndex == INDEX_NONE)
{
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("Dialogue script file is missing the required column '%s' for culture '%s'."), *LocalizationKeysProperty->GetName(), *InCultureName);
return false;
}
// Prepare the culture archive
TSharedPtr<FInternationalizationArchive> CultureArchive;
if (bIsNativeCulture)
{
CultureArchive = NativeArchive;
}
else
{
if (!FPaths::FileExists(InCultureArchiveFileName))
{
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("Failed to find archive '%s'."), *InCultureArchiveFileName);
return false;
}
const TSharedPtr<FJsonObject> ArchiveJsonObject = ReadJSONTextFile(InCultureArchiveFileName);
if (!ArchiveJsonObject.IsValid())
{
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("Failed to parse archive '%s'."), *InCultureArchiveFileName);
return false;
}
FJsonInternationalizationArchiveSerializer ArchiveSerializer;
CultureArchive = MakeShareable(new FInternationalizationArchive());
if (!ArchiveSerializer.DeserializeArchive(ArchiveJsonObject.ToSharedRef(), CultureArchive.ToSharedRef()))
{
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("Failed to deserialize archive '%s'."), *InCultureArchiveFileName);
return false;
}
}
bool bHasUpdatedArchive = false;
// Parse each row of the CSV data
for (int32 RowIndex = 1; RowIndex < Rows.Num(); ++RowIndex)
{
const auto& RowData = Rows[RowIndex];
FDialogueScriptEntry ParsedScriptEntry;
// Parse the SpokenDialogue data
{
const TCHAR* const CellData = RowData[SpokenDialogueColumnIndex];
if (SpokenDialogueProperty->ImportText(CellData, SpokenDialogueProperty->ContainerPtrToValuePtr<void>(&ParsedScriptEntry), PPF_None, nullptr) == nullptr)
{
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("Failed to parse the required column '%s' for row '%d' for culture '%s'."), *SpokenDialogueProperty->GetName(), RowIndex, *InCultureName);
continue;
}
}
// Parse the LocalizationKeys data
{
const TCHAR* const CellData = RowData[LocalizationKeysColumnIndex];
if (LocalizationKeysProperty->ImportText(CellData, LocalizationKeysProperty->ContainerPtrToValuePtr<void>(&ParsedScriptEntry), PPF_None, nullptr) == nullptr)
{
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("Failed to parse the required column '%s' for row '%d' for culture '%s'."), *LocalizationKeysProperty->GetName(), RowIndex, *InCultureName);
continue;
}
}
for (const FString& ContextLocalizationKey : ParsedScriptEntry.LocalizationKeys)
{
// Find the manifest entry so that we can find the corresponding archive entry
TSharedPtr<FManifestEntry> ContextManifestEntry = InternationalizationManifest->FindEntryByKey(FDialogueConstants::DialogueNamespace, ContextLocalizationKey);
if (!ContextManifestEntry.IsValid())
{
UE_LOG(LogImportDialogueScriptCommandlet, Log, TEXT("No internationalization manifest entry was found for context '%s' in culture '%s'. This context will be skipped."), *ContextLocalizationKey, *InCultureName);
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
// Find the correct source text (we might have a native translation that we should update instead of the source)
FString SourceText = ContextManifestEntry->Source.Text;
if (!bIsNativeCulture)
{
TSharedPtr<FArchiveEntry> NativeArchiveEntry = NativeArchive->FindEntryBySource(FDialogueConstants::DialogueNamespace, SourceText, ContextManifestEntryContext->KeyMetadataObj);
if (NativeArchiveEntry.IsValid())
{
SourceText = NativeArchiveEntry->Translation.Text;
}
}
// Update (or add) the entry in the archive
TSharedPtr<FArchiveEntry> ArchiveEntry = CultureArchive->FindEntryBySource(FDialogueConstants::DialogueNamespace, SourceText, ContextManifestEntryContext->KeyMetadataObj);
if (ArchiveEntry.IsValid())
{
if (!ArchiveEntry->Translation.Text.Equals(ParsedScriptEntry.SpokenDialogue, ESearchCase::CaseSensitive))
{
bHasUpdatedArchive = true;
ArchiveEntry->Translation.Text = ParsedScriptEntry.SpokenDialogue;
}
}
else
{
if (CultureArchive->AddEntry(FDialogueConstants::DialogueNamespace, SourceText, ParsedScriptEntry.SpokenDialogue, ContextManifestEntryContext->KeyMetadataObj, false))
{
bHasUpdatedArchive = true;
}
}
}
}
// Write out the updated archive file
if (bHasUpdatedArchive)
{
TSharedRef<FJsonObject> ArchiveJsonObj = MakeShareable(new FJsonObject());
FJsonInternationalizationArchiveSerializer ArchiveSerializer;
ArchiveSerializer.SerializeArchive(CultureArchive.ToSharedRef(), ArchiveJsonObj);
if (!WriteJSONToTextFile(ArchiveJsonObj, InCultureArchiveFileName, SourceControlInfo))
{
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("Failed to write archive file for culture '%s' to '%s'."), *InCultureName, *InCultureArchiveFileName);
return false;
}
}
return true;
}