Initial push to repo

This commit is contained in:
2026-07-03 19:56:31 +02:00
commit 4cf4176d57
1305 changed files with 43455 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
// Copyright Steve Streeting 2022
// Released under the MIT license https://opensource.org/license/MIT/
#include "SUDS.h"
#include "Modules/ModuleManager.h"
#define LOCTEXT_NAMESPACE "FSUDSModule"
void FSUDSModule::StartupModule()
{
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
}
void FSUDSModule::ShutdownModule()
{
// This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
// we call this function before unloading the module.
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FSUDSModule, SUDS)

View File

@@ -0,0 +1,4 @@
#include "SUDSCommon.h"
const FName FSUDSConstants::RandomItemSelectIndexVarName(SUDS_RANDOMITEM_VAR);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,465 @@
// Copyright Steve Streeting 2022
// Released under the MIT license https://opensource.org/license/MIT/
#include "SUDSExpression.h"
#include "SUDSLibrary.h"
#include "Misc/DefaultValueHelper.h"
#include "Internationalization/Regex.h"
bool FSUDSExpression::ParseFromString(const FString& Expression, FString* OutParseError)
{
// Assume invalid until we've parsed something
bIsValid = false;
Queue.Empty();
VariableNames.Empty();
SourceString = Expression;
// Shunting-yard algorithm
// Thanks to Nathan Reed https://www.reedbeta.com/blog/the-shunting-yard-algorithm/
// We take a natural string, parse it and then turn it into a queue of tokens (operators and operands)
// expressed in Reverse Polish Notation, which can be easily executed later
// Variables are not resolved at this point, only at execution time.
// Split into individual tokens; sections of the regex are:
// - {Variable}
// - Literal numbers (with or without decimal point, with or without preceding negation)
// - Arithmetic operators & parentheses
// - Boolean operators & comparisons
// - Predefined constants (Masculine, feminine, true, false etc)
// - Quoted strings "string"
// - Including ignoring escaped double quotes
// - Quoted names `name`
const FRegexPattern Pattern(TEXT("(\\{[\\w\\.]+\\}|-?\\d+(?:\\.\\d*)?|[-+*\\/%\\(\\)]|and|&&|\\|\\||or|not|\\<\\>|!=|!|\\<=?|\\>=?|==?|[mM]asculine|[fF]eminine|[nN]euter|[tT]rue|[fF]alse|\"(?:[^\"\\\\]|\\\\.)*\"|`([^`]*)`)"));
FRegexMatcher Regex(Pattern, Expression);
// Stacks that we use to construct
TArray<ESUDSExpressionItemType> OperatorStack;
bool bParsedSomething = false;
bool bErrors = false;
while (Regex.FindNext())
{
FString Str = Regex.GetCaptureGroup(1);
ESUDSExpressionItemType OpType = ParseOperator(Str);
if (OpType != ESUDSExpressionItemType::Null)
{
bParsedSomething = true;
if (OpType == ESUDSExpressionItemType::LParens)
{
OperatorStack.Push(OpType);
}
else if (OpType == ESUDSExpressionItemType::RParens)
{
if (OperatorStack.IsEmpty())
{
if (OutParseError)
*OutParseError = TEXT("Mismatched parentheses");
bErrors = true;
break;
}
while (OperatorStack.Num() > 0 && OperatorStack.Top() != ESUDSExpressionItemType::LParens)
{
Queue.Add(FSUDSExpressionItem(OperatorStack.Pop()));
}
if (OperatorStack.IsEmpty())
{
if (OutParseError)
*OutParseError = TEXT("Mismatched parentheses");
bErrors = true;
break;
}
else
{
// Discard left parens
OperatorStack.Pop();
}
}
else
{
// All operators are left-associative except not
const bool bLeftAssociative = OpType != ESUDSExpressionItemType::Not;
// Valid operator
// Apply anything on the operator stack which is higher / equal precedence
while (OperatorStack.Num() > 0 &&
// higher precedence applied now, and equal precedence if left-associative
(static_cast<int>(OperatorStack.Top()) < static_cast<int>(OpType) ||
(static_cast<int>(OperatorStack.Top()) <= static_cast<int>(OpType) && bLeftAssociative)))
{
Queue.Add(FSUDSExpressionItem(OperatorStack.Pop()));
}
OperatorStack.Push(OpType);
}
}
else
{
// Attempt to parse operand
FSUDSValue Operand;
if (ParseOperand(Str, Operand))
{
bParsedSomething = true;
Queue.Add(FSUDSExpressionItem(Operand));
}
else
{
if (OutParseError)
*OutParseError = FString::Printf(TEXT("Unrecognised token %s"), *Str);
bErrors = true;
}
}
}
// finish up
while (OperatorStack.Num() > 0)
{
if (OperatorStack.Top() == ESUDSExpressionItemType::LParens ||
OperatorStack.Top() == ESUDSExpressionItemType::RParens)
{
bErrors = true;
if (OutParseError)
*OutParseError = TEXT("Mismatched parentheses");
break;
}
Queue.Add(FSUDSExpressionItem(OperatorStack.Pop()));
}
if (!Validate() ||
(Expression.Len() > 0 && Queue.IsEmpty())) // Empty expressions validate correctly, but if there was text incoming that resolved to nothing, this is an error
{
bErrors = true;
if (OutParseError)
*OutParseError = FString::Printf(TEXT("Bad expression '%s'"), *Expression);
}
bIsValid = bParsedSomething && !bErrors;
// Build list of variables
if (bIsValid)
{
for (auto& Item : Queue)
{
if (Item.IsOperand() && Item.GetOperandValue().IsVariable())
{
VariableNames.AddUnique(Item.GetOperandValue().GetVariableNameValue());
}
}
}
return bIsValid;
}
void FSUDSExpression::Reset()
{
bIsValid = true;
Queue.Empty();
VariableNames.Empty();
SourceString = "";
}
bool FSUDSExpression::IsRandomCondition() const
{
if (Queue.Num() > 0 && Queue[0].GetType() == ESUDSExpressionItemType::Operand)
{
const auto& Operand = Queue[0].GetOperandValue();
return Operand.IsVariable() && Operand.GetVariableNameValue() == FSUDSConstants::RandomItemSelectIndexVarName;
}
return false;
}
ESUDSExpressionItemType FSUDSExpression::ParseOperator(const FString& OpStr)
{
if (OpStr == "+")
return ESUDSExpressionItemType::Add;
if (OpStr == "-")
return ESUDSExpressionItemType::Subtract;
if (OpStr == "*")
return ESUDSExpressionItemType::Multiply;
if (OpStr == "/")
return ESUDSExpressionItemType::Divide;
if (OpStr == "%")
return ESUDSExpressionItemType::Modulo;
if (OpStr == "and" || OpStr == "&&")
return ESUDSExpressionItemType::And;
if (OpStr == "or" || OpStr == "||")
return ESUDSExpressionItemType::Or;
if (OpStr == "not" || OpStr == "!")
return ESUDSExpressionItemType::Not;
if (OpStr == "==" || OpStr == "=")
return ESUDSExpressionItemType::Equal;
if (OpStr == ">=")
return ESUDSExpressionItemType::GreaterEqual;
if (OpStr == ">")
return ESUDSExpressionItemType::Greater;
if (OpStr == "<=")
return ESUDSExpressionItemType::LessEqual;
if (OpStr == "<")
return ESUDSExpressionItemType::Less;
if (OpStr == "<>" || OpStr == "!=")
return ESUDSExpressionItemType::NotEqual;
if (OpStr == "(")
return ESUDSExpressionItemType::LParens;
if (OpStr == ")")
return ESUDSExpressionItemType::RParens;
return ESUDSExpressionItemType::Null;
}
bool FSUDSExpression::ParseOperand(const FString& ValueStr, FSUDSValue& OutVal)
{
// Try Boolean first since only 2 options
{
if (ValueStr.Compare("true", ESearchCase::IgnoreCase) == 0)
{
OutVal = FSUDSValue(true);
return true;
}
if (ValueStr.Compare("false", ESearchCase::IgnoreCase) == 0)
{
OutVal = FSUDSValue(false);
return true;
}
}
// Try gender
{
if (ValueStr.Compare("masculine", ESearchCase::IgnoreCase) == 0)
{
OutVal = FSUDSValue(ETextGender::Masculine);
return true;
}
if (ValueStr.Compare("feminine", ESearchCase::IgnoreCase) == 0)
{
OutVal = FSUDSValue(ETextGender::Feminine);
return true;
}
if (ValueStr.Compare("neuter", ESearchCase::IgnoreCase) == 0)
{
OutVal = FSUDSValue(ETextGender::Neuter);
return true;
}
}
// Try quoted text (will be localised later in asset conversion)
{
const FRegexPattern Pattern(TEXT("^\"((?:[^\"\\\\]|\\\\.)*)\"$"));
FRegexMatcher Regex(Pattern, ValueStr);
if (Regex.FindNext())
{
FString Val = Regex.GetCaptureGroup(1);
// Consolidate any escaped double quotes into just quotes
Val.ReplaceInline(TEXT("\\\""), TEXT("\""));
OutVal = FSUDSValue(FText::FromString(Val));
return true;
}
}
// Try FName
{
const FRegexPattern Pattern(TEXT("^`([^`]*)`$"));
FRegexMatcher Regex(Pattern, ValueStr);
if (Regex.FindNext())
{
const FString Name = Regex.GetCaptureGroup(1);
OutVal = FSUDSValue(FName(Name), false);
return true;
}
}
// Try variable name
{
const FRegexPattern Pattern(TEXT("^\\{([^\\}]*)\\}$"));
FRegexMatcher Regex(Pattern, ValueStr);
if (Regex.FindNext())
{
const FName VariableName(Regex.GetCaptureGroup(1));
OutVal = FSUDSValue(VariableName, true);
return true;
}
}
// Try Numbers
{
float FloatVal;
int IntVal;
// look for int first; anything with a decimal point will fail
if (FDefaultValueHelper::ParseInt(ValueStr, IntVal))
{
OutVal = FSUDSValue(IntVal);
return true;
}
if (FDefaultValueHelper::ParseFloat(ValueStr, FloatVal))
{
OutVal = FSUDSValue(FloatVal);
return true;
}
}
return false;
}
bool FSUDSExpression::Validate()
{
// Empty expressions are always valid, mean "true"
if (Queue.IsEmpty())
return true;
// Same algorithm as Execute, we just don't execute
TArray<FSUDSExpressionItem> EvalStack;
const TMap<FName, FSUDSValue> TempVariables;
for (auto& Item : Queue)
{
if (Item.IsOperator())
{
FSUDSExpressionItem Arg1, Arg2;
if (Item.IsBinaryOperator())
{
if (EvalStack.IsEmpty())
return false;
Arg2 = EvalStack.Pop();
}
if (EvalStack.IsEmpty())
return false;
Arg1 = EvalStack.Pop();
EvalStack.Push(EvaluateOperator(Item.GetType(), Arg1, Arg2, TempVariables, TempVariables));
}
else
{
EvalStack.Push(Item);
}
}
// Must be one item left and must be an operand
return EvalStack.Num() == 1 && EvalStack[0].IsOperand();
}
FSUDSValue FSUDSExpression::Evaluate(const TMap<FName, FSUDSValue>& Variables, const TMap<FName, FSUDSValue>& GlobalVariables) const
{
checkf(bIsValid, TEXT("Cannot execute an invalid expression tree"));
// Blanks are mostly used for conditionals, for simplicity always return true
if (Queue.IsEmpty())
return FSUDSValue(true);
TArray<FSUDSExpressionItem> EvalStack;
// We could pre-optimise all literal expressions, but let's not for now
for (auto& Item : Queue)
{
if (Item.IsOperator())
{
FSUDSExpressionItem Arg1, Arg2;
// Arg2 (RHS) has to be popped first
if (Item.IsBinaryOperator())
{
checkf(!EvalStack.IsEmpty(), TEXT("Args missing before operator, bad expression"));
Arg2 = EvalStack.Pop();
}
checkf(!EvalStack.IsEmpty(), TEXT("Args missing before operator, bad expression"));
Arg1 = EvalStack.Pop();
EvalStack.Push(EvaluateOperator(Item.GetType(), Arg1, Arg2, Variables, GlobalVariables));
}
else
{
EvalStack.Push(Item);
}
}
checkf(EvalStack.Num() == 1, TEXT("We should end with a single item in the eval stack and it should be an operand"));
return EvaluateOperand(EvalStack.Top().GetOperandValue(), Variables, GlobalVariables);
}
bool FSUDSExpression::EvaluateBoolean(const TMap<FName, FSUDSValue>& Variables, const TMap<FName, FSUDSValue>& GlobalVariables, const FString& ErrorContext) const
{
const auto Result = Evaluate(Variables, GlobalVariables);
if (Result.GetType() != ESUDSValueType::Boolean &&
Result.GetType() != ESUDSValueType::Variable) // Allow unresolved variable, will assume false
{
UE_LOG(LogSUDS, Error, TEXT("%s: Condition '%s' did not return a boolean result"), *ErrorContext, *SourceString)
}
return Result.GetBooleanValue();
}
FSUDSExpressionItem FSUDSExpression::EvaluateOperator(ESUDSExpressionItemType Op,
const FSUDSExpressionItem& Arg1,
const FSUDSExpressionItem& Arg2,
const TMap<FName, FSUDSValue>& Variables,
const TMap<FName, FSUDSValue>& GlobalVariables) const
{
const FSUDSValue Val1 = EvaluateOperand(Arg1.GetOperandValue(), Variables, GlobalVariables);
FSUDSValue Val2;
if (Arg1.IsBinaryOperator())
{
Val2 = EvaluateOperand(Arg2.GetOperandValue(), Variables, GlobalVariables);
}
switch (Op)
{
case ESUDSExpressionItemType::Not:
return FSUDSExpressionItem(!Val1);
case ESUDSExpressionItemType::Multiply:
return FSUDSExpressionItem(Val1 * Val2);
case ESUDSExpressionItemType::Divide:
return FSUDSExpressionItem(Val1 / Val2);
case ESUDSExpressionItemType::Modulo:
return FSUDSExpressionItem(Val1 % Val2);
case ESUDSExpressionItemType::Add:
return FSUDSExpressionItem(Val1 + Val2);
case ESUDSExpressionItemType::Subtract:
return FSUDSExpressionItem(Val1 - Val2);
case ESUDSExpressionItemType::Less:
return FSUDSExpressionItem(Val1 < Val2);
case ESUDSExpressionItemType::LessEqual:
return FSUDSExpressionItem(Val1 <= Val2);
case ESUDSExpressionItemType::Greater:
return FSUDSExpressionItem(Val1 > Val2);
case ESUDSExpressionItemType::GreaterEqual:
return FSUDSExpressionItem(Val1 >= Val2);
case ESUDSExpressionItemType::Equal:
return FSUDSExpressionItem(Val1 == Val2);
case ESUDSExpressionItemType::NotEqual:
return FSUDSExpressionItem(Val1 != Val2);
case ESUDSExpressionItemType::And:
return FSUDSExpressionItem(Val1 && Val2);
case ESUDSExpressionItemType::Or:
return FSUDSExpressionItem(Val1 || Val2);
default: // these won't occur
case ESUDSExpressionItemType::Null:
case ESUDSExpressionItemType::Operand:
case ESUDSExpressionItemType::LParens:
case ESUDSExpressionItemType::RParens:
return FSUDSExpressionItem();
};
}
FSUDSValue FSUDSExpression::EvaluateOperand(const FSUDSValue& Operand,
const TMap<FName, FSUDSValue>& Variables,
const TMap<FName, FSUDSValue>& GlobalVariables) const
{
// Simplify conversion to variable values
if (Operand.IsVariable())
{
FName Name = Operand.GetVariableNameValue();
FName GlobalName;
if (USUDSLibrary::IsDialogueVariableGlobal(Name, GlobalName))
{
// This will have stripped the prefix so direct find is OK
if (const auto Var = GlobalVariables.Find(GlobalName))
{
return *Var;
}
}
if (const auto Var = Variables.Find(Operand.GetVariableNameValue()))
{
return *Var;
}
// Note: we're NOT warning about unset variables here, and just defaulting to initial values (false, 0 etc)
// This is more usable in practice than complaining about it
}
return Operand;
}

