Add new plugins, refactor exp, introduce stat forge to replace GAS

This commit is contained in:
2026-07-15 22:48:04 +02:00
parent 3c084d9669
commit da0a0b643f
267 changed files with 33330 additions and 48 deletions

View 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);

View 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;
}

View File

@@ -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);
}
}

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -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;
}

View 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();
}

View 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);
}
}
}
}

View 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);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}

View 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;
};

View File

@@ -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);
};

View 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);
};

View File

@@ -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);
};

View 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();
};

View 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);
};

View 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);
};

View 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);
};

View File

@@ -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
};

View 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 ...
}
);
}
}