Initial push to repo
This commit is contained in:
65
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditor.cpp
Normal file
65
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditor.cpp
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSEditor.h"
|
||||
|
||||
#include "ISettingsModule.h"
|
||||
#include "ISettingsSection.h"
|
||||
#include "SUDSEditorSettings.h"
|
||||
#include "SUDSScriptActions.h"
|
||||
#include "Interfaces/IPluginManager.h"
|
||||
#include "Styling/SlateStyle.h"
|
||||
#include "Styling/SlateStyleRegistry.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FSUDSModule"
|
||||
|
||||
void FSUDSEditorModule::StartupModule()
|
||||
{
|
||||
ScriptActions = MakeShared<FSUDSScriptActions>();
|
||||
FAssetToolsModule::GetModule().Get().RegisterAssetTypeActions(ScriptActions.ToSharedRef());
|
||||
|
||||
auto SudsPlugin = IPluginManager::Get().FindPlugin(TEXT("SUDS"));
|
||||
if (SudsPlugin.IsValid())
|
||||
{
|
||||
const FString IconDir = SudsPlugin->GetBaseDir() + "/Content/Editor/Slate/Icons";
|
||||
const FVector2D Sz16 = FVector2D(16.0f, 16.0f);
|
||||
const FVector2D Sz64 = FVector2D(64.0f, 64.0f);
|
||||
|
||||
StyleSet = MakeShared<FSlateStyleSet>("SUDSStyleSet");
|
||||
StyleSet->Set(TEXT("ClassIcon.SUDSScript"), new FSlateImageBrush(IconDir / TEXT("SUDSScript_16x.png"), Sz16));
|
||||
StyleSet->Set(TEXT("ClassThumbnail.SUDSScript"), new FSlateImageBrush(IconDir / TEXT("SUDSScript_64x.png"), Sz64));
|
||||
|
||||
FSlateStyleRegistry::RegisterSlateStyle(*StyleSet.Get());
|
||||
}
|
||||
|
||||
// register settings
|
||||
ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings");
|
||||
if (SettingsModule)
|
||||
{
|
||||
ISettingsSectionPtr SettingsSection = SettingsModule->RegisterSettings("Project", "Plugins", "SUDS Editor",
|
||||
LOCTEXT("SUDSEditorSettingsName", "SUDS Editor"),
|
||||
LOCTEXT("SUDSEditorSettingsDescription", "Configure the editor parts of SUDS."),
|
||||
GetMutableDefault<USUDSEditorSettings>()
|
||||
);
|
||||
}
|
||||
|
||||
UE_LOG(LogSUDSEditor, Log, TEXT("SUDS Editor Module Started"))
|
||||
|
||||
}
|
||||
|
||||
void FSUDSEditorModule::ShutdownModule()
|
||||
{
|
||||
if (StyleSet.IsValid())
|
||||
{
|
||||
FSlateStyleRegistry::UnRegisterSlateStyle(*StyleSet.Get());
|
||||
}
|
||||
|
||||
if (!FModuleManager::Get().IsModuleLoaded("AssetTools")) return;
|
||||
FAssetToolsModule::GetModule().Get().UnregisterAssetTypeActions(ScriptActions.ToSharedRef());
|
||||
|
||||
UE_LOG(LogSUDSEditor, Log, TEXT("SUDS Editor Module Shut Down"))
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
IMPLEMENT_MODULE(FSUDSEditorModule, SUDSEditor)
|
||||
DEFINE_LOG_CATEGORY(LogSUDSEditor);
|
||||
280
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorScriptTools.cpp
Normal file
280
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorScriptTools.cpp
Normal file
@@ -0,0 +1,280 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSEditorScriptTools.h"
|
||||
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptImporter.h"
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSScriptNodeGosub.h"
|
||||
#include "SUDSScriptNodeSet.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
#include "EditorFramework/AssetImportData.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/PackageName.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "UObject/Package.h"
|
||||
|
||||
void FSUDSEditorScriptTools::WriteBackTextIDs(USUDSScript* Script, FSUDSMessageLogger& Logger)
|
||||
{
|
||||
const auto& SrcData = Script->AssetImportData->SourceData;
|
||||
if (SrcData.SourceFiles.Num() == 1)
|
||||
{
|
||||
TArray<FString> Lines;
|
||||
FString SourceFile = SrcData.SourceFiles[0].RelativeFilename;
|
||||
auto Package = Script->GetPackage();
|
||||
if (FPaths::IsRelative(SourceFile) && Package)
|
||||
{
|
||||
FString PackagePath = FPackageName::LongPackageNameToFilename(FPackageName::GetLongPackagePath(Package->GetPathName()));
|
||||
SourceFile = FPaths::ConvertRelativePathToFull(PackagePath, SourceFile);
|
||||
}
|
||||
if (FFileHelper::LoadFileToStringArray(Lines, *SourceFile))
|
||||
{
|
||||
const int PrevErrs = Logger.NumErrors();
|
||||
const bool bHeaderChanges = WriteBackTextIDsFromNodes(Script->GetHeaderNodes(), Lines, Script->GetName(), Logger);
|
||||
const bool bBodyChanges = WriteBackTextIDsFromNodes(Script->GetNodes(), Lines, Script->GetName(), Logger);
|
||||
|
||||
if (Logger.NumErrors() > PrevErrs)
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Info,
|
||||
FText::FromString(FString::Printf(TEXT("Errors prevented saving updates to %s."), *Script->GetName())));
|
||||
}
|
||||
else if (bHeaderChanges || bBodyChanges)
|
||||
{
|
||||
// We need to re-hash file in memory before saving to prevent re-import
|
||||
int32 Length = 10;
|
||||
for(const FString& Line : Lines)
|
||||
{
|
||||
Length += Line.Len() + UE_ARRAY_COUNT(LINE_TERMINATOR);
|
||||
}
|
||||
FString CombinedString;
|
||||
CombinedString.Reserve(Length);
|
||||
|
||||
for(const FString& Line : Lines)
|
||||
{
|
||||
CombinedString += Line;
|
||||
CombinedString += LINE_TERMINATOR;
|
||||
}
|
||||
|
||||
// Write source file back; always encode as UTF8 not default ANSI/UTF16
|
||||
// Do this before updating asset import data so it picks up new file timestamp
|
||||
FFileHelper::SaveStringToFile(FStringView(CombinedString), *SourceFile, FFileHelper::EEncodingOptions::ForceUTF8);
|
||||
|
||||
/* BEGIN try to prevent re-import prompt
|
||||
* This unfortunately didn't work, so removed & let it reimport
|
||||
|
||||
const FMD5Hash FileHash = FSUDSScriptImporter::CalculateHash(*CombinedString, CombinedString.Len());
|
||||
Script->AssetImportData->Update(SourceFile, FileHash);
|
||||
|
||||
// Need to mark asset dirty since hash has changed
|
||||
// Try to find the outer package so we can dirty it up
|
||||
if (Script->GetOuter())
|
||||
{
|
||||
Script->GetOuter()->MarkPackageDirty();
|
||||
}
|
||||
else
|
||||
{
|
||||
Script->MarkPackageDirty();
|
||||
}
|
||||
|
||||
// Even this doesn't work:
|
||||
GUnrealEd->AutoReimportManager->IgnoreFileModification(SourceFile);
|
||||
|
||||
* END try to prevent re-import
|
||||
*/
|
||||
|
||||
Logger.AddMessage(EMessageSeverity::Info,
|
||||
FText::FromString(FString::Printf(TEXT("Successfully updated %s"), *SourceFile)));
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Info,
|
||||
FText::FromString(FString::Printf(TEXT("No changes were required to %s"), *SourceFile)));
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(FString::Printf(TEXT("Error opening source asset %s"), *SourceFile)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(
|
||||
FString::Printf(TEXT("No source files associated with asset %s"), *Script->GetName())));
|
||||
}
|
||||
}
|
||||
|
||||
bool FSUDSEditorScriptTools::WriteBackTextIDsFromNodes(const TArray<USUDSScriptNode*> Nodes, TArray<FString>& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger)
|
||||
{
|
||||
bool bAnyChanges = false;
|
||||
// For each speaker line, set line and choice edge, use source line no to append text ID
|
||||
for (const auto& N : Nodes)
|
||||
{
|
||||
if (N->GetNodeType() == ESUDSScriptNodeType::Text)
|
||||
{
|
||||
if (const auto* TN = Cast<USUDSScriptNodeText>(N))
|
||||
{
|
||||
bAnyChanges = WriteBackTextID(TN->GetText(), TN->GetSourceLineNo(), Lines, NameForErrors, Logger) || bAnyChanges;
|
||||
}
|
||||
}
|
||||
else if (N->GetNodeType() == ESUDSScriptNodeType::SetVariable)
|
||||
{
|
||||
if (const auto* SN = Cast<USUDSScriptNodeSet>(N))
|
||||
{
|
||||
if (SN->GetExpression().IsTextLiteral())
|
||||
{
|
||||
FText Literal = SN->GetExpression().GetTextLiteralValue();
|
||||
|
||||
bAnyChanges = WriteBackTextID(Literal, SN->GetSourceLineNo(), Lines, NameForErrors, Logger) || bAnyChanges;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else if (N->GetNodeType() == ESUDSScriptNodeType::Choice)
|
||||
{
|
||||
// Edges
|
||||
for (auto& Edge : N->GetEdges())
|
||||
{
|
||||
bAnyChanges = WriteBackTextID(Edge.GetText(), Edge.GetSourceLineNo(), Lines, NameForErrors, Logger) || bAnyChanges;
|
||||
}
|
||||
}
|
||||
else if (N->GetNodeType() == ESUDSScriptNodeType::Gosub)
|
||||
{
|
||||
if (const auto* GN = Cast<USUDSScriptNodeGosub>(N))
|
||||
// Need to write back Gosub IDs so that saved return stacks work
|
||||
bAnyChanges = WriteBackGosubID(GN->GetGosubID(), GN->GetSourceLineNo(), Lines, NameForErrors, Logger) || bAnyChanges;
|
||||
}
|
||||
}
|
||||
|
||||
return bAnyChanges;
|
||||
}
|
||||
|
||||
bool FSUDSEditorScriptTools::WriteBackTextID(const FText& AssetText, int LineNo, TArray<FString>& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger)
|
||||
{
|
||||
if (AssetText.IsEmpty())
|
||||
return false;
|
||||
|
||||
// Line numbers are 1-based
|
||||
const int Idx = LineNo - 1;
|
||||
|
||||
if (!Lines.IsValidIndex(Idx))
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(FString::Printf(
|
||||
TEXT("Cannot write back TextID to '%s', source line number %d is invalid (Text: '%s')"),
|
||||
*NameForErrors,
|
||||
LineNo,
|
||||
*AssetText.ToString())));
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString& SourceLine = Lines[Idx];
|
||||
const FString TextID = SUDS_GET_TEXT_KEY(AssetText);
|
||||
FString ExistingTextID;
|
||||
int ExistingNum;
|
||||
FStringView SourceLineView(SourceLine);
|
||||
const bool bFoundExisting = FSUDSScriptImporter::RetrieveTextIDFromLine(SourceLineView, ExistingTextID, ExistingNum);
|
||||
|
||||
// Existing TextID - replace if already there
|
||||
if (!bFoundExisting || ExistingTextID != TextID)
|
||||
{
|
||||
if (TextIDCheckMatch(AssetText, SourceLine))
|
||||
{
|
||||
FString Prefix(SourceLineView);
|
||||
FString UpdatedLine = FString::Printf(TEXT("%s %s"), *Prefix, *TextID);
|
||||
Lines[Idx] = UpdatedLine;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(FString::Printf(TEXT(
|
||||
"Tried to set TextID on line %d of %s but source file did not contain expected text '%s'"
|
||||
),
|
||||
LineNo,
|
||||
*NameForErrors,
|
||||
*AssetText.ToString())));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Same, no need to change
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
bool FSUDSEditorScriptTools::TextIDCheckMatch(const FText& AssetText, const FString& SourceLine)
|
||||
{
|
||||
// Text from our asset must be present in the text in the source line
|
||||
// However, in case this is a multi-line string, take the first line only
|
||||
FString AssetStr = AssetText.ToString();
|
||||
{
|
||||
FString L, R;
|
||||
if (AssetStr.Split("\n", &L, &R))
|
||||
{
|
||||
AssetStr = L.TrimStartAndEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// Bear in mind that this line might be a text line, a choice or a set line
|
||||
// therefore we only check if the source line contains the asset text
|
||||
|
||||
return SourceLine.Contains(AssetStr);
|
||||
|
||||
}
|
||||
|
||||
bool FSUDSEditorScriptTools::WriteBackGosubID(const FString& GosubID,
|
||||
int LineNo,
|
||||
TArray<FString>& Lines,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger& Logger)
|
||||
{
|
||||
|
||||
// Line numbers are 1-based
|
||||
const int Idx = LineNo - 1;
|
||||
|
||||
if (!Lines.IsValidIndex(Idx))
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(FString::Printf(
|
||||
TEXT("Cannot write back GosubID to '%s', source line number %d is invalid"),
|
||||
*NameForErrors,
|
||||
LineNo)));
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString& SourceLine = Lines[Idx];
|
||||
FString ExistingID;
|
||||
int ExistingNum;
|
||||
FStringView SourceLineView(SourceLine);
|
||||
const bool bFoundID = FSUDSScriptImporter::RetrieveGosubIDFromLine(SourceLineView, ExistingID, ExistingNum);
|
||||
|
||||
if (!bFoundID || ExistingID != GosubID)
|
||||
{
|
||||
// Check it's a gosub line
|
||||
if (SourceLine.Contains(TEXT("gosub")) || SourceLine.Contains(TEXT("go sub")))
|
||||
{
|
||||
FString Prefix(SourceLineView);
|
||||
FString UpdatedLine = FString::Printf(TEXT("%s %s"), *Prefix, *GosubID);
|
||||
Lines[Idx] = UpdatedLine;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(FString::Printf(TEXT(
|
||||
"Tried to set GosubID on line %d of %s but source file did not have a gosubu on that line"
|
||||
),
|
||||
LineNo,
|
||||
*NameForErrors)));
|
||||
return false;
|
||||
|
||||
}
|
||||
}
|
||||
// Same, no need to change
|
||||
return false;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSEditorSettings.h"
|
||||
#include "Misc/Paths.h"
|
||||
|
||||
bool USUDSEditorSettings::ShouldGenerateVoiceAssets(const FString& PackagePath) const
|
||||
{
|
||||
if (AlwaysAutoGenerateVoiceOverAssetsOnImport)
|
||||
return true;
|
||||
|
||||
for (auto Dir : DirectoriesToAutoGenerateVoiceOverAssetsOnImport)
|
||||
{
|
||||
if (FPaths::IsUnderDirectory(PackagePath, Dir.Path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FString USUDSEditorSettings::GetVoiceOutputDir(const FString& PackagePath, const FString& ScriptName) const
|
||||
{
|
||||
return GetOutputDir(DialogueVoiceAssetLocation, DialogueVoiceAssetSharedDir.Path, PackagePath, ScriptName);
|
||||
}
|
||||
|
||||
FString USUDSEditorSettings::GetWaveOutputDir(const FString& PackagePath, const FString& ScriptName) const
|
||||
{
|
||||
return GetOutputDir(DialogueWaveAssetLocation, DialogueWaveAssetSharedDir.Path, PackagePath, ScriptName);
|
||||
}
|
||||
|
||||
FString USUDSEditorSettings::GetOutputDir(ESUDSAssetLocation Location,
|
||||
const FString& SharedPath,
|
||||
const FString& PackagePath,
|
||||
const FString& ScriptName)
|
||||
{
|
||||
switch(Location)
|
||||
{
|
||||
default:
|
||||
case ESUDSAssetLocation::SharedDirectory:
|
||||
return SharedPath;
|
||||
case ESUDSAssetLocation::SharedDirectorySubdir:
|
||||
return FPaths::Combine(SharedPath, ScriptName);
|
||||
case ESUDSAssetLocation::ScriptDirectory:
|
||||
return PackagePath;
|
||||
case ESUDSAssetLocation::ScriptDirectorySubdir:
|
||||
return FPaths::Combine(PackagePath, ScriptName);
|
||||
}
|
||||
}
|
||||
1567
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.cpp
Normal file
1567
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.cpp
Normal file
File diff suppressed because it is too large
Load Diff
343
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.h
Normal file
343
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.h
Normal file
@@ -0,0 +1,343 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSValue.h"
|
||||
#include "Framework/Commands/Commands.h"
|
||||
#include "Framework/Text/BaseTextLayoutMarshaller.h"
|
||||
#include "Toolkits/AssetEditorToolkit.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "Widgets/Views/STableRow.h"
|
||||
|
||||
class SMultiLineEditableTextBox;
|
||||
struct FSUDSValue;
|
||||
class USUDSDialogue;
|
||||
class USUDSScript;
|
||||
|
||||
#define LOCTEXT_NAMESPACE "SUDS"
|
||||
|
||||
class FSUDSToolbarCommands
|
||||
: public TCommands<FSUDSToolbarCommands>
|
||||
{
|
||||
public:
|
||||
|
||||
FSUDSToolbarCommands()
|
||||
: TCommands<FSUDSToolbarCommands>(
|
||||
"SUDS",
|
||||
LOCTEXT("SUDS", "Steves Unreal Dialogue System"),
|
||||
NAME_None,
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0
|
||||
FAppStyle::GetAppStyleSetName()
|
||||
#else
|
||||
FEditorStyle::GetStyleSetName()
|
||||
#endif
|
||||
)
|
||||
{ }
|
||||
|
||||
public:
|
||||
|
||||
// TCommands interface
|
||||
|
||||
virtual void RegisterCommands() override
|
||||
{
|
||||
UI_COMMAND(StartDialogue, "Start Dialogue", "Start/restart dialogue", EUserInterfaceActionType::Button, FInputChord());
|
||||
UI_COMMAND(WriteBackTextIDs, "Write String Keys", "Write string keys back to script source to stabilise for localisation / voice asset links", EUserInterfaceActionType::Button, FInputChord());
|
||||
UI_COMMAND(GenerateVOAssets, "Generate Voice Assets", "Generate DialogueVoice and DialogueWave assets for VO", EUserInterfaceActionType::Button, FInputChord());
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
TSharedPtr<FUICommandInfo> StartDialogue;
|
||||
TSharedPtr<FUICommandInfo> WriteBackTextIDs;
|
||||
TSharedPtr<FUICommandInfo> GenerateVOAssets;
|
||||
};
|
||||
|
||||
|
||||
class FSUDSEditorOutputRow
|
||||
{
|
||||
public:
|
||||
FText Prefix;
|
||||
FText Line;
|
||||
FSlateColor PrefixColour;
|
||||
FSlateColor LineColour;
|
||||
FSlateColor BgColour;
|
||||
|
||||
FSUDSEditorOutputRow(const FText& InPrefix,
|
||||
const FText& InLine,
|
||||
const FSlateColor& InPrefixColour,
|
||||
const FSlateColor& InLineColour,
|
||||
const FSlateColor& InBgColour) :
|
||||
Prefix(InPrefix),
|
||||
Line(InLine),
|
||||
PrefixColour(InPrefixColour),
|
||||
LineColour(InLineColour),
|
||||
BgColour(InBgColour)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
class SSUDSEditorOutputItem : public SMultiColumnTableRow< TSharedPtr<FString> >
|
||||
{
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SSUDSEditorOutputItem)
|
||||
{}
|
||||
SLATE_ARGUMENT(float, InitialWidth)
|
||||
SLATE_ARGUMENT(FText, Prefix)
|
||||
SLATE_ARGUMENT(FText, Line)
|
||||
SLATE_ARGUMENT(FSlateColor, PrefixColour)
|
||||
SLATE_ARGUMENT(FSlateColor, LineColour)
|
||||
SLATE_ARGUMENT(FSlateColor, BgColour)
|
||||
|
||||
SLATE_END_ARGS()
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* Construct this widget. Called by the SNew() Slate macro.
|
||||
*
|
||||
* @param InArgs Declaration used by the SNew() macro to construct this widget.
|
||||
* @oaram InOwnerTableView The owner table into which this row is being placed.
|
||||
*/
|
||||
void Construct( const FArguments& InArgs, const TSharedRef<STableViewBase>& InOwnerTableView );
|
||||
|
||||
/**
|
||||
* Generates the widget for the specified column.
|
||||
*
|
||||
* @param ColumnName The name of the column to generate the widget for.
|
||||
* @return The widget.
|
||||
*/
|
||||
virtual TSharedRef<SWidget> GenerateWidgetForColumn( const FName& ColumnName ) override;
|
||||
protected:
|
||||
float InitialWidth = 70;
|
||||
FText Prefix;
|
||||
FText Line;
|
||||
FSlateColor PrefixColour;
|
||||
FSlateColor LineColour;
|
||||
};
|
||||
|
||||
|
||||
class FSUDSEditorVariableRow
|
||||
{
|
||||
public:
|
||||
FName Name;
|
||||
FSUDSValue Value;
|
||||
bool bIsManualOverride;
|
||||
|
||||
FSUDSEditorVariableRow(const FName& InName, const FSUDSValue& InValue, bool bIsManual) : Name(InName),
|
||||
Value(InValue),
|
||||
bIsManualOverride(bIsManual)
|
||||
{
|
||||
}
|
||||
|
||||
friend bool operator<(const FSUDSEditorVariableRow& Lhs, const FSUDSEditorVariableRow& RHS)
|
||||
{
|
||||
return Lhs.Name.LexicalLess(RHS.Name);
|
||||
}
|
||||
|
||||
friend bool operator<(const TSharedPtr<FSUDSEditorVariableRow>& Lhs, const TSharedPtr<FSUDSEditorVariableRow>& RHS)
|
||||
{
|
||||
return *Lhs < *RHS;
|
||||
}
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
class SSUDSEditorVariableItem : public SMultiColumnTableRow< TSharedPtr<FString> >
|
||||
{
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SSUDSEditorVariableItem)
|
||||
{}
|
||||
SLATE_ARGUMENT(float, InitialWidth)
|
||||
SLATE_ARGUMENT(FName, VariableName)
|
||||
SLATE_ARGUMENT(FSUDSValue, VariableValue)
|
||||
SLATE_ARGUMENT(bool, bIsManualOverride)
|
||||
SLATE_ARGUMENT(class FSUDSEditorToolkit*, Parent)
|
||||
SLATE_END_ARGS()
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* Construct this widget. Called by the SNew() Slate macro.
|
||||
*
|
||||
* @param InArgs Declaration used by the SNew() macro to construct this widget.
|
||||
* @oaram InOwnerTableView The owner table into which this row is being placed.
|
||||
*/
|
||||
void Construct( const FArguments& InArgs, const TSharedRef<STableViewBase>& InOwnerTableView );
|
||||
|
||||
public:
|
||||
/**
|
||||
* Generates the widget for the specified column.
|
||||
*
|
||||
* @param ColumnName The name of the column to generate the widget for.
|
||||
* @return The widget.
|
||||
*/
|
||||
virtual TSharedRef<SWidget> GenerateWidgetForColumn( const FName& ColumnName ) override;
|
||||
protected:
|
||||
float InitialWidth = 70;
|
||||
FName VariableName;
|
||||
FSUDSValue VariableValue;
|
||||
bool bIsManualOverride = false;
|
||||
class FSUDSEditorToolkit* Parent = nullptr;
|
||||
|
||||
TSharedRef<class SWidget> GetGenderMenu();
|
||||
void OnGenderSelected(ETextGender TextGender);
|
||||
virtual FVector2D ComputeDesiredSize(float) const override;
|
||||
|
||||
};
|
||||
|
||||
struct FSUDSTraceLogMessage
|
||||
{
|
||||
TSharedRef<FString> Message;
|
||||
FName Category;
|
||||
FSlateColor Colour;
|
||||
|
||||
FSUDSTraceLogMessage(FName InCategory, const FString& InMessage, const FSlateColor& InColour)
|
||||
: Message(MakeShared<FString>(InMessage))
|
||||
, Category(InCategory)
|
||||
, Colour(InColour)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
class FSUDSTraceLogMarshaller : public FBaseTextLayoutMarshaller
|
||||
{
|
||||
public:
|
||||
|
||||
FSUDSTraceLogMarshaller();
|
||||
|
||||
// ITextLayoutMarshaller
|
||||
virtual void SetText(const FString& SourceString, FTextLayout& TargetTextLayout) override;
|
||||
virtual void GetText(FString& TargetString, const FTextLayout& SourceTextLayout) override;
|
||||
|
||||
void AppendMessage(FName InCategory, int LineNo, const FString& Message, const FSlateColor& Colour);
|
||||
void ClearMessages();
|
||||
protected:
|
||||
TArray< TSharedPtr<FSUDSTraceLogMessage> > Messages;
|
||||
};
|
||||
|
||||
// Trace log widget, really just so we can tick and automatically scroll to the end
|
||||
class SSUDSTraceLog : public SCompoundWidget
|
||||
{
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SSUDSTraceLog)
|
||||
{}
|
||||
SLATE_END_ARGS()
|
||||
|
||||
SSUDSTraceLog() : bIsUserScrolled(false) {}
|
||||
|
||||
void Construct(const FArguments& InArgs);
|
||||
virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override;
|
||||
|
||||
void AppendMessage(FName InCategory, int LineNo, const FString& Message, const FSlateColor& Colour);
|
||||
void ClearMessages();
|
||||
void ScrollToEnd();
|
||||
|
||||
protected:
|
||||
bool bIsUserScrolled;
|
||||
TSharedPtr<FSUDSTraceLogMarshaller> TraceLogMarshaller;
|
||||
TSharedPtr<SMultiLineEditableTextBox> TraceLogTextBox;
|
||||
|
||||
void OnUserScrolled(float X);
|
||||
|
||||
};
|
||||
|
||||
class FSUDSEditorToolkit : public FAssetEditorToolkit
|
||||
{
|
||||
public:
|
||||
void InitEditor(const TArray<UObject*>& InObjects);
|
||||
|
||||
virtual void RegisterTabSpawners(const TSharedRef<FTabManager>& InTabManager) override;
|
||||
virtual void UnregisterTabSpawners(const TSharedRef<FTabManager>& InTabManager) override;
|
||||
|
||||
virtual FText GetBaseToolkitName() const override;
|
||||
virtual FName GetToolkitFName() const override;
|
||||
virtual FLinearColor GetWorldCentricTabColorScale() const override;
|
||||
virtual FString GetWorldCentricTabPrefix() const override;
|
||||
void UserEditVariable(const FName& Name, FSUDSValue Value);
|
||||
void DeleteVariable(const FName& Name);
|
||||
|
||||
protected:
|
||||
virtual void OnClose() override;
|
||||
|
||||
private:
|
||||
USUDSScript* Script = nullptr;
|
||||
USUDSDialogue* Dialogue = nullptr;
|
||||
float VarColumnWidth = 120;
|
||||
float PrefixColumnWidth = 100;
|
||||
FName StartLabel = NAME_None;
|
||||
bool bResetVarsOnStart = true;
|
||||
FDelegateHandle ReimportDelegateHandle;
|
||||
// FSUDSEditorDialogueRow needs to held by a TSharedPtr for SListView
|
||||
TSharedPtr<SListView<TSharedPtr<FSUDSEditorOutputRow>>> OutputListView;
|
||||
TArray<TSharedPtr<FSUDSEditorOutputRow>> OutputRows;
|
||||
TSharedPtr<SVerticalBox> ChoicesBox;
|
||||
TSharedPtr<SListView<TSharedPtr<FSUDSEditorVariableRow>>> VariablesListView;
|
||||
TArray<TSharedPtr<FSUDSEditorVariableRow>> VariableRows;
|
||||
TSharedPtr<SSUDSTraceLog> TraceLog;
|
||||
TMap<FName, FSUDSValue> ManualOverrideVariables;
|
||||
|
||||
static FName DialogueOutputTabName;
|
||||
static FName DetailsTabName;
|
||||
static FName VariablesTabName;
|
||||
static FName LogTabName;
|
||||
|
||||
|
||||
const FSlateColor SpeakerColour = FLinearColor(1.0f, 1.0f, 0.6f, 1.0f);
|
||||
const FSlateColor ChoiceColour = FLinearColor(0.4f, 1.0f, 0.4f, 1.0f);
|
||||
const FSlateColor EventColour = FLinearColor(0.2f, 0.6f, 1.0f, 1.0f);
|
||||
const FSlateColor VarSetColour = FLinearColor(0.8f, 0.6f, 0.9f, 1.0f);
|
||||
const FSlateColor VarEditColour = FLinearColor(0.6f, 0.3f, 1.0f, 1.0f);
|
||||
const FSlateColor StartColour = FLinearColor(1.0f, 0.5f, 0.0f, 1.0f);
|
||||
const FSlateColor FinishColour = FLinearColor(1.0f, 0.3f, 0.3f, 1.0f);
|
||||
const FSlateColor SelectColour = FLinearColor(1.0f, 0.0f, 0.5f, 1.0f);
|
||||
const FSlateColor RowBgColour1 = FLinearColor(0.15f, 0.15f, 0.15f, 1.0f);
|
||||
const FSlateColor RowBgColour2 = FLinearColor(0.3f, 0.3f, 0.3f, 1.0f);
|
||||
|
||||
void ExtendToolbar(FToolBarBuilder& ToolbarBuilder, TWeakPtr<SDockTab> Tab);
|
||||
TSharedRef<class SWidget> GetStartLabelMenu();
|
||||
FText GetSelectedStartLabel() const;
|
||||
void OnStartLabelSelected(FName Label);
|
||||
ECheckBoxState GetResetVarsCheckState() const;
|
||||
void OnResetVarsCheckStateChanged(ECheckBoxState NewState);
|
||||
FReply AddVariableClicked();
|
||||
void UpdateVariables();
|
||||
void EnsureTabsVisible();
|
||||
void StartDialogue();
|
||||
void DestroyDialogue();
|
||||
void UpdateOutput();
|
||||
void UpdateChoiceButtons();
|
||||
void AddOutputRow(const FText& Prefix, const FText& Line, const FSlateColor& PrefixColour, const FSlateColor& LineColour);
|
||||
void AddTraceLogRow(const FName& Category, int SourceLineNo, const FString& Message);
|
||||
|
||||
void AddDialogueStep(const FName& Category, int SourceLineNo, const FText& Description, const FText& Prefix);
|
||||
|
||||
void OnDialogueChoice(USUDSDialogue* Dialogue, int ChoiceIndex, int LineNo);
|
||||
void OnDialogueEvent(USUDSDialogue* Dialogue, FName EventName, const TArray<FSUDSValue>& Args, int LineNo);
|
||||
void OnDialogueFinished(USUDSDialogue* Dialogue);
|
||||
void OnDialogueProceeding(USUDSDialogue* Dialogue);
|
||||
void OnDialogueStarting(USUDSDialogue* Dialogue, FName LabelName);
|
||||
void OnDialogueSpeakerLine(USUDSDialogue* Dialogue, int LineNo);
|
||||
void OnDialogueSetVar(USUDSDialogue* Dialogue, FName VariableName, const FSUDSValue& ToValue, const FString& ExpressionStr, int LineNo);
|
||||
void OnDialogueUserEditedVar(USUDSDialogue* Dialogue, FName VariableName, const FSUDSValue& ToValue);
|
||||
void OnDialogueSelectEval(USUDSDialogue* Dialogue, const FString& ExpressionStr, bool bSuccess, int LineNo);
|
||||
void OnDialogueRandomEval(USUDSDialogue* Dialogue, const int RandomOutcome, int LineNo);
|
||||
|
||||
TSharedRef<ITableRow> OnGenerateRowForOutput(
|
||||
TSharedPtr<FSUDSEditorOutputRow> FsudsEditorDialogueRow,
|
||||
const TSharedRef<STableViewBase>& TableViewBase);
|
||||
TSharedRef<ITableRow> OnGenerateRowForVariable(TSharedPtr<FSUDSEditorVariableRow> Row,
|
||||
const TSharedRef<STableViewBase>& Table);
|
||||
FSlateColor GetColourForCategory(const FName& Category);
|
||||
void OnPostReimport(UObject* Object, bool bSuccess);
|
||||
void Clear();
|
||||
void WriteBackTextIDs();
|
||||
void GenerateVOAssets();
|
||||
|
||||
};
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,381 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSEditorVoiceOverTools.h"
|
||||
|
||||
#include "ObjectTools.h"
|
||||
#include "PackageTools.h"
|
||||
#include "SUDSEditorSettings.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "Internationalization/Regex.h"
|
||||
#include "Sound/DialogueVoice.h"
|
||||
#include "Sound/DialogueWave.h"
|
||||
#include "Sound/SoundWave.h"
|
||||
|
||||
|
||||
void FSUDSEditorVoiceOverTools::GenerateAssets(USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger)
|
||||
{
|
||||
// First check for problems
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
bool bError = false;
|
||||
if ((Settings->DialogueVoiceAssetLocation == ESUDSAssetLocation::SharedDirectory || Settings->
|
||||
DialogueVoiceAssetLocation == ESUDSAssetLocation::SharedDirectorySubdir) &&
|
||||
(Settings->DialogueVoiceAssetSharedDir.Path.IsEmpty() ||
|
||||
!FPackageName::IsValidPath(Settings->DialogueVoiceAssetSharedDir.Path)))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Dialogue Voice assets are set to generate to a shared dir, but the dir '%s' is not valid."),
|
||||
*Settings->DialogueVoiceAssetSharedDir.Path);
|
||||
bError = true;
|
||||
}
|
||||
if ((Settings->DialogueWaveAssetLocation == ESUDSAssetLocation::SharedDirectory || Settings->
|
||||
DialogueWaveAssetLocation == ESUDSAssetLocation::SharedDirectorySubdir) &&
|
||||
(Settings->DialogueWaveAssetSharedDir.Path.IsEmpty() ||
|
||||
!FPackageName::IsValidPath(Settings->DialogueWaveAssetSharedDir.Path)))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Dialogue Wave assets are set to generate to a shared dir, but the dir '%s' is not valid."),
|
||||
*Settings->DialogueWaveAssetSharedDir.Path);
|
||||
bError = true;
|
||||
}
|
||||
if (bError) { return; }
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error, TEXT("Unable to generate voice assets, settings not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
TMap<FString, UDialogueVoice*> UnsavedVoices;
|
||||
GenerateVoiceAssets(Script, Flags, Logger, UnsavedVoices);
|
||||
GenerateWaveAssets(Script, Flags, UnsavedVoices, Logger);
|
||||
}
|
||||
|
||||
void FSUDSEditorVoiceOverTools::GenerateVoiceAssets(USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger, TMap<FString, UDialogueVoice*> &OutCreatedVoices)
|
||||
{
|
||||
auto Registry = &FModuleManager::LoadModuleChecked<FAssetRegistryModule>(AssetRegistryConstants::ModuleName).Get();
|
||||
|
||||
FString Prefix;
|
||||
FString ParentDir;
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
Prefix = Settings->DialogueVoiceAssetPrefix;
|
||||
ParentDir = GetVoiceOutputDir(Script);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error, TEXT("Unable to generate voice assets, settings not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto Speaker : Script->GetSpeakers())
|
||||
{
|
||||
// All Dialogue Voice assets will be created in their own package
|
||||
FString PackageName;
|
||||
FString AssetName;
|
||||
if (GetSpeakerVoiceAssetNames(Script, Speaker, PackageName, AssetName))
|
||||
{
|
||||
if (FPackageName::DoesPackageExist(*PackageName))
|
||||
{
|
||||
// Package already exists, so try and import over the top of it, if it doesn't already have a source file path
|
||||
TArray<FAssetData> Assets;
|
||||
if (Registry->GetAssetsByPackageName(*PackageName, Assets))
|
||||
{
|
||||
if (Assets.Num() > 0)
|
||||
{
|
||||
if (!Assets[0].GetAsset()->IsA(UDialogueVoice::StaticClass()))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Asset %s already exists but is not a Dialogue Voice! Cannot replace, please move aside and re-import script."),
|
||||
*PackageName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Make sure we still link the existing voice
|
||||
Script->SetSpeakerVoice(Speaker, Cast<UDialogueVoice>(Assets[0].GetAsset()));
|
||||
Script->MarkPackageDirty();
|
||||
}
|
||||
// Either way nothing more to do
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we got here then Dialogue Voice didn't exist (although package might have)
|
||||
// It's safe to call CreatePackage either way, it'll return the existing one if needed
|
||||
UPackage* Package = CreatePackage(*PackageName);
|
||||
if (!ensure(Package))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT("Failed to create/retrieve package for voice asset %s"),
|
||||
*PackageName);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Logger->Logf(ELogVerbosity::Display, TEXT("Creating voice asset %s"), *PackageName);
|
||||
|
||||
UDialogueVoice* NewVoiceAsset = NewObject<UDialogueVoice>(Package, FName(AssetName), Flags);
|
||||
OutCreatedVoices.Add(Speaker, NewVoiceAsset);
|
||||
// there's nothing else to create here, voice is mostly a placeholder with the rest set up later by user
|
||||
FAssetRegistryModule::AssetCreated(NewVoiceAsset);
|
||||
|
||||
Script->SetSpeakerVoice(Speaker, NewVoiceAsset);
|
||||
Script->MarkPackageDirty();
|
||||
|
||||
Package->FullyLoad();
|
||||
NewVoiceAsset->MarkPackageDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FSUDSEditorVoiceOverTools::GetSpeakerVoicePackageName(USUDSScript* Script,
|
||||
const FString& SpeakerID,
|
||||
FString& OutPackageName)
|
||||
{
|
||||
FString NotUsed;
|
||||
return GetSpeakerVoiceAssetNames(Script, SpeakerID, OutPackageName, NotUsed);
|
||||
}
|
||||
|
||||
bool FSUDSEditorVoiceOverTools::GetSpeakerVoiceAssetNames(USUDSScript* Script,
|
||||
const FString& SpeakerID,
|
||||
FString& OutPackageName,
|
||||
FString& OutAssetName)
|
||||
{
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
FString Prefix = Settings->DialogueVoiceAssetPrefix;
|
||||
FString ParentDir = GetVoiceOutputDir(Script);
|
||||
|
||||
OutAssetName = FString::Printf(TEXT("%s%s"), *Prefix, *ObjectTools::SanitizeObjectName(SpeakerID));
|
||||
OutPackageName = UPackageTools::SanitizePackageName(ParentDir / OutAssetName);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
UDialogueVoice* FSUDSEditorVoiceOverTools::FindSpeakerVoice(USUDSScript* Script,
|
||||
const FString& SpeakerID)
|
||||
{
|
||||
FString PackageName;
|
||||
if (GetSpeakerVoicePackageName(Script, SpeakerID, PackageName))
|
||||
{
|
||||
|
||||
auto Registry = &FModuleManager::LoadModuleChecked<FAssetRegistryModule>(AssetRegistryConstants::ModuleName).Get();
|
||||
|
||||
if (FPackageName::DoesPackageExist(*PackageName))
|
||||
{
|
||||
// Package already exists, so try and import over the top of it, if it doesn't already have a source file path
|
||||
TArray<FAssetData> Assets;
|
||||
if (Registry->GetAssetsByPackageName(*PackageName, Assets))
|
||||
{
|
||||
if (Assets.Num() > 0)
|
||||
{
|
||||
if (Assets[0].GetAsset()->IsA(UDialogueVoice::StaticClass()))
|
||||
{
|
||||
return Cast<UDialogueVoice>(Assets[0].GetAsset());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void FSUDSEditorVoiceOverTools::GenerateWaveAssets(USUDSScript* Script, EObjectFlags Flags, TMap<FString, UDialogueVoice*> UnsavedVoices, FSUDSMessageLogger* Logger)
|
||||
{
|
||||
// We need to identify specific lines of dialogue to specify a wave asset to go with them, which means we
|
||||
// need to use the Text ID, just like we do for translations. This means that really, you shouldn't start to
|
||||
// assign sounds to wave assets until you've written the text IDs back to the script
|
||||
auto Registry = &FModuleManager::LoadModuleChecked<FAssetRegistryModule>(AssetRegistryConstants::ModuleName).Get();
|
||||
|
||||
FString Prefix;
|
||||
FString ParentDir;
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
Prefix = FString::Printf(TEXT("%s%s_"), *Settings->DialogueWaveAssetPrefix, *GetScriptNameAsPrefix(Script));
|
||||
ParentDir = GetWaveOutputDir(Script);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error, TEXT("Unable to generate wave assets, settings not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto Node : Script->GetNodes())
|
||||
{
|
||||
if (auto Line = Cast<USUDSScriptNodeText>(Node))
|
||||
{
|
||||
// We use the TextID to identify a specific line, just like for translation
|
||||
FString TextID = Line->GetTextID();
|
||||
|
||||
// Remove the '@' characters from the text ID for creating the asset name, they just turn into excessive '_'s
|
||||
const FRegexPattern TextIDPattern(TEXT("\\@([0-9a-fA-F]+)\\@"));
|
||||
FRegexMatcher TextIDRegex(TextIDPattern, TextID);
|
||||
if (TextIDRegex.FindNext())
|
||||
{
|
||||
TextID = TextIDRegex.GetCaptureGroup(1);
|
||||
}
|
||||
|
||||
// All Dialogue Voice assets will be created in their own package
|
||||
const FString SanitizedName = FString::Printf(TEXT("%s%s"),
|
||||
*Prefix,
|
||||
*ObjectTools::SanitizeObjectName(TextID));
|
||||
const FString PackageName = UPackageTools::SanitizePackageName(ParentDir / SanitizedName);
|
||||
|
||||
UDialogueWave* WaveAsset = nullptr;
|
||||
if (FPackageName::DoesPackageExist(*PackageName))
|
||||
{
|
||||
// Package already exists, so try and import over the top of it, if it doesn't already have a source file path
|
||||
TArray<FAssetData> Assets;
|
||||
if (Registry->GetAssetsByPackageName(*PackageName, Assets))
|
||||
{
|
||||
if (Assets.Num() > 0)
|
||||
{
|
||||
if (!Assets[0].GetAsset()->IsA(UDialogueWave::StaticClass()))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Asset %s already exists but is not a Dialogue Wave! Cannot replace, please move aside and re-import script."),
|
||||
*PackageName);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
WaveAsset = Cast<UDialogueWave>(Assets[0].GetAsset());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UDialogueVoice* SpeakerVoice = nullptr;
|
||||
// Finding speaker assets via FAssetRegistryModule does not work on unsaved assets, so we need to check
|
||||
// the unsaved voices that have been created in the previous step first
|
||||
if (auto pSV = UnsavedVoices.Find(Line->GetSpeakerID()))
|
||||
{
|
||||
SpeakerVoice = *pSV;
|
||||
}
|
||||
if (!SpeakerVoice)
|
||||
{
|
||||
// Now try assets
|
||||
SpeakerVoice = FindSpeakerVoice(Script, Line->GetSpeakerID());
|
||||
}
|
||||
if (!SpeakerVoice)
|
||||
{
|
||||
FString SpeakerPackageName;
|
||||
GetSpeakerVoicePackageName(Script, Line->GetSpeakerID(), SpeakerPackageName);
|
||||
Logger->Logf(ELogVerbosity::Error, TEXT("Error: speaker voice asset for %s is missing, expected to be at %s"), *Line->GetSpeakerID(), *SpeakerPackageName);
|
||||
continue;
|
||||
}
|
||||
|
||||
UPackage* Package = CreatePackage(*PackageName);
|
||||
if (!ensure(Package))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT("Failed to create/retrieve package for wave asset %s"),
|
||||
*PackageName);
|
||||
}
|
||||
else
|
||||
{
|
||||
FString NativeLangText = Line->GetText().ToString();
|
||||
USoundWave* SoundWave = nullptr;
|
||||
if (!WaveAsset)
|
||||
{
|
||||
//Logger->Logf(ELogVerbosity::Display, TEXT("Creating wave asset %s"), *PackageName);
|
||||
WaveAsset = NewObject<UDialogueWave>(Package, FName(SanitizedName), Flags);
|
||||
FAssetRegistryModule::AssetCreated(WaveAsset);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We will always update the existing asset, but will warn if the text is changing
|
||||
if (WaveAsset->ContextMappings.Num() > 0)
|
||||
{
|
||||
auto Mapping = WaveAsset->ContextMappings[0];
|
||||
if (IsValid(Mapping.SoundWave))
|
||||
{
|
||||
SoundWave = Mapping.SoundWave;
|
||||
// This is OK if the context is what we were going to create anyway, we'll just leave it alone
|
||||
if (Mapping.Context.Speaker != SpeakerVoice ||
|
||||
WaveAsset->SpokenText != NativeLangText)
|
||||
{
|
||||
// No good, user has assigned sound to this dialogue wave, but we need to change it to
|
||||
// a different speaker / line
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Dialogue Wave %s has an assigned Sound Wave but the speaker or text has changed. You should update the sound wave!"),
|
||||
*PackageName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set spoken text - native language only. Dialogue Wave handles the localisation of this separately,
|
||||
// we can't link it to our String Table unfortunately.
|
||||
WaveAsset->SpokenText = NativeLangText;
|
||||
// Set dialogue context
|
||||
// We need a Speaker Voice, we'll leave the sound and targets blank
|
||||
WaveAsset->UpdateContext(WaveAsset->ContextMappings[0], SoundWave, SpeakerVoice, TArray<UDialogueVoice*>());
|
||||
|
||||
// Now assign the wave to the line
|
||||
Line->SetWave(WaveAsset);
|
||||
Script->MarkPackageDirty();
|
||||
|
||||
Package->FullyLoad();
|
||||
WaveAsset->MarkPackageDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FString FSUDSEditorVoiceOverTools::GetVoiceOutputDir(USUDSScript* Script)
|
||||
{
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
const FString PackagePath = FPackageName::GetLongPackagePath(Script->GetOuter()->GetOutermost()->GetPathName());
|
||||
const FString ScriptPrefix = GetScriptNameAsPrefix(Script);
|
||||
return Settings->GetVoiceOutputDir(PackagePath, ScriptPrefix);
|
||||
}
|
||||
return FString();
|
||||
}
|
||||
|
||||
FString FSUDSEditorVoiceOverTools::GetWaveOutputDir(USUDSScript* Script)
|
||||
{
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
const FString PackagePath = FPackageName::GetLongPackagePath(Script->GetOuter()->GetOutermost()->GetPathName());
|
||||
const FString ScriptPrefix = GetScriptNameAsPrefix(Script);
|
||||
return Settings->GetWaveOutputDir(PackagePath, ScriptPrefix);
|
||||
}
|
||||
return FString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
FString FSUDSEditorVoiceOverTools::GetScriptNameAsPrefix(USUDSScript* Script)
|
||||
{
|
||||
FString Name = Script->GetName();
|
||||
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
if (Settings->StripScriptPrefixesWhenGeneratingNames)
|
||||
{
|
||||
int32 Index = INDEX_NONE;
|
||||
if (Name.FindChar('_', Index))
|
||||
{
|
||||
if (Index < Name.Len() - 1)
|
||||
{
|
||||
Name = Name.RightChop(Index + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Name;
|
||||
}
|
||||
93
Plugins/SUDS/Source/SUDSEditor/Private/SUDSMessageLogger.cpp
Normal file
93
Plugins/SUDS/Source/SUDSEditor/Private/SUDSMessageLogger.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "IMessageLogListing.h"
|
||||
#include "MessageLogModule.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
FSUDSMessageLogger::~FSUDSMessageLogger()
|
||||
{
|
||||
if (bWriteToMessageLog)
|
||||
{
|
||||
//Always clear the old message after an import or re-import
|
||||
const TCHAR* LogTitle = TEXT("SUDS");
|
||||
FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked<FMessageLogModule>("MessageLog");
|
||||
TSharedPtr<class IMessageLogListing> LogListing = MessageLogModule.GetLogListing(LogTitle);
|
||||
LogListing->SetLabel(FText::FromString("SUDS"));
|
||||
// Should NOT clear messages here, otherwise when FSUDSMessageLogger is used multiple times in the import process,
|
||||
// each one is clearing messages from previous stages of the import.
|
||||
//LogListing->ClearMessages();
|
||||
|
||||
if(ErrorMessages.Num() > 0)
|
||||
{
|
||||
LogListing->AddMessages(MoveTemp(ErrorMessages));
|
||||
LogListing->NotifyIfAnyMessages(NSLOCTEXT("SUDS", "ImportErrors", "There were issues with the import."), EMessageSeverity::Warning);
|
||||
MessageLogModule.OpenMessageLog(LogTitle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FSUDSMessageLogger::HasErrors() const
|
||||
{
|
||||
for (const TSharedRef<FTokenizedMessage>& Msg : ErrorMessages)
|
||||
{
|
||||
if (Msg->GetSeverity() == EMessageSeverity::Error)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int FSUDSMessageLogger::NumErrors() const
|
||||
{
|
||||
int Errs = 0;
|
||||
for (const TSharedRef<FTokenizedMessage>& Msg : ErrorMessages)
|
||||
{
|
||||
if (Msg->GetSeverity() == EMessageSeverity::Error)
|
||||
{
|
||||
++Errs;
|
||||
}
|
||||
}
|
||||
return Errs;
|
||||
}
|
||||
|
||||
bool FSUDSMessageLogger::HasWarnings() const
|
||||
{
|
||||
for (const TSharedRef<FTokenizedMessage>& Msg : ErrorMessages)
|
||||
{
|
||||
if (Msg->GetSeverity() == EMessageSeverity::Warning)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int FSUDSMessageLogger::NumWarnings() const
|
||||
{
|
||||
int Count = 0;
|
||||
for (const TSharedRef<FTokenizedMessage>& Msg : ErrorMessages)
|
||||
{
|
||||
if (Msg->GetSeverity() == EMessageSeverity::Warning)
|
||||
{
|
||||
++Count;
|
||||
}
|
||||
}
|
||||
return Count;
|
||||
}
|
||||
|
||||
void FSUDSMessageLogger::AddMessage(EMessageSeverity::Type Severity, const FText& Text)
|
||||
{
|
||||
ErrorMessages.Add(FTokenizedMessage::Create(Severity, Text));
|
||||
}
|
||||
|
||||
void FSUDSMessageLogger::ClearMessages()
|
||||
{
|
||||
const TCHAR* LogTitle = TEXT("SUDS");
|
||||
FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked<FMessageLogModule>("MessageLog");
|
||||
TSharedPtr<class IMessageLogListing> LogListing = MessageLogModule.GetLogListing(LogTitle);
|
||||
LogListing->SetLabel(FText::FromString("SUDS"));
|
||||
LogListing->ClearMessages();
|
||||
}
|
||||
|
||||
134
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptActions.cpp
Normal file
134
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptActions.cpp
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptActions.h"
|
||||
|
||||
#include "SUDSEditorScriptTools.h"
|
||||
#include "SUDSEditorSettings.h"
|
||||
#include "SUDSEditorToolkit.h"
|
||||
#include "SUDSEditorVoiceOverTools.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "ToolMenuSection.h"
|
||||
#include "EditorFramework/AssetImportData.h"
|
||||
#include "Misc/MessageDialog.h"
|
||||
#include "Widgets/Views/SListView.h"
|
||||
|
||||
FText FSUDSScriptActions::GetName() const
|
||||
{
|
||||
return INVTEXT("SUDS Script");
|
||||
}
|
||||
|
||||
FString FSUDSScriptActions::GetObjectDisplayName(UObject* Object) const
|
||||
{
|
||||
return Object->GetName();
|
||||
}
|
||||
|
||||
UClass* FSUDSScriptActions::GetSupportedClass() const
|
||||
{
|
||||
return USUDSScript::StaticClass();
|
||||
}
|
||||
|
||||
FColor FSUDSScriptActions::GetTypeColor() const
|
||||
{
|
||||
return FColor::Orange;
|
||||
}
|
||||
|
||||
uint32 FSUDSScriptActions::GetCategories()
|
||||
{
|
||||
return EAssetTypeCategories::Misc;
|
||||
}
|
||||
|
||||
void FSUDSScriptActions::GetResolvedSourceFilePaths(const TArray<UObject*>& TypeAssets,
|
||||
TArray<FString>& OutSourceFilePaths) const
|
||||
{
|
||||
for (auto& Asset : TypeAssets)
|
||||
{
|
||||
const auto Script = CastChecked<USUDSScript>(Asset);
|
||||
if (Script->AssetImportData)
|
||||
{
|
||||
Script->AssetImportData->ExtractFilenames(OutSourceFilePaths);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FSUDSScriptActions::HasActions(const TArray<UObject*>& InObjects) const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void FSUDSScriptActions::OpenAssetEditor(const TArray<UObject*>& InObjects,
|
||||
TSharedPtr<IToolkitHost> EditWithinLevelEditor)
|
||||
{
|
||||
MakeShared<FSUDSEditorToolkit>()->InitEditor(InObjects);
|
||||
}
|
||||
|
||||
void FSUDSScriptActions::GetActions(const TArray<UObject*>& InObjects, FToolMenuSection& Section)
|
||||
{
|
||||
const auto Scripts = GetTypedWeakObjectPtrs<USUDSScript>(InObjects);
|
||||
|
||||
Section.AddMenuEntry(
|
||||
"WriteBackTextIDs",
|
||||
NSLOCTEXT("SUDS", "WriteBackTextIDs", "Write Back String Keys"),
|
||||
NSLOCTEXT("SUDS",
|
||||
"WriteBackTextIDsTooltip",
|
||||
"Write string table keys back to source script to make them constant for localisation."),
|
||||
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Details"),
|
||||
FUIAction(
|
||||
FExecuteAction::CreateSP(this, &FSUDSScriptActions::WriteBackTextIDs, Scripts),
|
||||
FCanExecuteAction()
|
||||
)
|
||||
);
|
||||
Section.AddMenuEntry(
|
||||
"GenerateVOAssets",
|
||||
NSLOCTEXT("SUDS", "GenerateVOAssets", "Generate Voice Assets"),
|
||||
NSLOCTEXT("SUDS",
|
||||
"GenerateVOAssetsTooltip",
|
||||
"Generate Dialogue Voice / Dialogue Wave assets for the selected scripts"),
|
||||
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Toolbar.Export"),
|
||||
FUIAction(
|
||||
FExecuteAction::CreateSP(this, &FSUDSScriptActions::GenerateVOAssets, Scripts),
|
||||
FCanExecuteAction()
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
void FSUDSScriptActions::WriteBackTextIDs(TArray<TWeakObjectPtr<USUDSScript>> Scripts)
|
||||
{
|
||||
if (FMessageDialog::Open(EAppMsgType::YesNo,
|
||||
FText::FromString(
|
||||
"Are you sure you want to write string keys back to the selected scripts?"))
|
||||
== EAppReturnType::Yes)
|
||||
{
|
||||
FSUDSMessageLogger::ClearMessages();
|
||||
FSUDSMessageLogger Logger;
|
||||
for (auto Script : Scripts)
|
||||
{
|
||||
if (Script.IsValid())
|
||||
{
|
||||
FSUDSEditorScriptTools::WriteBackTextIDs(Script.Get(), Logger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void FSUDSScriptActions::GenerateVOAssets(TArray<TWeakObjectPtr<USUDSScript>> Scripts)
|
||||
{
|
||||
if (FMessageDialog::Open(EAppMsgType::YesNo,
|
||||
FText::FromString(
|
||||
"Are you sure you want to generate Dialogue Voice / Dialogue Wave assets for the selected scripts?"))
|
||||
== EAppReturnType::Yes)
|
||||
{
|
||||
EObjectFlags Flags = RF_Public | RF_Standalone | RF_Transactional;
|
||||
FSUDSMessageLogger::ClearMessages();
|
||||
FSUDSMessageLogger Logger;
|
||||
for (auto WeakScript : Scripts)
|
||||
{
|
||||
if (auto Script = WeakScript.Get())
|
||||
{
|
||||
FSUDSEditorVoiceOverTools::GenerateAssets(Script, Flags, &Logger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
262
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptFactory.cpp
Normal file
262
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptFactory.cpp
Normal file
@@ -0,0 +1,262 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptFactory.h"
|
||||
|
||||
#include "PackageTools.h"
|
||||
#include "SUDSEditorSettings.h"
|
||||
#include "SUDSEditorVoiceOverTools.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "EditorFramework/AssetImportData.h"
|
||||
#include "Internationalization/StringTable.h"
|
||||
#include "Editor.h"
|
||||
#include "ObjectTools.h"
|
||||
#include "Internationalization/StringTableCore.h"
|
||||
|
||||
|
||||
USUDSScriptFactory::USUDSScriptFactory()
|
||||
{
|
||||
SupportedClass = USUDSScript::StaticClass();
|
||||
bCreateNew = false;
|
||||
bEditorImport = true;
|
||||
bText = true;
|
||||
Formats.Add(TEXT("sud;SUDS Script File"));
|
||||
}
|
||||
|
||||
UObject* USUDSScriptFactory::FactoryCreateText(UClass* InClass,
|
||||
UObject* InParent,
|
||||
FName InName,
|
||||
EObjectFlags Flags,
|
||||
UObject* Context,
|
||||
const TCHAR* Type,
|
||||
const TCHAR*& Buffer,
|
||||
const TCHAR* BufferEnd,
|
||||
FFeedbackContext* Warn)
|
||||
{
|
||||
Flags |= RF_Transactional;
|
||||
FSUDSMessageLogger::ClearMessages();
|
||||
FSUDSMessageLogger Logger;
|
||||
|
||||
USUDSScript* Result = nullptr;
|
||||
|
||||
GEditor->GetEditorSubsystem<UImportSubsystem>()->BroadcastAssetPreImport(this, InClass, InParent, InName, Type);
|
||||
|
||||
const FString FactoryCurrentFilename = UFactory::GetCurrentFilename();
|
||||
FString CurrentSourcePath;
|
||||
FString FilenameNoExtension;
|
||||
FString UnusedExtension;
|
||||
FPaths::Split(FactoryCurrentFilename, CurrentSourcePath, FilenameNoExtension, UnusedExtension);
|
||||
const FString LongPackagePath = FPackageName::GetLongPackagePath(InParent->GetOutermost()->GetPathName());
|
||||
|
||||
const FString NameForErrors(InName.ToString());
|
||||
|
||||
// Now parse this using utility
|
||||
if(Importer.ImportFromBuffer(Buffer, BufferEnd - Buffer, NameForErrors, &Logger, false))
|
||||
{
|
||||
|
||||
// Populate with data
|
||||
Result = NewObject<USUDSScript>(InParent, InName, Flags);
|
||||
UStringTable* StringTable = CreateStringTable(InParent, InName, Result, Flags, &Logger);
|
||||
Importer.PopulateAsset(Result, StringTable);
|
||||
|
||||
// Register source info
|
||||
const FMD5Hash Hash = FSUDSScriptImporter::CalculateHash(Buffer, BufferEnd - Buffer);
|
||||
Result->AssetImportData->Update(FactoryCurrentFilename, Hash);
|
||||
|
||||
// VO assets at import time?
|
||||
if (ShouldGenerateVoiceAssets(LongPackagePath))
|
||||
{
|
||||
FSUDSEditorVoiceOverTools::GenerateAssets(Result, Flags, &Logger);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
GEditor->GetEditorSubsystem<UImportSubsystem>()->BroadcastAssetPostImport(this, Result);
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
bool USUDSScriptFactory::ShouldGenerateVoiceAssets(const FString& PackagePath) const
|
||||
{
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
return Settings->ShouldGenerateVoiceAssets(PackagePath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
UStringTable* USUDSScriptFactory::CreateStringTable(UObject* ScriptParent, FName InName, USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger)
|
||||
{
|
||||
auto Settings = GetDefault<USUDSEditorSettings>();
|
||||
const bool bCreateSeparatePackage = Settings && Settings->bCreateStringTablesAsSeparatePackages;
|
||||
|
||||
const FName StringTableName = FName(InName.ToString() + "Strings");
|
||||
UStringTable* Table = nullptr;
|
||||
auto Registry = &FModuleManager::LoadModuleChecked<FAssetRegistryModule>(AssetRegistryConstants::ModuleName).Get();
|
||||
const FString PackageDir = FPackageName::GetLongPackagePath(Script->GetOuter()->GetOutermost()->GetPathName());
|
||||
const FString PackageName = UPackageTools::SanitizePackageName(PackageDir / StringTableName.ToString());
|
||||
|
||||
if (bCreateSeparatePackage)
|
||||
{
|
||||
|
||||
// Create string table as its own package (.uasset)
|
||||
|
||||
{
|
||||
// Destroy any string table packed together with the script
|
||||
const FString ScriptPackageName = Script->GetPackage()->GetPathName();
|
||||
TArray<FAssetData> ScriptAssets;
|
||||
Registry->GetAssetsByPackageName(*ScriptPackageName, ScriptAssets);
|
||||
TArray<UObject*> StringTableAssets;
|
||||
for (auto& Asset : ScriptAssets)
|
||||
{
|
||||
if (Asset.GetClass() == UStringTable::StaticClass())
|
||||
{
|
||||
StringTableAssets.Add(Asset.GetAsset());
|
||||
}
|
||||
}
|
||||
if (StringTableAssets.Num() > 0)
|
||||
{
|
||||
ObjectTools::ForceDeleteObjects(StringTableAssets, false );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (FPackageName::DoesPackageExist(*PackageName))
|
||||
{
|
||||
// Package already exists, so try and import over the top of it, if it doesn't already have a source file path
|
||||
TArray<FAssetData> Assets;
|
||||
if (Registry->GetAssetsByPackageName(*PackageName, Assets))
|
||||
{
|
||||
if (Assets.Num() > 0)
|
||||
{
|
||||
if (Assets[0].GetAsset()->IsA(UStringTable::StaticClass()))
|
||||
{
|
||||
Table = Cast<UStringTable>(Assets[0].GetAsset());
|
||||
Table->GetMutableStringTable()->ClearSourceStrings();
|
||||
Table->MarkPackageDirty();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Asset %s already exists but is not a String Table! Cannot replace, please move aside and re-import script."),
|
||||
*PackageName);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Table)
|
||||
{
|
||||
// String Table didn't exist (although package might have)
|
||||
// It's safe to call CreatePackage either way, it'll return the existing one if needed
|
||||
UPackage* Package = CreatePackage(*PackageName);
|
||||
if (!ensure(Package))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT("Failed to create/retrieve package for string table %s"),
|
||||
*PackageName);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Logger->Logf(ELogVerbosity::Display, TEXT("Creating string table %s"), *PackageName);
|
||||
|
||||
// This constructor registers the string table with FStringTableRegistry
|
||||
Table = NewObject<UStringTable>(Package, StringTableName, Flags);
|
||||
Package->FullyLoad();
|
||||
Table->MarkPackageDirty();
|
||||
FAssetRegistryModule::AssetCreated(Table);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Destroy separate strings package if it exists (could flip this option off after importing)
|
||||
if (FPackageName::DoesPackageExist(*PackageName))
|
||||
{
|
||||
TArray<FAssetData> Assets;
|
||||
if (Registry->GetAssetsByPackageName(*PackageName, Assets))
|
||||
{
|
||||
if (Assets.Num() > 0)
|
||||
{
|
||||
// Note: we need to force deletion because old string table will be referenced
|
||||
// Unfortunately there is no ObjectTools::ForceDeleteAssets, only the inner
|
||||
// ForceDeleteObjects, so we need to do it ourselves
|
||||
ForceDeleteAssets(Assets);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default route, create string table inside script package
|
||||
Table = NewObject<UStringTable>(ScriptParent, StringTableName, Flags);
|
||||
|
||||
}
|
||||
|
||||
return Table;
|
||||
|
||||
}
|
||||
|
||||
void USUDSScriptFactory::ForceDeleteAssets(const TArray<FAssetData>& AssetsToDelete)
|
||||
{
|
||||
// Copied from ObjectTools::DeleteAssets, except that we're using ForceDeleteObjects so we
|
||||
// don't have to display any confusing messages about moving the string table into the shared asset
|
||||
|
||||
TArray<TWeakObjectPtr<UPackage>> PackageFilesToDelete;
|
||||
TArray<UObject*> ObjectsToDelete;
|
||||
for ( int i = 0; i < AssetsToDelete.Num(); i++ )
|
||||
{
|
||||
const FAssetData& AssetData = AssetsToDelete[i];
|
||||
UObject *ObjectToDelete = AssetData.GetAsset({ ULevel::LoadAllExternalObjectsTag });
|
||||
// Assets can be loaded even when their underlying type/class no longer exists...
|
||||
if ( ObjectToDelete!=nullptr )
|
||||
{
|
||||
ObjectsToDelete.Add( ObjectToDelete );
|
||||
}
|
||||
else if ( AssetData.IsUAsset() )
|
||||
{
|
||||
// ... In this cases there is no underlying asset or type so remove the package itself directly after confirming it's valid to do so.
|
||||
FString PackageFilename;
|
||||
if( !FPackageName::DoesPackageExist( AssetData.PackageName.ToString(), &PackageFilename ) )
|
||||
{
|
||||
// Could not determine filename for package so we can not delete
|
||||
continue;
|
||||
}
|
||||
|
||||
UPackage* Package = FindPackage(nullptr, *AssetData.PackageName.ToString());
|
||||
if ( Package )
|
||||
{
|
||||
PackageFilesToDelete.Add(Package);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int32 NumObjectsToDelete = ObjectsToDelete.Num();
|
||||
if ( NumObjectsToDelete > 0 )
|
||||
{
|
||||
ObjectTools::ForceDeleteObjects( ObjectsToDelete, false );
|
||||
}
|
||||
|
||||
const int32 NumPackagesToDelete = PackageFilesToDelete.Num();
|
||||
if (NumPackagesToDelete > 0)
|
||||
{
|
||||
TArray<UPackage*> PackagePointers;
|
||||
for ( const auto& PkgIt : PackageFilesToDelete )
|
||||
{
|
||||
UPackage* Package = PkgIt.Get();
|
||||
if ( Package )
|
||||
{
|
||||
PackagePointers.Add(Package);
|
||||
}
|
||||
}
|
||||
|
||||
if ( PackagePointers.Num() > 0 )
|
||||
{
|
||||
const bool bPerformReferenceCheck = true;
|
||||
ObjectTools::CleanupAfterSuccessfulDelete(PackagePointers, bPerformReferenceCheck);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2741
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptImporter.cpp
Normal file
2741
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptImporter.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,149 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptReimportFactory.h"
|
||||
|
||||
#include "SUDSEditor.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
#include "EditorFramework/AssetImportData.h"
|
||||
#include "HAL/FileManager.h"
|
||||
#include "Sound/DialogueWave.h"
|
||||
|
||||
USUDSScriptReimportFactory::USUDSScriptReimportFactory()
|
||||
{
|
||||
SupportedClass = USUDSScript::StaticClass();
|
||||
bCreateNew = false;
|
||||
// We need to have a unique priority vs the original factory, so go after
|
||||
ImportPriority = DefaultImportPriority - 1;
|
||||
}
|
||||
|
||||
bool USUDSScriptReimportFactory::CanReimport(UObject* Obj, TArray<FString>& OutFilenames)
|
||||
{
|
||||
USUDSScript* Script = Cast<USUDSScript>(Obj);
|
||||
if (Script && Script->AssetImportData)
|
||||
{
|
||||
Script->AssetImportData->ExtractFilenames(OutFilenames);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void USUDSScriptReimportFactory::SetReimportPaths(UObject* Obj, const TArray<FString>& NewReimportPaths)
|
||||
{
|
||||
USUDSScript* Script = Cast<USUDSScript>(Obj);
|
||||
if (Script && ensure(NewReimportPaths.Num() == 1))
|
||||
{
|
||||
Script->AssetImportData->UpdateFilenameOnly(NewReimportPaths[0]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
EReimportResult::Type USUDSScriptReimportFactory::Reimport(UObject* Obj)
|
||||
{
|
||||
USUDSScript* Script = Cast<USUDSScript>(Obj);
|
||||
if (!Script)
|
||||
{
|
||||
return EReimportResult::Failed;
|
||||
}
|
||||
|
||||
// Make sure file is valid and exists
|
||||
const FString Filename = Script->AssetImportData->GetFirstFilename();
|
||||
if (!Filename.Len() || IFileManager::Get().FileSize(*Filename) == INDEX_NONE)
|
||||
{
|
||||
return EReimportResult::Failed;
|
||||
}
|
||||
|
||||
// When a new script is created, it actually lives at the same address as the incoming one. UE must re-use objects
|
||||
// when you put them back at the same outer & asset name?
|
||||
// This means if we want to preserve anything from the previously imported object, such as generated VO asset links,
|
||||
// we need to copy those out now.
|
||||
TMap<FString, UDialogueVoice*> PrevSpeakerVoices = Script->GetSpeakerVoices();
|
||||
// Store the TextID -> DialogueWave, but also store the line text as well so we can detect whether it matches & warn if not
|
||||
TMap<FString, TPair<FString, UDialogueWave*> > PrevWaves;
|
||||
for (auto Node : Script->GetNodes())
|
||||
{
|
||||
if (auto TN = Cast<USUDSScriptNodeText>(Node))
|
||||
{
|
||||
if (auto W = TN->GetWave())
|
||||
{
|
||||
PrevWaves.Add(TN->GetTextID(), TPair<FString, UDialogueWave*>(TN->GetText().ToString(), W));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run the import again
|
||||
EReimportResult::Type Result = EReimportResult::Failed;
|
||||
bool OutCanceled = false;
|
||||
|
||||
if (ImportObject(Script->GetClass(), Script->GetOuter(), *Script->GetName(), RF_Public | RF_Standalone, Filename, nullptr, OutCanceled) != nullptr)
|
||||
{
|
||||
UE_LOG(LogSUDSEditor, Log, TEXT("Imported successfully"));
|
||||
|
||||
FSUDSMessageLogger Logger;
|
||||
// Now, try to restore the speaker voice / line wave links from before
|
||||
for (auto SpeakerID : Script->GetSpeakers())
|
||||
{
|
||||
if (auto pVoice = PrevSpeakerVoices.Find(SpeakerID))
|
||||
{
|
||||
Script->SetSpeakerVoice(SpeakerID, *pVoice);
|
||||
}
|
||||
}
|
||||
for (auto Node : Script->GetNodes())
|
||||
{
|
||||
if (auto TN = Cast<USUDSScriptNodeText>(Node))
|
||||
{
|
||||
if (auto pWavePair = PrevWaves.Find(TN->GetTextID()))
|
||||
{
|
||||
// Set the wave link either way
|
||||
TN->SetWave(pWavePair->Value);
|
||||
|
||||
// Check that the text is the same; if it's not, then lines have potentially changed
|
||||
// we still live with the assignment we have (might be a minor edit) but user should be aware
|
||||
if (TN->GetText().ToString() != pWavePair->Key)
|
||||
{
|
||||
Logger.Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"TextID %s is linked to Dialogue Wave %s, but text has changed. Check whether this line is linked to the correct wave, and consider Writing String Keys back to script before making more script changes in future."),
|
||||
*TN->GetTextID(),
|
||||
*pWavePair->Value->GetName());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Script->AssetImportData->Update(Filename);
|
||||
|
||||
// Try to find the outer package so we can dirty it up
|
||||
if (Script->GetOuter())
|
||||
{
|
||||
Script->GetOuter()->MarkPackageDirty();
|
||||
}
|
||||
else
|
||||
{
|
||||
Script->MarkPackageDirty();
|
||||
}
|
||||
Result = EReimportResult::Succeeded;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (OutCanceled)
|
||||
{
|
||||
UE_LOG(LogSUDSEditor, Warning, TEXT("-- import canceled"));
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogSUDSEditor, Warning, TEXT("-- import failed"));
|
||||
}
|
||||
|
||||
Result = EReimportResult::Failed;
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
int32 USUDSScriptReimportFactory::GetPriority() const
|
||||
{
|
||||
return ImportPriority;
|
||||
}
|
||||
24
Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditor.h
Normal file
24
Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditor.h
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Modules/ModuleInterface.h"
|
||||
|
||||
class FSUDSScriptActions;
|
||||
class FSlateStyleSet;
|
||||
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogSUDSEditor, Verbose, All);
|
||||
|
||||
class FSUDSEditorModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
|
||||
/** IModuleInterface implementation */
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
|
||||
protected:
|
||||
TSharedPtr<FSUDSScriptActions> ScriptActions;
|
||||
TSharedPtr<FSlateStyleSet> StyleSet;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
class USUDSScript;
|
||||
class USUDSScriptNode;
|
||||
struct FSUDSMessageLogger;
|
||||
|
||||
class FSUDSEditorScriptTools
|
||||
{
|
||||
public:
|
||||
|
||||
static void WriteBackTextIDs(USUDSScript* Script, FSUDSMessageLogger& Logger);
|
||||
static bool WriteBackTextIDsFromNodes(const TArray<USUDSScriptNode*> Nodes, TArray<FString>& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger);
|
||||
static bool WriteBackTextID(const FText& AssetText, int LineNo, TArray<FString>& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger);
|
||||
static bool WriteBackGosubID(const FString& GosubID, int LineNo, TArray<FString>& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger);
|
||||
static bool TextIDCheckMatch(const FText& AssetText, const FString& SourceLine);
|
||||
|
||||
|
||||
};
|
||||
73
Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditorSettings.h
Normal file
73
Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditorSettings.h
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "SUDSEditorSettings.generated.h"
|
||||
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class ESUDSAssetLocation : uint8
|
||||
{
|
||||
/// Use a single flat shared directory
|
||||
SharedDirectory,
|
||||
/// Use a shared base directory, but create subfolders based on the script asset name
|
||||
SharedDirectorySubdir,
|
||||
/// Place asset alongside the script that originated it
|
||||
ScriptDirectory,
|
||||
/// Place asset in a subfolder of the folder containing the script that generated it, named the same as the script
|
||||
ScriptDirectorySubdir
|
||||
};
|
||||
|
||||
class USUDSScript;
|
||||
/**
|
||||
* Settings for editor-specific aspects of SUDS (no effect at runtime)
|
||||
*/
|
||||
UCLASS(config = Editor, defaultconfig, meta=(DisplayName="SUDS Editor"))
|
||||
class SUDSEDITOR_API USUDSEditorSettings : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Where to place Dialogue Voice assets for speakers in scripts when generated", RelativeToGameContentDir, LongPackageName))
|
||||
ESUDSAssetLocation DialogueVoiceAssetLocation = ESUDSAssetLocation::SharedDirectory;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Shared directory for Dialogue Voice assets, if using a shared directory", RelativeToGameContentDir, LongPackageName))
|
||||
FDirectoryPath DialogueVoiceAssetSharedDir;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Where to place Dialogue Wave assets for speaker lines in scripts", RelativeToGameContentDir, LongPackageName))
|
||||
ESUDSAssetLocation DialogueWaveAssetLocation = ESUDSAssetLocation::ScriptDirectorySubdir;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Shared directory for Dialogue Wave assets, if using a shared directory", RelativeToGameContentDir, LongPackageName))
|
||||
FDirectoryPath DialogueWaveAssetSharedDir;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Prefix to give Dialogue Voice assets in front of their SpeakerID", RelativeToGameContentDir, LongPackageName))
|
||||
FString DialogueVoiceAssetPrefix = "DV_";
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Prefix to give Dialogue Wave assets in front of their SpeakerID", RelativeToGameContentDir, LongPackageName))
|
||||
FString DialogueWaveAssetPrefix = "DW_";
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "When generating subdirectories and wave asset names from script names, whether to strip characters before the first '_' to avoid including script prefix", RelativeToGameContentDir, LongPackageName))
|
||||
bool StripScriptPrefixesWhenGeneratingNames = true;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", AdvancedDisplay, meta = (Tooltip = "Whether to auto-generate Dialogue Voice/Wave assets for ALL dialogue scripts on import. Note: you can always generate VO assets manually."))
|
||||
bool AlwaysAutoGenerateVoiceOverAssetsOnImport = false;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", AdvancedDisplay, meta = (Tooltip = "Auto-generate Dialogue Voice/Wave assets for scripts in these directories (and subdirectories) on import. Note: you can always generate VO assets manually.", RelativeToGameContentDir, LongPackageName))
|
||||
TArray<FDirectoryPath> DirectoriesToAutoGenerateVoiceOverAssetsOnImport;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Choices", meta = (Tooltip = "Whether to generate a spoken line for choices (default false)."))
|
||||
bool AlwaysGenerateSpeakerLinesFromChoices = false;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Choices", meta = (Tooltip = "The SpeakerID to use for speaker lines generated from choices"))
|
||||
FString SpeakerIdForGeneratedLinesFromChoices = "Player";
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Assets", AdvancedDisplay, meta = (Tooltip = "Whether to create string tables as a separate package (.uasset) from the SUDS Script, which will cause them to appear separately in the Content Browser (requires script re-import)"))
|
||||
bool bCreateStringTablesAsSeparatePackages = true;
|
||||
|
||||
USUDSEditorSettings() {}
|
||||
|
||||
bool ShouldGenerateVoiceAssets(const FString& PackagePath) const;
|
||||
FString GetVoiceOutputDir(const FString& PackagePath, const FString& ScriptName) const;
|
||||
FString GetWaveOutputDir(const FString& PackagePath, const FString& ScriptName) const;
|
||||
static FString GetOutputDir(ESUDSAssetLocation Location, const FString& SharedPath, const FString& PackagePath, const FString& ScriptName);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
#include "Sound/DialogueVoice.h"
|
||||
|
||||
|
||||
struct FSUDSMessageLogger;
|
||||
class USUDSScript;
|
||||
|
||||
class SUDSEDITOR_API FSUDSEditorVoiceOverTools
|
||||
{
|
||||
public:
|
||||
static void GenerateAssets(USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger);
|
||||
protected:
|
||||
static void GenerateVoiceAssets(USUDSScript* Script,
|
||||
EObjectFlags Flags,
|
||||
FSUDSMessageLogger *Logger,
|
||||
TMap<FString, UDialogueVoice*> &OutCreatedVoices);
|
||||
static void GenerateWaveAssets(USUDSScript* Script, EObjectFlags Flags, TMap<FString, UDialogueVoice*>, FSUDSMessageLogger* Logger);
|
||||
static bool GetSpeakerVoicePackageName(USUDSScript* Script, const FString& SpeakerID, FString& OutPackageName);
|
||||
static bool GetSpeakerVoiceAssetNames(USUDSScript* Script,
|
||||
const FString& SpeakerID,
|
||||
FString& OutPackageName,
|
||||
FString& OutAssetName);
|
||||
static UDialogueVoice* FindSpeakerVoice(USUDSScript* Script, const FString& SpeakerID);
|
||||
static FString GetScriptNameAsPrefix(USUDSScript* Script);
|
||||
|
||||
static FString GetVoiceOutputDir(USUDSScript* Script);
|
||||
static FString GetWaveOutputDir(USUDSScript* Script);
|
||||
|
||||
};
|
||||
60
Plugins/SUDS/Source/SUDSEditor/Public/SUDSMessageLogger.h
Normal file
60
Plugins/SUDS/Source/SUDSEditor/Public/SUDSMessageLogger.h
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Logging/TokenizedMessage.h"
|
||||
|
||||
class FTokenizedMessage;
|
||||
|
||||
struct SUDSEDITOR_API FSUDSMessageLogger
|
||||
{
|
||||
protected:
|
||||
TArray<TSharedRef<FTokenizedMessage>> ErrorMessages;
|
||||
|
||||
bool bWriteToMessageLog = true;
|
||||
public:
|
||||
FSUDSMessageLogger() {}
|
||||
FSUDSMessageLogger(bool bWriteToMsg) : bWriteToMessageLog(bWriteToMsg) {}
|
||||
~FSUDSMessageLogger();
|
||||
|
||||
void SetWriteToMessageLog(bool bWrite) { bWriteToMessageLog = bWrite; }
|
||||
bool HasErrors() const;
|
||||
int NumErrors() const;
|
||||
bool HasWarnings() const;
|
||||
int NumWarnings() const;
|
||||
|
||||
bool GetWriteToMessageLog() const { return bWriteToMessageLog; }
|
||||
void AddMessage(EMessageSeverity::Type Severity, const FText& Text);
|
||||
|
||||
#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 6
|
||||
template <typename... Types>
|
||||
FORCEINLINE void Logf(ELogVerbosity::Type Verbosity, UE::Core::TCheckedFormatString<FString::FmtCharType, Types...> Fmt, Types... Args)
|
||||
#else
|
||||
template <typename FmtType, typename... Types>
|
||||
FORCEINLINE void Logf(ELogVerbosity::Type Verbosity, const FmtType& Fmt, Types... Args)
|
||||
#endif
|
||||
{
|
||||
EMessageSeverity::Type Sev = EMessageSeverity::Info;
|
||||
switch(Verbosity)
|
||||
{
|
||||
case ELogVerbosity::Fatal:
|
||||
case ELogVerbosity::Error:
|
||||
Sev = EMessageSeverity::Error;
|
||||
break;
|
||||
case ELogVerbosity::Warning:
|
||||
Sev = EMessageSeverity::Warning;
|
||||
break;
|
||||
default: ;
|
||||
}
|
||||
AddMessage(Sev, FText::FromString(FString::Printf(Fmt, Args...)));
|
||||
}
|
||||
|
||||
const TArray<TSharedRef<FTokenizedMessage>>& GetErrorMessages() const { return ErrorMessages; }
|
||||
|
||||
/// Clear messages in preparation for an import
|
||||
static void ClearMessages();
|
||||
|
||||
|
||||
|
||||
};
|
||||
37
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptActions.h
Normal file
37
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptActions.h
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "AssetTypeActions_Base.h"
|
||||
#include "SUDSScriptNodeGosub.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
|
||||
class USUDSScriptNode;
|
||||
class USUDSScript;
|
||||
struct FSUDSMessageLogger;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class FSUDSScriptActions : public FAssetTypeActions_Base
|
||||
{
|
||||
public:
|
||||
virtual FText GetName() const override;
|
||||
virtual FString GetObjectDisplayName(UObject* Object) const override;
|
||||
virtual UClass* GetSupportedClass() const override;
|
||||
virtual FColor GetTypeColor() const override;
|
||||
virtual uint32 GetCategories() override;
|
||||
virtual void GetResolvedSourceFilePaths(const TArray<UObject*>& TypeAssets,
|
||||
TArray<FString>& OutSourceFilePaths) const override;
|
||||
virtual bool IsImportedAsset() const override { return true; }
|
||||
virtual void GetActions(const TArray<UObject*>& InObjects, FToolMenuSection& Section) override;
|
||||
virtual bool HasActions(const TArray<UObject*>& InObjects) const override;
|
||||
virtual void OpenAssetEditor(const TArray<UObject*>& InObjects,
|
||||
TSharedPtr<IToolkitHost> EditWithinLevelEditor) override;
|
||||
|
||||
protected:
|
||||
void WriteBackTextIDs(TArray<TWeakObjectPtr<USUDSScript>> Scripts);
|
||||
|
||||
void GenerateVOAssets(TArray<TWeakObjectPtr<USUDSScript>> Scripts);
|
||||
|
||||
};
|
||||
37
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptFactory.h
Normal file
37
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptFactory.h
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSScriptImporter.h"
|
||||
#include "Factories/Factory.h"
|
||||
#include "SUDSScriptFactory.generated.h"
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class SUDSEDITOR_API USUDSScriptFactory : public UFactory
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
USUDSScriptFactory();
|
||||
protected:
|
||||
virtual UObject* FactoryCreateText(UClass* InClass,
|
||||
UObject* InParent,
|
||||
FName InName,
|
||||
EObjectFlags Flags,
|
||||
UObject* Context,
|
||||
const TCHAR* Type,
|
||||
const TCHAR*& Buffer,
|
||||
const TCHAR* BufferEnd,
|
||||
FFeedbackContext* Warn) override;
|
||||
|
||||
bool ShouldGenerateVoiceAssets(const FString& PackagePath) const;
|
||||
FSUDSScriptImporter Importer;
|
||||
|
||||
void ForceDeleteAssets(const TArray<FAssetData>& Assets);
|
||||
UStringTable* CreateStringTable(UObject* ScriptParent, FName InName, USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger);
|
||||
};
|
||||
520
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptImporter.h
Normal file
520
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptImporter.h
Normal file
@@ -0,0 +1,520 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSExpression.h"
|
||||
|
||||
struct FSUDSMessageLogger;
|
||||
class USUDSScript;
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogSUDSImporter, Verbose, All);
|
||||
|
||||
struct SUDSEDITOR_API FSUDSParsedEdge
|
||||
{
|
||||
public:
|
||||
/// Text associated with this edge (if a player choice option)
|
||||
FString Text;
|
||||
/// Identifier of the text, for the string table
|
||||
FString TextID;
|
||||
/// Metadata associated with text, for translator comments
|
||||
TMap<FName, FString> TextMetadata;
|
||||
/// The line this edge was created on
|
||||
int SourceLineNo;
|
||||
/// Condition expression that applies to this edge (for select nodes)
|
||||
FSUDSExpression ConditionExpression;
|
||||
/// Custom metadata for a node which the user can set. Can be used to annotate choice lines
|
||||
/// Could be used to disable a choice if you don't have the stats to take it, or anything else
|
||||
/// where you need a bit of extra information about a line that doesn't suit setting a dialogue variable for.
|
||||
TMap<FName, FSUDSExpression> UserMetadata;
|
||||
|
||||
int SourceNodeIdx = -1;
|
||||
int TargetNodeIdx = -1;
|
||||
|
||||
FSUDSParsedEdge(int LineNo) : SourceLineNo(LineNo){}
|
||||
|
||||
FSUDSParsedEdge(int FromNodeIdx, int ToNodeIdx, int LineNo, const FString& InText, const FString& InTextID, const TMap<FName, FString>& Metadata, const TMap<FName, FSUDSExpression>& UserMeta)
|
||||
: Text(InText),
|
||||
TextID(InTextID),
|
||||
TextMetadata(Metadata),
|
||||
SourceLineNo(LineNo),
|
||||
UserMetadata(UserMeta),
|
||||
SourceNodeIdx(FromNodeIdx),
|
||||
TargetNodeIdx(ToNodeIdx)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedEdge(int FromNodeIdx, int ToNodeIdx, int LineNo)
|
||||
: SourceLineNo(LineNo),
|
||||
SourceNodeIdx(FromNodeIdx),
|
||||
TargetNodeIdx(ToNodeIdx)
|
||||
|
||||
{
|
||||
}
|
||||
|
||||
void Reset()
|
||||
{
|
||||
*this = FSUDSParsedEdge(-1);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
enum class ESUDSParsedNodeType : uint8
|
||||
{
|
||||
/// Text node, displaying a line of dialogue
|
||||
Text,
|
||||
/// Choice node, displaying a series of user choices which navigate to other nodes
|
||||
Choice,
|
||||
/// Select node, automatically selecting one which navigates to another node based on state (also Random)
|
||||
Select,
|
||||
/// Goto node, redirects execution somewhere else
|
||||
/// Gotos are only nodes in the parsing structure, because they need to be discoverable as a fallthrough destination
|
||||
/// When converting to runtime use they just become edges
|
||||
Goto,
|
||||
/// Gosub node, acts like goto except stores return location
|
||||
Gosub,
|
||||
/// Return node, returns from a gosub
|
||||
Return,
|
||||
/// Set variable node
|
||||
SetVariable,
|
||||
/// Event node
|
||||
Event
|
||||
};
|
||||
|
||||
/// Intermediate parsed node from script text
|
||||
/// This will be converted into a final asset later
|
||||
struct SUDSEDITOR_API FSUDSParsedNode
|
||||
{
|
||||
public:
|
||||
ESUDSParsedNodeType NodeType;
|
||||
int OriginalIndent;
|
||||
/// Identifier is speaker ID, goto label, variable name etc
|
||||
FString Identifier;
|
||||
/// Text in native language
|
||||
FString Text;
|
||||
/// Identifier of the text, for the string table
|
||||
FString TextID;
|
||||
/// Metadata associated with text, for translator comments
|
||||
TMap<FName, FString> TextMetadata;
|
||||
/// Expression, for nodes that use it (e.g. set)
|
||||
FSUDSExpression Expression;
|
||||
/// Event arguments, for event nodes
|
||||
TArray<FSUDSExpression> EventArgs;
|
||||
/// Labels which lead to this node
|
||||
TArray<FString> Labels;
|
||||
/// Edges leading to other nodes
|
||||
TArray<FSUDSParsedEdge> Edges;
|
||||
/// The line this node was created on
|
||||
int SourceLineNo;
|
||||
/// Whether this is a valid fall-through target
|
||||
bool AllowFallthrough = true;
|
||||
|
||||
/// Custom metadata for a node which the user can set. Can be used to annotate speaker lines
|
||||
TMap<FName, FSUDSExpression> UserMetadata;
|
||||
|
||||
// Path hierarchy of choice nodes leading to this node, of the form "/C002/C006" etc, not including this node index
|
||||
// This helps us identify valid fallthroughs
|
||||
FString ChoicePath;
|
||||
// Path hierarchy of select nodes leading to this node, of the form "/S002/S006" etc, not including this node index
|
||||
// This helps us identify valid fallthroughs
|
||||
FString ConditionalPath;
|
||||
|
||||
/// Although multiple edges can lead here, this index is for the auto-connected parent (may be nothing)
|
||||
int ParentNodeIdx = -1;
|
||||
|
||||
FSUDSParsedNode(ESUDSParsedNodeType InNodeType, int Indent, int LineNo) : NodeType(InNodeType),
|
||||
OriginalIndent(Indent),
|
||||
SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedNode(const FString& Label,
|
||||
const FString& GosubID,
|
||||
int Indent,
|
||||
int LineNo) : NodeType(ESUDSParsedNodeType::Gosub),
|
||||
OriginalIndent(Indent),
|
||||
Identifier(Label),
|
||||
TextID(GosubID),
|
||||
SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedNode(const FString& InSpeaker, const FString& InText, const FString& InTextID, const TMap<FName, FString>& Metadata, const TMap<FName, FSUDSExpression>& UserMeta, int Indent, int LineNo)
|
||||
: NodeType(ESUDSParsedNodeType::Text),
|
||||
OriginalIndent(Indent),
|
||||
Identifier(InSpeaker),
|
||||
Text(InText),
|
||||
TextID(InTextID),
|
||||
TextMetadata(Metadata),
|
||||
SourceLineNo(LineNo),
|
||||
UserMetadata(UserMeta)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedNode(const FString& GotoLabel, int Indent, int LineNo)
|
||||
: NodeType(ESUDSParsedNodeType::Goto), OriginalIndent(Indent), Identifier(GotoLabel), SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedNode(const FString& VariableName, const FSUDSExpression& InExpr, int Indent, int LineNo)
|
||||
: NodeType(ESUDSParsedNodeType::SetVariable),
|
||||
OriginalIndent(Indent),
|
||||
Identifier(VariableName),
|
||||
Expression(InExpr),
|
||||
SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedNode(const FString& VariableName,
|
||||
const FSUDSExpression& InExpr,
|
||||
const FString& InTextID,
|
||||
int Indent,
|
||||
int LineNo)
|
||||
: NodeType(ESUDSParsedNodeType::SetVariable),
|
||||
OriginalIndent(Indent),
|
||||
Identifier(VariableName),
|
||||
TextID(InTextID),
|
||||
Expression(InExpr),
|
||||
SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
};
|
||||
class SUDSEDITOR_API FSUDSScriptImporter
|
||||
{
|
||||
public:
|
||||
bool ImportFromBuffer(const TCHAR* Buffer, int32 Len, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
void PopulateAsset(USUDSScript* Asset, UStringTable* StringTable);
|
||||
static FMD5Hash CalculateHash(const TCHAR* Buffer, int32 Len);
|
||||
static const FString EndGotoLabel;
|
||||
protected:
|
||||
static const FString TreePathSeparator;
|
||||
|
||||
enum class EConditionalStage : uint8
|
||||
{
|
||||
IfStage,
|
||||
ElseIfStage,
|
||||
ElseStage,
|
||||
RandomStage,
|
||||
RandomOptionStage
|
||||
};
|
||||
enum class EConditionalParent : uint8
|
||||
{
|
||||
Select,
|
||||
ElseIfStage,
|
||||
ElseStage
|
||||
};
|
||||
|
||||
// Struct for tracking if/elseif blocks
|
||||
struct ConditionalContext
|
||||
{
|
||||
/// Index of parent select node where else will be added
|
||||
int SelectNodeIdx = -1;
|
||||
/// Previous block index (parent for nesting)
|
||||
int PreviousBlockIdx = -1;
|
||||
/// Track whether we're in if/elseif/else
|
||||
EConditionalStage Stage;
|
||||
/// String identifying the current condition; for elseif or else contains original "if" context
|
||||
FString ConditionPathElement;
|
||||
|
||||
ConditionalContext(int InSelectNodeIdx, int InPrevBlockIdx, EConditionalStage InStage, const FString& InCondStr) :
|
||||
SelectNodeIdx(InSelectNodeIdx),
|
||||
PreviousBlockIdx(InPrevBlockIdx),
|
||||
Stage(InStage),
|
||||
ConditionPathElement(InCondStr)
|
||||
{
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/// Struct for tracking indents
|
||||
struct IndentContext
|
||||
{
|
||||
public:
|
||||
// The index of the Node which is the parent of this context
|
||||
// This potentially changes every time a sequential text node is encountered in the same context, so it's
|
||||
// always pointing to the last node encountered at this level, for connection
|
||||
int LastNodeIdx = -1;
|
||||
|
||||
/// The outermost indent level where this context lives
|
||||
/// You can indent things that don't create a new context, e.g.
|
||||
/// 1. Indent a text line under another text line: this is the same as no indent, just a continuation
|
||||
/// 2. Indent choices or conditions under a text line
|
||||
/// This is just good for readability, but does not create a new context, it's just a linear sequence
|
||||
/// Therefore the ThresholdIndent tracks the outermost indent relating to the current linear sequence, to know
|
||||
/// when you do in fact need to pop the current context off the stack.
|
||||
int ThresholdIndent = 0;
|
||||
|
||||
int LastTextNodeIdx = -1;
|
||||
|
||||
/// The path entry for this indent, to be combined with all previous levels to provide full path context
|
||||
FString PathEntry;
|
||||
|
||||
IndentContext(int NodeIdx, int Indent, const FString& Path) : LastNodeIdx(NodeIdx), ThresholdIndent(Indent), LastTextNodeIdx(-1), PathEntry(Path) {}
|
||||
|
||||
};
|
||||
|
||||
/// A tree of nodes. Contained to separate header nodes from body nodes
|
||||
struct ParsedTree
|
||||
{
|
||||
public:
|
||||
/// The indent context stack representing where we are in the indentation tree while parsing
|
||||
/// There must always be 1 level (root)
|
||||
TArray<IndentContext> IndentLevelStack;
|
||||
/// When encountering conditions and choice lines, we are building up details for an edge to another node, but
|
||||
/// we currently don't know the target node. We keep these pending details here
|
||||
int EdgeInProgressNodeIdx = -1;
|
||||
int EdgeInProgressEdgeIdx = -1;
|
||||
/// List of all nodes, appended to as parsing progresses
|
||||
/// Ordering is important, these nodes must be in the order encountered in the file
|
||||
TArray<FSUDSParsedNode> Nodes;
|
||||
/// Record of goto labels to node index, built up during parsing (forward refs are OK so not complete until end of parsing)
|
||||
TMap<FString, int> GotoLabelList;
|
||||
/// Goto labels which have been encountered but we haven't found a destination yet
|
||||
TArray<FString> PendingGotoLabels;
|
||||
/// Goto labels that lead directly to another goto and thus are just aliases
|
||||
TMap<FString, FString> AliasedGotoLabels;
|
||||
|
||||
/// Conditional blocks
|
||||
/// "if" creates a new context, uses current as parent
|
||||
/// "elseif" and "else" also create new contexts, but copies parent from current (sibling)
|
||||
/// "endif" ends the context
|
||||
/// You cannot have conditionals that were started in an indent context ending outside it
|
||||
TArray<ConditionalContext> ConditionalBlocks;
|
||||
/// Index of the current conditional block, if any
|
||||
int CurrentConditionalBlockIdx = -1;
|
||||
|
||||
void Reset()
|
||||
{
|
||||
IndentLevelStack.Reset();
|
||||
EdgeInProgressNodeIdx = EdgeInProgressEdgeIdx -1;
|
||||
Nodes.Reset();
|
||||
GotoLabelList.Reset();
|
||||
PendingGotoLabels.Reset();
|
||||
AliasedGotoLabels.Reset();
|
||||
ConditionalBlocks.Reset();
|
||||
CurrentConditionalBlockIdx = -1;
|
||||
}
|
||||
};
|
||||
|
||||
ParsedTree HeaderTree;
|
||||
ParsedTree BodyTree;
|
||||
|
||||
struct ParsedMetadata
|
||||
{
|
||||
public:
|
||||
FName Key;
|
||||
FString Value;
|
||||
int IndentLevel;
|
||||
|
||||
ParsedMetadata(const FName& InKey, const FString& InValue, int InIndentLevel)
|
||||
: Key(InKey),
|
||||
Value(InValue),
|
||||
IndentLevel(InIndentLevel)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/// Text Metadata applied to speaker lines / choices until reset
|
||||
/// For each key there's a stack of metadata, with most indented at the top
|
||||
TMap<FName, TArray<ParsedMetadata>> PersistentMetadata;
|
||||
/// Text Metadata applied just to the next speaker line or choice
|
||||
TMap<FName, ParsedMetadata> TransientMetadata;
|
||||
/// User metadata for the next speaker line or choice
|
||||
TMap<FName, FSUDSExpression> UserMetadata;
|
||||
|
||||
/// List of speakers, detected during parsing of lines of text
|
||||
TArray<FString> ReferencedSpeakers;
|
||||
|
||||
const int TabIndentValue = 4;
|
||||
bool bHeaderDone = false;
|
||||
bool bTooLateForHeader = false;
|
||||
bool bHeaderInProgress = false;
|
||||
TOptional<bool> bOverrideGenerateSpeakerLineForChoice;
|
||||
TOptional<FString> OverrideChoiceSpeakerID;
|
||||
bool bTextInProgress = false;
|
||||
int ChoiceUniqueId = 0;
|
||||
/// For generating text IDs
|
||||
int TextIDHighestNumber = 0;
|
||||
/// For generating gosub IDs
|
||||
int GosubIDHighestNumber = 0;
|
||||
/// Parse a single line
|
||||
bool ParseLine(const FStringView& Line, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool ParseHeaderLine(const FStringView& Line, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool ParseBodyLine(const FStringView& Line, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool ParseCommentMetadataLine(const FStringView& Line, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool IsLastNodeOfType(const ParsedTree& Tree, ESUDSParsedNodeType Type);
|
||||
bool ParseChoiceLine(const FStringView& Line, ParsedTree& Tree, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger*
|
||||
Logger,
|
||||
bool bSilent);
|
||||
FSUDSParsedEdge* GetEdgeInProgress(ParsedTree& Tree);
|
||||
void EnsureChoiceNodeExistsAboveSelect(ParsedTree& Tree, int IndentLevel, int LineNo);
|
||||
static bool IsConditionalLine(const FStringView& Line);
|
||||
bool ParseConditionalLine(const FStringView& Line, ParsedTree& Tree, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool ParseIfLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
const FString& ConditionStr,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseElseIfLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
const FString& ConditionStr,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
|
||||
bool ParseElseLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseEndIfLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger*
|
||||
Logger,
|
||||
bool bSilent);
|
||||
static bool IsRandomLine(const FStringView& Line);
|
||||
bool ParseRandomLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseBeginRandomLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseRandomOptionLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseEndRandomLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseGotoLabelLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseGotoLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseGosubLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseReturnLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseSetLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseEventLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseTextLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseImportSettingLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
TMap<FName, FString> GetTextMetadataForNextEntry(int CurrentLineIndent);
|
||||
TMap<FName, FSUDSExpression> ConsumeUserMetadata();
|
||||
bool IsCommentLine(const FStringView& TrimmedLine);
|
||||
FStringView TrimLine(const FStringView& Line, int& OutIndentLevel) const;
|
||||
int FindChoiceAfterTextNode(const FSUDSScriptImporter::ParsedTree& Tree, int TextNodeIdx, const FString& ConditionalPath);
|
||||
int RecurseChoiceNodeFindDeepest(const ParsedTree& Tree, int FromChoiceIdx, const FString& ConditionalPath);
|
||||
int RecurseSelectNodeFindDeepestChoice(const ParsedTree& Tree, int FromSelectNode, const FString& ConditionalPath);
|
||||
int FindEdge(const FSUDSScriptImporter::ParsedTree& Tree, int ParentNodeIdx, int TargetNodeIndex);
|
||||
FString MakeIfConditionPathElement(int SelectNodeIdx, const FString& ConditionStr);
|
||||
FString MakeElseIfConditionPathElement(int SelectNodeIdx, const FString& ConditionStr);
|
||||
FString MakeElseConditionPathElement(int SelectNodeIdx);
|
||||
int FindLastChoiceNode(const ParsedTree& Tree, int IndentLevel);
|
||||
int FindLastChoiceNode(const ParsedTree& Tree, int IndentLevel, int FromIndex, const FString& ConditionPath);
|
||||
void PopIndent(ParsedTree& Tree);
|
||||
void PushIndent(ParsedTree& Tree, int NodeIdx, int Indent, const FString& Path);
|
||||
FString GetCurrentTreePath(const FSUDSScriptImporter::ParsedTree& Tree);
|
||||
FString GetCurrentTreeConditionalPath(const FSUDSScriptImporter::ParsedTree& Tree);
|
||||
FString GetTreeConditionalPath(const ParsedTree& Tree, int NodeIndex);
|
||||
void SetFallthroughForNewNode(FSUDSScriptImporter::ParsedTree& Tree, FSUDSParsedNode& NewNode);
|
||||
int AppendNode(ParsedTree& Tree, const FSUDSParsedNode& InNode);
|
||||
bool SelectNodeIsMissingElsePath(const FSUDSScriptImporter::ParsedTree& Tree, const FSUDSParsedNode& Node);
|
||||
bool PostImportSanityCheck(const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool ChoiceNodeCheckPaths(const FSUDSParsedNode& ChoiceNode,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool RecurseChoiceNodeCheckPaths(const FSUDSParsedNode& OrigChoiceNode, const FSUDSParsedNode& CurrChoiceNode, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool RecurseChoiceNodeCheckPaths(const FSUDSParsedNode& ChoiceNode, const FSUDSParsedEdge& Edge, const FString& NameForErrors, FSUDSMessageLogger*
|
||||
Logger,
|
||||
bool bSilent);
|
||||
void ConnectRemainingNodes(ParsedTree& Tree, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
void GenerateTextIDs(ParsedTree& BodyTree);
|
||||
int FindFallthroughNodeIndex(ParsedTree& Tree, int StartNodeIndex, const FString& FromChoicePath, const FString& FromConditionalPath);
|
||||
bool RetrieveAndRemoveTextID(FStringView& InOutLine, FString& OutTextID);
|
||||
bool RetrieveAndRemoveGosubID(FStringView& InOutLine, FString& OutTextID);
|
||||
FString GenerateTextID();
|
||||
const FSUDSParsedNode* GetNode(const ParsedTree& Tree, int Index = 0);
|
||||
int GetGotoTargetNodeIndex(const ParsedTree& Tree, const FString& InLabel);
|
||||
void PopulateAssetFromTree(USUDSScript* Asset,
|
||||
const ParsedTree& Tree,
|
||||
TArray<TObjectPtr<class USUDSScriptNode>>* pOutNodes,
|
||||
TMap<FName, int>* pOutLabels,
|
||||
UStringTable* StringTable);
|
||||
|
||||
public:
|
||||
const FSUDSParsedNode* GetNode(int Index = 0);
|
||||
const FSUDSParsedNode* GetHeaderNode(int Index = 0);
|
||||
/// Resolve a goto label to a target index (after import), or -1 if not resolvable
|
||||
int GetGotoTargetNodeIndex(const FString& Label);
|
||||
static bool RetrieveTextIDFromLine(FStringView& InOutLine, FString& OutTextID, int& OutNumber);
|
||||
static bool RetrieveGosubIDFromLine(FStringView& InOutLine, FString& OutID, int& OutNumber);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/ObjectMacros.h"
|
||||
#include "EditorReimportHandler.h"
|
||||
#include "SUDSScriptFactory.h"
|
||||
|
||||
#include "SUDSScriptReimportFactory.generated.h"
|
||||
|
||||
// Reimports a USUDSScriptFactory asset
|
||||
// Necessary to get the import pop-up
|
||||
UCLASS()
|
||||
class USUDSScriptReimportFactory : public USUDSScriptFactory, public FReimportHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
USUDSScriptReimportFactory();
|
||||
|
||||
//~ Begin FReimportHandler Interface
|
||||
virtual bool CanReimport(UObject* Obj, TArray<FString>& OutFilenames) override;
|
||||
virtual void SetReimportPaths(UObject* Obj, const TArray<FString>& NewReimportPaths) override;
|
||||
virtual EReimportResult::Type Reimport(UObject* Obj) override;
|
||||
virtual int32 GetPriority() const override;
|
||||
//~ End FReimportHandler Interface
|
||||
};
|
||||
62
Plugins/SUDS/Source/SUDSEditor/SUDSEditor.Build.cs
Normal file
62
Plugins/SUDS/Source/SUDSEditor/SUDSEditor.Build.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class SUDSEditor : ModuleRules
|
||||
{
|
||||
public SUDSEditor(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add public include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add other private include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PublicDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"Core",
|
||||
"SUDS"
|
||||
// ... add other public dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
"InputCore", // needed by come Slate widgets
|
||||
"Projects", // So that we can use the IPluginManager, required for our custom style
|
||||
"ToolMenus",
|
||||
"MessageLog",
|
||||
"UnrealEd",
|
||||
"EditorStyle"
|
||||
// ... add private dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
DynamicallyLoadedModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
// ... add any modules that your module loads dynamically here ...
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user