View File

@@ -0,0 +1,41 @@
// Copyright Steve Streeting 2022
// Released under the MIT license https://opensource.org/license/MIT/
#pragma once
#include "SUDSSubsystem.h"
#include "SUDSValue.h"
inline const TMap<FName, FSUDSValue>& InternalGetGlobalVariables(UWorld* WorldContext)
{
if (auto Sub = GetSUDSSubsystem(WorldContext))
{
return Sub->GetGlobalVariables();
}
#if WITH_EDITORONLY_DATA
// In editor mode, return static global vars since we may be unit testing or in editor tester
return USUDSSubsystem::Test_DummyGlobalVariables;
#else
static TMap<FName, FSUDSValue> Blank;
return Blank;
#endif
}
// For our code only
inline void InternalSetGlobalVariable(UWorld* WorldContext, FName Name, const FSUDSValue& Value, bool bFromScript, const FString& ScriptName, int LineNo)
{
if (auto Sub = GetSUDSSubsystem(WorldContext))
{
Sub->InternalSetGlobalVariable(Name, Value, bFromScript, LineNo);
}
else
{
#if WITH_EDITORONLY_DATA
// In editor mode, update static global vars since we may be unit testing or in editor tester
USUDSSubsystem::Test_DummyGlobalVariables.Add(Name, Value);
#else
checkf(false, TEXT("%s: Attempted to set global variable %s with no world context, did you forget to set the dialogue owner?"), *ScriptName, *Name.ToString());
#endif
}
}

