2026-07-03 19:56:31 +02:00
// 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 (/)
2026-07-04 21:15:59 +02:00
# if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8
TStringBuilder < 256 > B ;
# else
2026-07-03 19:56:31 +02:00
FStringBuilderBase B ;
2026-07-04 21:15:59 +02:00
# endif
2026-07-03 19:56:31 +02:00
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
2026-07-04 21:15:59 +02:00
# if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8
TStringBuilder < 256 > B ;
# else
2026-07-03 19:56:31 +02:00
FStringBuilderBase B ;
2026-07-04 21:15:59 +02:00
# endif
2026-07-03 19:56:31 +02:00
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
2026-07-04 21:15:59 +02:00
# if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8
TStringBuilder < 256 > B ;
# else
2026-07-03 19:56:31 +02:00
FStringBuilderBase B ;
2026-07-04 21:15:59 +02:00
# endif
2026-07-03 19:56:31 +02:00
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 :
{
2026-07-04 21:15:59 +02:00
# if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8
StringTable - > GetMutableStringTable ( ) - > SetSourceString ( InNode . TextID , InNode . Text , " " ) ;
# else
2026-07-03 19:56:31 +02:00
StringTable - > GetMutableStringTable ( ) - > SetSourceString ( InNode . TextID , InNode . Text ) ;
2026-07-04 21:15:59 +02:00
# endif
2026-07-03 19:56:31 +02:00
// 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 ( ) )
{
2026-07-04 21:15:59 +02:00
# if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8
StringTable - > GetMutableStringTable ( ) - > SetSourceString ( InNode . TextID , Expr . GetTextLiteralValue ( ) . ToString ( ) , " " ) ;
# else
2026-07-03 19:56:31 +02:00
StringTable - > GetMutableStringTable ( ) - > SetSourceString ( InNode . TextID , Expr . GetTextLiteralValue ( ) . ToString ( ) ) ;
2026-07-04 21:15:59 +02:00
# endif
2026-07-03 19:56:31 +02:00
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 ( ) )
{
2026-07-04 21:15:59 +02:00
# if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8
StringTable - > GetMutableStringTable ( ) - > SetSourceString ( InEdge . TextID , InEdge . Text , " " ) ;
# else
2026-07-03 19:56:31 +02:00
StringTable - > GetMutableStringTable ( ) - > SetSourceString ( InEdge . TextID , InEdge . Text ) ;
2026-07-04 21:15:59 +02:00
# endif
2026-07-03 19:56:31 +02:00
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 ) ;
}
}
}