2766 lines
90 KiB
C++
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);
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|