View File

@@ -0,0 +1,154 @@
// Copyright Steve Streeting 2022
// Released under the MIT license https://opensource.org/license/MIT/
#include "SUDSLibrary.h"
#include "SUDSDialogue.h"
#include "SUDSScript.h"
#include "UObject/Package.h"
USUDSDialogue* USUDSLibrary::CreateDialogue(UObject* Owner, USUDSScript* Script, bool bStartImmediately, FName StartLabel)
{
if (IsValid(Script))
{
if (!IsValid(Owner))
{
Owner = GetTransientPackage();
}
const FName Name = MakeUniqueObjectName(Owner, USUDSDialogue::StaticClass(), Script->GetFName());
USUDSDialogue* Ret = NewObject<USUDSDialogue>(Owner, Name);
Ret->Initialise(Script);
if (bStartImmediately)
{
Ret->Start(StartLabel);
}
return Ret;
}
UE_LOG(LogSUDS, Error, TEXT("Called CreateDialogue with an invalid script"))
return nullptr;
}
USUDSDialogue* USUDSLibrary::CreateDialogueWithParticipants(UObject* Owner,
USUDSScript* Script,
const TArray<UObject*>& Participants, bool bStartImmediately, FName StartLabel)
{
if (IsValid(Script))
{
if (!IsValid(Owner))
{
Owner = GetTransientPackage();
}
// Don't use the base CreateDialogue to start, we want to set participants first before init/start
USUDSDialogue* Dlg = NewObject<USUDSDialogue>(Owner, Script->GetFName());
Dlg->SetParticipants(Participants);
Dlg->Initialise(Script);
if (bStartImmediately)
{
Dlg->Start(StartLabel);
}
return Dlg;
}
UE_LOG(LogSUDS, Error, TEXT("Called CreateDialogue with an invalid script"))
return nullptr;
}
USUDSDialogue* USUDSLibrary::CreateDialogueWithParticipant(UObject* Owner,
USUDSScript* Script,
UObject* Participant,
bool bStartImmediately,
FName StartLabel)
{
TArray<UObject*> Participants;
Participants.Add(Participant);
return CreateDialogueWithParticipants(Owner, Script, Participants, bStartImmediately, StartLabel);
}
bool USUDSLibrary::GetDialogueValueAsText(const FSUDSValue& Value, FText& TextValue)
{
if (Value.GetType() == ESUDSValueType::Text)
{
TextValue = Value.GetTextValue();
return true;
}
return false;
}
bool USUDSLibrary::GetDialogueValueAsBoolean(const FSUDSValue& Value, bool& BoolValue)
{
if (Value.GetType() == ESUDSValueType::Boolean)
{
BoolValue = Value.GetBooleanValue();
return true;
}
return false;
}
bool USUDSLibrary::GetDialogueValueAsInt(const FSUDSValue& Value, int& IntValue)
{
if (Value.GetType() == ESUDSValueType::Int)
{
IntValue = Value.GetIntValue();
return true;
}
return false;
}
bool USUDSLibrary::GetDialogueValueAsFloat(const FSUDSValue& Value, float& FloatValue)
{
if (Value.GetType() == ESUDSValueType::Float)
{
FloatValue = Value.GetFloatValue();
return true;
}
return false;
}
bool USUDSLibrary::GetDialogueValueAsGender(const FSUDSValue& Value, ETextGender& GenderValue)
{
if (Value.GetType() == ESUDSValueType::Gender)
{
GenderValue = Value.GetGenderValue();
return true;
}
return false;
}
bool USUDSLibrary::GetDialogueValueAsName(const FSUDSValue& Value, FName& NameValue)
{
if (Value.GetType() == ESUDSValueType::Name)
{
NameValue = Value.GetNameValue();
return true;
}
return false;
}
ESUDSValueType USUDSLibrary::GetDialogueValueType(const FSUDSValue& Value)
{
return Value.GetType();
}
bool USUDSLibrary::GetDialogueValueIsEmpty(const FSUDSValue& Value)
{
return Value.IsEmpty();
}
bool USUDSLibrary::IsDialogueVariableGlobal(const FName& Name, FName& OutName)
{
static const FString Prefix(TEXT("global."));
FString TempStr;
Name.ToString(TempStr);
if (TempStr.StartsWith(Prefix, ESearchCase::IgnoreCase))
{
TempStr.RightChopInline(Prefix.Len());
OutName = FName(TempStr);
return true;
}
else
{
OutName = Name;
return false;
}
}

