Files
Faye/Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptImporter.cpp
2026-07-04 21:15:59 +02:00

2766 lines
90 KiB
C++

// Copyright Steve Streeting 2022
// Released under the MIT license https://opensource.org/license/MIT/
#include "SUDSScriptImporter.h"
#include "SUDSEditorSettings.h"
#include "SUDSExpression.h"
#include "SUDSMessageLogger.h"
#include "SUDSScript.h"
#include "SUDSScriptNode.h"
#include "SUDSScriptNodeEvent.h"
#include "SUDSScriptNodeGosub.h"
#include "SUDSScriptNodeSet.h"
#include "SUDSScriptNodeText.h"
#include "Internationalization/Regex.h"
#include "Internationalization/StringTable.h"
#include "Internationalization/StringTableCore.h"
class USUDSEditorSettings;
const FString FSUDSScriptImporter::EndGotoLabel = "end";
const FString FSUDSScriptImporter::TreePathSeparator = "/";
DEFINE_LOG_CATEGORY(LogSUDSImporter)
bool FSUDSScriptImporter::ImportFromBuffer(const TCHAR *Start, int32 Length, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent)
{
static const TCHAR* LineEndings[] =
{
TEXT("\r\n"),
TEXT("\r"),
TEXT("\n"),
};
constexpr int32 NumDelims = UE_ARRAY_COUNT(LineEndings);
int LineNumber = 1;
HeaderTree.Reset();
BodyTree.Reset();
PersistentMetadata.Empty();
TransientMetadata.Empty();
UserMetadata.Empty();
bHeaderDone = false;
bHeaderInProgress = false;
bTooLateForHeader = false;
bool bImportedOK = true;
ChoiceUniqueId = 0;
TextIDHighestNumber = 0;
bOverrideGenerateSpeakerLineForChoice.Reset();
OverrideChoiceSpeakerID.Reset();
ReferencedSpeakers.Empty();
if (Start)
{
int32 SubstringBeginIndex = 0;
// Iterate through string.
for(int32 i = 0; i < Length;)
{
int32 SubstringEndIndex = INDEX_NONE;
int32 DelimiterLength = 0;
// Attempt each delimiter.
for(int32 DelimIndex = 0; DelimIndex < NumDelims; ++DelimIndex)
{
DelimiterLength = FCString::Strlen(LineEndings[DelimIndex]);
// If we found a delimiter...
if (FCString::Strncmp(Start + i, LineEndings[DelimIndex], DelimiterLength) == 0)
{
// Mark the end of the substring.
SubstringEndIndex = i;
break;
}
}
if (SubstringEndIndex != INDEX_NONE)
{
const int32 SubstringLength = SubstringEndIndex - SubstringBeginIndex;
FStringView Line = FStringView(Start + SubstringBeginIndex, SubstringLength);
if (!ParseLine(Line, LineNumber++, NameForErrors, Logger, bSilent))
{
// Abort, error
bImportedOK = false;
break;
}
// Next substring begins at the end of the discovered delimiter.
SubstringBeginIndex = SubstringEndIndex + DelimiterLength;
i = SubstringBeginIndex;
}
else
{
++i;
}
}
// Add any remaining characters after the last delimiter.
const int32 SubstringLength = Length - SubstringBeginIndex;
const FStringView Line = FStringView(Start + SubstringBeginIndex, SubstringLength);
bImportedOK = ParseLine(Line, LineNumber++, NameForErrors, Logger, bSilent) && bImportedOK;
}
ConnectRemainingNodes(HeaderTree, NameForErrors, Logger, bSilent);
ConnectRemainingNodes(BodyTree, NameForErrors, Logger, bSilent);
GenerateTextIDs(HeaderTree);
GenerateTextIDs(BodyTree);
bImportedOK = PostImportSanityCheck(NameForErrors, Logger, bSilent) && bImportedOK;
return bImportedOK;
}
bool FSUDSScriptImporter::ParseLine(const FStringView& Line, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent)
{
// Trim off any whitespace, but record how much of it there is since it can be relevant
int IndentLevel;
const FStringView TrimmedLine = TrimLine(Line, IndentLevel);
if (TrimmedLine.Len() == 0 && !bTextInProgress)
{
// We will skip any blank lines that aren't inside text
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:00: BLANK %s"), LineNo, *FString(Line));
return true;
}
if (IsCommentLine(TrimmedLine))
{
// Skip over comment lines
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: COMMENT %s"), LineNo, IndentLevel, *FString(Line));
// May be metadata in the comment though
ParseCommentMetadataLine(TrimmedLine, IndentLevel, LineNo, NameForErrors, Logger, bSilent);
return true;
}
// Check for headers
static const FStringView HeaderPrefix(TEXT("==="));
if (TrimmedLine.StartsWith(HeaderPrefix))
{
if (bHeaderDone)
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Failed to parse %s Line %d: Duplicate header section"), *NameForErrors, LineNo);
return false;
}
else if (bTooLateForHeader)
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Failed to parse %s Line %d: Header section must be at start"), *NameForErrors, LineNo);
return false;
}
if (bHeaderInProgress)
{
// End of header
bHeaderInProgress = false;
bHeaderDone = true;
}
else
{
bHeaderInProgress = true;
}
return true;
}
else if (bHeaderInProgress)
{
return ParseHeaderLine(TrimmedLine, IndentLevel, LineNo, NameForErrors, Logger, bSilent);
}
// Process body
return ParseBodyLine(TrimmedLine, IndentLevel, LineNo, NameForErrors, Logger, bSilent);
}
bool FSUDSScriptImporter::ParseCommentMetadataLine(const FStringView& Line,
int IndentLevel,
int LineNo,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
// Translator comments
// Comment metadata starts with:
// #= [Key:] Single Use Metadata (next line only)
// #+ [Key:] Persistent Metadata (apply to all lines until reset)
// [Key:] is optional; if omitted the key is "Comment"
// Persistent Metadata is reset when:
// - The same key is set again (can be set to blank to reset to empty)
// - A line that is more outdented than the source of the key is encountered
if (Line.StartsWith(TEXT("#=")) || Line.StartsWith(TEXT("#+")))
{
FString LineStr(Line);
const FRegexPattern MetaPattern(TEXT("^#([\\=\\+])\\s*(?:(\\S*)\\s*:\\s*)?(.*)$"));
FRegexMatcher MetaRegex(MetaPattern, LineStr);
if (MetaRegex.FindNext())
{
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: META : %s"), LineNo, IndentLevel, *FString(Line));
const bool bIsPersistent = MetaRegex.GetCaptureGroup(1) == "+";
// There is no "count" of capture groups, test highest to detect if key used
FString KeyStr = MetaRegex.GetCaptureGroup(2);
const FName Key = FName(KeyStr.IsEmpty() ? "Comment" :KeyStr);
const FString Value = MetaRegex.GetCaptureGroup(3).TrimStartAndEnd();
if (bIsPersistent)
{
TArray<ParsedMetadata>* pStack = PersistentMetadata.Find(Key);
if (!pStack)
{
// Only bother creating if non-empty
// If we find a blank and there's a stack there already, we do add an entry since blank overrides others in scope
if (!Value.IsEmpty())
{
pStack = &PersistentMetadata.Add(Key);
}
}
if (pStack)
{
// First we need to check if this line is less or equal indented; if so we have to strip out existing stack items
while (!pStack->IsEmpty() && IndentLevel <= pStack->Top().IndentLevel)
{
pStack->Pop();
}
pStack->Push(ParsedMetadata(Key, Value, IndentLevel));
}
}
else
{
if (Value.IsEmpty())
{
// Reset
TransientMetadata.Remove(Key);
}
else
{
TransientMetadata.Add(Key, ParsedMetadata(Key, Value, IndentLevel));
}
}
return true;
}
else
{
Logger->Logf(ELogVerbosity::Warning,
TEXT(
"%s: Malformed translator comment on line %d, ignoring"),
*NameForErrors,
LineNo);
return false;
}
}
else if (Line.StartsWith(TEXT("#%")))
{
// User custom metadata - always applies to next line only
// Similar syntax to set lines, except text isn't allowed (this is not player visible)
// #% Key = Value
// #% Key Value
FString LineStr(Line);
const FRegexPattern UserMetaPattern(TEXT("^#\\%\\s+(\\S+)\\s+(?:=\\s+)?(\\S.*)$"));
FRegexMatcher UserMetaRegex(UserMetaPattern, LineStr);
if (UserMetaRegex.FindNext())
{
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: USERMETA : %s"), LineNo, IndentLevel, *FString(Line));
FString Name = UserMetaRegex.GetCaptureGroup(1);
FString ExprStr = UserMetaRegex.GetCaptureGroup(2).TrimStartAndEnd(); // trim because capture accepts spaces in quotes
FSUDSExpression Expr;
{
FString ParseError;
if (Expr.ParseFromString(ExprStr, &ParseError))
{
if (Expr.IsTextLiteral())
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Warning, TEXT("Error in %s line %d: Text values not allowed in user metadata"), *NameForErrors, LineNo);
return false;
}
else
{
UserMetadata.Add(FName(Name), Expr);
return true;
}
}
else
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Warning, TEXT("Error in %s line %d: %s"), *NameForErrors, LineNo, *ParseError);
return false;
}
}
}
else
{
// Only a warning, in case other types of comment accidentally clash
Logger->Logf(ELogVerbosity::Warning,
TEXT(
"%s: Malformed user metadata comment on line %d, ignoring"),
*NameForErrors,
LineNo);
return false;
}
}
return false;
}
TMap<FName, FString> FSUDSScriptImporter::GetTextMetadataForNextEntry(int CurrentLineIndent)
{
TMap<FName, FString> Ret;
// For each key
for (auto It = PersistentMetadata.CreateIterator(); It; ++It)
{
auto& Stack = It->Value;
// Use top of stack, so long as equally or less indented than current line
while (!Stack.IsEmpty() && CurrentLineIndent < Stack.Top().IndentLevel)
{
Stack.Pop();
}
if (Stack.IsEmpty())
{
// Remove key entry if there's no values left on the stack
It.RemoveCurrent();
}
else
{
Ret.Add(It->Key, Stack.Top().Value);
}
}
// Add transient after so they override persistent
for (auto& Pair: TransientMetadata)
{
// Always apply transient ones to next case, don't check indent
Ret.Add(Pair.Key, Pair.Value.Value);
}
TransientMetadata.Empty();
return Ret;
}
TMap<FName, FSUDSExpression> FSUDSScriptImporter::ConsumeUserMetadata()
{
TMap<FName, FSUDSExpression> Ret = UserMetadata;
UserMetadata.Empty();
return Ret;
}
bool FSUDSScriptImporter::ParseHeaderLine(const FStringView& Line, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent)
{
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:00: HEADER: %s"), LineNo, *FString(Line));
// Header can still have indenting, only a very limited set of functions though (conditionals)
while (HeaderTree.IndentLevelStack.Num() > 1 &&
IndentLevel < HeaderTree.IndentLevelStack.Top().ThresholdIndent)
{
// Pop as much from the stack as necessary to return to this indent level
PopIndent(HeaderTree);
}
if (HeaderTree.IndentLevelStack.IsEmpty())
{
// Must be the first body line encountered. Add 1 indent level for the root
PushIndent(HeaderTree, -1, 0, "");
}
// Header lines include set / conditionals
// We ignore every other type of line
if (Line.StartsWith(TEXT('[')))
{
bool bParsed = ParseSetLine(Line, HeaderTree, 0, LineNo, NameForErrors, Logger, bSilent);
if (!bParsed)
bParsed = ParseImportSettingLine(Line, HeaderTree, 0, LineNo, NameForErrors, Logger, bSilent);;
}
return true;
}
bool FSUDSScriptImporter::ParseBodyLine(const FStringView& Line,
int IndentLevel,
int LineNo,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
// Once we've had anything other than comments or blanks and non-headers, it's too late for headers
bTooLateForHeader = true;
// Body indenting matters
// If less than "threshold indent", pop contexts from stack until that's no longer the case
// If more than threshold indent, may be a child of previous line. Increase threshold indent to this level
// ONLY a new context level IF previous was condition or choice. If previous was text then just a continuation
// This is why "threshold indent" remains as the outermost indent in this context
// If same as threshold indent, continuation of current context
while (BodyTree.IndentLevelStack.Num() > 1 &&
IndentLevel < BodyTree.IndentLevelStack.Top().ThresholdIndent)
{
// Pop as much from the stack as necessary to return to this indent level
PopIndent(BodyTree);
}
if (BodyTree.IndentLevelStack.IsEmpty())
{
// Must be the first body line encountered. Add 1 indent level for the root
PushIndent(BodyTree, -1, 0, "");
}
if (Line.StartsWith(TEXT('*')))
{
return ParseChoiceLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent);
}
else if (Line.StartsWith(TEXT(':')))
{
return ParseGotoLabelLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent);
}
else if (Line.StartsWith(TEXT('[')))
{
bool bParsed = ParseConditionalLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent);
if (!bParsed)
bParsed = ParseGotoLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent);
if (!bParsed)
bParsed = ParseSetLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent);
if (!bParsed)
bParsed = ParseEventLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent);
if (!bParsed)
bParsed = ParseGosubLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent);
if (!bParsed)
bParsed = ParseReturnLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent);
if (!bParsed)
bParsed = ParseImportSettingLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent);
if (!bParsed)
bParsed = ParseRandomLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent);
if (!bParsed)
{
if (!bSilent)
{
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: CMD : %s"), LineNo, IndentLevel, *FString(Line));
Logger->Logf(ELogVerbosity::Warning, TEXT("%s Line %d: Unrecognised command. Ignoring!"), *NameForErrors, LineNo);
}
// We still return true because we don't want to fail the entire import
}
return true;
}
else
{
return ParseTextLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent);
}
}
bool FSUDSScriptImporter::IsLastNodeOfType(const FSUDSScriptImporter::ParsedTree& Tree, ESUDSParsedNodeType Type)
{
auto& Ctx = Tree.IndentLevelStack.Top();
return Tree.Nodes.IsValidIndex(Ctx.LastNodeIdx) && Tree.Nodes[Ctx.LastNodeIdx].NodeType == Type;
}
int FSUDSScriptImporter::FindLastChoiceNode(const ParsedTree& Tree, int IndentLevel)
{
int Ret = FindLastChoiceNode(Tree, IndentLevel,Tree.Nodes.Num() - 1, GetCurrentTreeConditionalPath(Tree));
// if (Ret == -1)
// {
// // Fallback to try to find from previous text
// auto& Ctx = Tree.IndentLevelStack.Top();
//
// if (Tree.Nodes.IsValidIndex(Ctx.LastTextNodeIdx))
// {
// return FindChoiceAfterTextNode(Tree, Ctx.LastTextNodeIdx);
// }
// }
return Ret;
}
int FSUDSScriptImporter::FindChoiceAfterTextNode(const FSUDSScriptImporter::ParsedTree& Tree, int TextNodeIdx, const FString& ConditionalPath)
{
const auto& TextNode = Tree.Nodes[TextNodeIdx];
int RootChoiceIdx = -1;
if (TextNode.Edges.Num() == 1 && Tree.Nodes.IsValidIndex(TextNode.Edges[0].TargetNodeIdx))
{
int NextNodeIdx = TextNode.Edges[0].TargetNodeIdx;
while (Tree.Nodes.IsValidIndex(NextNodeIdx))
{
auto& Node = Tree.Nodes[NextNodeIdx];
switch (Node.NodeType)
{
case ESUDSParsedNodeType::Select:
case ESUDSParsedNodeType::Text:
// Didn't find
break;
case ESUDSParsedNodeType::Choice:
RootChoiceIdx = NextNodeIdx;
NextNodeIdx = -1; // to break out of loop
break;
case ESUDSParsedNodeType::SetVariable:
case ESUDSParsedNodeType::Goto:
case ESUDSParsedNodeType::Event:
// Cascade through events to find the first choice
if (Node.Edges.Num() == 1)
{
NextNodeIdx = Node.Edges[0].TargetNodeIdx;
}
else
{
NextNodeIdx = -1;
}
break;
default: ;
};
}
if (RootChoiceIdx == -1)
{
return -1;
}
// Now we've found the first choice after the text node
// BUT if there are conditional choices, there are select nodes underneath this, with
// more choice nodes. We want to find the deepest matching one and use the choice under that.
return RecurseChoiceNodeFindDeepest(Tree, RootChoiceIdx, ConditionalPath);
}
return -1;
}
int FSUDSScriptImporter::RecurseChoiceNodeFindDeepest(const ParsedTree& Tree,
int FromChoiceIdx,
const FString& ConditionalPath)
{
// We need to recurse from the Choice and any sequence of S->C->S->C is valid
// so long as the condition path matches. The deepest one is the best
// So here we're a Choice, and we can recurse into any Selects, and any Choices under that
// where the edge condition is a subset of ConditionalPath
int Ret = FromChoiceIdx;
if (Tree.Nodes.IsValidIndex(FromChoiceIdx))
{
const auto& Choice = Tree.Nodes[FromChoiceIdx];
for (int i = 0; i < Choice.Edges.Num(); ++i)
{
const auto& Edge = Choice.Edges[i];
if (Edge.Text.IsEmpty() && Tree.Nodes.IsValidIndex(Edge.TargetNodeIdx) && Tree.Nodes[Edge.TargetNodeIdx].NodeType == ESUDSParsedNodeType::Select)
{
int ChildIdx = RecurseSelectNodeFindDeepestChoice(Tree, Edge.TargetNodeIdx, ConditionalPath);
// There should be only one that matches conditional
if (ChildIdx != -1)
{
Ret = ChildIdx;
break;
}
}
}
}
return Ret;
}
int FSUDSScriptImporter::RecurseSelectNodeFindDeepestChoice(const ParsedTree& Tree,
int FromSelectNode,
const FString& ConditionalPath)
{
// Default to not having found a choice that matches conditional
int Ret = -1;
if (Tree.Nodes.IsValidIndex(FromSelectNode))
{
const auto& Select = Tree.Nodes[FromSelectNode];
for (int i = 0; i < Select.Edges.Num(); ++i)
{
const auto& Edge = Select.Edges[i];
if (Tree.Nodes.IsValidIndex(Edge.TargetNodeIdx) && Tree.Nodes[Edge.TargetNodeIdx].NodeType == ESUDSParsedNodeType::Choice)
{
// It might be this choice node, IF edge matches the conditional path
FString ChoiceConditionalPath = GetTreeConditionalPath(Tree, Edge.TargetNodeIdx);
if (ConditionalPath.StartsWith(ChoiceConditionalPath))
{
// But also we recurse from here because there may be other nested Selects which match closer
int ChildIdx = RecurseChoiceNodeFindDeepest(Tree, Edge.TargetNodeIdx, ConditionalPath);
// There should be only one that matches conditional
if (ChildIdx != -1)
{
Ret = ChildIdx;
break;
}
}
}
}
}
return Ret;
}
int FSUDSScriptImporter::FindLastChoiceNode(const ParsedTree& Tree, int IndentLevel, int FromIndex, const FString& ConditionPath)
{
// Scan backwards from end of tree looking for a choice node which has the same or higher indent and condition state as current
// But abort if we hit text nodes on that path
for (int i = FromIndex; i >= 0; --i)
{
auto& Node = Tree.Nodes[i];
if (ConditionPath.StartsWith(Node.ConditionalPath) &&
Node.NodeType == ESUDSParsedNodeType::Text &&
Node.OriginalIndent <= IndentLevel)
{
// We hit a parent text node, we can't go back any further
return -1;
}
// Only consider nodes on the same conditional path
// Note: NOT containing the conditional path. We don't want to skip over an intervening select node
if (ConditionPath == Node.ConditionalPath)
{
// note that we allow indents < as well as ==
// This is so that if you choose to aesthetically indent choices it still works
if (Node.NodeType == ESUDSParsedNodeType::Choice && Node.OriginalIndent <= IndentLevel)
{
return i;
}
}
}
return -1;
}
bool FSUDSScriptImporter::ParseChoiceLine(const FStringView& Line,
FSUDSScriptImporter::ParsedTree& Tree,
int IndentLevel,
int LineNo,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
if (Line.StartsWith('*'))
{
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: CHOICE: %s"), LineNo, IndentLevel, *FString(Line));
auto& Ctx = Tree.IndentLevelStack.Top();
// Find a previous choice node to join with
int ChoiceNodeIdx = - 1;
// however, if there are pending goto labels, they MUST split the choice
if (Tree.PendingGotoLabels.Num() == 0)
{
ChoiceNodeIdx = FindLastChoiceNode(Tree, IndentLevel);
}
// If the current indent context node is NOT a choice, create a choice node, and connect to previous node (using pending edge if needed)
if (ChoiceNodeIdx == -1)
{
// Didn't find a choice node we can connect to, so create one
// However, before we do that, if our parent is a select node, we must ensure that its parent is a choice node
// This is just so that we have a choice node outside the select that any choices that come after the [endif]
// can attach to. If a non-conditional choice was encountered first, it'll already be there. But if the first
// choice was conditional, the select node will have been created first.
// We still create a choice node here too, underneath the select, to act as a holder for potentially multiple
// choices within this select condition
EnsureChoiceNodeExistsAboveSelect(Tree, IndentLevel, LineNo);
ChoiceNodeIdx = AppendNode(Tree, FSUDSParsedNode(ESUDSParsedNodeType::Choice, IndentLevel, LineNo));
}
if (Tree.EdgeInProgressNodeIdx != -1)
{
// Must already have been a choice node but previous pending edge wasn't resolved
// This means it's a fallthrough, will be resolved at end
Tree.EdgeInProgressNodeIdx = Tree.EdgeInProgressEdgeIdx = -1;
}
auto& ChoiceNode = Tree.Nodes[ChoiceNodeIdx];
// Inside each choice, everything should be indented at least as much as 1 character inside the *
// We provide the edge with context C001, C002 etc for fallthrough
PushIndent(Tree, ChoiceNodeIdx, IndentLevel + 1, FString::Printf(TEXT("C%03d"), ++ChoiceUniqueId));
// Do we want to generate a speaker line for this choice
bool bGenerateSpeakerLine = false;
FString GeneratedSpeakerID;
if (const auto Settings = GetDefault<USUDSEditorSettings>())
{
bGenerateSpeakerLine = Settings->AlwaysGenerateSpeakerLinesFromChoices;
GeneratedSpeakerID = Settings->SpeakerIdForGeneratedLinesFromChoices;
}
if (bOverrideGenerateSpeakerLineForChoice.IsSet())
{
bGenerateSpeakerLine = bOverrideGenerateSpeakerLineForChoice.GetValue();
}
if (OverrideChoiceSpeakerID.IsSet())
{
GeneratedSpeakerID = OverrideChoiceSpeakerID.GetValue();
}
int ChoiceTextStart = 1;
if (Line.Len() > 1 && Line[1] == '-')
{
// *- prefix, override speaker line
bGenerateSpeakerLine = false;
ChoiceTextStart = 2;
}
// Add a pending edge, with the choice text
// Following things fill in the edge details, the next node to be parsed will finalise the destination
FString ChoiceTextID;
auto ChoiceTextView = Line.SubStr(ChoiceTextStart, Line.Len() - ChoiceTextStart).TrimStart();
// TextID might be blank after this, but that's OK, we fix later after we know what IDs are in use
RetrieveAndRemoveTextID(ChoiceTextView, ChoiceTextID);
const FString ChoiceText = FString(ChoiceTextView);
auto ChoiceTextMeta = GetTextMetadataForNextEntry(IndentLevel);
auto ChoiceUserMeta = ConsumeUserMetadata();
const int EdgeIdx = ChoiceNode.Edges.Add(FSUDSParsedEdge(ChoiceNodeIdx, -1, LineNo, ChoiceText, ChoiceTextID, ChoiceTextMeta, ChoiceUserMeta));
Tree.EdgeInProgressNodeIdx = ChoiceNodeIdx;
Tree.EdgeInProgressEdgeIdx = EdgeIdx;
if (bGenerateSpeakerLine)
{
// We use the same text & ID so this is just one localisation entry
Ctx.LastTextNodeIdx = AppendNode(Tree, FSUDSParsedNode(GeneratedSpeakerID, ChoiceText, ChoiceTextID, ChoiceTextMeta, ChoiceUserMeta, IndentLevel + 1, LineNo));
ReferencedSpeakers.AddUnique(GeneratedSpeakerID);
}
return true;
}
return false;
}
FSUDSParsedEdge* FSUDSScriptImporter::GetEdgeInProgress(ParsedTree& Tree)
{
if (Tree.Nodes.IsValidIndex(Tree.EdgeInProgressNodeIdx))
{
auto& N = Tree.Nodes[Tree.EdgeInProgressNodeIdx];
if (N.Edges.IsValidIndex(Tree.EdgeInProgressEdgeIdx))
{
return &N.Edges[Tree.EdgeInProgressEdgeIdx];
}
}
return nullptr;
}
void FSUDSScriptImporter::EnsureChoiceNodeExistsAboveSelect(ParsedTree& Tree, int IndentLevel, int LineNo)
{
auto& Ctx = Tree.IndentLevelStack.Top();
int ParentIdx = -1;
// Inserting is going to mess up EdgeInProgress
if (auto E = GetEdgeInProgress(Tree))
{
ParentIdx = E->SourceNodeIdx;
}
else
{
ParentIdx = Ctx.LastNodeIdx;
}
// Nothing to do if immediate parent isn't select
if (!Tree.Nodes.IsValidIndex(ParentIdx) || Tree.Nodes[ParentIdx].NodeType != ESUDSParsedNodeType::Select)
return;
int TopSelectIdx = ParentIdx;
// Find the node above the top of potentially nested select chain
while (Tree.Nodes.IsValidIndex(ParentIdx) && Tree.Nodes[ParentIdx].NodeType == ESUDSParsedNodeType::Select)
{
TopSelectIdx = ParentIdx;
ParentIdx = Tree.Nodes[ParentIdx].ParentNodeIdx;
}
if (Tree.Nodes.IsValidIndex(ParentIdx) && Tree.Nodes[ParentIdx].NodeType == ESUDSParsedNodeType::Choice)
{
// OK we found a choice above the select, this is OK
return;
}
if (ParentIdx == -1)
{
// This means we hit the top of the chain without finding choice or select
// We have to trust that FindLastChoiceNode will find it & it's valid (previous placement should resolve)
// Connect top select to choice under last text
if (Tree.Nodes.IsValidIndex(TopSelectIdx) && Tree.Nodes.IsValidIndex(Ctx.LastTextNodeIdx))
{
// Choice node might not be directly underneath
const int ChoiceIdx = FindChoiceAfterTextNode(Tree, Ctx.LastTextNodeIdx, Tree.Nodes[TopSelectIdx].ConditionalPath);
if (ChoiceIdx != -1)
{
auto& SelNode = Tree.Nodes[TopSelectIdx];
auto& ChoiceNode = Tree.Nodes[ChoiceIdx];
FSUDSParsedEdge NewEdge(LineNo);
NewEdge.SourceNodeIdx = ChoiceIdx;
NewEdge.TargetNodeIdx = TopSelectIdx;
ChoiceNode.Edges.Add(NewEdge);
SelNode.ParentNodeIdx = ChoiceIdx;
}
}
return;
}
// If we got here, we navigated to the point above the select(s) and didn't end up on a root choice
// So we need to insert one, after this index
const int InsertIdx = ParentIdx + 1; // this happens to return 0 for having hit the start of the tree (-1), which is fine
Tree.Nodes.Insert(FSUDSParsedNode(ESUDSParsedNodeType::Choice, IndentLevel, LineNo), InsertIdx);
auto& NewChoice = Tree.Nodes[InsertIdx];
auto& SelectNode = Tree.Nodes[InsertIdx + 1];
// Add edge to the select, fixup the parent nodes for both
NewChoice.Edges.Add(FSUDSParsedEdge(InsertIdx, InsertIdx + 1, LineNo));
NewChoice.ParentNodeIdx = SelectNode.ParentNodeIdx;
NewChoice.ChoicePath = SelectNode.ChoicePath;
NewChoice.ConditionalPath = SelectNode.ConditionalPath;
// Now for every other node after this, we have to fix up indexes that are >= InsertIdx
// We don't fix up anything before, because we want things that pointed forward to the select to now point at the choice
for (int i = InsertIdx + 1; i < Tree.Nodes.Num(); ++i)
{
if (i != InsertIdx)
{
auto& N = Tree.Nodes[i];
if (N.ParentNodeIdx >= InsertIdx)
++N.ParentNodeIdx;
for (auto& E : N.Edges)
{
if (E.SourceNodeIdx >= InsertIdx)
++E.SourceNodeIdx;
if (E.TargetNodeIdx >= InsertIdx)
++E.TargetNodeIdx;
}
}
}
// Also fix indent contexts
for (auto& I : Tree.IndentLevelStack)
{
if (I.LastNodeIdx >= InsertIdx)
++I.LastNodeIdx;
if (I.LastTextNodeIdx >= InsertIdx)
++I.LastTextNodeIdx;
}
// Also conditional blocks
for (auto& CB : Tree.ConditionalBlocks)
{
if (CB.SelectNodeIdx >= InsertIdx)
++CB.SelectNodeIdx;
}
// Fixup edge in progress
if (Tree.EdgeInProgressNodeIdx >= InsertIdx)
++Tree.EdgeInProgressNodeIdx;
// Manually change the select node parent index afterwards (if we change it before it'll get adjusted again)
SelectNode.ParentNodeIdx = InsertIdx;
SetFallthroughForNewNode(Tree, NewChoice);
}
bool FSUDSScriptImporter::IsConditionalLine(const FStringView& Line)
{
return Line.StartsWith(TEXT("[if")) ||
Line.StartsWith(TEXT("[else")) ||
Line.StartsWith(TEXT("[endif"));
}
bool FSUDSScriptImporter::ParseElseLine(const FStringView& Line,
FSUDSScriptImporter::ParsedTree& Tree,
int IndentLevel,
int LineNo,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
if (Tree.ConditionalBlocks.IsValidIndex(Tree.CurrentConditionalBlockIdx))
{
// "else" changes the current "if" or "elseif" block to "else" & creates a new condition-free edge
// Select or choice node should already be there
// Select node may be turned into a choice later if choice is the first thing encountered
auto& Block = Tree.ConditionalBlocks[Tree.CurrentConditionalBlockIdx];
if (Block.Stage != EConditionalStage::ElseStage)
{
Block.Stage = EConditionalStage::ElseStage;
// We need to give each else a unique condition string, otherwise sibling else's can be considered
// equivalent, when they in fact originate from different if's
// ID by select node index
Block.ConditionPathElement = MakeElseConditionPathElement(Block.SelectNodeIdx);
const int NodeIdx = Block.SelectNodeIdx;
auto& SelectNode = Tree.Nodes[NodeIdx];
const int EdgeIdx = SelectNode.Edges.Add(FSUDSParsedEdge(NodeIdx, -1, LineNo));
Tree.EdgeInProgressNodeIdx = NodeIdx;
Tree.EdgeInProgressEdgeIdx = EdgeIdx;
// Wind back the last node to select
auto& Ctx = Tree.IndentLevelStack.Top();
Ctx.LastNodeIdx = NodeIdx;
}
else
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: cannot have more than one 'else'"), *NameForErrors, LineNo);
}
}
else
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: 'else' with no matching 'if'"), *NameForErrors, LineNo);
}
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: ELSE : %s"), LineNo, IndentLevel, *FString(Line));
return true;
}
FString FSUDSScriptImporter::MakeIfConditionPathElement(int SelectNodeIdx,
const FString& ConditionStr)
{
return ConditionStr;
}
FString FSUDSScriptImporter::MakeElseIfConditionPathElement(int SelectNodeIdx,
const FString& ConditionStr)
{
return FString::Printf(TEXT("elseif-%d %s"), SelectNodeIdx, *ConditionStr);
}
FString FSUDSScriptImporter::MakeElseConditionPathElement(int SelectNodeIdx)
{
return FString::Printf(TEXT("else-%d"), SelectNodeIdx);
}
bool FSUDSScriptImporter::ParseEndIfLine(const FStringView& Line,
FSUDSScriptImporter::ParsedTree& Tree,
int IndentLevel,
int LineNo,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
if (Tree.ConditionalBlocks.IsValidIndex(Tree.CurrentConditionalBlockIdx))
{
// Endif finishes the current block
const auto& Block = Tree.ConditionalBlocks[Tree.CurrentConditionalBlockIdx];
Tree.CurrentConditionalBlockIdx = Block.PreviousBlockIdx;
// We must also clear the indent last node pointer, because we never want to auto-connect to conditionals
// We'll let the final fallthrough pass connect things
auto& Ctx = Tree.IndentLevelStack.Top();
Ctx.LastNodeIdx = -1;
}
else
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: 'endif' with no matching 'if'"), *NameForErrors, LineNo);
}
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: ENDIF : %s"), LineNo, IndentLevel, *FString(Line));
return true;
}
bool FSUDSScriptImporter::ParseIfLine(const FStringView& Line,
FSUDSScriptImporter::ParsedTree& Tree,
const FString& ConditionStr,
int IndentLevel,
int LineNo,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: IF : %s"), LineNo, IndentLevel, *FString(Line));
// New if level always creates select node
const int NewNodeIdx = AppendNode(Tree, FSUDSParsedNode(ESUDSParsedNodeType::Select, IndentLevel, LineNo));
auto& SelectNode = Tree.Nodes[NewNodeIdx];
const int EdgeIdx = SelectNode.Edges.Add(FSUDSParsedEdge(NewNodeIdx, -1, LineNo));
auto E = &SelectNode.Edges[EdgeIdx];
{
FString ParseError;
if (!E->ConditionExpression.ParseFromString(ConditionStr, &ParseError))
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: %s"), *NameForErrors, LineNo, *ParseError);
}
}
Tree.EdgeInProgressNodeIdx = NewNodeIdx;
Tree.EdgeInProgressEdgeIdx = EdgeIdx;
Tree.CurrentConditionalBlockIdx = Tree.ConditionalBlocks.Add(
ConditionalContext(NewNodeIdx, Tree.CurrentConditionalBlockIdx, EConditionalStage::IfStage, MakeIfConditionPathElement(NewNodeIdx, ConditionStr)));
return true;
}
bool FSUDSScriptImporter::ParseElseIfLine(const FStringView& Line,
ParsedTree& Tree,
const FString& ConditionStr,
int IndentLevel,
int LineNo,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: ELSEIF: %s"), LineNo, IndentLevel, *FString(Line));
// "elseif" changes the current block state
// Select or choice node should already be there
// Select node may be turned into a choice later if choice is the first thing encountered
if (Tree.ConditionalBlocks.IsValidIndex(Tree.CurrentConditionalBlockIdx))
{
auto& Block = Tree.ConditionalBlocks[Tree.CurrentConditionalBlockIdx];
if (Block.Stage != EConditionalStage::ElseStage)
{
Block.Stage = EConditionalStage::ElseIfStage;
// For the purposes of the block, the condition isn't just the condition contained here,
// it's also the negation of the original "if". This is to prevent multiple sibling
// elseifs merging if they contain the same condition but are attached to different ifs
Block.ConditionPathElement = MakeElseIfConditionPathElement(Block.SelectNodeIdx, ConditionStr);
const int NodeIdx = Block.SelectNodeIdx;
auto& SelectOrChoiceNode = Tree.Nodes[NodeIdx];
const int EdgeIdx = SelectOrChoiceNode.Edges.Add(FSUDSParsedEdge(NodeIdx, -1, LineNo));
auto E = &SelectOrChoiceNode.Edges[EdgeIdx];
{
FString ParseError;
if (!E->ConditionExpression.ParseFromString(ConditionStr, &ParseError))
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: %s"), *NameForErrors, LineNo, *ParseError);
}
}
Tree.EdgeInProgressNodeIdx = NodeIdx;
Tree.EdgeInProgressEdgeIdx = EdgeIdx;
}
else
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: 'elseif' occurs after 'else'"), *NameForErrors, LineNo);
}
}
else
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: 'elseif' with no matching 'if'"), *NameForErrors, LineNo);
}
return true;
}
bool FSUDSScriptImporter::ParseConditionalLine(const FStringView& Line,
FSUDSScriptImporter::ParsedTree& Tree,
int IndentLevel,
int LineNo,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
if (!IsConditionalLine(Line))
return false;
if (Line.Equals(TEXT("[else]")))
{
return ParseElseLine(Line, Tree, IndentLevel, LineNo, NameForErrors, Logger, bSilent);
}
else if (Line.Equals(TEXT("[endif]")))
{
return ParseEndIfLine(Line, Tree, IndentLevel, LineNo, NameForErrors, Logger, bSilent);
}
else
{
const FString LineStr(Line);
const FRegexPattern IfPattern(TEXT("^\\[if\\s+(.+)\\]$"));
FRegexMatcher IfRegex(IfPattern, LineStr);
if (IfRegex.FindNext())
{
const FString ConditionStr = IfRegex.GetCaptureGroup(1);
return ParseIfLine(Line, Tree, ConditionStr, IndentLevel, LineNo, NameForErrors, Logger, bSilent);
}
else
{
const FRegexPattern ElseIfPattern(TEXT("^\\[elseif\\s+(.+)\\]$"));
FRegexMatcher ElseIfRegex(ElseIfPattern, LineStr);
if (ElseIfRegex.FindNext())
{
const FString ConditionStr = ElseIfRegex.GetCaptureGroup(1);
return ParseElseIfLine(Line, Tree, ConditionStr, IndentLevel, LineNo, NameForErrors, Logger, bSilent);
}
}
}
return false;
}
bool FSUDSScriptImporter::ParseRandomLine(const FStringView& Line,
ParsedTree& Tree,
int IndentLevel,
int LineNo,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
if (!IsRandomLine(Line))
return false;
else if (Line.Equals(TEXT("[random]")))
{
return ParseBeginRandomLine(Line, Tree, IndentLevel, LineNo, NameForErrors, Logger, bSilent);
}
else if (Line.Equals(TEXT("[or]")))
{
return ParseRandomOptionLine(Line, Tree, IndentLevel, LineNo, NameForErrors, Logger, bSilent);
}
else if (Line.Equals(TEXT("[endrandom]")))
{
return ParseEndRandomLine(Line, Tree, IndentLevel, LineNo, NameForErrors, Logger, bSilent);
}
return false;
}
bool FSUDSScriptImporter::IsRandomLine(const FStringView& Line)
{
return Line.StartsWith(TEXT("[random")) ||
Line.StartsWith(TEXT("[or")) ||
Line.StartsWith(TEXT("[endrandom"));
}
bool FSUDSScriptImporter::ParseBeginRandomLine(const FStringView& Line,
ParsedTree& Tree,
int IndentLevel,
int LineNo,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: RANDOM: %s"), LineNo, IndentLevel, *FString(Line));
// New random level always creates node
const int NewNodeIdx = AppendNode(Tree, FSUDSParsedNode(ESUDSParsedNodeType::Select, IndentLevel, LineNo));
auto& SelectNode = Tree.Nodes[NewNodeIdx];
const int EdgeIdx = SelectNode.Edges.Add(FSUDSParsedEdge(NewNodeIdx, -1, LineNo));
FString ConditionStr = FString::Printf(TEXT("{%hs} == 0"), SUDS_RANDOMITEM_VAR);
auto E = &SelectNode.Edges[EdgeIdx];
{
// Set the condition automatically based on internal random number var
FString ParseError;
if (!E->ConditionExpression.ParseFromString(ConditionStr, &ParseError))
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: %s"), *NameForErrors, LineNo, *ParseError);
}
}
Tree.EdgeInProgressNodeIdx = NewNodeIdx;
Tree.EdgeInProgressEdgeIdx = EdgeIdx;
Tree.CurrentConditionalBlockIdx = Tree.ConditionalBlocks.Add(
ConditionalContext(NewNodeIdx, Tree.CurrentConditionalBlockIdx, EConditionalStage::RandomStage, ConditionStr));
return true;
}
bool FSUDSScriptImporter::ParseRandomOptionLine(const FStringView& Line,
ParsedTree& Tree,
int IndentLevel,
int LineNo,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
// This is kind of like an auto-generated elseif
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: OR: %s"), LineNo, IndentLevel, *FString(Line));
// "elseif" changes the current block state
// Select or choice node should already be there
// Select node may be turned into a choice later if choice is the first thing encountered
if (Tree.ConditionalBlocks.IsValidIndex(Tree.CurrentConditionalBlockIdx))
{
auto& Block = Tree.ConditionalBlocks[Tree.CurrentConditionalBlockIdx];
if (Block.Stage == EConditionalStage::RandomStage ||
Block.Stage == EConditionalStage::RandomOptionStage)
{
Block.Stage = EConditionalStage::RandomOptionStage;
const int NodeIdx = Block.SelectNodeIdx;
auto& SelectOrChoiceNode = Tree.Nodes[NodeIdx];
// Generate condition based on auto-generated random item choice
Block.ConditionPathElement = FString::Printf(TEXT("{%hs} == %d"), SUDS_RANDOMITEM_VAR, SelectOrChoiceNode.Edges.Num());
const int EdgeIdx = SelectOrChoiceNode.Edges.Add(FSUDSParsedEdge(NodeIdx, -1, LineNo));
auto E = &SelectOrChoiceNode.Edges[EdgeIdx];
{
FString ParseError;
if (!E->ConditionExpression.ParseFromString(Block.ConditionPathElement, &ParseError))
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: %s"), *NameForErrors, LineNo, *ParseError);
}
}
Tree.EdgeInProgressNodeIdx = NodeIdx;
Tree.EdgeInProgressEdgeIdx = EdgeIdx;
}
else
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: 'or' must occur after 'random'"), *NameForErrors, LineNo);
}
}
else
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: 'or' with no matching 'random'"), *NameForErrors, LineNo);
}
return true;
}
bool FSUDSScriptImporter::ParseEndRandomLine(const FStringView& Line,
ParsedTree& Tree,
int IndentLevel,
int LineNo,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
// Similar to "endif"; except that we need to turn the last option into an "else", otherwise at end of
// parsing we'll add an extra else edge
if (Tree.ConditionalBlocks.IsValidIndex(Tree.CurrentConditionalBlockIdx))
{
// Endrandom finishes the current block
const auto& Block = Tree.ConditionalBlocks[Tree.CurrentConditionalBlockIdx];
if (Block.Stage == EConditionalStage::RandomStage ||
Block.Stage == EConditionalStage::RandomOptionStage)
{
const int NodeIdx = Block.SelectNodeIdx;
auto& SelectNode = Tree.Nodes[NodeIdx];
if (SelectNode.Edges.Num() > 0)
{
auto& LastEdge = SelectNode.Edges[SelectNode.Edges.Num()-1];
LastEdge.ConditionExpression.Reset();
}
Tree.CurrentConditionalBlockIdx = Block.PreviousBlockIdx;
// We must also clear the indent last node pointer, because we never want to auto-connect to conditionals
// We'll let the final fallthrough pass connect things
auto& Ctx = Tree.IndentLevelStack.Top();
Ctx.LastNodeIdx = -1;
}
else
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: 'endrandom' with no matching 'random'"), *NameForErrors, LineNo);
}
}
else
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: 'endrandom' with no matching 'random'"), *NameForErrors, LineNo);
}
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: ENDRANDOM : %s"), LineNo, IndentLevel, *FString(Line));
return true;
}
bool FSUDSScriptImporter::ParseGotoLabelLine(const FStringView& Line,
FSUDSScriptImporter::ParsedTree& Tree,
int IndentLevel,
int LineNo,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
// We've already established that line starts with ':'
// There should not be any spaces in the label
const FString LineStr(Line);
const FRegexPattern LabelPattern(TEXT("^\\:\\s*(\\w+)$"));
FRegexMatcher LabelRegex(LabelPattern, LineStr);
if (LabelRegex.FindNext())
{
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: LABEL : %s"), LineNo, IndentLevel, *FString(Line));
// lowercase goto labels so case insensitive
FString Label = LabelRegex.GetCaptureGroup(1).ToLower();
if (Label == EndGotoLabel)
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: Label 'end' is reserved and cannot be used in the script, ignoring"), *NameForErrors, LineNo);
}
else
{
Tree.PendingGotoLabels.Add(Label);
// This will be connected to the next node created
// Is a list since multiple labels may resolve to the same place
}
}
else
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Warning, TEXT("Error in %s line %d: Badly formed goto label"), *NameForErrors, LineNo);
}
// Always return true to carry on, may not be used
return true;
}
bool FSUDSScriptImporter::ParseGotoLine(const FStringView& Line,
FSUDSScriptImporter::ParsedTree& Tree,
int IndentLevel,
int LineNo,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
// Unfortunately FRegexMatcher doesn't support FStringView
const FString LineStr(Line);
// Allow both 'goto' and 'go to'
const FRegexPattern GotoPattern(TEXT("^\\[go[ ]?to\\s+(\\w+)\\s*\\]$"));
FRegexMatcher GotoRegex(GotoPattern, LineStr);
if (GotoRegex.FindNext())
{
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: GOTO : %s"), LineNo, IndentLevel, *FString(Line));
// lower case label so case insensitive
const FString Label = GotoRegex.GetCaptureGroup(1).ToLower();
// note that we do NOT try to resolve the goto label here, to allow forward jumps.
const auto& Ctx = Tree.IndentLevelStack.Top();
// A goto is an edge from the current node to another node
// That means if we had pending labels that didn't hit a node before now, they're just aliases to THIS label
for (auto PendingLabel : Tree.PendingGotoLabels)
{
Tree.AliasedGotoLabels.Add(PendingLabel, Label);
}
Tree.PendingGotoLabels.Reset();
AppendNode(Tree, FSUDSParsedNode(Label, IndentLevel, LineNo));
return true;
}
return false;
}
bool FSUDSScriptImporter::ParseGosubLine(const FStringView& InLine,
ParsedTree& Tree,
int IndentLevel,
int LineNo,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
// Attempt to find existing ID
// We need these in order to save the return stack
FString GosubID;
FStringView Line = InLine;
// If this is a continuation line, we shouldn't generate one, but we need to trim it off if it's there
bool bFoundID = RetrieveAndRemoveGosubID(Line, GosubID);
// Unfortunately FRegexMatcher doesn't support FStringView
const FString LineStr(Line);
// Allow both 'gosub' and 'go sub'
const FRegexPattern GosubPattern(TEXT("^\\[go[ ]?sub\\s+(\\w+)\\s*\\]$"));
FRegexMatcher GosubRegex(GosubPattern, LineStr);
if (GosubRegex.FindNext())
{
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: GOSUB : %s"), LineNo, IndentLevel, *FString(Line));
// lower case label so case insensitive
const FString Label = GosubRegex.GetCaptureGroup(1).ToLower();
// You CANNOT "gosub end"
if (Label == EndGotoLabel)
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: You cannot 'gosub end', will never return. Did you mean goto?"), *NameForErrors, LineNo);
}
else
{
// note that we do NOT try to resolve the label here, to allow forward jumps.
const auto& Ctx = Tree.IndentLevelStack.Top();
// A gosub will become a node of its own in the final runtime
// Therefore we don't need to alias labels like we do with gotos
AppendNode(Tree, FSUDSParsedNode(Label, GosubID, IndentLevel, LineNo));
}
return true;
}
return false;
}
bool FSUDSScriptImporter::ParseReturnLine(const FStringView& Line,
ParsedTree& Tree,
int IndentLevel,
int LineNo,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
// Unfortunately FRegexMatcher doesn't support FStringView
const FString LineStr(Line);
const FRegexPattern ReturnPattern(TEXT("^\\[return\\s*\\]$"));
FRegexMatcher ReturnRegex(ReturnPattern, LineStr);
if (ReturnRegex.FindNext())
{
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: RETURN : %s"), LineNo, IndentLevel, *FString(Line));
AppendNode(Tree, FSUDSParsedNode(ESUDSParsedNodeType::Return, IndentLevel, LineNo));
return true;
}
return false;
}
bool FSUDSScriptImporter::ParseSetLine(const FStringView& InLine,
FSUDSScriptImporter::ParsedTree& Tree,
int IndentLevel,
int LineNo,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
// Attempt to find existing text ID, for string literals
// For multiple lines, may not be present until last line (but in fact can be on any line)
// We generate anyway, because it can be overriden by later lines, but makes sure we have one always
FString TextID;
FStringView Line = InLine;
// TextID may be blank after this, that's OK - we fix at the end once we know what IDs are used
RetrieveAndRemoveTextID(Line, TextID);
// Unfortunately FRegexMatcher doesn't support FStringView
const FString LineStr(Line);
// Accept forms:
// [set Var Expression]
// [set Var = Expression] (more readable in the case of non-trivial expressions)
const FRegexPattern SetPattern(TEXT("^\\[set\\s+(\\S+)\\s+(?:=\\s+)?([^\\]]+)\\]$"));
FRegexMatcher SetRegex(SetPattern, LineStr);
if (SetRegex.FindNext())
{
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: SET : %s"), LineNo, IndentLevel, *FString(Line));
FString Name = SetRegex.GetCaptureGroup(1);
FString ExprStr = SetRegex.GetCaptureGroup(2).TrimStartAndEnd(); // trim because capture accepts spaces in quotes
FSUDSExpression Expr;
{
FString ParseError;
if (Expr.ParseFromString(ExprStr, &ParseError))
{
if (Expr.IsTextLiteral())
{
AppendNode(Tree, FSUDSParsedNode(Name, Expr, TextID, IndentLevel, LineNo));
}
else
{
AppendNode(Tree, FSUDSParsedNode(Name, Expr, IndentLevel, LineNo));
}
return true;
}
else
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: %s"), *NameForErrors, LineNo, *ParseError);
}
}
}
return false;
}
bool FSUDSScriptImporter::ParseImportSettingLine(const FStringView& Line,
ParsedTree& Tree,
int IndentLevel,
int LineNo,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
// Unfortunately FRegexMatcher doesn't support FStringView
const FString LineStr(Line);
const FRegexPattern ImportSetPattern(TEXT("^\\[importsetting\\s+(\\S+)\\s+(?:=\\s+)?([^\\]]+)\\]$"));
FRegexMatcher ImportSetRegex(ImportSetPattern, LineStr);
if (ImportSetRegex.FindNext())
{
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: IMPORTSETTING: %s"), LineNo, IndentLevel, *FString(Line));
const FString Name = ImportSetRegex.GetCaptureGroup(1);
const FString ExprStr = ImportSetRegex.GetCaptureGroup(2).TrimStartAndEnd(); // trim because capture accepts spaces in quotes
FSUDSExpression Expr;
{
FString ParseError;
if (Expr.ParseFromString(ExprStr, &ParseError))
{
if (Expr.IsLiteral())
{
// Import settings affect importer state directly, no nodes
if (Name.Compare("GenerateSpeakerLinesFromChoices", ESearchCase::IgnoreCase) == 0)
{
if (Expr.GetLiteralValue().GetType() == ESUDSValueType::Boolean)
{
bOverrideGenerateSpeakerLineForChoice = Expr.GetBooleanLiteralValue();
}
else
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: [importsetting GenerateSpeakerLinesFromChoices ...] requires a boolean literal"), *NameForErrors, LineNo);
}
}
else if (Name.Compare("SpeakerIDForGeneratedLinesFromChoices", ESearchCase::IgnoreCase) == 0)
{
if (Expr.GetLiteralValue().GetType() == ESUDSValueType::Name)
{
// Speaker IDs are strings, but we go via FName to avoid translation
OverrideChoiceSpeakerID = Expr.GetNameLiteralValue().ToString();
}
else
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: [importsetting SpeakerIDForGeneratedLinesFromChoices ...] requires a Name literal e.g. (`Value`)"), *NameForErrors, LineNo);
}
}
return true;
}
else
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: importsetting only accepts literal values"), *NameForErrors, LineNo);
}
}
else
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: %s"), *NameForErrors, LineNo, *ParseError);
}
}
}
return false;
}
bool FSUDSScriptImporter::ParseEventLine(const FStringView& Line,
FSUDSScriptImporter::ParsedTree& Tree,
int IndentLevel,
int LineNo,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
const FString LineStr(Line);
const FRegexPattern EventPattern(TEXT("^\\[event\\s+([\\w\\.]+)([^\\]]*)\\]$"));
FRegexMatcher EventRegex(EventPattern, LineStr);
if (EventRegex.FindNext())
{
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: EVENT : %s"), LineNo, IndentLevel, *FString(Line));
FSUDSParsedNode Node(ESUDSParsedNodeType::Event, IndentLevel, LineNo);
Node.Identifier = EventRegex.GetCaptureGroup(1);
if (EventRegex.GetCaptureGroupBeginning(2) != INDEX_NONE)
{
// Has arguments, all lumped together
// Capture using a sub-regex which can detect quoted strings, split by commas
FString AllArgs = EventRegex.GetCaptureGroup(2).TrimStartAndEnd();
const FRegexPattern ArgPattern(TEXT("((\\\"[^\\\"]*\\\"|[^,\\\"]+))"));
FRegexMatcher ArgRegex(ArgPattern, AllArgs);
while (ArgRegex.FindNext())
{
// then process the quote
FString ArgStr = ArgRegex.GetCaptureGroup(1).TrimStartAndEnd();
if (ArgStr.Len() == 0)
continue;
FSUDSExpression Expr;
FString ParseError;
if (Expr.ParseFromString(ArgStr, &ParseError))
{
// note: no localisation of event literals, they're just strings
// we assume the receiver of the event will set localised text to variables if they want
Node.EventArgs.Add(Expr);
}
else
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Warning, TEXT("Error in %s line %d: Literal argument %d ('%s') invalid: %s"), *NameForErrors, LineNo, Node.EventArgs.Num() + 1, *ArgStr, *ParseError);
}
}
}
AppendNode(Tree, Node);
return true;
}
return false;
}
bool FSUDSScriptImporter::ParseTextLine(const FStringView& InLine,
FSUDSScriptImporter::ParsedTree& Tree,
int IndentLevel,
int LineNo,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
auto& Ctx = Tree.IndentLevelStack.Top();
// Attempt to find existing text ID
// For multiple lines, may not be present until last line (but in fact can be on any line)
FString TextID;
FStringView Line = InLine;
// Retrieve, but don't generate text ID at this point
// If this is a continuation line, we shouldn't generate one, but we need to trim it off if it's there
// TextID may be blank after this, that's OK - we fix at the end once we know what IDs are used
RetrieveAndRemoveTextID(Line, TextID);
const FString LineStr(Line);
const FRegexPattern SpeakerPattern(TEXT("^(\\S+)\\:\\s*(.+)$"));
FRegexMatcher SpeakerRegex(SpeakerPattern, LineStr);
if (SpeakerRegex.FindNext())
{
// OK this is a speaker line, in which case this is a new text node
const FString Speaker = SpeakerRegex.GetCaptureGroup(1);
const FString Text = SpeakerRegex.GetCaptureGroup(2);
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: TEXT : %s"), LineNo, IndentLevel, *FString(Line));
// New text node
// Text nodes can never introduce another indent context
// We've already backed out to the outer indent in caller
Ctx.LastTextNodeIdx = AppendNode(Tree, FSUDSParsedNode(Speaker, Text, TextID, GetTextMetadataForNextEntry(IndentLevel), ConsumeUserMetadata(), IndentLevel, LineNo));
ReferencedSpeakers.AddUnique(Speaker);
return true;
}
// If we fell through, this line is appended to the last text node
if (!bSilent)
UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: TEXT+ : %s"), LineNo, IndentLevel, *FString(Line));
if (Tree.Nodes.IsValidIndex(Ctx.LastNodeIdx))
{
auto& Node = Tree.Nodes[Ctx.LastNodeIdx];
if (Node.NodeType == ESUDSParsedNodeType::Text)
{
Node.Text.Appendf(TEXT("\n%s"), *LineStr);
}
else
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Warning, TEXT("Error in %s line %d: Text newline continuation is not immediately after a speaker line. Ignoring."), *NameForErrors, LineNo);
// We still return true to allow continue
}
}
return true;
}
bool FSUDSScriptImporter::RetrieveAndRemoveTextID(FStringView& InOutLine, FString& OutTextID)
{
FString LineWithout;
int Number;
if (RetrieveTextIDFromLine(InOutLine, OutTextID, Number))
{
TextIDHighestNumber = FMath::Max(TextIDHighestNumber, Number);
return true;
}
return false;
}
bool FSUDSScriptImporter::RetrieveTextIDFromLine(FStringView& InOutLine, FString& OutTextID, int& OutNumber)
{
const FString LineStr(InOutLine);
const FRegexPattern TextIDPattern(TEXT("(\\@([0-9a-fA-F]+)\\@)"));
FRegexMatcher TextIDRegex(TextIDPattern, LineStr);
if (TextIDRegex.FindNext())
{
OutTextID = TextIDRegex.GetCaptureGroup(1);
// Chop the incoming string to the left of the TextID
InOutLine = InOutLine.Left(TextIDRegex.GetCaptureGroupBeginning(1));
// Also trim right
InOutLine = InOutLine.TrimEnd();
// FDefaultValueHelper::ParseInt requires an "0x" prefix but we're not using that
// Plus does extra checking we don't need
OutNumber = FCString::Strtoi(*TextIDRegex.GetCaptureGroup(2), nullptr, 16);
return true;
}
return false;
}
bool FSUDSScriptImporter::RetrieveAndRemoveGosubID(FStringView& InOutLine, FString& OutTextID)
{
FString LineWithout;
int Number;
if (RetrieveGosubIDFromLine(InOutLine, OutTextID, Number))
{
GosubIDHighestNumber = Number;
return true;
}
return false;
}
bool FSUDSScriptImporter::RetrieveGosubIDFromLine(FStringView& InOutLine, FString& OutID, int& OutNumber)
{
const FString LineStr(InOutLine);
const FRegexPattern IDPattern(TEXT("(\\@GS([0-9a-fA-F]+)\\@)"));
FRegexMatcher IDRegex(IDPattern, LineStr);
if (IDRegex.FindNext())
{
OutID = IDRegex.GetCaptureGroup(1);
// Chop the incoming string to the left of the TextID
InOutLine = InOutLine.Left(IDRegex.GetCaptureGroupBeginning(1));
// Also trim right
InOutLine = InOutLine.TrimEnd();
// FDefaultValueHelper::ParseInt requires an "0x" prefix but we're not using that
// Plus does extra checking we don't need
OutNumber = FCString::Strtoi(*IDRegex.GetCaptureGroup(2), nullptr, 16);
return true;
}
return false;
}
FString FSUDSScriptImporter::GenerateTextID()
{
// Generate a new text ID just based on ascending numbers
// We don't actually base this on the line but we have it for future possible use
// Since it's a string, format exactly as in the sud file
return FString::Printf(TEXT("@%04x@"), ++TextIDHighestNumber);
}
FString FSUDSScriptImporter::GetCurrentTreePath(const FSUDSScriptImporter::ParsedTree& Tree)
{
// This is just a path of all the choice / select nodes AND their edges leading to this point, for fallthrough
// * Choice (/C000/)
// * Nested choice (/C000/C001/)
// Fallthrough from here
// * Choice (/C002/C003/)
// Do NOT fallthrough to here
// Fallthrough to here instead (/)
#if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8
TStringBuilder<256> B;
#else
FStringBuilderBase B;
#endif
for (auto Indent : Tree.IndentLevelStack)
{
B.Appendf(TEXT("%s%s"), *Indent.PathEntry, *TreePathSeparator);
}
return B.ToString();
}
FString FSUDSScriptImporter::GetCurrentTreeConditionalPath(const FSUDSScriptImporter::ParsedTree& Tree)
{
// Like GetCurrentTreePath, but for conditional blocks
// Cannot fall through to blocks that aren't on the same conditional path
#if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8
TStringBuilder<256> B;
#else
FStringBuilderBase B;
#endif
int BlockIdx = Tree.CurrentConditionalBlockIdx;
B.Append(TreePathSeparator);
// work backwards, hence prepend
while (BlockIdx != -1)
{
const FString& ConditionStr = Tree.ConditionalBlocks[BlockIdx].ConditionPathElement;
// Note: add the "/" even if ConditionStr is empty, because it means it's an else level
// Not including it can cause an if block to fall through to its own else
B.Prepend(FString::Printf(TEXT("%s%s"), *TreePathSeparator, *ConditionStr));
BlockIdx = Tree.ConditionalBlocks[BlockIdx].PreviousBlockIdx;
}
return B.ToString();
}
FString FSUDSScriptImporter::GetTreeConditionalPath(const FSUDSScriptImporter::ParsedTree& Tree, int NodeIndex)
{
// Like GetCurrentTreeConditionalPath, but we're not in the block context. Follow the nodes back up
#if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8
TStringBuilder<256> B;
#else
FStringBuilderBase B;
#endif
B.Append(TreePathSeparator);
while (Tree.Nodes.IsValidIndex(NodeIndex))
{
const auto Node = Tree.Nodes[NodeIndex];
if (Tree.Nodes.IsValidIndex(Node.ParentNodeIdx))
{
const auto& Parent = Tree.Nodes[Node.ParentNodeIdx];
if (Parent.NodeType == ESUDSParsedNodeType::Select)
{
const int SelectNodeIdx = Node.ParentNodeIdx;
const int EdgeIdx = FindEdge(Tree, SelectNodeIdx, NodeIndex);
if (Parent.Edges.IsValidIndex(EdgeIdx))
{
const auto& Edge = Parent.Edges[EdgeIdx];
const bool bHasCondition = !Edge.ConditionExpression.IsEmpty();
FString ConditionalPathElem;
if (EdgeIdx == 0)
{
// First edge is always if
ConditionalPathElem = MakeIfConditionPathElement(SelectNodeIdx, Edge.ConditionExpression.GetSourceString());
}
else
{
if (bHasCondition)
{
// non-zero index edge with a condition is elseif
ConditionalPathElem = MakeElseIfConditionPathElement(SelectNodeIdx, Edge.ConditionExpression.GetSourceString());
}
else
{
ConditionalPathElem = MakeElseConditionPathElement(SelectNodeIdx);
}
}
// working backwards, hence prepend
B.Prepend(FString::Printf(TEXT("%s%s"), *TreePathSeparator, *ConditionalPathElem));
}
}
}
NodeIndex = Node.ParentNodeIdx;
}
return B.ToString();
}
int FSUDSScriptImporter::FindEdge(const FSUDSScriptImporter::ParsedTree& Tree, int ParentNodeIdx, int TargetNodeIndex)
{
if (Tree.Nodes.IsValidIndex(ParentNodeIdx))
{
const auto& Parent = Tree.Nodes[ParentNodeIdx];
for (int i = 0; i < Parent.Edges.Num(); ++i)
{
if (Parent.Edges[i].TargetNodeIdx == TargetNodeIndex)
{
return i;
}
}
}
return -1;
}
void FSUDSScriptImporter::SetFallthroughForNewNode(FSUDSScriptImporter::ParsedTree& Tree, FSUDSParsedNode& NewNode)
{
// Choice nodes are allowed to be falled through to now
if (NewNode.NodeType == ESUDSParsedNodeType::Choice)
{
// But, disable fallthrough for any parent select nodes all the way up the chain
int PrevIdx = NewNode.ParentNodeIdx;
while (Tree.Nodes.IsValidIndex(PrevIdx) && Tree.Nodes[PrevIdx].NodeType == ESUDSParsedNodeType::Select)
{
Tree.Nodes[PrevIdx].AllowFallthrough = false;
PrevIdx = Tree.Nodes[PrevIdx].ParentNodeIdx;
}
}
else
{
if (Tree.Nodes.IsValidIndex(NewNode.ParentNodeIdx))
{
auto& PrevNode = Tree.Nodes[NewNode.ParentNodeIdx];
// Do not allow fallthrough to this node for selects that are themselves children of other selects or choices
// It means they're compound conditionals / choices and we should only fall through to the root of that
if (NewNode.NodeType == ESUDSParsedNodeType::Select &&
(PrevNode.NodeType == ESUDSParsedNodeType::Choice || PrevNode.NodeType == ESUDSParsedNodeType::Select))
{
NewNode.AllowFallthrough = false;
}
}
}
}
int FSUDSScriptImporter::AppendNode(FSUDSScriptImporter::ParsedTree& Tree, const FSUDSParsedNode& InNode)
{
auto& Ctx = Tree.IndentLevelStack.Top();
const int NewIndex = Tree.Nodes.Add(InNode);
// Set the tree path of the node (post-add)
auto& NewNode = Tree.Nodes[NewIndex];
NewNode.ChoicePath = GetCurrentTreePath(Tree);
NewNode.ConditionalPath = GetCurrentTreeConditionalPath(Tree);
// Use pending edge if present; that could be because this is under a choice node, or a condition
if (auto E = GetEdgeInProgress(Tree))
{
E->TargetNodeIdx = NewIndex;
const int PrevNodeIdx = E->SourceNodeIdx;
NewNode.ParentNodeIdx = PrevNodeIdx;
SetFallthroughForNewNode(Tree, NewNode);
Tree.EdgeInProgressNodeIdx = -1;
Tree.EdgeInProgressEdgeIdx = -1;
}
else
{
const int PrevNodeIdx = Ctx.LastNodeIdx;
if (Tree.Nodes.IsValidIndex(PrevNodeIdx))
{
// Append this node onto the last one
auto& PrevNode = Tree.Nodes[PrevNodeIdx];
// Auto-connect new nodes to previous nodes
// Valid for nodes with only one output node
// A new node with no pending edge following any other type may be connected via fallthrough at
// the end of parsing
// Don't allow connection of fallthrough nodes to choices, unless they're to another select node
if (PrevNode.NodeType != ESUDSParsedNodeType::Choice ||
(NewNode.NodeType == ESUDSParsedNodeType::Select))
{
PrevNode.Edges.Add(FSUDSParsedEdge(PrevNodeIdx, NewIndex, InNode.SourceLineNo));
NewNode.ParentNodeIdx = PrevNodeIdx;
}
// Don't throw an error otherwise, because prev index can be a choice due to fallthrough
// This will be connected up at the end
}
SetFallthroughForNewNode(Tree, NewNode);
}
// All goto labels in scope at this point now point to this node
for (auto Label : Tree.PendingGotoLabels)
{
Tree.GotoLabelList.Add(Label, NewIndex);
}
Tree.PendingGotoLabels.Reset();
Ctx.LastNodeIdx = NewIndex;
Ctx.ThresholdIndent = FMath::Min(Ctx.ThresholdIndent, InNode.OriginalIndent);
return NewIndex;
}
bool FSUDSScriptImporter::SelectNodeIsMissingElsePath(const FSUDSScriptImporter::ParsedTree& Tree, const FSUDSParsedNode& Node)
{
for (auto& E : Node.Edges)
{
if (E.ConditionExpression.IsEmpty())
{
// This is an else
return false;
}
}
// If the first node the select points to (other than a nested select) is a choice, we never add an else
// note: you should never have an if/else where one branch has a set/text node and the other has a choice node, this is badly formed
// perhaps we should validate that
if (Node.Edges.Num() > 0)
{
int NextIdx = Node.Edges[0].TargetNodeIdx;
// While so that we can follow nested selects to first resolved
while (Tree.Nodes.IsValidIndex(NextIdx))
{
auto N = Tree.Nodes[NextIdx];
if (N.NodeType == ESUDSParsedNodeType::Select)
{
// Nested select, cascade down
if (N.Edges.Num() > 0)
{
NextIdx = N.Edges[0].TargetNodeIdx;
}
else
{
NextIdx = -1;
}
}
else
{
if (N.NodeType == ESUDSParsedNodeType::Choice)
{
// We never add fallthrough else paths for selection between choices
return false;
}
NextIdx = -1;
}
}
}
return true;
}
void FSUDSScriptImporter::PopIndent(FSUDSScriptImporter::ParsedTree& Tree)
{
Tree.IndentLevelStack.Pop();
// We *could* reset the pending goto list if there are labels at higher indents than current that never resolved to a node
// but I'm choosing not to right now and letting them fall through
}
void FSUDSScriptImporter::PushIndent(FSUDSScriptImporter::ParsedTree& Tree, int NodeIdx, int Indent, const FString& Path)
{
Tree.IndentLevelStack.Push(IndentContext(NodeIdx, Indent, Path));
}
bool FSUDSScriptImporter::IsCommentLine(const FStringView& TrimmedLine)
{
return TrimmedLine.StartsWith('#');
}
FStringView FSUDSScriptImporter::TrimLine(const FStringView& Line, int& OutIndentLevel) const
{
OutIndentLevel = 0;
int32 SkippedChars = 0;
for (const TCHAR Char : Line)
{
if (!TChar<TCHAR>::IsWhitespace(Char))
{
break;
}
++SkippedChars;
if (Char == '\t')
OutIndentLevel += TabIndentValue;
else
++OutIndentLevel;
}
FStringView LeftTrimmed = FStringView(Line.GetData() + SkippedChars, Line.Len() - SkippedChars);
// Trim end, don't need to know how much it was
return LeftTrimmed.TrimEnd();
}
void FSUDSScriptImporter::ConnectRemainingNodes(FSUDSScriptImporter::ParsedTree& Tree, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent)
{
// Now we go through all nodes, resolving gotos, and finding links that don't go anywhere & making
// them fall through to the next appropriate outdented node (or the end)
// Firstly turn all the alias gotos into final gotos
for (auto& Alias : Tree.AliasedGotoLabels)
{
if (int* pIdx = Tree.GotoLabelList.Find(Alias.Value))
{
Tree.GotoLabelList.Add(Alias.Key, *pIdx);
}
}
Tree.AliasedGotoLabels.Reset();
// We go through top-to-bottom, which is the order of lines in the file as well
// We don't need to cascade for this
for (int i = 0; i < Tree.Nodes.Num(); ++i)
{
auto& Node = Tree.Nodes[i];
// We check for dead-end nodes, and for select nodes with no "else" (in case all conditions fail)
const bool bIsSelectNodeMissingElse = Node.NodeType == ESUDSParsedNodeType::Select && SelectNodeIsMissingElsePath(Tree, Node);
if (Node.Edges.IsEmpty() || bIsSelectNodeMissingElse)
{
if (Node.NodeType == ESUDSParsedNodeType::Goto)
{
// Try to resolve goto now that we've parsed all labels
// We don't actually create edges here, the label is enough so long as it leads somewhere
// Check aliases first
FString Label = Node.Identifier;
// Special case 'end' which needs no further checking
if (Label != EndGotoLabel)
{
const int GotoNodeIdx = GetGotoTargetNodeIndex(Tree, Node.Identifier);
if (GotoNodeIdx == -1)
{
if (!bSilent)
Logger->Logf(ELogVerbosity::Warning, TEXT("Error in %s line %d: Goto label '%s' was not found, references to it will goto End"), *NameForErrors, Node.SourceLineNo, *Node.Identifier);
}
}
}
// Return nodes never fall through, they're always going back
// Gosubs DO come through here though, fallthrough is AFTER the return
else if (Node.NodeType != ESUDSParsedNodeType::Return)
{
// Find the next node which is at a higher indent level than this
// For a select node missing else, treat the indent as 1 inward, since it's really falling through from a nested part of the select
const int IndentLessThan = bIsSelectNodeMissingElse ? Node.OriginalIndent + 1 : Node.OriginalIndent;
const auto FallthroughIdx = FindFallthroughNodeIndex(Tree, i+1, Node.ChoicePath, Node.ConditionalPath);
if (Tree.Nodes.IsValidIndex(FallthroughIdx))
{
Node.Edges.Add(FSUDSParsedEdge(i, FallthroughIdx, Node.SourceLineNo));
}
else
{
// If no node to fallthrough to, default will be to end
}
}
}
else
{
for (auto& Edge : Node.Edges)
{
if (!Tree.Nodes.IsValidIndex(Edge.TargetNodeIdx))
{
// Usually this is a choice line without anything under it, or a condition with nothing in it
const auto FallthroughIdx = FindFallthroughNodeIndex(Tree, i+1, Node.ChoicePath, Node.ConditionalPath);
if (Tree.Nodes.IsValidIndex(FallthroughIdx))
{
Edge.TargetNodeIdx = FallthroughIdx;
}
else
{
// If no node to fallthrough to, goto end
Edge.TargetNodeIdx = -1;
}
}
}
}
}
}
void FSUDSScriptImporter::GenerateTextIDs(ParsedTree& Tree)
{
// This is where we generate any missing TextIDs for Text nodes, Set nodes (optionally) and Choice edges
// We don't want to do it as we go along, because if a script has some lines with TextIDs and new inserted lines
// in between, we won't know from a top-down scan which IDs have already been used. Later explicit TextIDs in
// the script could cause an ID clash with generated ones for inserted lines
for (auto& Node : Tree.Nodes)
{
switch (Node.NodeType)
{
case ESUDSParsedNodeType::Text:
if (Node.TextID.IsEmpty())
{
Node.TextID = GenerateTextID();
}
break;
case ESUDSParsedNodeType::Choice:
// Text for choices is on edges
{
for (auto& Edge : Node.Edges)
{
if (!Edge.Text.IsEmpty() && Edge.TextID.IsEmpty())
{
Edge.TextID = GenerateTextID();
}
// If this choice generated a speaker line, we need to give that the same text ID
// It will always be directly after the choice edge
if (Tree.Nodes.IsValidIndex(Edge.TargetNodeIdx))
{
auto& NextNode = Tree.Nodes[Edge.TargetNodeIdx];
if (NextNode.NodeType == ESUDSParsedNodeType::Text && NextNode.TextID.IsEmpty() &&
NextNode.Text == Edge.Text)
{
NextNode.TextID = Edge.TextID;
}
}
}
}
break;
case ESUDSParsedNodeType::SetVariable:
if (Node.Expression.IsTextLiteral())
{
// Text must be localised
if (Node.TextID.IsEmpty())
{
Node.TextID = GenerateTextID();
}
}
break;
default:
break;
}
}
}
bool FSUDSScriptImporter::PostImportSanityCheck(const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent)
{
bool bOK = true;
for (int i = 0; i < BodyTree.Nodes.Num(); ++i)
{
auto& Node = BodyTree.Nodes[i];
if (Node.NodeType == ESUDSParsedNodeType::Choice)
{
// Check all of them so we can report all errors, rather than early-out
bOK = ChoiceNodeCheckPaths(Node, NameForErrors, Logger, bSilent) && bOK;
}
}
// check for unfinished conditional blocks
if (BodyTree.ConditionalBlocks.IsValidIndex(BodyTree.CurrentConditionalBlockIdx))
{
auto& Block = BodyTree.ConditionalBlocks[BodyTree.CurrentConditionalBlockIdx];
auto SelectNode = BodyTree.Nodes.IsValidIndex(Block.SelectNodeIdx) ? &BodyTree.Nodes[Block.SelectNodeIdx] : nullptr;
if (!bSilent && SelectNode)
{
FString BlockStartDesc;
FString BlockEndDesc;
switch (Block.Stage)
{
case EConditionalStage::IfStage:
case EConditionalStage::ElseIfStage:
case EConditionalStage::ElseStage:
BlockStartDesc = "if";
BlockEndDesc = "endif";
break;
case EConditionalStage::RandomStage:
case EConditionalStage::RandomOptionStage:
BlockStartDesc = "random";
BlockEndDesc = "endrandom";
break;
}
Logger->Logf(ELogVerbosity::Error,
TEXT(
"%s: '%s' block started on line %d is missing '%s'"),
*NameForErrors,
*BlockStartDesc,
SelectNode->SourceLineNo,
*BlockEndDesc);
}
}
// Now check everything is referenced
TArray<bool> ReferencedNodes;
ReferencedNodes.SetNumZeroed(BodyTree.Nodes.Num());
if (!ReferencedNodes.IsEmpty())
{
// First node is always reachable
ReferencedNodes[0] = true;
}
for (const auto& Goto : BodyTree.GotoLabelList)
{
// Goto creates a reference
ReferencedNodes[Goto.Value] = true;
}
for (const auto& Node : BodyTree.Nodes)
{
for (const auto& Edge : Node.Edges)
{
if (ReferencedNodes.IsValidIndex(Edge.TargetNodeIdx))
{
ReferencedNodes[Edge.TargetNodeIdx] = true;
}
}
}
for (int i = 0; i < ReferencedNodes.Num(); ++i)
{
if (!ReferencedNodes[i])
{
Logger->Logf(ELogVerbosity::Warning,
TEXT(
"%s: Line %d is unreachable. Check your indenting particularly under choice lines."),
*NameForErrors,
BodyTree.Nodes[i].SourceLineNo);
}
}
return bOK;
}
bool FSUDSScriptImporter::ChoiceNodeCheckPaths(const FSUDSParsedNode& ChoiceNode,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
return RecurseChoiceNodeCheckPaths(ChoiceNode, ChoiceNode, NameForErrors, Logger, bSilent);
}
bool FSUDSScriptImporter::RecurseChoiceNodeCheckPaths(const FSUDSParsedNode& OrigChoiceNode,
const FSUDSParsedNode& CurrChoiceNode,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
bool bOK = true;
for (const auto& Edge: CurrChoiceNode.Edges)
{
// We want to make sure that every choice path leads to a speaker line, before it leads to another choice
// A choice that leads directly to another choice can't be properly represented in dialogue; choices have to
// be anchored by speaker lines so proceeding to another choice directly after a choice is made is wrong
// Usually this will be caused by a bad goto but could also be just bad nesting
// Every path has to lead to a speaker node before a choice
bOK = RecurseChoiceNodeCheckPaths(OrigChoiceNode, Edge, NameForErrors, Logger, bSilent) && bOK;
}
return bOK;
}
bool FSUDSScriptImporter::RecurseChoiceNodeCheckPaths(const FSUDSParsedNode& ChoiceNode, const FSUDSParsedEdge& Edge,
const FString& NameForErrors,
FSUDSMessageLogger* Logger,
bool bSilent)
{
const FSUDSParsedNode* TargetNode = GetNode(Edge.TargetNodeIdx);
while (TargetNode)
{
switch (TargetNode->NodeType)
{
case ESUDSParsedNodeType::Text:
// We're OK, found a speaker line
return true;
case ESUDSParsedNodeType::Choice:
// Definitely not ok, we found a choice node before a speaker node
Logger->Logf(ELogVerbosity::Error,
TEXT(
"%s: Choice '%s' on line %d needs a speaker line between it and the next choice at line %d. Choices MUST show another speaker line before the next choice."),
*NameForErrors,
*Edge.Text,
Edge.SourceLineNo,
TargetNode->SourceLineNo);
return false;
case ESUDSParsedNodeType::Select:
{
// Recurse selects & randoms; but in this case we can have nested choices underneath
bool bOK = true;
for (const auto& SelEdge : TargetNode->Edges)
{
const auto SelTarget = GetNode(SelEdge.TargetNodeIdx);
if (SelTarget)
{
if (SelTarget->NodeType == ESUDSParsedNodeType::Choice)
{
// First level of nested choices is OK; they will be combined with the original choice
// We don't need to recurse here since this choice will itself
}
else
{
bOK = RecurseChoiceNodeCheckPaths(ChoiceNode, SelEdge, NameForErrors, Logger, bSilent);
}
}
}
return bOK;
}
case ESUDSParsedNodeType::SetVariable:
case ESUDSParsedNodeType::Event:
// Continue (linear)
if (TargetNode->Edges.Num() > 0)
{
TargetNode = GetNode(TargetNode->Edges[0].TargetNodeIdx);
}
else
{
// This can happen if the event is the last line, but also nested in a choice
return true;
}
break;
case ESUDSParsedNodeType::Gosub:
case ESUDSParsedNodeType::Return:
// We can't really check the gosub/return statically, this will be a runtime error
return true;
case ESUDSParsedNodeType::Goto:
{
// Follow the goto (if end, will result in null)
const int GotoIdx = GetGotoTargetNodeIndex(BodyTree, TargetNode->Identifier);
TargetNode = GetNode(GotoIdx);
break;
}
}
}
return true;
}
int FSUDSScriptImporter::FindFallthroughNodeIndex(FSUDSScriptImporter::ParsedTree& Tree,
int StartNodeIndex,
const FString& FromChoicePath,
const FString& FromConditionalPath)
{
// In order to be a valid fallthrough, also needs to be on the same choice (or select) path
// E.g. it's possible to have:
//
// * Choice (C1)
// * Nested choice (C1.1)
// Fallthrough from here (F1)
// * Choice (C2)
// Do NOT fallthrough to here (T1)
// Fallthrough to here instead (T2)
//
// Just testing the indent would fallthrough to T1 which is incorrect because it's not on the same choice path
// We need to check that the fallthrough point is on the same choice path (not just a common parent)
// Fallthrough from is on path /C1/C1.1
// Point T1 is on /C2 which is NOT a subset of /C1/C1.1 so not OK
// Point T2 is on /, which is a subset of /C1/C1/1 so OK
//
// Also need to deal with this case:
//
// * Choice (C1)
// * Nested choice (C1.1)
// Fallthrough from here (F1)
// Fallthrough to here (T1) then from here (F2)
// * Choice (C2)
// Do NOT fallthrough to here (T2)
// Finally fall through to here (T3)
//
// In this case, fallthrough should first go to T1, then to T3
// - Fallthrough F1 is on path /C1/C1.1
// - Point T1 is on /C1 which IS a subset of /C1/C1.1 so OK
// - Fallthrough F2 is on path /C1
// - Point T2 is on /C2 which is NOT a subset of /C1 so not OK
// - Point T3 is on / which is a subset of /C1 so OK
// We'll form these paths just from node indexes rather than C1/C2 etc. Nesting can be for a choice or a select
for (int i = StartNodeIndex; i < Tree.Nodes.Num(); ++i)
{
auto N = Tree.Nodes[i];
// We used to require that N.OriginalIndent < IndentLessThan here
// However, this is actually not needed, since indentation only controls association with choice paths, otherwise
// it's irrelevant. And we already check that things only fall through if they're on the same choice/conditional
// path (or a superset of it).
if (N.AllowFallthrough &&
FromChoicePath.StartsWith(N.ChoicePath) &&
FromConditionalPath.StartsWith(N.ConditionalPath))
{
return i;
}
}
return -1;
}
const FSUDSParsedNode* FSUDSScriptImporter::GetNode(const FSUDSScriptImporter::ParsedTree& Tree, int Index)
{
if (Tree.Nodes.IsValidIndex(Index))
{
return &Tree.Nodes[Index];
}
return nullptr;
}
const FSUDSParsedNode* FSUDSScriptImporter::GetHeaderNode(int Index)
{
return GetNode(HeaderTree, Index);
}
const FSUDSParsedNode* FSUDSScriptImporter::GetNode(int Index)
{
return GetNode(BodyTree, Index);
}
int FSUDSScriptImporter::GetGotoTargetNodeIndex(const FString& InLabel)
{
// Assume Body for this public version
return GetGotoTargetNodeIndex(BodyTree, InLabel);
}
int FSUDSScriptImporter::GetGotoTargetNodeIndex(const ParsedTree& Tree, const FString& InLabel)
{
FString Label = InLabel;
if (const FString* AliasLabel = Tree.AliasedGotoLabels.Find(Label))
{
Label = *AliasLabel;
}
// Resolve using goto list
if (const int *pGotoIdx = Tree.GotoLabelList.Find(Label))
{
return *pGotoIdx;
}
return -1;
}
void FSUDSScriptImporter::PopulateAsset(USUDSScript* Asset, UStringTable* StringTable)
{
// This is only called if the parsing was successful
// Populate the runtime asset
TArray<TObjectPtr<USUDSScriptNode>> *pOutNodes = nullptr;
TArray<TObjectPtr<USUDSScriptNode>> *pOutHeaderNodes = nullptr;
TMap<FName, int> *pOutLabels = nullptr;
TMap<FName, int> *pOutHeaderLabels = nullptr;
TArray<FString> *pOutSpeakers = nullptr;
Asset->StartImport(&pOutNodes, &pOutHeaderNodes, &pOutLabels, &pOutHeaderLabels, &pOutSpeakers);
pOutSpeakers->Append(ReferencedSpeakers);
PopulateAssetFromTree(Asset, HeaderTree, pOutHeaderNodes, pOutHeaderLabels, StringTable);
PopulateAssetFromTree(Asset, BodyTree, pOutNodes, pOutLabels, StringTable);
Asset->FinishImport();
}
FMD5Hash FSUDSScriptImporter::CalculateHash(const TCHAR* Buffer, int32 Len)
{
FMD5Hash Hash;
FMD5 MD5;
MD5.Update((uint8*)Buffer, Len * sizeof(TCHAR));
Hash.Set(MD5);
return Hash;
}
void FSUDSScriptImporter::PopulateAssetFromTree(USUDSScript* Asset,
const FSUDSScriptImporter::ParsedTree& Tree,
TArray<TObjectPtr<USUDSScriptNode>>* pOutNodes,
TMap<FName, int>* pOutLabels,
UStringTable* StringTable)
{
if (pOutNodes && pOutLabels)
{
TArray<int> IndexRemap;
int OutIndex = 0;
// First pass, create all the nodes
for (const auto& InNode : Tree.Nodes)
{
// Gotos are dealt with in the node that references them, so ignore them
// We're going to be removing Goto nodes in the parse structure, because they were useful while parsing
// (letting you fallthrough to a goto node) but in the final runtime we just want them to be edges
// So firstly we need to figure out what the indexes of other nodes are going to be with them removed
if (InNode.NodeType == ESUDSParsedNodeType::Goto)
{
// note that this one goes nowhere, and don't increment dest index
IndexRemap.Add(-1);
}
else
{
IndexRemap.Add(OutIndex++);
USUDSScriptNode* Node = nullptr;
switch (InNode.NodeType)
{
case ESUDSParsedNodeType::Text:
{
#if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8
StringTable->GetMutableStringTable()->SetSourceString(InNode.TextID, InNode.Text, "");
#else
StringTable->GetMutableStringTable()->SetSourceString(InNode.TextID, InNode.Text);
#endif
// Always include speaker metadata
StringTable->GetMutableStringTable()->SetMetaData(InNode.TextID, FName("Speaker"), InNode.Identifier);
// Other metadata
for (auto Pair : InNode.TextMetadata)
{
StringTable->GetMutableStringTable()->SetMetaData(InNode.TextID, Pair.Key, Pair.Value);
}
auto TextNode = NewObject<USUDSScriptNodeText>(Asset);
TextNode->Init(InNode.Identifier, FText::FromStringTable (StringTable->GetStringTableId(), InNode.TextID), InNode.SourceLineNo);
TextNode->SetUserMetadata(InNode.UserMetadata);
Node = TextNode;
break;
}
case ESUDSParsedNodeType::Choice:
{
auto ChoiceNode = NewObject<USUDSScriptNode>(Asset);
ChoiceNode->InitChoice(InNode.SourceLineNo);
Node = ChoiceNode;
break;
}
case ESUDSParsedNodeType::Select:
{
auto SelectNode = NewObject<USUDSScriptNode>(Asset);
SelectNode->InitSelect(InNode.SourceLineNo);
Node = SelectNode;
break;
}
case ESUDSParsedNodeType::SetVariable:
{
auto SetNode = NewObject<USUDSScriptNodeSet>(Asset);
// For text literals, re-point to string table
FSUDSExpression Expr = InNode.Expression;
if (Expr.IsTextLiteral())
{
#if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8
StringTable->GetMutableStringTable()->SetSourceString(InNode.TextID, Expr.GetTextLiteralValue().ToString(), "");
#else
StringTable->GetMutableStringTable()->SetSourceString(InNode.TextID, Expr.GetTextLiteralValue().ToString());
#endif
Expr.SetTextLiteralValue(FText::FromStringTable (StringTable->GetStringTableId(), InNode.TextID));
}
SetNode->Init(InNode.Identifier, Expr, InNode.SourceLineNo);
Node = SetNode;
break;
}
case ESUDSParsedNodeType::Event:
{
auto EvtNode = NewObject<USUDSScriptNodeEvent>(Asset);
EvtNode->Init(InNode.Identifier, InNode.EventArgs, InNode.SourceLineNo);
Node = EvtNode;
break;
}
case ESUDSParsedNodeType::Gosub:
{
// Validate gosub label at this point
auto GosubNode = NewObject<USUDSScriptNodeGosub>(Asset);
GosubNode->Init(InNode.Identifier, InNode.TextID, InNode.SourceLineNo);
Node = GosubNode;
break;
}
case ESUDSParsedNodeType::Return:
{
auto ReturnNode = NewObject<USUDSScriptNode>(Asset);
ReturnNode->InitReturn(InNode.SourceLineNo);
Node = ReturnNode;
break;
}
case ESUDSParsedNodeType::Goto:
// Gotos do not become nodes, just fixed edges
default: ;
break;
}
pOutNodes->Add(Node);
}
}
// Second pass, create edges between nodes now that we know where everything is
for (int i = 0; i < Tree.Nodes.Num(); ++i)
{
const FSUDSParsedNode& InNode = Tree.Nodes[i];
if (InNode.NodeType != ESUDSParsedNodeType::Goto)
{
USUDSScriptNode* Node = (*pOutNodes)[IndexRemap[i]];
// Edges
if (InNode.Edges.Num() == 0)
{
// This normally happens with the final node in the script
// Make it an edge to nullptr for consistency
Node->AddEdge(FSUDSScriptEdge(nullptr, ESUDSEdgeType::Continue, InNode.SourceLineNo));
}
else
{
for (auto& InEdge : InNode.Edges)
{
ESUDSEdgeType NewEdgeType;
const FSUDSParsedNode *InTargetNode = GetNode(Tree, InEdge.TargetNodeIdx);
switch (InNode.NodeType)
{
case ESUDSParsedNodeType::Text:
if (InTargetNode && InTargetNode->NodeType == ESUDSParsedNodeType::Choice)
{
// Text -> Choice is chained
NewEdgeType = ESUDSEdgeType::Chained;
}
else
{
NewEdgeType = ESUDSEdgeType::Continue;
}
break;
case ESUDSParsedNodeType::Choice:
if (InEdge.Text.IsEmpty() && InTargetNode && InTargetNode->NodeType == ESUDSParsedNodeType::Select)
{
// Choice->Select with no text is chained
NewEdgeType = ESUDSEdgeType::Chained;
}
else
{
NewEdgeType = ESUDSEdgeType::Decision;
}
break;
case ESUDSParsedNodeType::Select:
// All edges under selects are conditions
NewEdgeType = ESUDSEdgeType::Condition;
break;
default:
case ESUDSParsedNodeType::SetVariable:
case ESUDSParsedNodeType::Goto:
case ESUDSParsedNodeType::Event:
NewEdgeType = ESUDSEdgeType::Continue;
break;
};
USUDSScriptNode* TargetNode = nullptr;
if (InTargetNode)
{
if (InTargetNode->NodeType == ESUDSParsedNodeType::Goto)
{
// Resolve GOTOs immediately, point them directly at node goto points to
int Idx = GetGotoTargetNodeIndex(Tree, InTargetNode->Identifier);
// -1 means "Goto end", leave target null in that case
if (Idx != -1)
{
const int NewTargetIndex = IndexRemap[Idx];
TargetNode = (*pOutNodes)[NewTargetIndex];
}
}
else
{
const int NewTargetIndex = IndexRemap[InEdge.TargetNodeIdx];
TargetNode = (*pOutNodes)[NewTargetIndex];
}
}
FSUDSScriptEdge NewEdge(TargetNode, NewEdgeType, InEdge.SourceLineNo);
NewEdge.SetCondition(InEdge.ConditionExpression);
NewEdge.SetTargetNode(TargetNode);
if (!InEdge.TextID.IsEmpty() && !InEdge.Text.IsEmpty())
{
#if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8
StringTable->GetMutableStringTable()->SetSourceString(InEdge.TextID, InEdge.Text, "");
#else
StringTable->GetMutableStringTable()->SetSourceString(InEdge.TextID, InEdge.Text);
#endif
NewEdge.SetText(FText::FromStringTable(StringTable->GetStringTableId(), InEdge.TextID));
// Always include speaker metadata, always the player in a choice
// Identify that it's a choice so translators know that there may be more limited space
StringTable->GetMutableStringTable()->SetMetaData(InEdge.TextID, FName("Speaker"), "Player (Choice)");
// Other metadata
for (auto Pair : InEdge.TextMetadata)
{
StringTable->GetMutableStringTable()->SetMetaData(InEdge.TextID, Pair.Key, Pair.Value);
}
}
NewEdge.SetUserMetadata(InEdge.UserMetadata);
Node->AddEdge(NewEdge);
}
}
}
}
// Add labels, so that dialogue can be entered at any label
// Aliases have already been resolved
for (auto& Elem : Tree.GotoLabelList)
{
int NewIndex = IndexRemap[Elem.Value];
pOutLabels->Add(FName(Elem.Key), NewIndex);
}
}
}