View File

@@ -0,0 +1,5 @@
// Copyright Steve Streeting 2022
// Released under the MIT license https://opensource.org/license/MIT/
#include "SUDSParticipant.h"

View File

@@ -0,0 +1,276 @@
// Copyright Steve Streeting 2022
// Released under the MIT license https://opensource.org/license/MIT/
#include "SUDSScript.h"
#include "SUDSScriptNode.h"
#include "SUDSScriptNodeGosub.h"
#include "SUDSScriptNodeText.h"
#include "EditorFramework/AssetImportData.h"
void USUDSScript::StartImport(TArray<TObjectPtr<USUDSScriptNode>>** ppNodes,
TArray<TObjectPtr<USUDSScriptNode>>** ppHeaderNodes,
TMap<FName, int>** ppLabelList,
TMap<FName, int>** ppHeaderLabelList,
TArray<FString>** ppSpeakerList)
{
*ppNodes = &Nodes;
*ppHeaderNodes = &HeaderNodes;
*ppLabelList = &LabelList;
*ppHeaderLabelList = &HeaderLabelList;
*ppSpeakerList = &Speakers;
}
USUDSScriptNode* USUDSScript::GetNextNode(const USUDSScriptNode* Node) const
{
switch (Node->GetEdgeCount())
{
case 0:
return nullptr;
case 1:
return Node->GetEdge(0)->GetTargetNode().Get();
default:
UE_LOG(LogSUDS, Error, TEXT("Called GetNextNode on a node with more than one edge"));
return nullptr;
}
}
#define kChoiceFound 1
#define kChoiceNotFoundBeforeText -1
#define kChoiceNotFoundBeforeEnd 0
bool USUDSScript::DoesAnyPathAfterLeadToChoice(USUDSScriptNode* FromNode)
{
// Look for any possible choice following a node (text or gosub)
// If it's possible to find a choice in one of the paths ahead, before another text node, the return true
// Given that there might be conditionals, not all paths might lead to a choice, but we only care if one of them does
// We recurse into conditional paths where they exist until we know.
// For a gosub this is looking for the next after a return, not inside the sub
USUDSScriptNode* CurrNode = GetNextNode(FromNode);
return RecurseLookForChoice(CurrNode) == kChoiceFound;
}
int USUDSScript::RecurseLookForChoice(USUDSScriptNode* CurrNode)
{
// Return int so that we can differentiate:
// 1 = we found a choice
// 0 = we didn't find a choice, but also didn't hit another text node (reached end, or gosub return)
// -1 = we hit a text node
while (CurrNode)
{
switch (CurrNode->GetNodeType())
{
case ESUDSScriptNodeType::Text:
// if we hit a text node, there was no choice
return kChoiceNotFoundBeforeText;
case ESUDSScriptNodeType::Choice:
// we found a choice
return kChoiceFound;
case ESUDSScriptNodeType::Select:
{
// Explore all possible routes
int WorstResult = kChoiceNotFoundBeforeEnd;
for (auto& Edge : CurrNode->GetEdges())
{
auto TargetNode = Edge.GetTargetNode();
if (TargetNode.IsValid())
{
const int ConditionalPath = RecurseLookForChoice(TargetNode.Get());
if (ConditionalPath == kChoiceFound)
return kChoiceFound;
WorstResult = FMath::Min(ConditionalPath, WorstResult);
}
}
return WorstResult;
}
case ESUDSScriptNodeType::Event:
case ESUDSScriptNodeType::SetVariable:
CurrNode = GetNextNode(CurrNode);
break;
case ESUDSScriptNodeType::Gosub:
// When we hit a gosub here we go into it, not after it
if (auto GosubNode = Cast<USUDSScriptNodeGosub>(CurrNode))
{
int SubResult = RecurseLookForChoice(GetNodeByLabel(GosubNode->GetLabelName()));
if (SubResult != 0)
{
// Found definitive result (choice or text) inside sub
return SubResult;
}
}
// Otherwise, we didn't conclude within the sub, continue following it
CurrNode = GetNextNode(CurrNode);
break;
default: ;
case ESUDSScriptNodeType::Return:
// this is when we're exploring a sub for the choice
return kChoiceNotFoundBeforeEnd;
};
}
return kChoiceNotFoundBeforeEnd;
}
void USUDSScript::FinishImport()
{
// As an optimisation, make all text/gosub nodes pre-scan their follow-on nodes for choice nodes
// We can actually have intermediate nodes, for example set nodes which run for all choices that are placed
// between the text and the first choice. Resolve whether they exist now
for (auto Node : Nodes)
{
if (Node->GetNodeType() == ESUDSScriptNodeType::Text ||
Node->GetNodeType() == ESUDSScriptNodeType::Gosub)
{
if (DoesAnyPathAfterLeadToChoice(Node))
{
switch (Node->GetNodeType())
{
case ESUDSScriptNodeType::Text:
{
if (auto TextNode = Cast<USUDSScriptNodeText>(Node))
{
TextNode->NotifyMayHaveChoices();
}
break;
}
case ESUDSScriptNodeType::Gosub:
{
if (auto GosubNode = Cast<USUDSScriptNodeGosub>(Node))
{
GosubNode->NotifyMayHaveChoices();
}
break;
}
default: break;
}
}
}
}
}
USUDSScriptNode* USUDSScript::GetHeaderNode() const
{
if (HeaderNodes.Num() > 0)
return HeaderNodes[0];
return nullptr;
}
USUDSScriptNode* USUDSScript::GetFirstNode() const
{
if (Nodes.Num() > 0)
return Nodes[0];
return nullptr;
}
USUDSScriptNode* USUDSScript::GetNodeByLabel(const FName& Label) const
{
if (const int* pIdx = LabelList.Find(Label))
{
return Nodes[*pIdx];
}
return nullptr;
}
USUDSScriptNodeText* USUDSScript::GetNodeByTextID(const FString& TextID) const
{
for (auto N : Nodes)
{
if (N->GetNodeType() == ESUDSScriptNodeType::Text)
{
if (auto TN = Cast<USUDSScriptNodeText>(N))
{
if (TextID.Equals(TN->GetTextID()))
{
return TN;
}
}
}
}
return nullptr;
}
USUDSScriptNodeGosub* USUDSScript::GetNodeByGosubID(const FString& ID) const
{
for (auto N : Nodes)
{
if (N->GetNodeType() == ESUDSScriptNodeType::Text)
{
if (auto GN = Cast<USUDSScriptNodeGosub>(N))
{
if (ID.Equals(GN->GetGosubID()))
{
return GN;
}
}
}
}
return nullptr;
}
UDialogueVoice* USUDSScript::GetSpeakerVoice(const FString& SpeakerID) const
{
if (auto pVoice = SpeakerVoices.Find(SpeakerID))
{
return *pVoice;
}
return nullptr;
}
void USUDSScript::SetSpeakerVoice(const FString& SpeakerID, UDialogueVoice* Voice)
{
SpeakerVoices.Add(SpeakerID, Voice);
}
#if WITH_EDITORONLY_DATA
void USUDSScript::PostInitProperties()
{
if (!HasAnyFlags(RF_ClassDefaultObject))
{
AssetImportData = NewObject<UAssetImportData>(this, TEXT("AssetImportData"));
}
Super::PostInitProperties();
}
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4
void USUDSScript::GetAssetRegistryTags(FAssetRegistryTagsContext Context) const
{
if (AssetImportData)
{
Context.AddTag( FAssetRegistryTag(SourceFileTagName(), AssetImportData->GetSourceData().ToJson(), FAssetRegistryTag::TT_Hidden) );
}
Super::GetAssetRegistryTags(Context);
}
#else
void USUDSScript::GetAssetRegistryTags(TArray<FAssetRegistryTag>& OutTags) const
{
if (AssetImportData)
{
OutTags.Add( FAssetRegistryTag(SourceFileTagName(), AssetImportData->GetSourceData().ToJson(), FAssetRegistryTag::TT_Hidden) );
}
Super::GetAssetRegistryTags(OutTags);
}
#endif
void USUDSScript::Serialize(FArchive& Ar)
{
Super::Serialize(Ar);
if (Ar.IsLoading() && Ar.UEVer() < VER_UE4_ASSET_IMPORT_DATA_AS_JSON && !AssetImportData)
{
// AssetImportData should always be valid
AssetImportData = NewObject<UAssetImportData>(this, TEXT("AssetImportData"));
}
}
#endif

View File

@@ -0,0 +1,79 @@
// Copyright Steve Streeting 2022
// Released under the MIT license https://opensource.org/license/MIT/
#include "SUDSScriptEdge.h"
#include "SUDSScriptNode.h"
FSUDSScriptEdge::FSUDSScriptEdge(USUDSScriptNode* ToNode, ESUDSEdgeType InType, int LineNo): Type(InType),
TargetNode(ToNode),
SourceLineNo(LineNo)
{
}
FSUDSScriptEdge::FSUDSScriptEdge(const FText& InText, USUDSScriptNode* ToNode, int LineNo): Text(InText),
Type(ESUDSEdgeType::Decision),
TargetNode(ToNode),
SourceLineNo(LineNo)
{
}
void FSUDSScriptEdge::ExtractFormat() const
{
// Only do this on demand, and only once
TextFormat = Text;
ParameterNames.Empty();
TArray<FString> TextParams;
TextFormat.GetFormatArgumentNames(TextParams);
for (auto Param : TextParams)
{
ParameterNames.Add(FName(Param));
}
bFormatExtracted = true;
}
FString FSUDSScriptEdge::GetTextID() const
{
return SUDS_GET_TEXT_KEY(Text);
}
void FSUDSScriptEdge::SetText(const FText& InText)
{
Text = InText;
bFormatExtracted = false;
}
void FSUDSScriptEdge::SetTargetNode(const TWeakObjectPtr<USUDSScriptNode>& InTargetNode)
{
TargetNode = InTargetNode;
}
const FTextFormat& FSUDSScriptEdge::GetTextFormat() const
{
if (!bFormatExtracted)
{
ExtractFormat();
}
return TextFormat;
}
const TArray<FName>& FSUDSScriptEdge::GetParameterNames() const
{
if (!bFormatExtracted)
{
ExtractFormat();
}
return ParameterNames;
}
bool FSUDSScriptEdge::HasParameters() const
{
if (!bFormatExtracted)
{
ExtractFormat();
}
return !ParameterNames.IsEmpty();
}

View File

@@ -0,0 +1,38 @@
// Copyright Steve Streeting 2022
// Released under the MIT license https://opensource.org/license/MIT/
#include "SUDSScriptNode.h"
USUDSScriptNode::USUDSScriptNode()
{
}
void USUDSScriptNode::InitChoice(int LineNo)
{
NodeType = ESUDSScriptNodeType::Choice;
SourceLineNo = LineNo;
}
void USUDSScriptNode::InitSelect(int LineNo)
{
NodeType = ESUDSScriptNodeType::Select;
SourceLineNo = LineNo;
}
void USUDSScriptNode::InitReturn(int LineNo)
{
NodeType = ESUDSScriptNodeType::Return;
SourceLineNo = LineNo;
}
bool USUDSScriptNode::IsRandomSelect() const
{
return NodeType == ESUDSScriptNodeType::Select &&
GetEdgeCount() > 0 &&
GetEdge(0)->GetCondition().IsRandomCondition();
}
void USUDSScriptNode::AddEdge(const FSUDSScriptEdge& NewEdge)
{
Edges.Add(NewEdge);
}

View File

@@ -0,0 +1,12 @@
// Copyright Steve Streeting 2022
// Released under the MIT license https://opensource.org/license/MIT/
#include "SUDSScriptNodeEvent.h"
void USUDSScriptNodeEvent::Init(const FString& EvtName, const TArray<FSUDSExpression>& InArgs, int LineNo)
{
NodeType = ESUDSScriptNodeType::Event;
EventName = FName(EvtName);
Args = InArgs;
SourceLineNo = LineNo;
}

View File

@@ -0,0 +1,11 @@
// Copyright Steve Streeting 2022
// Released under the MIT license https://opensource.org/license/MIT/
#include "SUDSScriptNodeSet.h"
void USUDSScriptNodeSet::Init(const FString& VarName, const FSUDSExpression& InExpression, int LineNo)
{
NodeType = ESUDSScriptNodeType::SetVariable;
Identifier = FName(VarName);
Expression = InExpression;
SourceLineNo = LineNo;
}

View File

@@ -0,0 +1,62 @@
// Copyright Steve Streeting 2022
// Released under the MIT license https://opensource.org/license/MIT/
#include "SUDSScriptNodeText.h"
void USUDSScriptNodeText::Init(const FString& InSpeakerID, const FText& InText, int LineNo)
{
NodeType = ESUDSScriptNodeType::Text;
SpeakerID = InSpeakerID;
Text = InText;
TextFormat = Text;
SourceLineNo = LineNo;
bFormatExtracted = false;
}
FString USUDSScriptNodeText::GetTextID() const
{
return SUDS_GET_TEXT_KEY(Text);
}
const FTextFormat& USUDSScriptNodeText::GetTextFormat() const
{
if (!bFormatExtracted)
{
ExtractFormat();
}
return TextFormat;
}
const TArray<FName>& USUDSScriptNodeText::GetParameterNames() const
{
if (!bFormatExtracted)
{
ExtractFormat();
}
return ParameterNames;
}
bool USUDSScriptNodeText::HasParameters() const
{
if (!bFormatExtracted)
{
ExtractFormat();
}
return !ParameterNames.IsEmpty();
}
void USUDSScriptNodeText::ExtractFormat() const
{
// Only do this on demand, and only once
TextFormat = Text;
ParameterNames.Empty();
TArray<FString> TextParams;
TextFormat.GetFormatArgumentNames(TextParams);
for (auto Param : TextParams)
{
ParameterNames.Add(FName(Param));
}
bFormatExtracted = true;
}

View File

@@ -0,0 +1,199 @@
// Copyright Steve Streeting 2022
// Released under the MIT license https://opensource.org/license/MIT/
#include "SUDSSubsystem.h"
#include "Sound/SoundConcurrency.h"
DEFINE_LOG_CATEGORY(LogSUDSSubsystem)
#if WITH_EDITORONLY_DATA
TMap<FName, FSUDSValue> USUDSSubsystem::Test_DummyGlobalVariables;
#endif
void USUDSSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
// Default to a single voice line being played at once
VoiceConcurrency = NewObject<USoundConcurrency>(this);
VoiceConcurrency->Concurrency.MaxCount = 1;
}
void USUDSSubsystem::Deinitialize()
{
Super::Deinitialize();
}
void USUDSSubsystem::SetMaxConcurrentVoicedLines(int ConcurrentLines)
{
if (IsValid(VoiceConcurrency))
{
VoiceConcurrency->Concurrency.MaxCount = ConcurrentLines;
}
}
int USUDSSubsystem::GetMaxConcurrentVoicedLines() const
{
if (IsValid(VoiceConcurrency))
{
return VoiceConcurrency->Concurrency.MaxCount;
}
return 1;
}
void USUDSSubsystem::ResetGlobalState(bool bResetVariables)
{
if (bResetVariables)
GlobalVariableState.Empty();
}
FSUDSGlobalState USUDSSubsystem::GetSavedGlobalState() const
{
return FSUDSGlobalState(GlobalVariableState);
}
void USUDSSubsystem::RestoreSavedGlobalState(const FSUDSGlobalState& State)
{
ResetGlobalState();
GlobalVariableState.Append(State.GetGlobalVariables());
}
FText USUDSSubsystem::GetGlobalVariableText(FName Name) const
{
if (const auto Arg = GlobalVariableState.Find(Name))
{
if (Arg->GetType() == ESUDSValueType::Text)
{
return Arg->GetTextValue();
}
else
{
UE_LOG(LogSUDSSubsystem, Error, TEXT("Requested variable %s of type text but was not a compatible type"), *Name.ToString());
}
}
return FText();
}
void USUDSSubsystem::SetGlobalVariableInt(FName Name, int32 Value)
{
SetGlobalVariable(Name, Value);
}
int USUDSSubsystem::GetGlobalVariableInt(FName Name) const
{
if (const auto Arg = GlobalVariableState.Find(Name))
{
switch (Arg->GetType())
{
case ESUDSValueType::Int:
return Arg->GetIntValue();
case ESUDSValueType::Float:
UE_LOG(LogSUDSSubsystem, Warning, TEXT("Casting variable %s to int, data loss may occur"), *Name.ToString());
return Arg->GetFloatValue();
default:
case ESUDSValueType::Gender:
case ESUDSValueType::Text:
UE_LOG(LogSUDSSubsystem, Error, TEXT("Variable %s is not a compatible integer type"), *Name.ToString());
}
}
return 0;
}
void USUDSSubsystem::SetGlobalVariableFloat(FName Name, float Value)
{
SetGlobalVariable(Name, Value);
}
float USUDSSubsystem::GetGlobalVariableFloat(FName Name) const
{
if (const auto Arg = GlobalVariableState.Find(Name))
{
switch (Arg->GetType())
{
case ESUDSValueType::Int:
return Arg->GetIntValue();
case ESUDSValueType::Float:
return Arg->GetFloatValue();
default:
case ESUDSValueType::Gender:
case ESUDSValueType::Text:
UE_LOG(LogSUDSSubsystem, Error, TEXT("Variable %s is not a compatible float type"), *Name.ToString());
}
}
return 0;
}
void USUDSSubsystem::SetGlobalVariableGender(FName Name, ETextGender Value)
{
SetGlobalVariable(Name, Value);
}
ETextGender USUDSSubsystem::GetGlobalVariableGender(FName Name) const
{
if (const auto Arg = GlobalVariableState.Find(Name))
{
switch (Arg->GetType())
{
case ESUDSValueType::Gender:
return Arg->GetGenderValue();
default:
case ESUDSValueType::Int:
case ESUDSValueType::Float:
case ESUDSValueType::Text:
UE_LOG(LogSUDSSubsystem, Error, TEXT("Variable %s is not a compatible gender type"), *Name.ToString());
}
}
return ETextGender::Neuter;
}
void USUDSSubsystem::SetGlobalVariableBoolean(FName Name, bool Value)
{
// Use explicit FSUDSValue constructor to avoid default int conversion
SetGlobalVariable(Name, FSUDSValue(Value));
}
bool USUDSSubsystem::GetGlobalVariableBoolean(FName Name) const
{
if (const auto Arg = GlobalVariableState.Find(Name))
{
switch (Arg->GetType())
{
case ESUDSValueType::Boolean:
return Arg->GetBooleanValue();
case ESUDSValueType::Int:
return Arg->GetIntValue() != 0;
default:
case ESUDSValueType::Float:
case ESUDSValueType::Gender:
case ESUDSValueType::Text:
UE_LOG(LogSUDSSubsystem, Error, TEXT("Variable %s is not a compatible boolean type"), *Name.ToString());
}
}
return false;
}
void USUDSSubsystem::SetGlobalVariableName(FName Name, FName Value)
{
SetGlobalVariable(Name, FSUDSValue(Value, false));
}
FName USUDSSubsystem::GetGlobalVariableName(FName Name) const
{
if (const auto Arg = GlobalVariableState.Find(Name))
{
if (Arg->GetType() == ESUDSValueType::Name)
{
return Arg->GetNameValue();
}
else
{
UE_LOG(LogSUDSSubsystem, Error, TEXT("Requested variable %s of type text but was not a compatible type"), *Name.ToString());
}
}
return NAME_None;
}
void USUDSSubsystem::UnSetGlobalVariable(FName Name)
{
GlobalVariableState.Remove(Name);
}

View File

@@ -0,0 +1,93 @@
// Copyright Steve Streeting 2022
// Released under the MIT license https://opensource.org/license/MIT/
#include "SUDSValue.h"
FArchive& operator<<(FArchive& Ar, FSUDSValue& Value)
{
// Custom serialisation since we can't auto-serialise union, TOptional
uint8 TypeAsInt = (uint8)Value.Type;
Ar << TypeAsInt;
if (Ar.IsLoading())
Value.Type = static_cast<ESUDSValueType>(TypeAsInt);
// This gets/sets float value too
Ar << Value.IntValue;
if (Value.Type == ESUDSValueType::Text)
{
FText Text = Value.TextValue.Get(FText::GetEmpty());
Ar << Text;
if (Ar.IsLoading())
Value.TextValue = Text;
}
else if (Value.Type == ESUDSValueType::Variable || Value.Type == ESUDSValueType::Name)
{
FString VarNameStr = Value.Name.Get(NAME_None).ToString();
Ar << VarNameStr;
if (Ar.IsLoading())
Value.Name = FName(VarNameStr);
}
return Ar;
}
void operator<<(FStructuredArchive::FSlot Slot, FSUDSValue& Value)
{
FStructuredArchive::FRecord Record = Slot.EnterRecord();
Record
<< SA_VALUE(TEXT("Type"), Value.Type)
<< SA_VALUE(TEXT("IntValue"), Value.IntValue); // gets/sets float/boolean/gender too
if (Value.Type == ESUDSValueType::Text)
{
Record << SA_VALUE(TEXT("TextValue"), Value.TextValue);
}
else if (Value.Type == ESUDSValueType::Variable || Value.Type == ESUDSValueType::Name)
{
Record << SA_VALUE(TEXT("Name"), Value.Name);
}
}
FString FSUDSValue::ToString() const
{
switch (Type)
{
case ESUDSValueType::Text:
return GetTextValue().ToString();
case ESUDSValueType::Int:
return FString::FromInt(GetIntValue());
case ESUDSValueType::Float:
return FString::SanitizeFloat(GetFloatValue());
case ESUDSValueType::Boolean:
return GetBooleanValue() ? "True" : "False";
case ESUDSValueType::Gender:
return StaticEnum<ETextGender>()->GetNameStringByValue((int64)GetGenderValue());
case ESUDSValueType::Name:
return GetNameValue().ToString();
case ESUDSValueType::Variable:
return GetVariableNameValue().ToString();
default:
case ESUDSValueType::Empty:
return "Empty";
}
}
bool FSUDSValue::ExportTextItem(FString& ValueStr,
FSUDSValue const& DefaultValue,
UObject* Parent,
int32 PortFlags,
UObject* ExportRootScope) const
{
// This is used to generate the blueprint debugger, but also used in serialisation
// We need to only implement it for debugging to avoid breaking anything else
if (0 != (PortFlags & EPropertyPortFlags::PPF_BlueprintDebugView))
{
ValueStr.Appendf(TEXT("Type=%s Value=%s"), *StaticEnum<ESUDSValueType>()->GetDisplayValueAsText(Type).ToString(), *ToString());
return true;
}
// Use the default for everything else
return false;
}