Initial push to repo
This commit is contained in:
21
Plugins/SUDS/Source/SUDS/Private/SUDS.cpp
Normal file
21
Plugins/SUDS/Source/SUDS/Private/SUDS.cpp
Normal 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)
|
||||
4
Plugins/SUDS/Source/SUDS/Private/SUDSCommon.cpp
Normal file
4
Plugins/SUDS/Source/SUDS/Private/SUDSCommon.cpp
Normal file
@@ -0,0 +1,4 @@
|
||||
#include "SUDSCommon.h"
|
||||
|
||||
const FName FSUDSConstants::RandomItemSelectIndexVarName(SUDS_RANDOMITEM_VAR);
|
||||
|
||||
1379
Plugins/SUDS/Source/SUDS/Private/SUDSDialogue.cpp
Normal file
1379
Plugins/SUDS/Source/SUDS/Private/SUDSDialogue.cpp
Normal file
File diff suppressed because it is too large
Load Diff
465
Plugins/SUDS/Source/SUDS/Private/SUDSExpression.cpp
Normal file
465
Plugins/SUDS/Source/SUDS/Private/SUDSExpression.cpp
Normal 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;
|
||||
}
|
||||
41
Plugins/SUDS/Source/SUDS/Private/SUDSInternal.h
Normal file
41
Plugins/SUDS/Source/SUDS/Private/SUDSInternal.h
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
154
Plugins/SUDS/Source/SUDS/Private/SUDSLibrary.cpp
Normal file
154
Plugins/SUDS/Source/SUDS/Private/SUDSLibrary.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
5
Plugins/SUDS/Source/SUDS/Private/SUDSParticipant.cpp
Normal file
5
Plugins/SUDS/Source/SUDS/Private/SUDSParticipant.cpp
Normal file
@@ -0,0 +1,5 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSParticipant.h"
|
||||
|
||||
|
||||
276
Plugins/SUDS/Source/SUDS/Private/SUDSScript.cpp
Normal file
276
Plugins/SUDS/Source/SUDS/Private/SUDSScript.cpp
Normal 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
|
||||
79
Plugins/SUDS/Source/SUDS/Private/SUDSScriptEdge.cpp
Normal file
79
Plugins/SUDS/Source/SUDS/Private/SUDSScriptEdge.cpp
Normal 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();
|
||||
|
||||
}
|
||||
38
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNode.cpp
Normal file
38
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNode.cpp
Normal 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);
|
||||
}
|
||||
|
||||
12
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeEvent.cpp
Normal file
12
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeEvent.cpp
Normal 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;
|
||||
|
||||
}
|
||||
11
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeSet.cpp
Normal file
11
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeSet.cpp
Normal 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;
|
||||
}
|
||||
62
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeText.cpp
Normal file
62
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeText.cpp
Normal 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;
|
||||
}
|
||||
199
Plugins/SUDS/Source/SUDS/Private/SUDSSubsystem.cpp
Normal file
199
Plugins/SUDS/Source/SUDS/Private/SUDSSubsystem.cpp
Normal 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);
|
||||
}
|
||||
93
Plugins/SUDS/Source/SUDS/Private/SUDSValue.cpp
Normal file
93
Plugins/SUDS/Source/SUDS/Private/SUDSValue.cpp
Normal 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;
|
||||
|
||||
}
|
||||
15
Plugins/SUDS/Source/SUDS/Public/SUDS.h
Normal file
15
Plugins/SUDS/Source/SUDS/Public/SUDS.h
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Modules/ModuleInterface.h"
|
||||
|
||||
class FSUDSModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
|
||||
/** IModuleInterface implementation */
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
};
|
||||
23
Plugins/SUDS/Source/SUDS/Public/SUDSCommon.h
Normal file
23
Plugins/SUDS/Source/SUDS/Public/SUDSCommon.h
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
#include "CoreMinimal.h"
|
||||
#include "Runtime/Launch/Resources/Version.h"
|
||||
|
||||
// Use DECLARE_LOG_CATEGORY_CLASS not DECLARE_LOG_CATEGORY_EXTERN because we use UE_LOG in headers
|
||||
DECLARE_LOG_CATEGORY_CLASS(LogSUDS, Warning, All)
|
||||
|
||||
#define SUDS_RANDOMITEM_VAR "SUDS.RandomItem"
|
||||
|
||||
struct FSUDSConstants
|
||||
{
|
||||
/// Reserved variable named use to create random results from select nodes
|
||||
static const FName RandomItemSelectIndexVarName;
|
||||
|
||||
};
|
||||
|
||||
#if ENGINE_MINOR_VERSION >= 5
|
||||
#define SUDS_GET_TEXT_KEY(Text) FTextInspector::GetTextId(Text).GetKey().ToString()
|
||||
#else
|
||||
#define SUDS_GET_TEXT_KEY(Text) FTextInspector::GetTextId(Text).GetKey().GetChars()
|
||||
#endif
|
||||
694
Plugins/SUDS/Source/SUDS/Public/SUDSDialogue.h
Normal file
694
Plugins/SUDS/Source/SUDS/Public/SUDSDialogue.h
Normal file
@@ -0,0 +1,694 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSExpression.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "SUDSDialogue.generated.h"
|
||||
|
||||
class USUDSScriptNodeGosub;
|
||||
class USUDSScriptNodeText;
|
||||
struct FSUDSScriptEdge;
|
||||
class USUDSScriptNode;
|
||||
class USUDSScript;
|
||||
class UDialogueWave;
|
||||
class UDialogueVoice;
|
||||
class USoundBase;
|
||||
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDialogueSpeakerLine, class USUDSDialogue*, Dialogue);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnDialogueChoice, class USUDSDialogue*, Dialogue, int, ChoiceIndex);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDialogueProceeding, class USUDSDialogue*, Dialogue);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnDialogueStarting, class USUDSDialogue*, Dialogue, FName, AtLabel);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDialogueFinished, class USUDSDialogue*, Dialogue);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnDialogueEvent, class USUDSDialogue*, Dialogue, FName, EventName, const TArray<FSUDSValue>&, Arguments);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FOnVariableChangedEvent, class USUDSDialogue*, Dialogue, FName, VariableName, const FSUDSValue&, Value, bool, bFromScript);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnVariableRequestedEvent, class USUDSDialogue*, Dialogue, FName, VariableName);
|
||||
|
||||
#if WITH_EDITOR
|
||||
// Non-dynamic events for editor use
|
||||
DECLARE_DELEGATE_TwoParams(FOnDialogueSpeakerLineInternal, class USUDSDialogue* /* Dialogue */, int /*SourceLineNo*/);
|
||||
DECLARE_DELEGATE_ThreeParams(FOnDialogueChoiceInternal, class USUDSDialogue* /* Dialogue*/, int /*ChoiceIndex*/, int /*SourceLineNo*/);
|
||||
DECLARE_DELEGATE_OneParam(FOnDialogueProceedingInternal, class USUDSDialogue* /*Dialogue*/);
|
||||
DECLARE_DELEGATE_TwoParams(FOnDialogueStartingInternal, class USUDSDialogue* /*Dialogue*/, FName /*AtLabel*/);
|
||||
DECLARE_DELEGATE_OneParam(FOnDialogueFinishedInternal, class USUDSDialogue* /*Dialogue*/);
|
||||
DECLARE_DELEGATE_FourParams(FOnDialogueEventInternal, class USUDSDialogue* /*Dialogue*/, FName /*EventName*/, const TArray<FSUDSValue>& /*Arguments*/, int /*SourceLineNo*/);
|
||||
DECLARE_DELEGATE_FiveParams(FOnDialogueVarChangedByScriptInternal, class USUDSDialogue* /* Dialogue*/, FName /*VariableName*/, const FSUDSValue& /*Value*/, const FString& /*ExprString*/, int /*SourceLineNo*/);
|
||||
DECLARE_DELEGATE_ThreeParams(FOnDialogueVarChangedByCodeInternal, class USUDSDialogue* /* Dialogue*/, FName /*VariableName*/, const FSUDSValue& /*Value*/);
|
||||
DECLARE_DELEGATE_FourParams(FOnDialogueSelectEval, class USUDSDialogue* /*Dialogue*/, const FString& /*ConditionString*/, bool /*bResult*/, int /*SourceLineNo*/);
|
||||
#endif
|
||||
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogSUDSDialogue, Verbose, All);
|
||||
|
||||
/// Copy of the internal state of a dialogue
|
||||
USTRUCT(BlueprintType)
|
||||
struct FSUDSDialogueState
|
||||
{
|
||||
GENERATED_BODY()
|
||||
protected:
|
||||
UPROPERTY(BlueprintReadOnly, SaveGame, Category="SUDS|Dialogue")
|
||||
FString TextNodeID;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, SaveGame, Category="SUDS|Dialogue")
|
||||
TMap<FName, FSUDSValue> Variables;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, SaveGame, Category="SUDS|Dialogue")
|
||||
TArray<FString> ChoicesTaken;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, SaveGame, Category="SUDS|Dialogue")
|
||||
TArray<FString> ReturnStack;
|
||||
|
||||
public:
|
||||
FSUDSDialogueState() {}
|
||||
|
||||
FSUDSDialogueState(const FString& TxtID,
|
||||
const TMap<FName, FSUDSValue>& InVars,
|
||||
const TSet<FString>& InChoices,
|
||||
const TArray<FString>& InReturnStack) : TextNodeID(TxtID),
|
||||
Variables(InVars),
|
||||
ChoicesTaken(InChoices.Array()),
|
||||
ReturnStack(InReturnStack)
|
||||
{
|
||||
}
|
||||
|
||||
const FString& GetTextNodeID() const { return TextNodeID; }
|
||||
const TMap<FName, FSUDSValue>& GetVariables() const { return Variables; }
|
||||
const TArray<FString>& GetChoicesTaken() const { return ChoicesTaken; }
|
||||
const TArray<FString>& GetReturnStack() const { return ReturnStack; }
|
||||
|
||||
SUDS_API friend FArchive& operator<<(FArchive& Ar, FSUDSDialogueState& Value);
|
||||
SUDS_API friend void operator<<(FStructuredArchive::FSlot Slot, FSUDSDialogueState& Value);
|
||||
bool Serialize(FStructuredArchive::FSlot Slot)
|
||||
{
|
||||
Slot << *this;
|
||||
return true;
|
||||
}
|
||||
bool Serialize(FArchive& Ar)
|
||||
{
|
||||
Ar << *this;
|
||||
return true;
|
||||
}
|
||||
|
||||
};
|
||||
/**
|
||||
* A Dialogue is a runtime instance of a Script (the asset on which the dialogue is based)
|
||||
* An Dialogue always stops on a speaker line, which may have player choices. It progresses when you call Continue()
|
||||
* or Choose() and will run that continuation until it hits the next speaker line. In between, other things may occur
|
||||
* such as setting variables, raising events etc, depending on the script.
|
||||
* Each dialogue instance has its own state, so you can invoke the same Script multiple times as different dialogues if you want.
|
||||
* Each dialogue maintains its own internal state, which includes a set of variables.
|
||||
* Dialogues can have Participants, which are objects closely involved in the dialogue and which have the best access to
|
||||
* supply and retrieve variables and get events first. Other objects can simply listen to the exposed events; while they
|
||||
* can manipulate dialogue state too, they have less controllable access in terms of *when* this happens. It's best to
|
||||
* have at least one Participant driving state on the dialogue (relaying it to external objects), and to have read-only
|
||||
* users like UIs use the event delegates instead.
|
||||
* Dialogues need to be owned by an object, mainly for garbage collection. It's recommended that you set the owner to
|
||||
* one of the NPCs in the dialogue.
|
||||
* You can save/restore the state of a dialogue via GetSavedState/RestoreSavedState.
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class SUDS_API USUDSDialogue : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
/// Event raised when dialogue progresses and a new speaker line, potentially with new choices, is ready to be displayed
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnDialogueSpeakerLine OnSpeakerLine;
|
||||
/// Event raised when a choice is made in the dialogue by the player. At this point, the dialogue has not progressed
|
||||
/// as a result of that choice so the index passed can be used to reference the choice
|
||||
/// This event is ONLY raised if there's a choice of paths, not for just continuing a linear path.
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnDialogueChoice OnChoice;
|
||||
/// Event raised when the dialog is about to proceed away from the current speaker line (because of a choice or continue)
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnDialogueProceeding OnProceeding;
|
||||
/// Event raised when an event is sent from the dialogue script. Any listeners or participants can process the event.
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnDialogueEvent OnEvent;
|
||||
/// Event raised when a variable is changed. "FromScript" is true if the variable was set by the script, false if set from code
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnVariableChangedEvent OnVariableChanged;
|
||||
/// Event raised when a variable is requested by the dialogue script. You can use this hook to set variables in the
|
||||
/// dialogue on-demand rather than up-front; anything set during this hook will be immediately used by the dialogue
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnVariableRequestedEvent OnVariableRequested;
|
||||
/// Event raised when the dialogue is starting, before the first speaker line
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnDialogueStarting OnStarting;
|
||||
/// Event raised when the dialogue finishes
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnDialogueFinished OnFinished;
|
||||
protected:
|
||||
UPROPERTY()
|
||||
TObjectPtr<const USUDSScript> BaseScript;
|
||||
UPROPERTY()
|
||||
TObjectPtr<USUDSScriptNodeText> CurrentSpeakerNode;
|
||||
UPROPERTY()
|
||||
TObjectPtr<const USUDSScriptNode> CurrentRootChoiceNode;
|
||||
|
||||
/// External objects which want to closely participate in the dialogue (not just listen to events)
|
||||
UPROPERTY()
|
||||
TArray<TObjectPtr<UObject>> Participants;
|
||||
|
||||
|
||||
/// All of the dialogue variables
|
||||
/// Dialogue variable state is all held locally. Dialogue participants can retrieve or set values in state.
|
||||
/// All state is saved with the dialogue. Variables can be used as text substitution parameters, conditionals,
|
||||
/// or communication with external state.
|
||||
typedef TMap<FName, FSUDSValue> FSUDSValueMap;
|
||||
FSUDSValueMap VariableState;
|
||||
|
||||
/// Stack of Gosub nodes to return to
|
||||
UPROPERTY()
|
||||
TArray<TObjectPtr<USUDSScriptNodeGosub>> GosubReturnStack;
|
||||
|
||||
/// Set of all the TextIDs of choices taken already in this dialogue
|
||||
TSet<FString> ChoicesTaken;
|
||||
|
||||
TSet<FName> CurrentRequestedParamNames;
|
||||
bool bParamNamesExtracted;
|
||||
|
||||
/// Cached derived info
|
||||
mutable FText CurrentSpeakerDisplayName;
|
||||
/// All valid choices
|
||||
TArray<FSUDSScriptEdge> CurrentChoices;
|
||||
int CurrentSourceLineNo;
|
||||
static const FText DummyText;
|
||||
static const FString DummyString;
|
||||
|
||||
void InitVariables();
|
||||
void RunUntilNextSpeakerNodeOrEnd(USUDSScriptNode* FromNode, bool bRaiseAtEnd);
|
||||
const USUDSScriptNode* WalkToNextChoiceNode(USUDSScriptNode* FromNode, bool bExecute);
|
||||
USUDSScriptNode* RecurseWalkToNextChoiceOrTextNode(USUDSScriptNode* Node, bool bExecute, TArray<TObjectPtr<USUDSScriptNodeGosub>>& LocalGosubStack);
|
||||
const USUDSScriptNode* RunUntilNextChoiceNode(USUDSScriptNode* FromTextNode);
|
||||
const USUDSScriptNode* FindNextChoiceNode(USUDSScriptNode* FromNode);
|
||||
void SetCurrentSpeakerNode(USUDSScriptNodeText* Node, bool bQuietly);
|
||||
void SortParticipants();
|
||||
void RaiseStarting(FName StartLabel);
|
||||
void RaiseFinished();
|
||||
void RaiseNewSpeakerLine();
|
||||
void RaiseChoiceMade(int Index, int LineNo);
|
||||
void RaiseProceeding();
|
||||
void RaiseVariableChange(const FName& VarName, const FSUDSValue& Value, bool bFromScript, int LineNo);
|
||||
void RaiseVariableRequested(const FName& VarName, int LineNo);
|
||||
void RaiseExpressionVariablesRequested(const FSUDSExpression& Expression, int LineNo);
|
||||
const TMap<FName, FSUDSValue>& GetGlobalVariables() const;
|
||||
|
||||
USUDSScriptNode* GetNextNode(USUDSScriptNode* Node);
|
||||
bool IsChoiceOrTextNode(ESUDSScriptNodeType Type);
|
||||
USUDSScriptNode* RunNode(USUDSScriptNode* Node);
|
||||
USUDSScriptNode* RunSelectNode(USUDSScriptNode* Node);
|
||||
USUDSScriptNode* RunSetVariableNode(USUDSScriptNode* Node);
|
||||
USUDSScriptNode* RunEventNode(USUDSScriptNode* Node);
|
||||
USUDSScriptNode* RunGosubNode(USUDSScriptNode* Node);
|
||||
USUDSScriptNode* RunReturnNode(USUDSScriptNode* Node);
|
||||
void UpdateChoices();
|
||||
void RecurseAppendChoices(const USUDSScriptNode* Node, TArray<FSUDSScriptEdge>& OutChoices);
|
||||
USoundBase* GetSoundForCurrentLine(bool bAllowAnyTarget) const;
|
||||
UDialogueVoice* GetTargetVoice() const;
|
||||
class USoundConcurrency* GetVoiceSoundConcurrency() const;
|
||||
|
||||
FText ResolveParameterisedText(const TArray<FName> Params, const FTextFormat& TextFormat, int LineNo);
|
||||
void GetTextFormatArgs(const TArray<FName>& ArgNames, FFormatNamedArguments& OutArgs) const;
|
||||
bool CurrentNodeHasChoices() const;
|
||||
void SetVariableImpl(FName Name, const FSUDSValue& Value, bool bFromScript, int LineNo)
|
||||
{
|
||||
const FSUDSValue OldValue = GetVariable(Name);
|
||||
if (!IsVariableSet(Name) ||
|
||||
(OldValue != Value).GetBooleanValue())
|
||||
{
|
||||
VariableState.Add(Name, Value);
|
||||
RaiseVariableChange(Name, Value, bFromScript, LineNo);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public:
|
||||
USUDSDialogue();
|
||||
// virtual ~USUDSDialogue() override
|
||||
// {
|
||||
// UE_LOG(LogTemp, Warning, TEXT("*********** Destroyed Dialogue!"));
|
||||
// }
|
||||
void Initialise(const USUDSScript* Script);
|
||||
|
||||
/// Get the script asset this dialogue is based on
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
const USUDSScript* GetScript() const { return BaseScript; }
|
||||
|
||||
/**
|
||||
* Begin the dialogue. Make sure you've added all participants before calling this.
|
||||
* This may not be the first time you've started this dialogue. All previous state is maintained to enable you
|
||||
* for example to take branching paths based on whether you've spoken to this character before.
|
||||
* If you want to reset *all* state, call Restart(true). However this is an extreme case; if you want to just
|
||||
* reset some variables then use the header section of the script to set variables to a default starting point.
|
||||
* @param Label The start point for this dialogue. If None, starts from the beginning.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void Start(FName Label = NAME_None);
|
||||
|
||||
|
||||
/**
|
||||
* Add a participant to this dialogue instance.
|
||||
* Participants are objects which want to be more closely involved in the dialogue. As opposed to event listeners,
|
||||
* participants get advance notice of events in the dialogue, and are also called in a known order, determined by
|
||||
* their priority. If you're providing variables to the dialogue, it is best to do it as a participant since it
|
||||
* gives you much more control.
|
||||
* @param Participant The participant object, which must implement ISUDSParticipant
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void AddParticipant(UObject* Participant);
|
||||
|
||||
/// Retrieve participants from this dialogue
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
const TArray<UObject*>& GetParticipants() const { return ObjectPtrDecay(Participants); }
|
||||
|
||||
/**
|
||||
* Set the complete list of participants for this dialogue instance.
|
||||
* Participants are objects which want to be more closely involved in the dialogue. As opposed to event listeners,
|
||||
* participants get advance notice of events in the dialogue, and are also called in a known order, determined by
|
||||
* their priority. If you're providing variables to the dialogue, it is best to do it as a participant since it
|
||||
* gives you much more control.
|
||||
* @param NewParticipants List of new participants. Each should implement ISUDSParticipant
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetParticipants(const TArray<UObject*>& NewParticipants);
|
||||
|
||||
|
||||
/// Get the speech text for the current dialogue node
|
||||
/// Any parameters required will be requested from participants in the dialogue and replaced
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
FText GetText();
|
||||
|
||||
/// Get the DialogueWave associated with the current dialogue node
|
||||
/// Returns null if there is no wave for this line.
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
UDialogueWave* GetWave() const;
|
||||
|
||||
/// Return whether the current dialogue node has a Dialogue Wave associated with it
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
bool IsCurrentLineVoiced() const;
|
||||
|
||||
/// Get the ID of the current speaker
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
const FString& GetSpeakerID() const;
|
||||
|
||||
/// Get the display name of the current speaker
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
FText GetSpeakerDisplayName() const;
|
||||
|
||||
/// Get the Dialogue Voice belonging to the current speaker, if voiced (Null otherwise)
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
UDialogueVoice* GetSpeakerVoice() const;
|
||||
|
||||
/// Get the Dialogue Voice belonging to the named participant, if voiced (Null otherwise)
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
UDialogueVoice* GetVoice(FString Name) const;
|
||||
|
||||
/** If the current line is voiced, plays it in 2D.
|
||||
* @param VolumeMultiplier A linear scalar multiplied with the volume, in order to make the sound louder or softer.
|
||||
* @param PitchMultiplier A linear scalar multiplied with the pitch.
|
||||
* @param bLooselyMatchTarget When finding the sound, don't require the target DialogueVoice to match precisely (recommended)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue", meta=(AdvancedDisplay = "2", UnsafeDuringActorConstruction = "true", Keywords = "play"))
|
||||
void PlayVoicedLine2D(float VolumeMultiplier = 1.f, float PitchMultiplier = 1.f, bool bLooselyMatchTarget = true);
|
||||
|
||||
/** If the current line is voiced, plays it at the given location.
|
||||
* @param Location World position to play dialogue at
|
||||
* @param Rotation World rotation to play dialogue at
|
||||
* @param VolumeMultiplier A linear scalar multiplied with the volume, in order to make the sound louder or softer.
|
||||
* @param PitchMultiplier A linear scalar multiplied with the pitch.
|
||||
* @param AttenuationSettings Override attenuation settings package to play sound with
|
||||
* @param bLooselyMatchTarget When finding the sound, don't require the target DialogueVoice to match precisely (recommended)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue", meta=(AdvancedDisplay = "4", UnsafeDuringActorConstruction = "true", Keywords = "play"))
|
||||
void PlayVoicedLineAtLocation(FVector Location,
|
||||
FRotator Rotation,
|
||||
float VolumeMultiplier = 1.f,
|
||||
float PitchMultiplier = 1.f,
|
||||
USoundAttenuation* AttenuationSettings = nullptr,
|
||||
bool bLooselyMatchTarget = true);
|
||||
|
||||
/** If the current line is voiced, spawn a sound for it in 2D. Use this if you want to control the sound while it's playing.
|
||||
* @param VolumeMultiplier A linear scalar multiplied with the volume, in order to make the sound louder or softer.
|
||||
* @param PitchMultiplier A linear scalar multiplied with the pitch.
|
||||
* @param bLooselyMatchTarget When finding the sound, don't require the target DialogueVoice to match precisely (recommended)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue", meta=(AdvancedDisplay = "2", UnsafeDuringActorConstruction = "true", Keywords = "play"))
|
||||
UAudioComponent* SpawnVoicedLine2D(float VolumeMultiplier = 1.f, float PitchMultiplier = 1.f, bool bLooselyMatchTarget = true);
|
||||
|
||||
/** If the current line is voiced, spawn a sound for it at the given location. Unlike PlayVoicedLineAtLocation you can
|
||||
* attach this sound to a moving object if you want
|
||||
* @param Location World position to play dialogue at
|
||||
* @param Rotation World rotation to play dialogue at
|
||||
* @param VolumeMultiplier A linear scalar multiplied with the volume, in order to make the sound louder or softer.
|
||||
* @param PitchMultiplier A linear scalar multiplied with the pitch.
|
||||
* @param AttenuationSettings Override attenuation settings package to play sound with
|
||||
* @param bLooselyMatchTarget When finding the sound, don't require the target DialogueVoice to match precisely (recommended)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue", meta=(AdvancedDisplay = "4", UnsafeDuringActorConstruction = "true", Keywords = "play"))
|
||||
UAudioComponent* SpawnVoicedLineAtLocation(FVector Location,
|
||||
FRotator Rotation,
|
||||
float VolumeMultiplier = 1.f,
|
||||
float PitchMultiplier = 1.f,
|
||||
USoundAttenuation* AttenuationSettings = nullptr,
|
||||
bool bLooselyMatchTarget = true);
|
||||
|
||||
/** If the current line is voiced, get the sound which would be played for it.
|
||||
* @param bLooselyMatchTarget When finding the sound, don't require the target DialogueVoice to match precisely (recommended)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue", meta=(AdvancedDisplay = "4", UnsafeDuringActorConstruction = "true", Keywords = "play"))
|
||||
USoundBase* GetVoicedLineSound(bool bLooselyMatchTarget = true);
|
||||
|
||||
/**
|
||||
* Get the number of choices available from this node.
|
||||
* Note, this will return 1 in the case of just linear text progression. The difference between just linked text
|
||||
* lines and a choice with only 1 option is whether the choice text is blank or not.
|
||||
* See also IsSimpleContinue()
|
||||
* @return The number of choices available
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
int GetNumberOfChoices() const;
|
||||
|
||||
/**
|
||||
* Return whether to progress from here is a simple continue (no choices, no text), meaning you probably want
|
||||
* to display a simpler prompt to the player.
|
||||
* This will return false even if there's only one choice, if that choice has text associated with it.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
bool IsSimpleContinue() const;
|
||||
|
||||
/**
|
||||
* Get the text associated with a choice.
|
||||
* @param Index The index of the choice
|
||||
* @return The text. This may be blank if this represents just a link between 2 nodes and not a choice at all.
|
||||
* Note that if you want to have only 1 choice but with associated text, this is fine and should be a choice
|
||||
* line just like any other.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
FText GetChoiceText(int Index);
|
||||
|
||||
/// Get all the current choices available, if you prefer this format
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
const TArray<FSUDSScriptEdge>& GetChoices() const;
|
||||
|
||||
/** Returns whether the choice at the given index has been taken previously.
|
||||
* This is saved in dialogue state so will be remembered across save/restore.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
bool HasChoiceIndexBeenTakenPreviously(int Index);
|
||||
|
||||
/** Returns whether a choice has been taken previously.
|
||||
* This is saved in dialogue state so will be remembered across save/restore.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
bool HasChoiceBeenTakenPreviously(const FSUDSScriptEdge& Choice);
|
||||
|
||||
|
||||
/**
|
||||
* Continues the dialogue if (and ONLY if) there is only one valid path/choice out of the current node.
|
||||
* @return True if the dialogue continues after this, false if the dialogue is now at an end.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
bool Continue();
|
||||
|
||||
/**
|
||||
* Picks one of the available choices
|
||||
* If there's only 1 you can still call this with Index = 0, but also see Continue
|
||||
* @param Index The index of the choice to make
|
||||
* @return True if the dialogue continues, false if it has now reached the end.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
bool Choose(int Index);
|
||||
|
||||
/// Returns true if the dialogue has reached the end
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
bool IsEnded() const;
|
||||
|
||||
/// Returns whether the current speaker line is the last line of dialogue, i.e. there are no
|
||||
/// further choices and the next continue will end the dialogue. This allows you to anticipate
|
||||
/// the end of dialogue (IsEnded() will still return false until the last continue is taken)
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
bool IsFinalLine() const;
|
||||
|
||||
|
||||
/// End the dialogue early
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void End(bool bQuietly);
|
||||
|
||||
/// Get the source line number of the current position of the dialogue (returns 0 if not applicable)
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
int GetCurrentSourceLine() const;
|
||||
|
||||
|
||||
/**
|
||||
* Restart the dialogue, either from the start or from a named label.
|
||||
* @param bResetState Whether to reset ALL dialogue state, as if the dialogue had been created anew. You mostly don't want
|
||||
* to do this; if you have certain things you want to reset every time, then use [set] commands in the header section
|
||||
* which runs every time the dialogue starts.
|
||||
* @param StartLabel Label to start running from; if None start from the beginning.
|
||||
* @param bReRunHeader If true (default), re-runs the header nodes before starting. Header nodes let you initialise
|
||||
* state that should always be reset when the dialogue is restarted
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void Restart(bool bResetState = false, FName StartLabel = NAME_None, bool bReRunHeader = true);
|
||||
|
||||
/**
|
||||
* Reset the state of this dialogue.
|
||||
* @param bResetVariables If true, resets all variable state
|
||||
* @param bResetPosition If true, resets the current position in the dialogue (which speaker line is next)
|
||||
* @param bResetVisited If true, resets the memory of which choices have been made
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void ResetState(bool bResetVariables = true, bool bResetPosition = true, bool bResetVisited = true);
|
||||
|
||||
/** Retrieve a copy of the state of this dialogue.
|
||||
* This is useful for saving the state of this dialogue.
|
||||
* @return A static copy of the current state of this dialogue. This struct can be serialised with your save data,
|
||||
* and contains both the state of variables and the current speaking node ID.
|
||||
* @note If you save/load mid-dialogue then you're need to have written Text ID's into the source text to ensure they
|
||||
* stay the same between edits, as you do for localisation. If you only save/load after dialogue has ended then
|
||||
* you don't need to worry about this since the dialogue will always start from the beginning
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
FSUDSDialogueState GetSavedState() const;
|
||||
|
||||
/** Restore the saved state of this dialogue.
|
||||
* This is useful for restoring the state of this dialogue. It will attempt to restore both the value of variables,
|
||||
* and the current speaking node in the dialogue. If you expect to be able to restore to a point mid-dialogue,
|
||||
* it's important that Text IDs are defined in your source file (as for localisation) since that's used as the
|
||||
* identifier of the current speaking node. If you only save/load after dialogue has ended then you don't need
|
||||
* to worry about this as dialogue will restart each time.
|
||||
* @param State Dialogue state that you previously retrieved from GetSavedState().
|
||||
* @note After restoring, you'll want to either call Start() or Continue(), depending on whether you restored
|
||||
* mid-dialogue or not (see IsEnded() to tell whether you did)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void RestoreSavedState(const FSUDSDialogueState& State);
|
||||
|
||||
/// Get the set of text parameters that are actually being asked for in the current state of the dialogue.
|
||||
/// This will include parameters in the text, and parameters in any current choices being displayed.
|
||||
/// Use this if you want to be more specific about what parameters you supply when ISUDSParticipant::UpdateDialogueParameters
|
||||
/// is called.
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
TSet<FName> GetParametersInUse();
|
||||
|
||||
|
||||
/// Set a variable in dialogue state
|
||||
/// This is mostly only useful if you happen to already have a general purpose FSUDSValue.
|
||||
/// See SetVariableText, SetVariableInt etc for literal-friendly versions
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetVariable(FName Name, FSUDSValue Value)
|
||||
{
|
||||
SetVariableImpl(Name, Value, false, 0);
|
||||
}
|
||||
|
||||
/// Get a variable in dialogue state as a general value type
|
||||
/// See GetDialogueText, GetDialogueInt etc for more type friendly versions, but if you want to access the state
|
||||
/// as a type-flexible value then you can do so with this function.
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
FSUDSValue GetVariable(FName Name) const
|
||||
{
|
||||
if (const auto Arg = VariableState.Find(Name))
|
||||
{
|
||||
return *Arg;
|
||||
}
|
||||
return FSUDSValue();
|
||||
}
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
bool IsVariableSet(FName Name) const
|
||||
{
|
||||
return VariableState.Contains(Name);
|
||||
}
|
||||
|
||||
/// Get all variables
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
const TMap<FName, FSUDSValue>& GetVariables() const { return VariableState; }
|
||||
|
||||
/**
|
||||
* Set a text dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetVariableText(FName Name, FText Value)
|
||||
{
|
||||
SetVariable(Name, Value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a text dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
FText GetVariableText(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a dialogue variable on the passed in parameters collection.
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetVariableInt(FName Name, int32 Value);
|
||||
|
||||
/**
|
||||
* Get an int dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
int GetVariableInt(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a float dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetVariableFloat(FName Name, float Value);
|
||||
|
||||
/**
|
||||
* Get a float dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
float GetVariableFloat(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a gender dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetVariableGender(FName Name, ETextGender Value);
|
||||
|
||||
/**
|
||||
* Get a gender dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
ETextGender GetVariableGender(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a boolean dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetVariableBoolean(FName Name, bool Value);
|
||||
|
||||
/**
|
||||
* Get a boolean dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
bool GetVariableBoolean(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a name dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetVariableName(FName Name, FName Value);
|
||||
|
||||
/**
|
||||
* Get a name dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
FName GetVariableName(FName Name) const;
|
||||
|
||||
|
||||
/**
|
||||
* Remove the definition of a variable.
|
||||
* This has much same effect as setting the variable back to the default value for this type, since attempting to
|
||||
* retrieve a missing variable result in a default value.
|
||||
* @param Name The name of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void UnSetVariable(FName Name);
|
||||
|
||||
/**
|
||||
* Get a piece of user-specified metadata for the current speaker line.
|
||||
* User metadata is assigned by adding a special comment before a speaker line, and can be used
|
||||
* for anything where you need additional data associated with a particular line (as opposed to
|
||||
* setting a variable or sending an event).
|
||||
* @param Key The metadata key
|
||||
* @return The current metadata value
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
FSUDSValue GetSpeakerLineUserMetadata(FName Key) const;
|
||||
|
||||
/**
|
||||
* Get all user-specified metadata for the current speaker line.
|
||||
* User metadata is assigned by adding a special comment before a speaker line, and can be used
|
||||
* for anything where you need additional data associated with a particular line (as opposed to
|
||||
* setting a variable or sending an event).
|
||||
* @return All key/value user metadata for the current speaker line
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
TMap<FName, FSUDSValue> GetAllSpeakerLineUserMetadata() const;
|
||||
|
||||
/**
|
||||
* Get a piece of user-specified metadata for a choice in the current list of choices.
|
||||
* User metadata is assigned by adding a special comment before a choice line, and can be used
|
||||
* for anything where you need additional data associated with a particular choice, such as RPG
|
||||
* stat requirements.
|
||||
* @param Index The choice index
|
||||
* @param Key The metadata key
|
||||
* @return The current metadata value
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
FSUDSValue GetChoiceUserMetadata(int Index, FName Key) const;
|
||||
|
||||
/**
|
||||
* Get all user-specified metadata for a choice in the current list of choices.
|
||||
* User metadata is assigned by adding a special comment before a choice line, and can be used
|
||||
* for anything where you need additional data associated with a particular choice, such as RPG
|
||||
* stat requirements.
|
||||
* @param Index The choice index
|
||||
* @return All key/value user metadata for the choice
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
TMap<FName, FSUDSValue> GetAllChoiceUserMetadata(int Index) const;
|
||||
|
||||
#if WITH_EDITOR
|
||||
FOnDialogueSpeakerLineInternal InternalOnSpeakerLine;
|
||||
FOnDialogueChoiceInternal InternalOnChoice;
|
||||
FOnDialogueProceedingInternal InternalOnProceeding;
|
||||
FOnDialogueEventInternal InternalOnEvent;
|
||||
FOnDialogueVarChangedByScriptInternal InternalOnSetVar;
|
||||
FOnDialogueVarChangedByCodeInternal InternalOnSetVarByCode;
|
||||
FOnDialogueSelectEval InternalOnSelectEval;
|
||||
FOnDialogueStartingInternal InternalOnStarting;
|
||||
FOnDialogueFinishedInternal InternalOnFinished;
|
||||
#endif
|
||||
};
|
||||
234
Plugins/SUDS/Source/SUDS/Public/SUDSExpression.h
Normal file
234
Plugins/SUDS/Source/SUDS/Public/SUDSExpression.h
Normal file
@@ -0,0 +1,234 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
#include "SUDSValue.h"
|
||||
#include "SUDSExpression.generated.h"
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class ESUDSExpressionItemType : uint8
|
||||
{
|
||||
Null = 0 UMETA(Hidden),
|
||||
// Operators (must be 0-127, in order of precedence, highest first - gaps left in case we need them)
|
||||
Not = 4,
|
||||
Multiply = 10,
|
||||
Divide = 11,
|
||||
Modulo = 12,
|
||||
Add = 20,
|
||||
Subtract = 21,
|
||||
Less = 30,
|
||||
LessEqual = 31,
|
||||
Greater = 32,
|
||||
GreaterEqual = 33,
|
||||
Equal = 34,
|
||||
NotEqual = 35,
|
||||
And = 40,
|
||||
Or = 41,
|
||||
|
||||
LParens = 100,
|
||||
RParens = 101,
|
||||
|
||||
// Operands (must be 128+)
|
||||
Operand = 128
|
||||
|
||||
};
|
||||
|
||||
/// An item in an expression queue, can be operator or operand
|
||||
USTRUCT(BlueprintType)
|
||||
struct SUDS_API FSUDSExpressionItem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS|Expression")
|
||||
ESUDSExpressionItemType Type;
|
||||
|
||||
// Value if an operand node
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS|Expression")
|
||||
FSUDSValue OperandValue;
|
||||
|
||||
public:
|
||||
|
||||
FSUDSExpressionItem() : Type(ESUDSExpressionItemType::Operand) {}
|
||||
FSUDSExpressionItem(ESUDSExpressionItemType Operator) : Type(Operator) {}
|
||||
|
||||
FSUDSExpressionItem(const FSUDSValue& LiteralOrVariable)
|
||||
: Type(ESUDSExpressionItemType::Operand),
|
||||
OperandValue(LiteralOrVariable)
|
||||
{
|
||||
}
|
||||
|
||||
ESUDSExpressionItemType GetType() const { return Type; }
|
||||
// Only valid if optype is operand
|
||||
const FSUDSValue& GetOperandValue() const { return OperandValue; }
|
||||
void SetOperandValue(const FSUDSValue& NewVal) { OperandValue = NewVal; }
|
||||
|
||||
bool IsOperator() const { return static_cast<uint8>(Type) < 128; }
|
||||
bool IsOperand() const { return !IsOperator(); }
|
||||
bool IsBinaryOperator() const
|
||||
{
|
||||
// Only not is unary right now
|
||||
return Type != ESUDSExpressionItemType::Not;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/// An expression holds an executable expression, whether it's a simple single literal
|
||||
/// or a compound expression with variables
|
||||
USTRUCT(BlueprintType)
|
||||
struct SUDS_API FSUDSExpression
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
// The output queue in Reverse Polish Notation order
|
||||
UPROPERTY()
|
||||
TArray<FSUDSExpressionItem> Queue;
|
||||
|
||||
/// Whether the tree is valid to execute
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS|Expression")
|
||||
bool bIsValid;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<FName> VariableNames;
|
||||
|
||||
|
||||
/// The original string version of the expression, for reference
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS|Expression")
|
||||
FString SourceString;
|
||||
|
||||
FSUDSExpressionItem EvaluateOperator(ESUDSExpressionItemType Op,
|
||||
const FSUDSExpressionItem& Arg1,
|
||||
const FSUDSExpressionItem& Arg2,
|
||||
const TMap<FName, FSUDSValue>& Variables,
|
||||
const TMap<FName, FSUDSValue>& GlobalVariables) const;
|
||||
FSUDSValue EvaluateOperand(const FSUDSValue& Operand, const TMap<FName, FSUDSValue>& Variables, const TMap<FName, FSUDSValue>& GlobalVariables) const;
|
||||
|
||||
bool Validate();
|
||||
|
||||
public:
|
||||
|
||||
FSUDSExpression() : bIsValid(true) {}
|
||||
|
||||
/// Initialise an expression tree just with a single literal or variable
|
||||
FSUDSExpression(const FSUDSValue& LiteralOrVariable)
|
||||
{
|
||||
Queue.Add(FSUDSExpressionItem(LiteralOrVariable));
|
||||
if (LiteralOrVariable.IsVariable())
|
||||
VariableNames.Add(LiteralOrVariable.GetVariableNameValue());
|
||||
bIsValid = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to parse an expression from a string
|
||||
* @param Expression The string to parse
|
||||
* @param OutParseError If there are any errors, pointer to a string to complete with the details
|
||||
* @return Whether the parsing was successful
|
||||
*/
|
||||
bool ParseFromString(const FString& Expression, FString* OutParseError);
|
||||
|
||||
/// Reset the expression to return true
|
||||
void Reset();
|
||||
|
||||
|
||||
/// Evaluate the expression and return the result, using a given variable state
|
||||
FSUDSValue Evaluate(const TMap<FName, FSUDSValue>& Variables, const TMap<FName, FSUDSValue>& GlobalVariables) const;
|
||||
|
||||
/// Evaluate the expression and return the result as a boolean, using a given variable state
|
||||
bool EvaluateBoolean(const TMap<FName, FSUDSValue>& Variables, const TMap<FName, FSUDSValue>& GlobalVariables, const FString& ErrorContext) const;
|
||||
|
||||
/// Get the original source of the expression as a string
|
||||
const FString& GetSourceString() const { return SourceString; }
|
||||
|
||||
/// Whether this expression can be run (or is empty)
|
||||
bool IsValid() const { return bIsValid; }
|
||||
|
||||
/// Whether this expression is blank
|
||||
bool IsEmpty() const { return Queue.IsEmpty(); }
|
||||
|
||||
/// Get the list of variables this expression needs
|
||||
const TArray<FName>& GetVariableNames() const { return VariableNames; }
|
||||
|
||||
/// Return whether this expression is a generated random condition
|
||||
bool IsRandomCondition() const;
|
||||
|
||||
/**
|
||||
* Attempt to parse an operand from a string. Returns true if this string is a valid operand, which means a literal
|
||||
* (int, float, quoted string, boolean, gender), or a variable reference ({VariableName})
|
||||
* @param ValueStr The string to parse
|
||||
* @param OutVal The operand value which will be populated if successful
|
||||
* @return True if successful, false if not
|
||||
*/
|
||||
static bool ParseOperand(const FString& ValueStr, FSUDSValue& OutVal);
|
||||
|
||||
// Attempt to parse an operator from an incoming string
|
||||
static ESUDSExpressionItemType ParseOperator(const FString& OpStr);
|
||||
|
||||
/// Access the internal RPN execution queue
|
||||
const TArray<FSUDSExpressionItem>& GetQueue() { return Queue; }
|
||||
|
||||
/// Return whether this is a single literal
|
||||
bool IsLiteral() const
|
||||
{
|
||||
return bIsValid && Queue.Num() == 1 && Queue[0].IsOperand() && Queue[0].GetOperandValue().GetType() != ESUDSValueType::Variable;
|
||||
}
|
||||
|
||||
/// Helper method to get literal values
|
||||
FSUDSValue GetLiteralValue() const
|
||||
{
|
||||
check(IsLiteral());
|
||||
return Queue[0].GetOperandValue();
|
||||
}
|
||||
|
||||
/// Return whenter this is a text literal
|
||||
bool IsTextLiteral() const
|
||||
{
|
||||
return bIsValid && Queue.Num() == 1 && Queue[0].IsOperand() && Queue[0].GetOperandValue().GetType() == ESUDSValueType::Text;
|
||||
}
|
||||
|
||||
/// Helper method to get a text literal value, for easier localisation
|
||||
FText GetTextLiteralValue() const
|
||||
{
|
||||
check(IsTextLiteral());
|
||||
return GetLiteralValue().GetTextValue();
|
||||
}
|
||||
/// Helper method to override a text literal
|
||||
void SetTextLiteralValue(const FText& NewLiteral)
|
||||
{
|
||||
check(IsTextLiteral());
|
||||
Queue[0].SetOperandValue(NewLiteral);
|
||||
}
|
||||
|
||||
/// Helper method to get boolean literal value
|
||||
bool GetBooleanLiteralValue() const
|
||||
{
|
||||
check(IsLiteral() && GetLiteralValue().GetType() == ESUDSValueType::Boolean);
|
||||
return GetLiteralValue().GetBooleanValue();
|
||||
}
|
||||
/// Helper method to get int literal value
|
||||
int GetIntLiteralValue() const
|
||||
{
|
||||
check(IsLiteral() && GetLiteralValue().GetType() == ESUDSValueType::Int);
|
||||
return GetLiteralValue().GetIntValue();
|
||||
}
|
||||
/// Helper method to get float literal value
|
||||
float GetFloatLiteralValue() const
|
||||
{
|
||||
check(IsLiteral() && GetLiteralValue().GetType() == ESUDSValueType::Float);
|
||||
return GetLiteralValue().GetFloatValue();
|
||||
}
|
||||
/// Helper method to get gender literal value
|
||||
ETextGender GetGenderLiteralValue() const
|
||||
{
|
||||
check(IsLiteral() && GetLiteralValue().GetType() == ESUDSValueType::Gender);
|
||||
return GetLiteralValue().GetGenderValue();
|
||||
}
|
||||
/// Helper method to get name literal value
|
||||
FName GetNameLiteralValue() const
|
||||
{
|
||||
check(IsLiteral() && GetLiteralValue().GetType() == ESUDSValueType::Name);
|
||||
return GetLiteralValue().GetNameValue();
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
145
Plugins/SUDS/Source/SUDS/Public/SUDSLibrary.h
Normal file
145
Plugins/SUDS/Source/SUDS/Public/SUDSLibrary.h
Normal file
@@ -0,0 +1,145 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSValue.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "SUDSLibrary.generated.h"
|
||||
|
||||
class USUDSScript;
|
||||
class USUDSDialogue;
|
||||
UCLASS()
|
||||
class SUDS_API USUDSLibrary : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Create a dialogue instance based on a script, with no participants.
|
||||
* You should subsequently call "SetParticipants" or "AddParticipant" on the returned dialogue if you expect any
|
||||
* parameters or speaker names to work.
|
||||
* @param Owner The owner of this instance. Can be any object but determines the lifespan of this dialogue,
|
||||
* could make sense to make the owner the NPC you're talking to for example.
|
||||
* @param Script The script to base this dialogue on
|
||||
* @param bStartImmediately Whether to call Start() on the dialogue automatically before returning
|
||||
* @param StartLabel If set to start immediately, which label to start from (None means start from the beginning)
|
||||
* @return The dialogue instance.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static USUDSDialogue* CreateDialogue(UObject* Owner,
|
||||
USUDSScript* Script,
|
||||
bool bStartImmediately = false,
|
||||
FName StartLabel = NAME_None);
|
||||
|
||||
/**
|
||||
* Create a dialogue instance based on a script, with an initial set of participants.
|
||||
* @param Owner The owner of this instance. Can be any object but determines the lifespan of this dialogue,
|
||||
* could make sense to make the owner the NPC you're talking to for example.
|
||||
* @param Script The script to base this dialogue on
|
||||
* @param Participants List of participants, each of which must implement the ISUDSParticipant interface to be used.
|
||||
* Participants are objects that want to be closely involved in the dialogue to provide variables and receive all events.
|
||||
* Other objects can subscribe to events separately but do not have as much control.
|
||||
* @param bStartImmediately Whether to call Start() on the dialogue automatically before returning
|
||||
* @param StartLabel If set to start immediately, which label to start from (None means start from the beginning)
|
||||
* @return The dialogue instance.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static USUDSDialogue* CreateDialogueWithParticipants(UObject* Owner,
|
||||
USUDSScript* Script,
|
||||
const TArray<UObject*>& Participants,
|
||||
bool bStartImmediately = false,
|
||||
FName StartLabel = NAME_None);
|
||||
|
||||
|
||||
/**
|
||||
* Create a dialogue instance based on a script, with a single participants.
|
||||
* @param Owner The owner of this instance. Can be any object but determines the lifespan of this dialogue,
|
||||
* could make sense to make the owner the NPC you're talking to for example.
|
||||
* @param Script The script to base this dialogue on
|
||||
* @param Participant The participant, which must implement the ISUDSParticipant interface to be used.
|
||||
* Participants are objects that want to be closely involved in the dialogue to provide variables and receive all events.
|
||||
* Other objects can subscribe to events separately but do not have as much control.
|
||||
* @param bStartImmediately Whether to call Start() on the dialogue automatically before returning
|
||||
* @param StartLabel If set to start immediately, which label to start from (None means start from the beginning)
|
||||
* @return The dialogue instance.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static USUDSDialogue* CreateDialogueWithParticipant(UObject* Owner,
|
||||
USUDSScript* Script,
|
||||
UObject* Participant,
|
||||
bool bStartImmediately = false,
|
||||
FName StartLabel = NAME_None);
|
||||
|
||||
/**
|
||||
* Try to extract a text value from a general SUDS value.
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @param TextValue The text value
|
||||
* @return True if the value was of type text and extracted correctly. False if not.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static UPARAM(DisplayName="Success") bool GetDialogueValueAsText(const FSUDSValue& Value, FText& TextValue);
|
||||
/**
|
||||
* Try to extract a boolean value from a general SUDS value.
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @param BoolValue The boolean value
|
||||
* @return True if the value was of type boolean and extracted correctly. False if not.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static UPARAM(DisplayName="Success") bool GetDialogueValueAsBoolean(const FSUDSValue& Value, bool& BoolValue);
|
||||
/**
|
||||
* Try to extract an integer value from a general SUDS value.
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @param IntValue The integer value
|
||||
* @return True if the value was of type integer and extracted correctly. False if not.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static UPARAM(DisplayName="Success") bool GetDialogueValueAsInt(const FSUDSValue& Value, int& IntValue);
|
||||
/**
|
||||
* Try to extract a float value from a general SUDS value.
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @param FloatValue The float value
|
||||
* @return True if the value was of type float and extracted correctly. False if not.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static UPARAM(DisplayName="Success") bool GetDialogueValueAsFloat(const FSUDSValue& Value, float& FloatValue);
|
||||
/**
|
||||
* Try to extract a gender value from a general SUDS value.
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @param GenderValue The gender value
|
||||
* @return True if the value was of type gender and extracted correctly. False if not.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static UPARAM(DisplayName="Success") bool GetDialogueValueAsGender(const FSUDSValue& Value, ETextGender& GenderValue);
|
||||
/**
|
||||
* Try to extract a Name value from a general SUDS value.
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @param NameValue The Name value
|
||||
* @return True if the value was of type Name and extracted correctly. False if not.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static UPARAM(DisplayName="Success") bool GetDialogueValueAsName(const FSUDSValue& Value, FName& NameValue);
|
||||
|
||||
/** Retrieve the type of a SUDS value.
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @return The value type
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static ESUDSValueType GetDialogueValueType(const FSUDSValue& Value);
|
||||
|
||||
/** Determine whether a SUDS value is empty (uninitialised).
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @return Whether the value is empty
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static bool GetDialogueValueIsEmpty(const FSUDSValue& Value);
|
||||
|
||||
/**
|
||||
* Determine whether a SUDS variable name refers to a global variable
|
||||
* @param Name The full name of the variable
|
||||
* @param OutName The name of the variable, trimmed if necessary to remove a global prefix
|
||||
* @return Whether the variable was global
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static bool IsDialogueVariableGlobal(const FName& Name, UPARAM(ref) FName& OutName);
|
||||
};
|
||||
129
Plugins/SUDS/Source/SUDS/Public/SUDSParticipant.h
Normal file
129
Plugins/SUDS/Source/SUDS/Public/SUDSParticipant.h
Normal file
@@ -0,0 +1,129 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "SUDSValue.h"
|
||||
#include "SUDSParticipant.generated.h"
|
||||
|
||||
class USUDSDialogue;
|
||||
UINTERFACE(MinimalAPI)
|
||||
class USUDSParticipant : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface to be implemented by participant objects in a given dialogue.
|
||||
* A participant is simply any object which wants to be closely involved in supplying data to, or retrieving data from,
|
||||
* the dialogue. Although you could do this simply by subscribing to the delegate events on a dialogue, the advantage
|
||||
* of making a participant is that you have better control over the ordering of multiple participants, in case for example
|
||||
* there's some common variable that they both want to set.
|
||||
* It's also a clearer interface to look for vs ad-hoc delegate hooks.
|
||||
* Generally we recommend that:
|
||||
* - Objects providing data to the dialogue should implement ISUDSParticipant
|
||||
* - Anything that just wants to observe the dialogue (like a UI) should just listen to events
|
||||
*
|
||||
* Participants are *guaranteed* to be called earlier than delegates, which means you can set variables from the
|
||||
* Participant callback and when the UI delegate reads text back, all substitution variables will be up to date.
|
||||
*
|
||||
*/
|
||||
class SUDS_API ISUDSParticipant
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* Called when a dialogue involving this participant is starting.
|
||||
* The implementation should probably set any starting variables referenced by the dialogue here (or you can do that
|
||||
* later during other functions). At this point there is no active speaker line, we're bootstrapping.
|
||||
* @param Dialogue The dialogue
|
||||
* @param AtLabel The label that the dialogue has started at (None if starting at the beginning)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueStarting(USUDSDialogue* Dialogue, FName AtLabel);
|
||||
|
||||
/**
|
||||
* Called when a dialogue finishes.
|
||||
* @param Dialogue The dialogue
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueFinished(USUDSDialogue* Dialogue);
|
||||
|
||||
/**
|
||||
* Called when a new speaker line, potentially with attached choices, has become active in the dialogue.
|
||||
* This participant can provide any variable updates if it needs to at this point.
|
||||
* Participants will be called before any dialogue event listeners.
|
||||
* @param Dialogue The dialogue
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueSpeakerLine(USUDSDialogue* Dialogue);
|
||||
|
||||
/**
|
||||
* Called when a choice is made by the player.
|
||||
* At this point, the dialogue has not progressed as a result of that choice, so the index passed can be used to
|
||||
* reference the choice.
|
||||
* This event is ONLY raised if there's a choice of paths, not for just continuing a linear path.
|
||||
* See OnDialogueProceeding for a more general callback.
|
||||
* Participants will be called before any dialogue event listeners.
|
||||
* @param Dialogue The dialogue
|
||||
* @param ChoiceIndex The index of the choice that was made
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueChoiceMade(USUDSDialogue* Dialogue, int ChoiceIndex);
|
||||
|
||||
/**
|
||||
* Called just before proceeding with the dialogue from the current speaker line; just after either a choice is made by the player
|
||||
* or the dialogue is just prompted to proceed with its single path.
|
||||
* Participants will be called before any dialogue event listeners.
|
||||
* @param Dialogue The dialogue
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueProceeding(USUDSDialogue* Dialogue);
|
||||
|
||||
|
||||
/**
|
||||
* Called when an event is raised from dialogue
|
||||
* @param Dialogue The dialogue instance
|
||||
* @param EventName The name of the event that has been raised
|
||||
* @param Arguments
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueEvent(USUDSDialogue* Dialogue, FName EventName, const TArray<FSUDSValue>& Arguments);
|
||||
|
||||
/**
|
||||
* Called when a variable changes value in the dialogue
|
||||
* @param Dialogue The dialogue instance
|
||||
* @param VariableName The name of the variable which has changed value
|
||||
* @param Value The new value
|
||||
* @param bFromScript True if the value changed because of a script line, false if it changed because of code calling SetVariable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueVariableChanged(USUDSDialogue* Dialogue, FName VariableName, const FSUDSValue& Value, bool bFromScript);
|
||||
|
||||
/**
|
||||
* Called when a variable value is requested by the dialogue script.
|
||||
* While you can set variables on the dialogue at any time and they're persistent, you can implement this method to
|
||||
* provide on-demand variable values (call SetVariable on the dialogue) if you want. This hook is called just before
|
||||
* the variables are used.
|
||||
* @param Dialogue The dialogue instance
|
||||
* @param VariableName The name of the variable which has changed value
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueVariableRequested(USUDSDialogue* Dialogue, FName VariableName);
|
||||
|
||||
/**
|
||||
* Return the priority of this participant (default 0).
|
||||
* If for some reason you need to control the order multiple participants in a dialogue are called,
|
||||
* override this method; higher priority participants will be called *later* so that their variables etc override
|
||||
* previously set values.
|
||||
* @return Relative priority, default 0, higher numbers override lower ones.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
int GetDialogueParticipantPriority() const;
|
||||
|
||||
};
|
||||
|
||||
|
||||
114
Plugins/SUDS/Source/SUDS/Public/SUDSScript.h
Normal file
114
Plugins/SUDS/Source/SUDS/Public/SUDSScript.h
Normal file
@@ -0,0 +1,114 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Runtime/Launch/Resources/Version.h"
|
||||
#include "Sound/DialogueVoice.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "SUDSScript.generated.h"
|
||||
|
||||
class USUDSScriptNode;
|
||||
class USUDSScriptNodeText;
|
||||
class USUDSScriptNodeGosub;
|
||||
/**
|
||||
* A single SUDS script asset.
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class SUDS_API USUDSScript : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
|
||||
/// Array of nodes (static after import)
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
TArray<TObjectPtr<USUDSScriptNode>> Nodes;
|
||||
|
||||
/// Map of labels to nodes
|
||||
UPROPERTY(BlueprintReadOnly, VisibleDefaultsOnly, Category="SUDS")
|
||||
TMap<FName, int> LabelList;
|
||||
|
||||
// Header equivalents for startup
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
TArray<TObjectPtr<USUDSScriptNode>> HeaderNodes;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
TMap<FName, int> HeaderLabelList;
|
||||
|
||||
/// Array of all speaker IDs found in this script
|
||||
UPROPERTY(BlueprintReadOnly, VisibleDefaultsOnly, Category="SUDS")
|
||||
TArray<FString> Speakers;
|
||||
|
||||
/// When using VO, Dialogue Voice assets are associated with speaker IDs
|
||||
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category="SUDS")
|
||||
TMap<FString, TObjectPtr<UDialogueVoice>> SpeakerVoices;
|
||||
|
||||
bool DoesAnyPathAfterLeadToChoice(USUDSScriptNode* FromNode);
|
||||
int RecurseLookForChoice(USUDSScriptNode* CurrNode);
|
||||
|
||||
public:
|
||||
void StartImport(TArray<TObjectPtr<USUDSScriptNode>>** Nodes,
|
||||
TArray<TObjectPtr<USUDSScriptNode>>** HeaderNodes,
|
||||
TMap<FName, int>** LabelList,
|
||||
TMap<FName, int>** ppHeaderLabelList,
|
||||
TArray<FString>** SpeakerList);
|
||||
void FinishImport();
|
||||
|
||||
const TArray<USUDSScriptNode*>& GetNodes() const { return ObjectPtrDecay(Nodes); }
|
||||
const TArray<USUDSScriptNode*>& GetHeaderNodes() const { return ObjectPtrDecay(HeaderNodes); }
|
||||
const TMap<FName, int>& GetLabelList() const { return LabelList; }
|
||||
const TMap<FName, int>& GetHeaderLabelList() const { return HeaderLabelList; }
|
||||
|
||||
|
||||
/// Get the first header node, if any (header nodes are run every time the script starts)
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS")
|
||||
USUDSScriptNode* GetHeaderNode() const;
|
||||
|
||||
/// Get the first node of the script, if starting from the beginning
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS")
|
||||
USUDSScriptNode* GetFirstNode() const;
|
||||
|
||||
/// Get the next node after a given node, ONLY if there's only one way to go
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
USUDSScriptNode* GetNextNode(const USUDSScriptNode* Node) const;
|
||||
|
||||
/// Get the first node of the script following a label, or null if the label wasn't found
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
USUDSScriptNode* GetNodeByLabel(const FName& Label) const;
|
||||
|
||||
/// Try to find a speaker node by its text ID
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
USUDSScriptNodeText* GetNodeByTextID(const FString& TextID) const;
|
||||
/// Try to find a gosub node by its gosub ID
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
USUDSScriptNodeGosub* GetNodeByGosubID(const FString& ID) const;
|
||||
|
||||
|
||||
/// Get the list of speakers
|
||||
const TArray<FString>& GetSpeakers() const { return Speakers; }
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
UDialogueVoice* GetSpeakerVoice(const FString& SpeakerID) const;
|
||||
|
||||
/// Set up the speaker voice association
|
||||
void SetSpeakerVoice(const FString& SpeakerID, UDialogueVoice* Voice);
|
||||
const TMap<FString, UDialogueVoice*>& GetSpeakerVoices() const { return ObjectPtrDecay(SpeakerVoices); }
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
// Import data for this
|
||||
UPROPERTY(VisibleAnywhere, Instanced, Category=ImportSettings)
|
||||
TObjectPtr<class UAssetImportData> AssetImportData;
|
||||
|
||||
// UObject interface
|
||||
virtual void PostInitProperties() override;
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4
|
||||
virtual void GetAssetRegistryTags(FAssetRegistryTagsContext Context) const override;
|
||||
#else
|
||||
virtual void GetAssetRegistryTags(TArray<FAssetRegistryTag>& OutTags) const override;
|
||||
#endif
|
||||
virtual void Serialize(FArchive& Ar) override;
|
||||
// End of UObject interface
|
||||
#endif
|
||||
|
||||
};
|
||||
88
Plugins/SUDS/Source/SUDS/Public/SUDSScriptEdge.h
Normal file
88
Plugins/SUDS/Source/SUDS/Public/SUDSScriptEdge.h
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSExpression.h"
|
||||
#include "SUDSScriptEdge.generated.h"
|
||||
|
||||
class USUDSScriptNode;
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class ESUDSEdgeType : uint8
|
||||
{
|
||||
/// A simple continuation; usually for sequences with no choices
|
||||
Continue,
|
||||
/// A decision made by the player from a list of choices
|
||||
Decision,
|
||||
/// A conditional path, taken automatically based on the situation
|
||||
Condition,
|
||||
/// An edge which forms a chain of nodes which are supposed to be considered together
|
||||
/// This links a text node to its choices, and also potentially compound choices underneath if there are selects
|
||||
Chained
|
||||
|
||||
};
|
||||
/**
|
||||
* Edge in the script graph. An edge leads to another node (unidirectional)
|
||||
* Edges can have conditions which mean whether they're valid or not, either as automatic
|
||||
* choices or player choices.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct SUDS_API FSUDSScriptEdge
|
||||
{
|
||||
GENERATED_BODY()
|
||||
protected:
|
||||
// Text, if a user choice. Always references a string table
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
FText Text;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
ESUDSEdgeType Type;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
TWeakObjectPtr<USUDSScriptNode> TargetNode;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
FSUDSExpression Condition;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
int SourceLineNo;
|
||||
|
||||
/// User metadata associated with this edge. May include derived expressions
|
||||
UPROPERTY()
|
||||
TMap<FName, FSUDSExpression> UserMetadata;
|
||||
|
||||
|
||||
mutable bool bFormatExtracted = false;
|
||||
mutable TArray<FName> ParameterNames;
|
||||
mutable FTextFormat TextFormat;
|
||||
|
||||
void ExtractFormat() const;
|
||||
|
||||
public:
|
||||
FSUDSScriptEdge(): Type(ESUDSEdgeType::Continue), SourceLineNo(0)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSScriptEdge(USUDSScriptNode* ToNode, ESUDSEdgeType InType, int LineNo);
|
||||
|
||||
FSUDSScriptEdge(const FText& InText, USUDSScriptNode* ToNode, int LineNo);
|
||||
|
||||
FText GetText() const { return Text; }
|
||||
FString GetTextID() const;
|
||||
ESUDSEdgeType GetType() const { return Type; }
|
||||
TWeakObjectPtr<USUDSScriptNode> GetTargetNode() const { return TargetNode; }
|
||||
const FSUDSExpression& GetCondition() const { return Condition; }
|
||||
int GetSourceLineNo() const { return SourceLineNo; }
|
||||
const TMap<FName, FSUDSExpression>& GetUserMetadata() const { return UserMetadata; }
|
||||
|
||||
void SetText(const FText& Text);
|
||||
void SetType(ESUDSEdgeType InType) { Type = InType; }
|
||||
void SetTargetNode(const TWeakObjectPtr<USUDSScriptNode>& InTargetNode);
|
||||
void SetCondition(const FSUDSExpression& InCondition) { Condition = InCondition; }
|
||||
void SetUserMetadata(const TMap<FName, FSUDSExpression>& Meta) { UserMetadata = Meta; }
|
||||
|
||||
const FTextFormat& GetTextFormat() const;
|
||||
const TArray<FName>& GetParameterNames() const;
|
||||
bool HasParameters() const;
|
||||
};
|
||||
81
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNode.h
Normal file
81
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNode.h
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSScriptEdge.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "SUDSScriptNode.generated.h"
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class ESUDSScriptNodeType : uint8
|
||||
{
|
||||
/// Text node, displaying a line of dialogue
|
||||
Text,
|
||||
/// Choice node, displaying a series of user choices which navigate to other nodes
|
||||
Choice,
|
||||
/// Select node, automatically selecting one which navigates to another node based on state
|
||||
Select,
|
||||
/// Set variable node
|
||||
SetVariable,
|
||||
/// Event node
|
||||
Event,
|
||||
/// Gosub node
|
||||
Gosub,
|
||||
/// Return node
|
||||
Return,
|
||||
};
|
||||
/**
|
||||
* A node in the script graph.
|
||||
* Nodes are either text, or branch points (user choice or automatic branching logic)
|
||||
* Text nodes always lead to a single next step, be that another text node or a branch.
|
||||
* Branch nodes are separate from the text so that jumping can return to a choice point without emitting more text.
|
||||
* At runtime if a text node's next step is a user choice, it will be available immediately. Otherwise it's not
|
||||
* evaluated until the dialogue progresses.
|
||||
* Edges connect everything. Edges may have text if they're user choices (even if that's a single choice), and
|
||||
* may have conditions.
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class SUDS_API USUDSScriptNode : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
|
||||
/// Type of node
|
||||
/// To make it easier to check rather than having to cast to subtypes blindly. And also not all types need a subtype
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
ESUDSScriptNodeType NodeType = ESUDSScriptNodeType::Text;
|
||||
/// Links to other nodes
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
TArray<FSUDSScriptEdge> Edges;
|
||||
/// The line number in the script that this node came from
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
int SourceLineNo;
|
||||
|
||||
|
||||
public:
|
||||
USUDSScriptNode();
|
||||
|
||||
ESUDSScriptNodeType GetNodeType() const { return NodeType; }
|
||||
const TArray<FSUDSScriptEdge>& GetEdges() const { return Edges; }
|
||||
int GetSourceLineNo() const { return SourceLineNo; }
|
||||
|
||||
void AddEdge(const FSUDSScriptEdge& NewEdge);
|
||||
void InitChoice(int LineNo);
|
||||
void InitSelect(int LineNo);
|
||||
void InitReturn(int LineNo);
|
||||
|
||||
int GetEdgeCount() const { return Edges.Num(); }
|
||||
const FSUDSScriptEdge* GetEdge(int Index) const
|
||||
{
|
||||
if (Edges.IsValidIndex(Index))
|
||||
{
|
||||
return &Edges[Index];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/// Determine if this node is a Select node that's representing a [random]
|
||||
bool IsRandomSelect() const;
|
||||
};
|
||||
33
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeEvent.h
Normal file
33
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeEvent.h
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSExpression.h"
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSScriptNodeEvent.generated.h"
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class SUDS_API USUDSScriptNodeEvent : public USUDSScriptNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
protected:
|
||||
// Variable identifier
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
FName EventName;
|
||||
|
||||
/// Literal arguments
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
TArray<FSUDSExpression> Args;
|
||||
|
||||
public:
|
||||
|
||||
void Init(const FString& EvtName, const TArray<FSUDSExpression>& InArgs, int LineNo);
|
||||
FName GetEventName() const { return EventName; }
|
||||
const TArray<FSUDSExpression>& GetArgs() const { return Args; }
|
||||
|
||||
|
||||
};
|
||||
49
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeGosub.h
Normal file
49
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeGosub.h
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSScriptNodeGosub.generated.h"
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class SUDS_API USUDSScriptNodeGosub : public USUDSScriptNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
protected:
|
||||
/// Name of the label which we'll jump to before returning
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
FName LabelName;
|
||||
|
||||
/// Generated ID for use when saving state
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
FString GosubID;
|
||||
|
||||
/// Convenience flag to let you know whether this node MAY HAVE any choices directly after it
|
||||
/// Internally this also lets us know to look for the next choice node after returning
|
||||
/// It's possible that where there are conditionals ahead, there are only choices on some of the paths.
|
||||
/// This flag is to let us know to look for choices, but if conditionals apply we may not find any using actual dialogue state.
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
bool bHasChoices = false;
|
||||
|
||||
public:
|
||||
|
||||
void Init(const FString& Label, const FString ID, int LineNo)
|
||||
{
|
||||
NodeType = ESUDSScriptNodeType::Gosub;
|
||||
LabelName = FName(Label);
|
||||
GosubID = ID;
|
||||
SourceLineNo = LineNo;
|
||||
}
|
||||
FName GetLabelName() const { return LabelName; }
|
||||
const FString& GetGosubID() const { return GosubID; }
|
||||
/// Whether on one select path or another a choice was found
|
||||
/// Doesn't help if within a Gosub as call site may be anywhere
|
||||
bool MayHaveChoices() const { return bHasChoices; }
|
||||
|
||||
void NotifyMayHaveChoices() { bHasChoices = true; }
|
||||
|
||||
};
|
||||
33
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeSet.h
Normal file
33
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeSet.h
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSExpression.h"
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSScriptNodeSet.generated.h"
|
||||
|
||||
/**
|
||||
* Set variable node
|
||||
*/
|
||||
UCLASS()
|
||||
class SUDS_API USUDSScriptNodeSet : public USUDSScriptNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
// Variable identifier
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
FName Identifier;
|
||||
|
||||
/// Expression to provide value to set
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
FSUDSExpression Expression;
|
||||
|
||||
public:
|
||||
|
||||
void Init(const FString& VarName, const FSUDSExpression& InExpression, int LineNo);
|
||||
const FName& GetIdentifier() const { return Identifier; }
|
||||
const FSUDSExpression& GetExpression() const { return Expression; }
|
||||
|
||||
};
|
||||
72
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeText.h
Normal file
72
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeText.h
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSScriptNodeText.generated.h"
|
||||
|
||||
class UDialogueWave;
|
||||
|
||||
/**
|
||||
* A node which contains speaker text
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class SUDS_API USUDSScriptNodeText : public USUDSScriptNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
/// Identifier of the speaker for text nodes
|
||||
UPROPERTY(BlueprintReadOnly, VisibleDefaultsOnly, Category="SUDS")
|
||||
FString SpeakerID;
|
||||
/// Text, always references a string table. Parameters will not have been completed.
|
||||
/// Note: if you're using voiced dialogue, see the Wave property and its subtitle functionality
|
||||
UPROPERTY(BlueprintReadOnly, VisibleDefaultsOnly, Category="SUDS")
|
||||
FText Text;
|
||||
/// DialogueWave asset link for voiced dialogue
|
||||
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category="SUDS")
|
||||
TObjectPtr<UDialogueWave> Wave;
|
||||
|
||||
/// Convenience flag to let you know whether this text node MAY HAVE choices attached
|
||||
/// If false, there's only one way to proceed from here and no text associated with that
|
||||
/// If true, either there can be > 1 choice options, or a single choice with associated text (this can be when
|
||||
/// you have no choice but want text rather than just a continue button)
|
||||
/// Internally this also lets us know to look for the next choice node
|
||||
/// It's possible that where there are conditionals ahead, there are only choices on some of the paths.
|
||||
/// This flag is to let us know to look for choices, but if conditionals apply we may not find any using actual dialogue state.
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
bool bHasChoices = false;
|
||||
|
||||
/// User metadata associated with this speaker line. May include derived expressions
|
||||
UPROPERTY()
|
||||
TMap<FName, FSUDSExpression> UserMetadata;
|
||||
|
||||
mutable bool bFormatExtracted = false;
|
||||
mutable TArray<FName> ParameterNames;
|
||||
mutable FTextFormat TextFormat;
|
||||
|
||||
void ExtractFormat() const;
|
||||
|
||||
public:
|
||||
const FString& GetSpeakerID() const { return SpeakerID; }
|
||||
const FText& GetText() const { return Text; }
|
||||
FString GetTextID() const;
|
||||
UDialogueWave* GetWave() const { return Wave; }
|
||||
/// Whether on one select path or another a choice was found
|
||||
/// Doesn't help if within a Gosub as call site may be anywhere
|
||||
bool MayHaveChoices() const { return bHasChoices; }
|
||||
|
||||
void Init(const FString& SpeakerID, const FText& Text, int LineNo);
|
||||
void SetWave(UDialogueWave* InWave) { Wave = InWave; }
|
||||
const FTextFormat& GetTextFormat() const;
|
||||
const TArray<FName>& GetParameterNames() const;
|
||||
bool HasParameters() const;
|
||||
|
||||
void NotifyMayHaveChoices() { bHasChoices = true; }
|
||||
|
||||
const TMap<FName, FSUDSExpression>& GetUserMetadata() const { return UserMetadata; }
|
||||
void SetUserMetadata(const TMap<FName, FSUDSExpression>& Meta) { UserMetadata = Meta; }
|
||||
|
||||
|
||||
};
|
||||
292
Plugins/SUDS/Source/SUDS/Public/SUDSSubsystem.h
Normal file
292
Plugins/SUDS/Source/SUDS/Public/SUDSSubsystem.h
Normal file
@@ -0,0 +1,292 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSValue.h"
|
||||
#include "Subsystems/GameInstanceSubsystem.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "SUDSSubsystem.generated.h"
|
||||
|
||||
class USUDSDialogue;
|
||||
class USUDSScript;
|
||||
class USoundConcurrency;
|
||||
struct FSoundConcurrencySettings;
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogSUDSSubsystem, Log, All);
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnGlobalVariableChangedEvent, FName, VariableName, const FSUDSValue&, Value, bool, bFromScript);
|
||||
|
||||
/// Copy of the global state of the system
|
||||
USTRUCT(BlueprintType)
|
||||
struct FSUDSGlobalState
|
||||
{
|
||||
GENERATED_BODY()
|
||||
protected:
|
||||
UPROPERTY(BlueprintReadOnly, SaveGame, Category="SUDS")
|
||||
TMap<FName, FSUDSValue> GlobalVariables;
|
||||
|
||||
public:
|
||||
FSUDSGlobalState() {}
|
||||
|
||||
FSUDSGlobalState(const TMap<FName, FSUDSValue>& InGlobalVars) : GlobalVariables(InGlobalVars)
|
||||
{
|
||||
}
|
||||
|
||||
const TMap<FName, FSUDSValue>& GetGlobalVariables() const { return GlobalVariables; }
|
||||
|
||||
SUDS_API friend FArchive& operator<<(FArchive& Ar, FSUDSGlobalState& Value);
|
||||
SUDS_API friend void operator<<(FStructuredArchive::FSlot Slot, FSUDSGlobalState& Value);
|
||||
bool Serialize(FStructuredArchive::FSlot Slot)
|
||||
{
|
||||
Slot << *this;
|
||||
return true;
|
||||
}
|
||||
bool Serialize(FArchive& Ar)
|
||||
{
|
||||
Ar << *this;
|
||||
return true;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class SUDS_API USUDSSubsystem : public UGameInstanceSubsystem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||
virtual void Deinitialize() override;
|
||||
|
||||
/// Event raised when a global variable is changed. "FromScript" is true if the variable was set by the script, false if set from code
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnGlobalVariableChangedEvent OnGlobalVariableChanged;
|
||||
|
||||
protected:
|
||||
UPROPERTY()
|
||||
TObjectPtr<USoundConcurrency> VoiceConcurrency;
|
||||
|
||||
/// Global variable state
|
||||
TMap<FName, FSUDSValue> GlobalVariableState;
|
||||
|
||||
void SetGlobalVariableImpl(FName Name, const FSUDSValue& Value, bool bFromScript, int LineNo)
|
||||
{
|
||||
const FSUDSValue OldValue = GetGlobalVariable(Name);
|
||||
if (!IsGlobalVariableSet(Name) ||
|
||||
(OldValue != Value).GetBooleanValue())
|
||||
{
|
||||
GlobalVariableState.Add(Name, Value);
|
||||
OnGlobalVariableChanged.Broadcast(Name, Value, bFromScript);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
/**
|
||||
* Sets the number of voiced lines that can be played at once. Defaults to 1, so that when a new voiced line is
|
||||
* played while another is still playing, the previous one is stopped. If you would prefer that they overlap, set
|
||||
* this to >1
|
||||
* @param ConcurrentLines The number of voice lines that can be played at once.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Settings")
|
||||
void SetMaxConcurrentVoicedLines(int ConcurrentLines);
|
||||
|
||||
/**
|
||||
* Gets the number of voiced lines that can be played at once. Defaults to 1, so that when a new voiced line is
|
||||
* played while another is still playing, the previous one is stopped.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Settings")
|
||||
int GetMaxConcurrentVoicedLines() const;
|
||||
|
||||
USoundConcurrency* GetVoicedLineConcurrency() const { return VoiceConcurrency; }
|
||||
|
||||
|
||||
/**
|
||||
* Reset the global state of the system.
|
||||
* @param bResetVariables If true, resets all variable state
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global State")
|
||||
void ResetGlobalState(bool bResetVariables = true);
|
||||
|
||||
/** Retrieve a copy of the global state of the system.
|
||||
* This is useful for saving the state of e.g. global variables.
|
||||
* @return A static copy of the current global state. This struct can be serialised with your save data,
|
||||
* and contains the state of global variables.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global State")
|
||||
FSUDSGlobalState GetSavedGlobalState() const;
|
||||
|
||||
/** Restore the global saved state.
|
||||
* This is useful for restoring global state eg global variables when you're loading a game.
|
||||
* @param State Global state that you previously retrieved from GetSavedGlobalState().
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void RestoreSavedGlobalState(const FSUDSGlobalState& State);
|
||||
|
||||
/// Set a global variable
|
||||
/// This is mostly only useful if you happen to already have a general purpose FSUDSValue.
|
||||
/// See SetGlobalVariableText, SetGlobalVariableInt etc for literal-friendly versions
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void SetGlobalVariable(FName Name, FSUDSValue Value)
|
||||
{
|
||||
SetGlobalVariableImpl(Name, Value, false, 0);
|
||||
}
|
||||
|
||||
/// Internal use only
|
||||
void InternalSetGlobalVariable(FName Name, const FSUDSValue& Value, bool bFromScript, int LineNo) { SetGlobalVariableImpl(Name, Value, bFromScript, LineNo); }
|
||||
|
||||
/// Get a variable in dialogue state as a general value type
|
||||
/// See GetDialogueText, GetDialogueInt etc for more type friendly versions, but if you want to access the state
|
||||
/// as a type-flexible value then you can do so with this function.
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
FSUDSValue GetGlobalVariable(FName Name) const
|
||||
{
|
||||
if (const auto Arg = GlobalVariableState.Find(Name))
|
||||
{
|
||||
return *Arg;
|
||||
}
|
||||
return FSUDSValue();
|
||||
}
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
bool IsGlobalVariableSet(FName Name) const
|
||||
{
|
||||
return GlobalVariableState.Contains(Name);
|
||||
}
|
||||
|
||||
/// Get all variables
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
const TMap<FName, FSUDSValue>& GetGlobalVariables() const { return GlobalVariableState; }
|
||||
|
||||
/**
|
||||
* Set a text global variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void SetGlobalVariableText(FName Name, FText Value)
|
||||
{
|
||||
SetGlobalVariable(Name, Value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a text global variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
FText GetGlobalVariableText(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a global variable on the passed in parameters collection.
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void SetGlobalVariableInt(FName Name, int32 Value);
|
||||
|
||||
/**
|
||||
* Get an int global variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
int GetGlobalVariableInt(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a float global variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void SetGlobalVariableFloat(FName Name, float Value);
|
||||
|
||||
/**
|
||||
* Get a float global variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
float GetGlobalVariableFloat(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a gender global variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void SetGlobalVariableGender(FName Name, ETextGender Value);
|
||||
|
||||
/**
|
||||
* Get a gender global variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
ETextGender GetGlobalVariableGender(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a boolean global variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void SetGlobalVariableBoolean(FName Name, bool Value);
|
||||
|
||||
/**
|
||||
* Get a boolean global variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
bool GetGlobalVariableBoolean(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a name global variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void SetGlobalVariableName(FName Name, FName Value);
|
||||
|
||||
/**
|
||||
* Get a name global variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
FName GetGlobalVariableName(FName Name) const;
|
||||
|
||||
|
||||
/**
|
||||
* Remove the definition of a global variable.
|
||||
* This has much same effect as setting the variable back to the default value for this type, since attempting to
|
||||
* retrieve a missing variable result in a default value.
|
||||
* @param Name The name of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void UnSetGlobalVariable(FName Name);
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
/// Only for use by tests / editor tools when real subsystem isn't running
|
||||
static TMap<FName, FSUDSValue> Test_DummyGlobalVariables;
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
inline USUDSSubsystem* GetSUDSSubsystem(UWorld* WorldContext)
|
||||
{
|
||||
if (IsValid(WorldContext) && WorldContext->IsGameWorld())
|
||||
{
|
||||
auto GI = WorldContext->GetGameInstance();
|
||||
if (IsValid(GI))
|
||||
return GI->GetSubsystem<USUDSSubsystem>();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
416
Plugins/SUDS/Source/SUDS/Public/SUDSValue.h
Normal file
416
Plugins/SUDS/Source/SUDS/Public/SUDSValue.h
Normal file
@@ -0,0 +1,416 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "SUDSCommon.h"
|
||||
#include "SUDSValue.generated.h"
|
||||
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class ESUDSValueType : uint8
|
||||
{
|
||||
Text = 0,
|
||||
Int = 1,
|
||||
Float = 2,
|
||||
Boolean = 3,
|
||||
Gender = 4,
|
||||
Name = 5,
|
||||
/// Access the value of another variable
|
||||
Variable = 10,
|
||||
|
||||
Empty = 99
|
||||
};
|
||||
/// Struct which can hold any of the value types that SUDS needs to use, in a Blueprint friendly manner
|
||||
/// For getting / setting these values from blueprints, see blueprint library functions SetSUDSValue<Type>() / GetSUDSValue<Type>()
|
||||
/// For convenience these are wrapped in USUDSDialogue but in e.g. event callbacks they're not
|
||||
USTRUCT(BlueprintType)
|
||||
struct SUDS_API FSUDSValue
|
||||
{
|
||||
GENERATED_BODY()
|
||||
protected:
|
||||
ESUDSValueType Type;
|
||||
union
|
||||
{
|
||||
int32 IntValue;
|
||||
float FloatValue;
|
||||
};
|
||||
TOptional<FText> TextValue;
|
||||
// Used for variables and name values
|
||||
TOptional<FName> Name;
|
||||
public:
|
||||
|
||||
FSUDSValue() : Type(ESUDSValueType::Empty), IntValue(0), TextValue(FText::GetEmpty()) {}
|
||||
|
||||
FSUDSValue(const int32 Value)
|
||||
: Type(ESUDSValueType::Int) { IntValue = Value; }
|
||||
|
||||
FSUDSValue(const float Value)
|
||||
: Type(ESUDSValueType::Float) { FloatValue = Value; }
|
||||
|
||||
FSUDSValue(const FText& Value)
|
||||
: Type(ESUDSValueType::Text),
|
||||
IntValue(0),
|
||||
TextValue(Value)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSValue(FText&& Value)
|
||||
: Type(ESUDSValueType::Text), IntValue(0), TextValue(MoveTemp(Value))
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSValue(ETextGender Value)
|
||||
: Type(ESUDSValueType::Gender), IntValue(0)
|
||||
{
|
||||
IntValue = static_cast<int32>(Value);
|
||||
}
|
||||
|
||||
FSUDSValue(bool Value)
|
||||
: Type(ESUDSValueType::Boolean), IntValue(0)
|
||||
{
|
||||
IntValue = Value ? 1 : 0;
|
||||
}
|
||||
|
||||
FSUDSValue(const FName& ReferencedName, bool bIsVariable)
|
||||
: Type(bIsVariable ? ESUDSValueType::Variable : ESUDSValueType::Name),
|
||||
IntValue(0),
|
||||
Name(ReferencedName)
|
||||
{
|
||||
}
|
||||
|
||||
// Construct a default value of a given type
|
||||
explicit FSUDSValue(ESUDSValueType ValType)
|
||||
: Type(ValType), IntValue(0)
|
||||
{
|
||||
}
|
||||
|
||||
/// Whether this value is empty, i.e. hasn't been set to anything
|
||||
FORCEINLINE bool IsEmpty() const
|
||||
{
|
||||
return Type == ESUDSValueType::Empty;
|
||||
}
|
||||
|
||||
FORCEINLINE ESUDSValueType GetType() const
|
||||
{
|
||||
return Type;
|
||||
}
|
||||
|
||||
FORCEINLINE int32 GetIntValue() const
|
||||
{
|
||||
// We don't warn for unset variables / uninitialised values, use the defaults
|
||||
if (!IsEmpty() && Type != ESUDSValueType::Int && Type != ESUDSValueType::Variable)
|
||||
UE_LOG(LogSUDS, Warning, TEXT("Getting value as int but was type %s"), *StaticEnum<ESUDSValueType>()->GetValueAsString(Type))
|
||||
|
||||
return IntValue;
|
||||
}
|
||||
|
||||
FORCEINLINE float GetFloatValue() const
|
||||
{
|
||||
// We don't warn for unset variables, use the defaults
|
||||
if (!IsEmpty() && !IsNumeric() && Type != ESUDSValueType::Variable)
|
||||
UE_LOG(LogSUDS, Warning, TEXT("Getting value as float but was type %s"), *StaticEnum<ESUDSValueType>()->GetValueAsString(Type))
|
||||
|
||||
if (Type == ESUDSValueType::Int)
|
||||
{
|
||||
// Allow int widening to float
|
||||
return GetIntValue();
|
||||
}
|
||||
return FloatValue;
|
||||
}
|
||||
|
||||
FORCEINLINE const FText& GetTextValue() const
|
||||
{
|
||||
// We don't warn for unset variables, use the defaults
|
||||
if (!IsEmpty() && Type != ESUDSValueType::Text && Type != ESUDSValueType::Variable)
|
||||
UE_LOG(LogSUDS, Warning, TEXT("Getting value as text but was type %s"), *StaticEnum<ESUDSValueType>()->GetValueAsString(Type))
|
||||
|
||||
if (TextValue.IsSet())
|
||||
return TextValue.GetValue();
|
||||
|
||||
return FText::GetEmpty();
|
||||
}
|
||||
|
||||
FORCEINLINE ETextGender GetGenderValue() const
|
||||
{
|
||||
// We don't warn for unset variables, use the defaults
|
||||
if (!IsEmpty() && Type != ESUDSValueType::Gender && Type != ESUDSValueType::Variable)
|
||||
UE_LOG(LogSUDS, Warning, TEXT("Getting value as float but was type %s"), *StaticEnum<ESUDSValueType>()->GetValueAsString(Type))
|
||||
|
||||
return static_cast<ETextGender>(IntValue);
|
||||
}
|
||||
|
||||
FORCEINLINE bool GetBooleanValue() const
|
||||
{
|
||||
// We don't warn for unset variables, use the defaults
|
||||
if (!IsEmpty() && Type != ESUDSValueType::Boolean && Type != ESUDSValueType::Variable)
|
||||
UE_LOG(LogSUDS, Warning, TEXT("Getting value as boolean but was type %s"), *StaticEnum<ESUDSValueType>()->GetValueAsString(Type))
|
||||
|
||||
return IntValue != 0;
|
||||
}
|
||||
|
||||
FORCEINLINE FName GetNameValue() const
|
||||
{
|
||||
// We don't warn for unset variables, use the defaults
|
||||
if (!IsEmpty() && Type != ESUDSValueType::Name && Type != ESUDSValueType::Variable)
|
||||
UE_LOG(LogSUDS, Warning, TEXT("Getting value as Name but was type %s"), *StaticEnum<ESUDSValueType>()->GetValueAsString(Type))
|
||||
|
||||
if (Name.IsSet())
|
||||
return Name.GetValue();
|
||||
|
||||
return NAME_None;
|
||||
}
|
||||
|
||||
FORCEINLINE FName GetVariableNameValue() const
|
||||
{
|
||||
if (!IsEmpty() && Type != ESUDSValueType::Variable)
|
||||
UE_LOG(LogSUDS, Warning, TEXT("Getting value as variable name but was type %s"), *StaticEnum<ESUDSValueType>()->GetValueAsString(Type))
|
||||
|
||||
if (Name.IsSet())
|
||||
return Name.GetValue();
|
||||
|
||||
return NAME_None;
|
||||
}
|
||||
|
||||
FORCEINLINE bool IsVariable() const
|
||||
{
|
||||
return Type == ESUDSValueType::Variable;
|
||||
}
|
||||
|
||||
bool IsNumeric() const
|
||||
{
|
||||
return Type == ESUDSValueType::Float || Type == ESUDSValueType::Int;
|
||||
}
|
||||
|
||||
FFormatArgumentValue ToFormatArg() const
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
default:
|
||||
case ESUDSValueType::Text:
|
||||
return FFormatArgumentValue(TextValue.GetValue());
|
||||
case ESUDSValueType::Int:
|
||||
return FFormatArgumentValue(GetIntValue());
|
||||
case ESUDSValueType::Boolean:
|
||||
return FFormatArgumentValue(GetBooleanValue());
|
||||
case ESUDSValueType::Gender:
|
||||
return FFormatArgumentValue(GetGenderValue());
|
||||
case ESUDSValueType::Float:
|
||||
return FFormatArgumentValue(GetFloatValue());
|
||||
case ESUDSValueType::Name:
|
||||
// return something useful from FName even though technically shouldn't use in visible text
|
||||
return FFormatArgumentValue(FText::FromName(GetNameValue()));
|
||||
case ESUDSValueType::Empty:
|
||||
case ESUDSValueType::Variable:
|
||||
return FFormatArgumentValue();
|
||||
}
|
||||
}
|
||||
|
||||
void SetValue(const FSUDSValue& RValue)
|
||||
{
|
||||
*this = RValue;
|
||||
}
|
||||
|
||||
/// Not operation, technically only valid on booleans, but not enforced so as to allow unset vars & conversions
|
||||
FSUDSValue operator!() const
|
||||
{
|
||||
return FSUDSValue(!GetBooleanValue());
|
||||
}
|
||||
|
||||
FSUDSValue operator*(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// We don't force types to be numeric here so that we can safely use unset variables
|
||||
// If both int, keep int
|
||||
if (Type == ESUDSValueType::Int && Rhs.Type == Type)
|
||||
{
|
||||
return FSUDSValue(GetIntValue() * Rhs.GetIntValue());
|
||||
}
|
||||
// Otherwise we'll let the type widening handle mixed types for us (ternary will always resolve to float)
|
||||
return FSUDSValue(
|
||||
(Type == ESUDSValueType::Float ? GetFloatValue() : GetIntValue())
|
||||
*
|
||||
(Rhs.GetType() == ESUDSValueType::Float ? Rhs.GetFloatValue() : Rhs.GetIntValue()));
|
||||
}
|
||||
FSUDSValue operator/(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// We don't force types to be numeric here so that we can safely use unset variables
|
||||
// If both int, keep int
|
||||
if (Type == ESUDSValueType::Int && Rhs.Type == Type)
|
||||
{
|
||||
return FSUDSValue(GetIntValue() / Rhs.GetIntValue());
|
||||
}
|
||||
// Otherwise we'll let the type widening handle mixed types for us (ternary will always resolve to float)
|
||||
return FSUDSValue(
|
||||
(Type == ESUDSValueType::Float ? GetFloatValue() : GetIntValue())
|
||||
/
|
||||
(Rhs.GetType() == ESUDSValueType::Float ? Rhs.GetFloatValue() : Rhs.GetIntValue()));
|
||||
}
|
||||
FSUDSValue operator%(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// We don't force types to be numeric here so that we can safely use unset variables
|
||||
// If both int, keep int
|
||||
if (Type == ESUDSValueType::Int && Rhs.Type == Type)
|
||||
{
|
||||
return FSUDSValue(GetIntValue() % Rhs.GetIntValue());
|
||||
}
|
||||
// Otherwise we'll let the type widening handle mixed types for us (ternary will always resolve to float)
|
||||
float lval = (Type == ESUDSValueType::Float ? GetFloatValue() : GetIntValue());
|
||||
float rval = (Rhs.GetType() == ESUDSValueType::Float ? Rhs.GetFloatValue() : Rhs.GetIntValue());
|
||||
// Prevent NaN and errors reported when executing FMath::Fmod with invalid values.
|
||||
return FSUDSValue(rval != 0 ? FMath::Fmod(lval, rval) : 0.0f);
|
||||
}
|
||||
FSUDSValue operator+(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// We don't force types to be numeric here so that we can safely use unset variables
|
||||
// If both int, keep int
|
||||
if (Type == ESUDSValueType::Int && Rhs.Type == Type)
|
||||
{
|
||||
return FSUDSValue(GetIntValue() + Rhs.GetIntValue());
|
||||
}
|
||||
// Otherwise we'll let the type widening handle mixed types for us (ternary will always resolve to float)
|
||||
return FSUDSValue(
|
||||
(Type == ESUDSValueType::Float ? GetFloatValue() : GetIntValue())
|
||||
+
|
||||
(Rhs.GetType() == ESUDSValueType::Float ? Rhs.GetFloatValue() : Rhs.GetIntValue()));
|
||||
}
|
||||
FSUDSValue operator-(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// We don't force types to be numeric here so that we can safely use unset variables
|
||||
// If both int, keep int
|
||||
if (Type == ESUDSValueType::Int && Rhs.Type == Type)
|
||||
{
|
||||
return FSUDSValue(GetIntValue() - Rhs.GetIntValue());
|
||||
}
|
||||
// Otherwise we'll let the type widening handle mixed types for us (ternary will always resolve to float)
|
||||
return FSUDSValue(
|
||||
(Type == ESUDSValueType::Float ? GetFloatValue() : GetIntValue())
|
||||
-
|
||||
(Rhs.GetType() == ESUDSValueType::Float ? Rhs.GetFloatValue() : Rhs.GetIntValue()));
|
||||
}
|
||||
FSUDSValue operator<(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// Don't check types here. We'll fall back on int comparisons which is important for cases where
|
||||
// a variable hasn't been set
|
||||
// result is boolean so no need to protect types
|
||||
return FSUDSValue(
|
||||
(Type == ESUDSValueType::Float ? GetFloatValue() : GetIntValue())
|
||||
<
|
||||
(Rhs.GetType() == ESUDSValueType::Float ? Rhs.GetFloatValue() : Rhs.GetIntValue()));
|
||||
|
||||
}
|
||||
FSUDSValue operator==(const FSUDSValue& Rhs) const
|
||||
{
|
||||
if (IsNumeric() || Rhs.IsNumeric())
|
||||
{
|
||||
if (GetType() == ESUDSValueType::Float || Rhs.GetType() == ESUDSValueType::Float)
|
||||
{
|
||||
// For floats, use tolerance
|
||||
return FSUDSValue(FMath::IsNearlyEqual(
|
||||
GetType() == ESUDSValueType::Int ? (float)GetIntValue() : GetFloatValue(),
|
||||
Rhs.GetType() == ESUDSValueType::Int ? (float)Rhs.GetIntValue() : Rhs.GetFloatValue()));
|
||||
}
|
||||
else
|
||||
{
|
||||
return FSUDSValue(GetIntValue() == Rhs.GetIntValue());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// no auto conversion here
|
||||
// however, tolerate unresolved variables, they will return initial values
|
||||
if (Type != Rhs.Type && Type != ESUDSValueType::Variable && Rhs.Type != ESUDSValueType::Variable)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare using type from whichever one isn't a variable (get values on unset variables will return defaults)
|
||||
ESUDSValueType UseType = IsVariable() ? Rhs.GetType() : GetType();
|
||||
switch (UseType)
|
||||
{
|
||||
case ESUDSValueType::Text:
|
||||
return FSUDSValue(GetTextValue().EqualTo(Rhs.GetTextValue()));
|
||||
case ESUDSValueType::Boolean:
|
||||
return FSUDSValue(GetBooleanValue() == Rhs.GetBooleanValue());
|
||||
case ESUDSValueType::Gender:
|
||||
return FSUDSValue(GetGenderValue() == Rhs.GetGenderValue());
|
||||
case ESUDSValueType::Variable:
|
||||
return FSUDSValue(GetVariableNameValue() == Rhs.GetVariableNameValue());
|
||||
case ESUDSValueType::Name:
|
||||
return FSUDSValue(GetNameValue() == Rhs.GetNameValue());
|
||||
// deal with int/float again here, this mops up cases where one side is an unset variable
|
||||
case ESUDSValueType::Int:
|
||||
return FSUDSValue(GetIntValue() == Rhs.GetIntValue());
|
||||
case ESUDSValueType::Float:
|
||||
return FSUDSValue(GetFloatValue() == Rhs.GetFloatValue());
|
||||
default:
|
||||
break;
|
||||
};
|
||||
}
|
||||
return FSUDSValue(false);
|
||||
}
|
||||
FSUDSValue operator<=(const FSUDSValue& Rhs) const
|
||||
{
|
||||
if ((*this < Rhs).GetBooleanValue())
|
||||
return FSUDSValue(true);
|
||||
return (*this == Rhs);
|
||||
}
|
||||
|
||||
FSUDSValue operator>(const FSUDSValue& Rhs) const
|
||||
{
|
||||
return Rhs < *this;
|
||||
}
|
||||
|
||||
FSUDSValue operator>=(const FSUDSValue& Rhs) const
|
||||
{
|
||||
return Rhs <= *this;
|
||||
}
|
||||
|
||||
FSUDSValue operator!=(const FSUDSValue& Rhs) const
|
||||
{
|
||||
return !(*this == Rhs);
|
||||
}
|
||||
|
||||
FSUDSValue operator&&(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// We always let unset variables degrade to false
|
||||
check(Type == ESUDSValueType::Boolean || Type == ESUDSValueType::Variable);
|
||||
check(Rhs.Type == ESUDSValueType::Boolean || Rhs.Type == ESUDSValueType::Variable);
|
||||
return FSUDSValue(GetBooleanValue() && Rhs.GetBooleanValue());
|
||||
}
|
||||
|
||||
FSUDSValue operator||(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// We always let unset variables degrade to false
|
||||
check(Type == ESUDSValueType::Boolean || Type == ESUDSValueType::Variable);
|
||||
check(Rhs.Type == ESUDSValueType::Boolean || Rhs.Type == ESUDSValueType::Variable);
|
||||
return FSUDSValue(GetBooleanValue() || Rhs.GetBooleanValue());
|
||||
}
|
||||
|
||||
SUDS_API friend FArchive& operator<<(FArchive& Ar, FSUDSValue& Value);
|
||||
SUDS_API friend void operator<<(FStructuredArchive::FSlot Slot, FSUDSValue& Value);
|
||||
bool Serialize(FArchive& Ar)
|
||||
{
|
||||
Ar << *this;
|
||||
return true;
|
||||
}
|
||||
bool Serialize(FStructuredArchive::FSlot Slot)
|
||||
{
|
||||
Slot << *this;
|
||||
return true;
|
||||
}
|
||||
|
||||
FString ToString() const;
|
||||
|
||||
bool ExportTextItem(FString& ValueStr, FSUDSValue const& DefaultValue, UObject* Parent, int32 PortFlags, UObject* ExportRootScope) const;
|
||||
};
|
||||
template<>
|
||||
struct TStructOpsTypeTraits<FSUDSValue> : public TStructOpsTypeTraitsBase2<FSUDSValue>
|
||||
{
|
||||
enum
|
||||
{
|
||||
WithSerializer = true,
|
||||
WithExportTextItem = true
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
53
Plugins/SUDS/Source/SUDS/SUDS.Build.cs
Normal file
53
Plugins/SUDS/Source/SUDS/SUDS.Build.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class SUDS : ModuleRules
|
||||
{
|
||||
public SUDS(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add public include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add other private include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PublicDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"Core",
|
||||
// ... add other public dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
// ... add private dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
DynamicallyLoadedModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
// ... add any modules that your module loads dynamically here ...
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
65
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditor.cpp
Normal file
65
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditor.cpp
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSEditor.h"
|
||||
|
||||
#include "ISettingsModule.h"
|
||||
#include "ISettingsSection.h"
|
||||
#include "SUDSEditorSettings.h"
|
||||
#include "SUDSScriptActions.h"
|
||||
#include "Interfaces/IPluginManager.h"
|
||||
#include "Styling/SlateStyle.h"
|
||||
#include "Styling/SlateStyleRegistry.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FSUDSModule"
|
||||
|
||||
void FSUDSEditorModule::StartupModule()
|
||||
{
|
||||
ScriptActions = MakeShared<FSUDSScriptActions>();
|
||||
FAssetToolsModule::GetModule().Get().RegisterAssetTypeActions(ScriptActions.ToSharedRef());
|
||||
|
||||
auto SudsPlugin = IPluginManager::Get().FindPlugin(TEXT("SUDS"));
|
||||
if (SudsPlugin.IsValid())
|
||||
{
|
||||
const FString IconDir = SudsPlugin->GetBaseDir() + "/Content/Editor/Slate/Icons";
|
||||
const FVector2D Sz16 = FVector2D(16.0f, 16.0f);
|
||||
const FVector2D Sz64 = FVector2D(64.0f, 64.0f);
|
||||
|
||||
StyleSet = MakeShared<FSlateStyleSet>("SUDSStyleSet");
|
||||
StyleSet->Set(TEXT("ClassIcon.SUDSScript"), new FSlateImageBrush(IconDir / TEXT("SUDSScript_16x.png"), Sz16));
|
||||
StyleSet->Set(TEXT("ClassThumbnail.SUDSScript"), new FSlateImageBrush(IconDir / TEXT("SUDSScript_64x.png"), Sz64));
|
||||
|
||||
FSlateStyleRegistry::RegisterSlateStyle(*StyleSet.Get());
|
||||
}
|
||||
|
||||
// register settings
|
||||
ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings");
|
||||
if (SettingsModule)
|
||||
{
|
||||
ISettingsSectionPtr SettingsSection = SettingsModule->RegisterSettings("Project", "Plugins", "SUDS Editor",
|
||||
LOCTEXT("SUDSEditorSettingsName", "SUDS Editor"),
|
||||
LOCTEXT("SUDSEditorSettingsDescription", "Configure the editor parts of SUDS."),
|
||||
GetMutableDefault<USUDSEditorSettings>()
|
||||
);
|
||||
}
|
||||
|
||||
UE_LOG(LogSUDSEditor, Log, TEXT("SUDS Editor Module Started"))
|
||||
|
||||
}
|
||||
|
||||
void FSUDSEditorModule::ShutdownModule()
|
||||
{
|
||||
if (StyleSet.IsValid())
|
||||
{
|
||||
FSlateStyleRegistry::UnRegisterSlateStyle(*StyleSet.Get());
|
||||
}
|
||||
|
||||
if (!FModuleManager::Get().IsModuleLoaded("AssetTools")) return;
|
||||
FAssetToolsModule::GetModule().Get().UnregisterAssetTypeActions(ScriptActions.ToSharedRef());
|
||||
|
||||
UE_LOG(LogSUDSEditor, Log, TEXT("SUDS Editor Module Shut Down"))
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
IMPLEMENT_MODULE(FSUDSEditorModule, SUDSEditor)
|
||||
DEFINE_LOG_CATEGORY(LogSUDSEditor);
|
||||
280
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorScriptTools.cpp
Normal file
280
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorScriptTools.cpp
Normal file
@@ -0,0 +1,280 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSEditorScriptTools.h"
|
||||
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptImporter.h"
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSScriptNodeGosub.h"
|
||||
#include "SUDSScriptNodeSet.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
#include "EditorFramework/AssetImportData.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/PackageName.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "UObject/Package.h"
|
||||
|
||||
void FSUDSEditorScriptTools::WriteBackTextIDs(USUDSScript* Script, FSUDSMessageLogger& Logger)
|
||||
{
|
||||
const auto& SrcData = Script->AssetImportData->SourceData;
|
||||
if (SrcData.SourceFiles.Num() == 1)
|
||||
{
|
||||
TArray<FString> Lines;
|
||||
FString SourceFile = SrcData.SourceFiles[0].RelativeFilename;
|
||||
auto Package = Script->GetPackage();
|
||||
if (FPaths::IsRelative(SourceFile) && Package)
|
||||
{
|
||||
FString PackagePath = FPackageName::LongPackageNameToFilename(FPackageName::GetLongPackagePath(Package->GetPathName()));
|
||||
SourceFile = FPaths::ConvertRelativePathToFull(PackagePath, SourceFile);
|
||||
}
|
||||
if (FFileHelper::LoadFileToStringArray(Lines, *SourceFile))
|
||||
{
|
||||
const int PrevErrs = Logger.NumErrors();
|
||||
const bool bHeaderChanges = WriteBackTextIDsFromNodes(Script->GetHeaderNodes(), Lines, Script->GetName(), Logger);
|
||||
const bool bBodyChanges = WriteBackTextIDsFromNodes(Script->GetNodes(), Lines, Script->GetName(), Logger);
|
||||
|
||||
if (Logger.NumErrors() > PrevErrs)
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Info,
|
||||
FText::FromString(FString::Printf(TEXT("Errors prevented saving updates to %s."), *Script->GetName())));
|
||||
}
|
||||
else if (bHeaderChanges || bBodyChanges)
|
||||
{
|
||||
// We need to re-hash file in memory before saving to prevent re-import
|
||||
int32 Length = 10;
|
||||
for(const FString& Line : Lines)
|
||||
{
|
||||
Length += Line.Len() + UE_ARRAY_COUNT(LINE_TERMINATOR);
|
||||
}
|
||||
FString CombinedString;
|
||||
CombinedString.Reserve(Length);
|
||||
|
||||
for(const FString& Line : Lines)
|
||||
{
|
||||
CombinedString += Line;
|
||||
CombinedString += LINE_TERMINATOR;
|
||||
}
|
||||
|
||||
// Write source file back; always encode as UTF8 not default ANSI/UTF16
|
||||
// Do this before updating asset import data so it picks up new file timestamp
|
||||
FFileHelper::SaveStringToFile(FStringView(CombinedString), *SourceFile, FFileHelper::EEncodingOptions::ForceUTF8);
|
||||
|
||||
/* BEGIN try to prevent re-import prompt
|
||||
* This unfortunately didn't work, so removed & let it reimport
|
||||
|
||||
const FMD5Hash FileHash = FSUDSScriptImporter::CalculateHash(*CombinedString, CombinedString.Len());
|
||||
Script->AssetImportData->Update(SourceFile, FileHash);
|
||||
|
||||
// Need to mark asset dirty since hash has changed
|
||||
// Try to find the outer package so we can dirty it up
|
||||
if (Script->GetOuter())
|
||||
{
|
||||
Script->GetOuter()->MarkPackageDirty();
|
||||
}
|
||||
else
|
||||
{
|
||||
Script->MarkPackageDirty();
|
||||
}
|
||||
|
||||
// Even this doesn't work:
|
||||
GUnrealEd->AutoReimportManager->IgnoreFileModification(SourceFile);
|
||||
|
||||
* END try to prevent re-import
|
||||
*/
|
||||
|
||||
Logger.AddMessage(EMessageSeverity::Info,
|
||||
FText::FromString(FString::Printf(TEXT("Successfully updated %s"), *SourceFile)));
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Info,
|
||||
FText::FromString(FString::Printf(TEXT("No changes were required to %s"), *SourceFile)));
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(FString::Printf(TEXT("Error opening source asset %s"), *SourceFile)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(
|
||||
FString::Printf(TEXT("No source files associated with asset %s"), *Script->GetName())));
|
||||
}
|
||||
}
|
||||
|
||||
bool FSUDSEditorScriptTools::WriteBackTextIDsFromNodes(const TArray<USUDSScriptNode*> Nodes, TArray<FString>& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger)
|
||||
{
|
||||
bool bAnyChanges = false;
|
||||
// For each speaker line, set line and choice edge, use source line no to append text ID
|
||||
for (const auto& N : Nodes)
|
||||
{
|
||||
if (N->GetNodeType() == ESUDSScriptNodeType::Text)
|
||||
{
|
||||
if (const auto* TN = Cast<USUDSScriptNodeText>(N))
|
||||
{
|
||||
bAnyChanges = WriteBackTextID(TN->GetText(), TN->GetSourceLineNo(), Lines, NameForErrors, Logger) || bAnyChanges;
|
||||
}
|
||||
}
|
||||
else if (N->GetNodeType() == ESUDSScriptNodeType::SetVariable)
|
||||
{
|
||||
if (const auto* SN = Cast<USUDSScriptNodeSet>(N))
|
||||
{
|
||||
if (SN->GetExpression().IsTextLiteral())
|
||||
{
|
||||
FText Literal = SN->GetExpression().GetTextLiteralValue();
|
||||
|
||||
bAnyChanges = WriteBackTextID(Literal, SN->GetSourceLineNo(), Lines, NameForErrors, Logger) || bAnyChanges;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else if (N->GetNodeType() == ESUDSScriptNodeType::Choice)
|
||||
{
|
||||
// Edges
|
||||
for (auto& Edge : N->GetEdges())
|
||||
{
|
||||
bAnyChanges = WriteBackTextID(Edge.GetText(), Edge.GetSourceLineNo(), Lines, NameForErrors, Logger) || bAnyChanges;
|
||||
}
|
||||
}
|
||||
else if (N->GetNodeType() == ESUDSScriptNodeType::Gosub)
|
||||
{
|
||||
if (const auto* GN = Cast<USUDSScriptNodeGosub>(N))
|
||||
// Need to write back Gosub IDs so that saved return stacks work
|
||||
bAnyChanges = WriteBackGosubID(GN->GetGosubID(), GN->GetSourceLineNo(), Lines, NameForErrors, Logger) || bAnyChanges;
|
||||
}
|
||||
}
|
||||
|
||||
return bAnyChanges;
|
||||
}
|
||||
|
||||
bool FSUDSEditorScriptTools::WriteBackTextID(const FText& AssetText, int LineNo, TArray<FString>& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger)
|
||||
{
|
||||
if (AssetText.IsEmpty())
|
||||
return false;
|
||||
|
||||
// Line numbers are 1-based
|
||||
const int Idx = LineNo - 1;
|
||||
|
||||
if (!Lines.IsValidIndex(Idx))
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(FString::Printf(
|
||||
TEXT("Cannot write back TextID to '%s', source line number %d is invalid (Text: '%s')"),
|
||||
*NameForErrors,
|
||||
LineNo,
|
||||
*AssetText.ToString())));
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString& SourceLine = Lines[Idx];
|
||||
const FString TextID = SUDS_GET_TEXT_KEY(AssetText);
|
||||
FString ExistingTextID;
|
||||
int ExistingNum;
|
||||
FStringView SourceLineView(SourceLine);
|
||||
const bool bFoundExisting = FSUDSScriptImporter::RetrieveTextIDFromLine(SourceLineView, ExistingTextID, ExistingNum);
|
||||
|
||||
// Existing TextID - replace if already there
|
||||
if (!bFoundExisting || ExistingTextID != TextID)
|
||||
{
|
||||
if (TextIDCheckMatch(AssetText, SourceLine))
|
||||
{
|
||||
FString Prefix(SourceLineView);
|
||||
FString UpdatedLine = FString::Printf(TEXT("%s %s"), *Prefix, *TextID);
|
||||
Lines[Idx] = UpdatedLine;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(FString::Printf(TEXT(
|
||||
"Tried to set TextID on line %d of %s but source file did not contain expected text '%s'"
|
||||
),
|
||||
LineNo,
|
||||
*NameForErrors,
|
||||
*AssetText.ToString())));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Same, no need to change
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
bool FSUDSEditorScriptTools::TextIDCheckMatch(const FText& AssetText, const FString& SourceLine)
|
||||
{
|
||||
// Text from our asset must be present in the text in the source line
|
||||
// However, in case this is a multi-line string, take the first line only
|
||||
FString AssetStr = AssetText.ToString();
|
||||
{
|
||||
FString L, R;
|
||||
if (AssetStr.Split("\n", &L, &R))
|
||||
{
|
||||
AssetStr = L.TrimStartAndEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// Bear in mind that this line might be a text line, a choice or a set line
|
||||
// therefore we only check if the source line contains the asset text
|
||||
|
||||
return SourceLine.Contains(AssetStr);
|
||||
|
||||
}
|
||||
|
||||
bool FSUDSEditorScriptTools::WriteBackGosubID(const FString& GosubID,
|
||||
int LineNo,
|
||||
TArray<FString>& Lines,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger& Logger)
|
||||
{
|
||||
|
||||
// Line numbers are 1-based
|
||||
const int Idx = LineNo - 1;
|
||||
|
||||
if (!Lines.IsValidIndex(Idx))
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(FString::Printf(
|
||||
TEXT("Cannot write back GosubID to '%s', source line number %d is invalid"),
|
||||
*NameForErrors,
|
||||
LineNo)));
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString& SourceLine = Lines[Idx];
|
||||
FString ExistingID;
|
||||
int ExistingNum;
|
||||
FStringView SourceLineView(SourceLine);
|
||||
const bool bFoundID = FSUDSScriptImporter::RetrieveGosubIDFromLine(SourceLineView, ExistingID, ExistingNum);
|
||||
|
||||
if (!bFoundID || ExistingID != GosubID)
|
||||
{
|
||||
// Check it's a gosub line
|
||||
if (SourceLine.Contains(TEXT("gosub")) || SourceLine.Contains(TEXT("go sub")))
|
||||
{
|
||||
FString Prefix(SourceLineView);
|
||||
FString UpdatedLine = FString::Printf(TEXT("%s %s"), *Prefix, *GosubID);
|
||||
Lines[Idx] = UpdatedLine;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(FString::Printf(TEXT(
|
||||
"Tried to set GosubID on line %d of %s but source file did not have a gosubu on that line"
|
||||
),
|
||||
LineNo,
|
||||
*NameForErrors)));
|
||||
return false;
|
||||
|
||||
}
|
||||
}
|
||||
// Same, no need to change
|
||||
return false;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSEditorSettings.h"
|
||||
#include "Misc/Paths.h"
|
||||
|
||||
bool USUDSEditorSettings::ShouldGenerateVoiceAssets(const FString& PackagePath) const
|
||||
{
|
||||
if (AlwaysAutoGenerateVoiceOverAssetsOnImport)
|
||||
return true;
|
||||
|
||||
for (auto Dir : DirectoriesToAutoGenerateVoiceOverAssetsOnImport)
|
||||
{
|
||||
if (FPaths::IsUnderDirectory(PackagePath, Dir.Path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FString USUDSEditorSettings::GetVoiceOutputDir(const FString& PackagePath, const FString& ScriptName) const
|
||||
{
|
||||
return GetOutputDir(DialogueVoiceAssetLocation, DialogueVoiceAssetSharedDir.Path, PackagePath, ScriptName);
|
||||
}
|
||||
|
||||
FString USUDSEditorSettings::GetWaveOutputDir(const FString& PackagePath, const FString& ScriptName) const
|
||||
{
|
||||
return GetOutputDir(DialogueWaveAssetLocation, DialogueWaveAssetSharedDir.Path, PackagePath, ScriptName);
|
||||
}
|
||||
|
||||
FString USUDSEditorSettings::GetOutputDir(ESUDSAssetLocation Location,
|
||||
const FString& SharedPath,
|
||||
const FString& PackagePath,
|
||||
const FString& ScriptName)
|
||||
{
|
||||
switch(Location)
|
||||
{
|
||||
default:
|
||||
case ESUDSAssetLocation::SharedDirectory:
|
||||
return SharedPath;
|
||||
case ESUDSAssetLocation::SharedDirectorySubdir:
|
||||
return FPaths::Combine(SharedPath, ScriptName);
|
||||
case ESUDSAssetLocation::ScriptDirectory:
|
||||
return PackagePath;
|
||||
case ESUDSAssetLocation::ScriptDirectorySubdir:
|
||||
return FPaths::Combine(PackagePath, ScriptName);
|
||||
}
|
||||
}
|
||||
1567
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.cpp
Normal file
1567
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.cpp
Normal file
File diff suppressed because it is too large
Load Diff
343
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.h
Normal file
343
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.h
Normal file
@@ -0,0 +1,343 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSValue.h"
|
||||
#include "Framework/Commands/Commands.h"
|
||||
#include "Framework/Text/BaseTextLayoutMarshaller.h"
|
||||
#include "Toolkits/AssetEditorToolkit.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "Widgets/Views/STableRow.h"
|
||||
|
||||
class SMultiLineEditableTextBox;
|
||||
struct FSUDSValue;
|
||||
class USUDSDialogue;
|
||||
class USUDSScript;
|
||||
|
||||
#define LOCTEXT_NAMESPACE "SUDS"
|
||||
|
||||
class FSUDSToolbarCommands
|
||||
: public TCommands<FSUDSToolbarCommands>
|
||||
{
|
||||
public:
|
||||
|
||||
FSUDSToolbarCommands()
|
||||
: TCommands<FSUDSToolbarCommands>(
|
||||
"SUDS",
|
||||
LOCTEXT("SUDS", "Steves Unreal Dialogue System"),
|
||||
NAME_None,
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0
|
||||
FAppStyle::GetAppStyleSetName()
|
||||
#else
|
||||
FEditorStyle::GetStyleSetName()
|
||||
#endif
|
||||
)
|
||||
{ }
|
||||
|
||||
public:
|
||||
|
||||
// TCommands interface
|
||||
|
||||
virtual void RegisterCommands() override
|
||||
{
|
||||
UI_COMMAND(StartDialogue, "Start Dialogue", "Start/restart dialogue", EUserInterfaceActionType::Button, FInputChord());
|
||||
UI_COMMAND(WriteBackTextIDs, "Write String Keys", "Write string keys back to script source to stabilise for localisation / voice asset links", EUserInterfaceActionType::Button, FInputChord());
|
||||
UI_COMMAND(GenerateVOAssets, "Generate Voice Assets", "Generate DialogueVoice and DialogueWave assets for VO", EUserInterfaceActionType::Button, FInputChord());
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
TSharedPtr<FUICommandInfo> StartDialogue;
|
||||
TSharedPtr<FUICommandInfo> WriteBackTextIDs;
|
||||
TSharedPtr<FUICommandInfo> GenerateVOAssets;
|
||||
};
|
||||
|
||||
|
||||
class FSUDSEditorOutputRow
|
||||
{
|
||||
public:
|
||||
FText Prefix;
|
||||
FText Line;
|
||||
FSlateColor PrefixColour;
|
||||
FSlateColor LineColour;
|
||||
FSlateColor BgColour;
|
||||
|
||||
FSUDSEditorOutputRow(const FText& InPrefix,
|
||||
const FText& InLine,
|
||||
const FSlateColor& InPrefixColour,
|
||||
const FSlateColor& InLineColour,
|
||||
const FSlateColor& InBgColour) :
|
||||
Prefix(InPrefix),
|
||||
Line(InLine),
|
||||
PrefixColour(InPrefixColour),
|
||||
LineColour(InLineColour),
|
||||
BgColour(InBgColour)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
class SSUDSEditorOutputItem : public SMultiColumnTableRow< TSharedPtr<FString> >
|
||||
{
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SSUDSEditorOutputItem)
|
||||
{}
|
||||
SLATE_ARGUMENT(float, InitialWidth)
|
||||
SLATE_ARGUMENT(FText, Prefix)
|
||||
SLATE_ARGUMENT(FText, Line)
|
||||
SLATE_ARGUMENT(FSlateColor, PrefixColour)
|
||||
SLATE_ARGUMENT(FSlateColor, LineColour)
|
||||
SLATE_ARGUMENT(FSlateColor, BgColour)
|
||||
|
||||
SLATE_END_ARGS()
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* Construct this widget. Called by the SNew() Slate macro.
|
||||
*
|
||||
* @param InArgs Declaration used by the SNew() macro to construct this widget.
|
||||
* @oaram InOwnerTableView The owner table into which this row is being placed.
|
||||
*/
|
||||
void Construct( const FArguments& InArgs, const TSharedRef<STableViewBase>& InOwnerTableView );
|
||||
|
||||
/**
|
||||
* Generates the widget for the specified column.
|
||||
*
|
||||
* @param ColumnName The name of the column to generate the widget for.
|
||||
* @return The widget.
|
||||
*/
|
||||
virtual TSharedRef<SWidget> GenerateWidgetForColumn( const FName& ColumnName ) override;
|
||||
protected:
|
||||
float InitialWidth = 70;
|
||||
FText Prefix;
|
||||
FText Line;
|
||||
FSlateColor PrefixColour;
|
||||
FSlateColor LineColour;
|
||||
};
|
||||
|
||||
|
||||
class FSUDSEditorVariableRow
|
||||
{
|
||||
public:
|
||||
FName Name;
|
||||
FSUDSValue Value;
|
||||
bool bIsManualOverride;
|
||||
|
||||
FSUDSEditorVariableRow(const FName& InName, const FSUDSValue& InValue, bool bIsManual) : Name(InName),
|
||||
Value(InValue),
|
||||
bIsManualOverride(bIsManual)
|
||||
{
|
||||
}
|
||||
|
||||
friend bool operator<(const FSUDSEditorVariableRow& Lhs, const FSUDSEditorVariableRow& RHS)
|
||||
{
|
||||
return Lhs.Name.LexicalLess(RHS.Name);
|
||||
}
|
||||
|
||||
friend bool operator<(const TSharedPtr<FSUDSEditorVariableRow>& Lhs, const TSharedPtr<FSUDSEditorVariableRow>& RHS)
|
||||
{
|
||||
return *Lhs < *RHS;
|
||||
}
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
class SSUDSEditorVariableItem : public SMultiColumnTableRow< TSharedPtr<FString> >
|
||||
{
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SSUDSEditorVariableItem)
|
||||
{}
|
||||
SLATE_ARGUMENT(float, InitialWidth)
|
||||
SLATE_ARGUMENT(FName, VariableName)
|
||||
SLATE_ARGUMENT(FSUDSValue, VariableValue)
|
||||
SLATE_ARGUMENT(bool, bIsManualOverride)
|
||||
SLATE_ARGUMENT(class FSUDSEditorToolkit*, Parent)
|
||||
SLATE_END_ARGS()
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* Construct this widget. Called by the SNew() Slate macro.
|
||||
*
|
||||
* @param InArgs Declaration used by the SNew() macro to construct this widget.
|
||||
* @oaram InOwnerTableView The owner table into which this row is being placed.
|
||||
*/
|
||||
void Construct( const FArguments& InArgs, const TSharedRef<STableViewBase>& InOwnerTableView );
|
||||
|
||||
public:
|
||||
/**
|
||||
* Generates the widget for the specified column.
|
||||
*
|
||||
* @param ColumnName The name of the column to generate the widget for.
|
||||
* @return The widget.
|
||||
*/
|
||||
virtual TSharedRef<SWidget> GenerateWidgetForColumn( const FName& ColumnName ) override;
|
||||
protected:
|
||||
float InitialWidth = 70;
|
||||
FName VariableName;
|
||||
FSUDSValue VariableValue;
|
||||
bool bIsManualOverride = false;
|
||||
class FSUDSEditorToolkit* Parent = nullptr;
|
||||
|
||||
TSharedRef<class SWidget> GetGenderMenu();
|
||||
void OnGenderSelected(ETextGender TextGender);
|
||||
virtual FVector2D ComputeDesiredSize(float) const override;
|
||||
|
||||
};
|
||||
|
||||
struct FSUDSTraceLogMessage
|
||||
{
|
||||
TSharedRef<FString> Message;
|
||||
FName Category;
|
||||
FSlateColor Colour;
|
||||
|
||||
FSUDSTraceLogMessage(FName InCategory, const FString& InMessage, const FSlateColor& InColour)
|
||||
: Message(MakeShared<FString>(InMessage))
|
||||
, Category(InCategory)
|
||||
, Colour(InColour)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
class FSUDSTraceLogMarshaller : public FBaseTextLayoutMarshaller
|
||||
{
|
||||
public:
|
||||
|
||||
FSUDSTraceLogMarshaller();
|
||||
|
||||
// ITextLayoutMarshaller
|
||||
virtual void SetText(const FString& SourceString, FTextLayout& TargetTextLayout) override;
|
||||
virtual void GetText(FString& TargetString, const FTextLayout& SourceTextLayout) override;
|
||||
|
||||
void AppendMessage(FName InCategory, int LineNo, const FString& Message, const FSlateColor& Colour);
|
||||
void ClearMessages();
|
||||
protected:
|
||||
TArray< TSharedPtr<FSUDSTraceLogMessage> > Messages;
|
||||
};
|
||||
|
||||
// Trace log widget, really just so we can tick and automatically scroll to the end
|
||||
class SSUDSTraceLog : public SCompoundWidget
|
||||
{
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SSUDSTraceLog)
|
||||
{}
|
||||
SLATE_END_ARGS()
|
||||
|
||||
SSUDSTraceLog() : bIsUserScrolled(false) {}
|
||||
|
||||
void Construct(const FArguments& InArgs);
|
||||
virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override;
|
||||
|
||||
void AppendMessage(FName InCategory, int LineNo, const FString& Message, const FSlateColor& Colour);
|
||||
void ClearMessages();
|
||||
void ScrollToEnd();
|
||||
|
||||
protected:
|
||||
bool bIsUserScrolled;
|
||||
TSharedPtr<FSUDSTraceLogMarshaller> TraceLogMarshaller;
|
||||
TSharedPtr<SMultiLineEditableTextBox> TraceLogTextBox;
|
||||
|
||||
void OnUserScrolled(float X);
|
||||
|
||||
};
|
||||
|
||||
class FSUDSEditorToolkit : public FAssetEditorToolkit
|
||||
{
|
||||
public:
|
||||
void InitEditor(const TArray<UObject*>& InObjects);
|
||||
|
||||
virtual void RegisterTabSpawners(const TSharedRef<FTabManager>& InTabManager) override;
|
||||
virtual void UnregisterTabSpawners(const TSharedRef<FTabManager>& InTabManager) override;
|
||||
|
||||
virtual FText GetBaseToolkitName() const override;
|
||||
virtual FName GetToolkitFName() const override;
|
||||
virtual FLinearColor GetWorldCentricTabColorScale() const override;
|
||||
virtual FString GetWorldCentricTabPrefix() const override;
|
||||
void UserEditVariable(const FName& Name, FSUDSValue Value);
|
||||
void DeleteVariable(const FName& Name);
|
||||
|
||||
protected:
|
||||
virtual void OnClose() override;
|
||||
|
||||
private:
|
||||
USUDSScript* Script = nullptr;
|
||||
USUDSDialogue* Dialogue = nullptr;
|
||||
float VarColumnWidth = 120;
|
||||
float PrefixColumnWidth = 100;
|
||||
FName StartLabel = NAME_None;
|
||||
bool bResetVarsOnStart = true;
|
||||
FDelegateHandle ReimportDelegateHandle;
|
||||
// FSUDSEditorDialogueRow needs to held by a TSharedPtr for SListView
|
||||
TSharedPtr<SListView<TSharedPtr<FSUDSEditorOutputRow>>> OutputListView;
|
||||
TArray<TSharedPtr<FSUDSEditorOutputRow>> OutputRows;
|
||||
TSharedPtr<SVerticalBox> ChoicesBox;
|
||||
TSharedPtr<SListView<TSharedPtr<FSUDSEditorVariableRow>>> VariablesListView;
|
||||
TArray<TSharedPtr<FSUDSEditorVariableRow>> VariableRows;
|
||||
TSharedPtr<SSUDSTraceLog> TraceLog;
|
||||
TMap<FName, FSUDSValue> ManualOverrideVariables;
|
||||
|
||||
static FName DialogueOutputTabName;
|
||||
static FName DetailsTabName;
|
||||
static FName VariablesTabName;
|
||||
static FName LogTabName;
|
||||
|
||||
|
||||
const FSlateColor SpeakerColour = FLinearColor(1.0f, 1.0f, 0.6f, 1.0f);
|
||||
const FSlateColor ChoiceColour = FLinearColor(0.4f, 1.0f, 0.4f, 1.0f);
|
||||
const FSlateColor EventColour = FLinearColor(0.2f, 0.6f, 1.0f, 1.0f);
|
||||
const FSlateColor VarSetColour = FLinearColor(0.8f, 0.6f, 0.9f, 1.0f);
|
||||
const FSlateColor VarEditColour = FLinearColor(0.6f, 0.3f, 1.0f, 1.0f);
|
||||
const FSlateColor StartColour = FLinearColor(1.0f, 0.5f, 0.0f, 1.0f);
|
||||
const FSlateColor FinishColour = FLinearColor(1.0f, 0.3f, 0.3f, 1.0f);
|
||||
const FSlateColor SelectColour = FLinearColor(1.0f, 0.0f, 0.5f, 1.0f);
|
||||
const FSlateColor RowBgColour1 = FLinearColor(0.15f, 0.15f, 0.15f, 1.0f);
|
||||
const FSlateColor RowBgColour2 = FLinearColor(0.3f, 0.3f, 0.3f, 1.0f);
|
||||
|
||||
void ExtendToolbar(FToolBarBuilder& ToolbarBuilder, TWeakPtr<SDockTab> Tab);
|
||||
TSharedRef<class SWidget> GetStartLabelMenu();
|
||||
FText GetSelectedStartLabel() const;
|
||||
void OnStartLabelSelected(FName Label);
|
||||
ECheckBoxState GetResetVarsCheckState() const;
|
||||
void OnResetVarsCheckStateChanged(ECheckBoxState NewState);
|
||||
FReply AddVariableClicked();
|
||||
void UpdateVariables();
|
||||
void EnsureTabsVisible();
|
||||
void StartDialogue();
|
||||
void DestroyDialogue();
|
||||
void UpdateOutput();
|
||||
void UpdateChoiceButtons();
|
||||
void AddOutputRow(const FText& Prefix, const FText& Line, const FSlateColor& PrefixColour, const FSlateColor& LineColour);
|
||||
void AddTraceLogRow(const FName& Category, int SourceLineNo, const FString& Message);
|
||||
|
||||
void AddDialogueStep(const FName& Category, int SourceLineNo, const FText& Description, const FText& Prefix);
|
||||
|
||||
void OnDialogueChoice(USUDSDialogue* Dialogue, int ChoiceIndex, int LineNo);
|
||||
void OnDialogueEvent(USUDSDialogue* Dialogue, FName EventName, const TArray<FSUDSValue>& Args, int LineNo);
|
||||
void OnDialogueFinished(USUDSDialogue* Dialogue);
|
||||
void OnDialogueProceeding(USUDSDialogue* Dialogue);
|
||||
void OnDialogueStarting(USUDSDialogue* Dialogue, FName LabelName);
|
||||
void OnDialogueSpeakerLine(USUDSDialogue* Dialogue, int LineNo);
|
||||
void OnDialogueSetVar(USUDSDialogue* Dialogue, FName VariableName, const FSUDSValue& ToValue, const FString& ExpressionStr, int LineNo);
|
||||
void OnDialogueUserEditedVar(USUDSDialogue* Dialogue, FName VariableName, const FSUDSValue& ToValue);
|
||||
void OnDialogueSelectEval(USUDSDialogue* Dialogue, const FString& ExpressionStr, bool bSuccess, int LineNo);
|
||||
void OnDialogueRandomEval(USUDSDialogue* Dialogue, const int RandomOutcome, int LineNo);
|
||||
|
||||
TSharedRef<ITableRow> OnGenerateRowForOutput(
|
||||
TSharedPtr<FSUDSEditorOutputRow> FsudsEditorDialogueRow,
|
||||
const TSharedRef<STableViewBase>& TableViewBase);
|
||||
TSharedRef<ITableRow> OnGenerateRowForVariable(TSharedPtr<FSUDSEditorVariableRow> Row,
|
||||
const TSharedRef<STableViewBase>& Table);
|
||||
FSlateColor GetColourForCategory(const FName& Category);
|
||||
void OnPostReimport(UObject* Object, bool bSuccess);
|
||||
void Clear();
|
||||
void WriteBackTextIDs();
|
||||
void GenerateVOAssets();
|
||||
|
||||
};
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,381 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSEditorVoiceOverTools.h"
|
||||
|
||||
#include "ObjectTools.h"
|
||||
#include "PackageTools.h"
|
||||
#include "SUDSEditorSettings.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "Internationalization/Regex.h"
|
||||
#include "Sound/DialogueVoice.h"
|
||||
#include "Sound/DialogueWave.h"
|
||||
#include "Sound/SoundWave.h"
|
||||
|
||||
|
||||
void FSUDSEditorVoiceOverTools::GenerateAssets(USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger)
|
||||
{
|
||||
// First check for problems
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
bool bError = false;
|
||||
if ((Settings->DialogueVoiceAssetLocation == ESUDSAssetLocation::SharedDirectory || Settings->
|
||||
DialogueVoiceAssetLocation == ESUDSAssetLocation::SharedDirectorySubdir) &&
|
||||
(Settings->DialogueVoiceAssetSharedDir.Path.IsEmpty() ||
|
||||
!FPackageName::IsValidPath(Settings->DialogueVoiceAssetSharedDir.Path)))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Dialogue Voice assets are set to generate to a shared dir, but the dir '%s' is not valid."),
|
||||
*Settings->DialogueVoiceAssetSharedDir.Path);
|
||||
bError = true;
|
||||
}
|
||||
if ((Settings->DialogueWaveAssetLocation == ESUDSAssetLocation::SharedDirectory || Settings->
|
||||
DialogueWaveAssetLocation == ESUDSAssetLocation::SharedDirectorySubdir) &&
|
||||
(Settings->DialogueWaveAssetSharedDir.Path.IsEmpty() ||
|
||||
!FPackageName::IsValidPath(Settings->DialogueWaveAssetSharedDir.Path)))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Dialogue Wave assets are set to generate to a shared dir, but the dir '%s' is not valid."),
|
||||
*Settings->DialogueWaveAssetSharedDir.Path);
|
||||
bError = true;
|
||||
}
|
||||
if (bError) { return; }
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error, TEXT("Unable to generate voice assets, settings not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
TMap<FString, UDialogueVoice*> UnsavedVoices;
|
||||
GenerateVoiceAssets(Script, Flags, Logger, UnsavedVoices);
|
||||
GenerateWaveAssets(Script, Flags, UnsavedVoices, Logger);
|
||||
}
|
||||
|
||||
void FSUDSEditorVoiceOverTools::GenerateVoiceAssets(USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger, TMap<FString, UDialogueVoice*> &OutCreatedVoices)
|
||||
{
|
||||
auto Registry = &FModuleManager::LoadModuleChecked<FAssetRegistryModule>(AssetRegistryConstants::ModuleName).Get();
|
||||
|
||||
FString Prefix;
|
||||
FString ParentDir;
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
Prefix = Settings->DialogueVoiceAssetPrefix;
|
||||
ParentDir = GetVoiceOutputDir(Script);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error, TEXT("Unable to generate voice assets, settings not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto Speaker : Script->GetSpeakers())
|
||||
{
|
||||
// All Dialogue Voice assets will be created in their own package
|
||||
FString PackageName;
|
||||
FString AssetName;
|
||||
if (GetSpeakerVoiceAssetNames(Script, Speaker, PackageName, AssetName))
|
||||
{
|
||||
if (FPackageName::DoesPackageExist(*PackageName))
|
||||
{
|
||||
// Package already exists, so try and import over the top of it, if it doesn't already have a source file path
|
||||
TArray<FAssetData> Assets;
|
||||
if (Registry->GetAssetsByPackageName(*PackageName, Assets))
|
||||
{
|
||||
if (Assets.Num() > 0)
|
||||
{
|
||||
if (!Assets[0].GetAsset()->IsA(UDialogueVoice::StaticClass()))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Asset %s already exists but is not a Dialogue Voice! Cannot replace, please move aside and re-import script."),
|
||||
*PackageName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Make sure we still link the existing voice
|
||||
Script->SetSpeakerVoice(Speaker, Cast<UDialogueVoice>(Assets[0].GetAsset()));
|
||||
Script->MarkPackageDirty();
|
||||
}
|
||||
// Either way nothing more to do
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we got here then Dialogue Voice didn't exist (although package might have)
|
||||
// It's safe to call CreatePackage either way, it'll return the existing one if needed
|
||||
UPackage* Package = CreatePackage(*PackageName);
|
||||
if (!ensure(Package))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT("Failed to create/retrieve package for voice asset %s"),
|
||||
*PackageName);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Logger->Logf(ELogVerbosity::Display, TEXT("Creating voice asset %s"), *PackageName);
|
||||
|
||||
UDialogueVoice* NewVoiceAsset = NewObject<UDialogueVoice>(Package, FName(AssetName), Flags);
|
||||
OutCreatedVoices.Add(Speaker, NewVoiceAsset);
|
||||
// there's nothing else to create here, voice is mostly a placeholder with the rest set up later by user
|
||||
FAssetRegistryModule::AssetCreated(NewVoiceAsset);
|
||||
|
||||
Script->SetSpeakerVoice(Speaker, NewVoiceAsset);
|
||||
Script->MarkPackageDirty();
|
||||
|
||||
Package->FullyLoad();
|
||||
NewVoiceAsset->MarkPackageDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FSUDSEditorVoiceOverTools::GetSpeakerVoicePackageName(USUDSScript* Script,
|
||||
const FString& SpeakerID,
|
||||
FString& OutPackageName)
|
||||
{
|
||||
FString NotUsed;
|
||||
return GetSpeakerVoiceAssetNames(Script, SpeakerID, OutPackageName, NotUsed);
|
||||
}
|
||||
|
||||
bool FSUDSEditorVoiceOverTools::GetSpeakerVoiceAssetNames(USUDSScript* Script,
|
||||
const FString& SpeakerID,
|
||||
FString& OutPackageName,
|
||||
FString& OutAssetName)
|
||||
{
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
FString Prefix = Settings->DialogueVoiceAssetPrefix;
|
||||
FString ParentDir = GetVoiceOutputDir(Script);
|
||||
|
||||
OutAssetName = FString::Printf(TEXT("%s%s"), *Prefix, *ObjectTools::SanitizeObjectName(SpeakerID));
|
||||
OutPackageName = UPackageTools::SanitizePackageName(ParentDir / OutAssetName);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
UDialogueVoice* FSUDSEditorVoiceOverTools::FindSpeakerVoice(USUDSScript* Script,
|
||||
const FString& SpeakerID)
|
||||
{
|
||||
FString PackageName;
|
||||
if (GetSpeakerVoicePackageName(Script, SpeakerID, PackageName))
|
||||
{
|
||||
|
||||
auto Registry = &FModuleManager::LoadModuleChecked<FAssetRegistryModule>(AssetRegistryConstants::ModuleName).Get();
|
||||
|
||||
if (FPackageName::DoesPackageExist(*PackageName))
|
||||
{
|
||||
// Package already exists, so try and import over the top of it, if it doesn't already have a source file path
|
||||
TArray<FAssetData> Assets;
|
||||
if (Registry->GetAssetsByPackageName(*PackageName, Assets))
|
||||
{
|
||||
if (Assets.Num() > 0)
|
||||
{
|
||||
if (Assets[0].GetAsset()->IsA(UDialogueVoice::StaticClass()))
|
||||
{
|
||||
return Cast<UDialogueVoice>(Assets[0].GetAsset());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void FSUDSEditorVoiceOverTools::GenerateWaveAssets(USUDSScript* Script, EObjectFlags Flags, TMap<FString, UDialogueVoice*> UnsavedVoices, FSUDSMessageLogger* Logger)
|
||||
{
|
||||
// We need to identify specific lines of dialogue to specify a wave asset to go with them, which means we
|
||||
// need to use the Text ID, just like we do for translations. This means that really, you shouldn't start to
|
||||
// assign sounds to wave assets until you've written the text IDs back to the script
|
||||
auto Registry = &FModuleManager::LoadModuleChecked<FAssetRegistryModule>(AssetRegistryConstants::ModuleName).Get();
|
||||
|
||||
FString Prefix;
|
||||
FString ParentDir;
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
Prefix = FString::Printf(TEXT("%s%s_"), *Settings->DialogueWaveAssetPrefix, *GetScriptNameAsPrefix(Script));
|
||||
ParentDir = GetWaveOutputDir(Script);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error, TEXT("Unable to generate wave assets, settings not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto Node : Script->GetNodes())
|
||||
{
|
||||
if (auto Line = Cast<USUDSScriptNodeText>(Node))
|
||||
{
|
||||
// We use the TextID to identify a specific line, just like for translation
|
||||
FString TextID = Line->GetTextID();
|
||||
|
||||
// Remove the '@' characters from the text ID for creating the asset name, they just turn into excessive '_'s
|
||||
const FRegexPattern TextIDPattern(TEXT("\\@([0-9a-fA-F]+)\\@"));
|
||||
FRegexMatcher TextIDRegex(TextIDPattern, TextID);
|
||||
if (TextIDRegex.FindNext())
|
||||
{
|
||||
TextID = TextIDRegex.GetCaptureGroup(1);
|
||||
}
|
||||
|
||||
// All Dialogue Voice assets will be created in their own package
|
||||
const FString SanitizedName = FString::Printf(TEXT("%s%s"),
|
||||
*Prefix,
|
||||
*ObjectTools::SanitizeObjectName(TextID));
|
||||
const FString PackageName = UPackageTools::SanitizePackageName(ParentDir / SanitizedName);
|
||||
|
||||
UDialogueWave* WaveAsset = nullptr;
|
||||
if (FPackageName::DoesPackageExist(*PackageName))
|
||||
{
|
||||
// Package already exists, so try and import over the top of it, if it doesn't already have a source file path
|
||||
TArray<FAssetData> Assets;
|
||||
if (Registry->GetAssetsByPackageName(*PackageName, Assets))
|
||||
{
|
||||
if (Assets.Num() > 0)
|
||||
{
|
||||
if (!Assets[0].GetAsset()->IsA(UDialogueWave::StaticClass()))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Asset %s already exists but is not a Dialogue Wave! Cannot replace, please move aside and re-import script."),
|
||||
*PackageName);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
WaveAsset = Cast<UDialogueWave>(Assets[0].GetAsset());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UDialogueVoice* SpeakerVoice = nullptr;
|
||||
// Finding speaker assets via FAssetRegistryModule does not work on unsaved assets, so we need to check
|
||||
// the unsaved voices that have been created in the previous step first
|
||||
if (auto pSV = UnsavedVoices.Find(Line->GetSpeakerID()))
|
||||
{
|
||||
SpeakerVoice = *pSV;
|
||||
}
|
||||
if (!SpeakerVoice)
|
||||
{
|
||||
// Now try assets
|
||||
SpeakerVoice = FindSpeakerVoice(Script, Line->GetSpeakerID());
|
||||
}
|
||||
if (!SpeakerVoice)
|
||||
{
|
||||
FString SpeakerPackageName;
|
||||
GetSpeakerVoicePackageName(Script, Line->GetSpeakerID(), SpeakerPackageName);
|
||||
Logger->Logf(ELogVerbosity::Error, TEXT("Error: speaker voice asset for %s is missing, expected to be at %s"), *Line->GetSpeakerID(), *SpeakerPackageName);
|
||||
continue;
|
||||
}
|
||||
|
||||
UPackage* Package = CreatePackage(*PackageName);
|
||||
if (!ensure(Package))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT("Failed to create/retrieve package for wave asset %s"),
|
||||
*PackageName);
|
||||
}
|
||||
else
|
||||
{
|
||||
FString NativeLangText = Line->GetText().ToString();
|
||||
USoundWave* SoundWave = nullptr;
|
||||
if (!WaveAsset)
|
||||
{
|
||||
//Logger->Logf(ELogVerbosity::Display, TEXT("Creating wave asset %s"), *PackageName);
|
||||
WaveAsset = NewObject<UDialogueWave>(Package, FName(SanitizedName), Flags);
|
||||
FAssetRegistryModule::AssetCreated(WaveAsset);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We will always update the existing asset, but will warn if the text is changing
|
||||
if (WaveAsset->ContextMappings.Num() > 0)
|
||||
{
|
||||
auto Mapping = WaveAsset->ContextMappings[0];
|
||||
if (IsValid(Mapping.SoundWave))
|
||||
{
|
||||
SoundWave = Mapping.SoundWave;
|
||||
// This is OK if the context is what we were going to create anyway, we'll just leave it alone
|
||||
if (Mapping.Context.Speaker != SpeakerVoice ||
|
||||
WaveAsset->SpokenText != NativeLangText)
|
||||
{
|
||||
// No good, user has assigned sound to this dialogue wave, but we need to change it to
|
||||
// a different speaker / line
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Dialogue Wave %s has an assigned Sound Wave but the speaker or text has changed. You should update the sound wave!"),
|
||||
*PackageName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set spoken text - native language only. Dialogue Wave handles the localisation of this separately,
|
||||
// we can't link it to our String Table unfortunately.
|
||||
WaveAsset->SpokenText = NativeLangText;
|
||||
// Set dialogue context
|
||||
// We need a Speaker Voice, we'll leave the sound and targets blank
|
||||
WaveAsset->UpdateContext(WaveAsset->ContextMappings[0], SoundWave, SpeakerVoice, TArray<UDialogueVoice*>());
|
||||
|
||||
// Now assign the wave to the line
|
||||
Line->SetWave(WaveAsset);
|
||||
Script->MarkPackageDirty();
|
||||
|
||||
Package->FullyLoad();
|
||||
WaveAsset->MarkPackageDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FString FSUDSEditorVoiceOverTools::GetVoiceOutputDir(USUDSScript* Script)
|
||||
{
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
const FString PackagePath = FPackageName::GetLongPackagePath(Script->GetOuter()->GetOutermost()->GetPathName());
|
||||
const FString ScriptPrefix = GetScriptNameAsPrefix(Script);
|
||||
return Settings->GetVoiceOutputDir(PackagePath, ScriptPrefix);
|
||||
}
|
||||
return FString();
|
||||
}
|
||||
|
||||
FString FSUDSEditorVoiceOverTools::GetWaveOutputDir(USUDSScript* Script)
|
||||
{
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
const FString PackagePath = FPackageName::GetLongPackagePath(Script->GetOuter()->GetOutermost()->GetPathName());
|
||||
const FString ScriptPrefix = GetScriptNameAsPrefix(Script);
|
||||
return Settings->GetWaveOutputDir(PackagePath, ScriptPrefix);
|
||||
}
|
||||
return FString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
FString FSUDSEditorVoiceOverTools::GetScriptNameAsPrefix(USUDSScript* Script)
|
||||
{
|
||||
FString Name = Script->GetName();
|
||||
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
if (Settings->StripScriptPrefixesWhenGeneratingNames)
|
||||
{
|
||||
int32 Index = INDEX_NONE;
|
||||
if (Name.FindChar('_', Index))
|
||||
{
|
||||
if (Index < Name.Len() - 1)
|
||||
{
|
||||
Name = Name.RightChop(Index + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Name;
|
||||
}
|
||||
93
Plugins/SUDS/Source/SUDSEditor/Private/SUDSMessageLogger.cpp
Normal file
93
Plugins/SUDS/Source/SUDSEditor/Private/SUDSMessageLogger.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "IMessageLogListing.h"
|
||||
#include "MessageLogModule.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
FSUDSMessageLogger::~FSUDSMessageLogger()
|
||||
{
|
||||
if (bWriteToMessageLog)
|
||||
{
|
||||
//Always clear the old message after an import or re-import
|
||||
const TCHAR* LogTitle = TEXT("SUDS");
|
||||
FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked<FMessageLogModule>("MessageLog");
|
||||
TSharedPtr<class IMessageLogListing> LogListing = MessageLogModule.GetLogListing(LogTitle);
|
||||
LogListing->SetLabel(FText::FromString("SUDS"));
|
||||
// Should NOT clear messages here, otherwise when FSUDSMessageLogger is used multiple times in the import process,
|
||||
// each one is clearing messages from previous stages of the import.
|
||||
//LogListing->ClearMessages();
|
||||
|
||||
if(ErrorMessages.Num() > 0)
|
||||
{
|
||||
LogListing->AddMessages(MoveTemp(ErrorMessages));
|
||||
LogListing->NotifyIfAnyMessages(NSLOCTEXT("SUDS", "ImportErrors", "There were issues with the import."), EMessageSeverity::Warning);
|
||||
MessageLogModule.OpenMessageLog(LogTitle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FSUDSMessageLogger::HasErrors() const
|
||||
{
|
||||
for (const TSharedRef<FTokenizedMessage>& Msg : ErrorMessages)
|
||||
{
|
||||
if (Msg->GetSeverity() == EMessageSeverity::Error)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int FSUDSMessageLogger::NumErrors() const
|
||||
{
|
||||
int Errs = 0;
|
||||
for (const TSharedRef<FTokenizedMessage>& Msg : ErrorMessages)
|
||||
{
|
||||
if (Msg->GetSeverity() == EMessageSeverity::Error)
|
||||
{
|
||||
++Errs;
|
||||
}
|
||||
}
|
||||
return Errs;
|
||||
}
|
||||
|
||||
bool FSUDSMessageLogger::HasWarnings() const
|
||||
{
|
||||
for (const TSharedRef<FTokenizedMessage>& Msg : ErrorMessages)
|
||||
{
|
||||
if (Msg->GetSeverity() == EMessageSeverity::Warning)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int FSUDSMessageLogger::NumWarnings() const
|
||||
{
|
||||
int Count = 0;
|
||||
for (const TSharedRef<FTokenizedMessage>& Msg : ErrorMessages)
|
||||
{
|
||||
if (Msg->GetSeverity() == EMessageSeverity::Warning)
|
||||
{
|
||||
++Count;
|
||||
}
|
||||
}
|
||||
return Count;
|
||||
}
|
||||
|
||||
void FSUDSMessageLogger::AddMessage(EMessageSeverity::Type Severity, const FText& Text)
|
||||
{
|
||||
ErrorMessages.Add(FTokenizedMessage::Create(Severity, Text));
|
||||
}
|
||||
|
||||
void FSUDSMessageLogger::ClearMessages()
|
||||
{
|
||||
const TCHAR* LogTitle = TEXT("SUDS");
|
||||
FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked<FMessageLogModule>("MessageLog");
|
||||
TSharedPtr<class IMessageLogListing> LogListing = MessageLogModule.GetLogListing(LogTitle);
|
||||
LogListing->SetLabel(FText::FromString("SUDS"));
|
||||
LogListing->ClearMessages();
|
||||
}
|
||||
|
||||
134
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptActions.cpp
Normal file
134
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptActions.cpp
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptActions.h"
|
||||
|
||||
#include "SUDSEditorScriptTools.h"
|
||||
#include "SUDSEditorSettings.h"
|
||||
#include "SUDSEditorToolkit.h"
|
||||
#include "SUDSEditorVoiceOverTools.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "ToolMenuSection.h"
|
||||
#include "EditorFramework/AssetImportData.h"
|
||||
#include "Misc/MessageDialog.h"
|
||||
#include "Widgets/Views/SListView.h"
|
||||
|
||||
FText FSUDSScriptActions::GetName() const
|
||||
{
|
||||
return INVTEXT("SUDS Script");
|
||||
}
|
||||
|
||||
FString FSUDSScriptActions::GetObjectDisplayName(UObject* Object) const
|
||||
{
|
||||
return Object->GetName();
|
||||
}
|
||||
|
||||
UClass* FSUDSScriptActions::GetSupportedClass() const
|
||||
{
|
||||
return USUDSScript::StaticClass();
|
||||
}
|
||||
|
||||
FColor FSUDSScriptActions::GetTypeColor() const
|
||||
{
|
||||
return FColor::Orange;
|
||||
}
|
||||
|
||||
uint32 FSUDSScriptActions::GetCategories()
|
||||
{
|
||||
return EAssetTypeCategories::Misc;
|
||||
}
|
||||
|
||||
void FSUDSScriptActions::GetResolvedSourceFilePaths(const TArray<UObject*>& TypeAssets,
|
||||
TArray<FString>& OutSourceFilePaths) const
|
||||
{
|
||||
for (auto& Asset : TypeAssets)
|
||||
{
|
||||
const auto Script = CastChecked<USUDSScript>(Asset);
|
||||
if (Script->AssetImportData)
|
||||
{
|
||||
Script->AssetImportData->ExtractFilenames(OutSourceFilePaths);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FSUDSScriptActions::HasActions(const TArray<UObject*>& InObjects) const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void FSUDSScriptActions::OpenAssetEditor(const TArray<UObject*>& InObjects,
|
||||
TSharedPtr<IToolkitHost> EditWithinLevelEditor)
|
||||
{
|
||||
MakeShared<FSUDSEditorToolkit>()->InitEditor(InObjects);
|
||||
}
|
||||
|
||||
void FSUDSScriptActions::GetActions(const TArray<UObject*>& InObjects, FToolMenuSection& Section)
|
||||
{
|
||||
const auto Scripts = GetTypedWeakObjectPtrs<USUDSScript>(InObjects);
|
||||
|
||||
Section.AddMenuEntry(
|
||||
"WriteBackTextIDs",
|
||||
NSLOCTEXT("SUDS", "WriteBackTextIDs", "Write Back String Keys"),
|
||||
NSLOCTEXT("SUDS",
|
||||
"WriteBackTextIDsTooltip",
|
||||
"Write string table keys back to source script to make them constant for localisation."),
|
||||
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Details"),
|
||||
FUIAction(
|
||||
FExecuteAction::CreateSP(this, &FSUDSScriptActions::WriteBackTextIDs, Scripts),
|
||||
FCanExecuteAction()
|
||||
)
|
||||
);
|
||||
Section.AddMenuEntry(
|
||||
"GenerateVOAssets",
|
||||
NSLOCTEXT("SUDS", "GenerateVOAssets", "Generate Voice Assets"),
|
||||
NSLOCTEXT("SUDS",
|
||||
"GenerateVOAssetsTooltip",
|
||||
"Generate Dialogue Voice / Dialogue Wave assets for the selected scripts"),
|
||||
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Toolbar.Export"),
|
||||
FUIAction(
|
||||
FExecuteAction::CreateSP(this, &FSUDSScriptActions::GenerateVOAssets, Scripts),
|
||||
FCanExecuteAction()
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
void FSUDSScriptActions::WriteBackTextIDs(TArray<TWeakObjectPtr<USUDSScript>> Scripts)
|
||||
{
|
||||
if (FMessageDialog::Open(EAppMsgType::YesNo,
|
||||
FText::FromString(
|
||||
"Are you sure you want to write string keys back to the selected scripts?"))
|
||||
== EAppReturnType::Yes)
|
||||
{
|
||||
FSUDSMessageLogger::ClearMessages();
|
||||
FSUDSMessageLogger Logger;
|
||||
for (auto Script : Scripts)
|
||||
{
|
||||
if (Script.IsValid())
|
||||
{
|
||||
FSUDSEditorScriptTools::WriteBackTextIDs(Script.Get(), Logger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void FSUDSScriptActions::GenerateVOAssets(TArray<TWeakObjectPtr<USUDSScript>> Scripts)
|
||||
{
|
||||
if (FMessageDialog::Open(EAppMsgType::YesNo,
|
||||
FText::FromString(
|
||||
"Are you sure you want to generate Dialogue Voice / Dialogue Wave assets for the selected scripts?"))
|
||||
== EAppReturnType::Yes)
|
||||
{
|
||||
EObjectFlags Flags = RF_Public | RF_Standalone | RF_Transactional;
|
||||
FSUDSMessageLogger::ClearMessages();
|
||||
FSUDSMessageLogger Logger;
|
||||
for (auto WeakScript : Scripts)
|
||||
{
|
||||
if (auto Script = WeakScript.Get())
|
||||
{
|
||||
FSUDSEditorVoiceOverTools::GenerateAssets(Script, Flags, &Logger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
262
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptFactory.cpp
Normal file
262
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptFactory.cpp
Normal file
@@ -0,0 +1,262 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptFactory.h"
|
||||
|
||||
#include "PackageTools.h"
|
||||
#include "SUDSEditorSettings.h"
|
||||
#include "SUDSEditorVoiceOverTools.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "EditorFramework/AssetImportData.h"
|
||||
#include "Internationalization/StringTable.h"
|
||||
#include "Editor.h"
|
||||
#include "ObjectTools.h"
|
||||
#include "Internationalization/StringTableCore.h"
|
||||
|
||||
|
||||
USUDSScriptFactory::USUDSScriptFactory()
|
||||
{
|
||||
SupportedClass = USUDSScript::StaticClass();
|
||||
bCreateNew = false;
|
||||
bEditorImport = true;
|
||||
bText = true;
|
||||
Formats.Add(TEXT("sud;SUDS Script File"));
|
||||
}
|
||||
|
||||
UObject* USUDSScriptFactory::FactoryCreateText(UClass* InClass,
|
||||
UObject* InParent,
|
||||
FName InName,
|
||||
EObjectFlags Flags,
|
||||
UObject* Context,
|
||||
const TCHAR* Type,
|
||||
const TCHAR*& Buffer,
|
||||
const TCHAR* BufferEnd,
|
||||
FFeedbackContext* Warn)
|
||||
{
|
||||
Flags |= RF_Transactional;
|
||||
FSUDSMessageLogger::ClearMessages();
|
||||
FSUDSMessageLogger Logger;
|
||||
|
||||
USUDSScript* Result = nullptr;
|
||||
|
||||
GEditor->GetEditorSubsystem<UImportSubsystem>()->BroadcastAssetPreImport(this, InClass, InParent, InName, Type);
|
||||
|
||||
const FString FactoryCurrentFilename = UFactory::GetCurrentFilename();
|
||||
FString CurrentSourcePath;
|
||||
FString FilenameNoExtension;
|
||||
FString UnusedExtension;
|
||||
FPaths::Split(FactoryCurrentFilename, CurrentSourcePath, FilenameNoExtension, UnusedExtension);
|
||||
const FString LongPackagePath = FPackageName::GetLongPackagePath(InParent->GetOutermost()->GetPathName());
|
||||
|
||||
const FString NameForErrors(InName.ToString());
|
||||
|
||||
// Now parse this using utility
|
||||
if(Importer.ImportFromBuffer(Buffer, BufferEnd - Buffer, NameForErrors, &Logger, false))
|
||||
{
|
||||
|
||||
// Populate with data
|
||||
Result = NewObject<USUDSScript>(InParent, InName, Flags);
|
||||
UStringTable* StringTable = CreateStringTable(InParent, InName, Result, Flags, &Logger);
|
||||
Importer.PopulateAsset(Result, StringTable);
|
||||
|
||||
// Register source info
|
||||
const FMD5Hash Hash = FSUDSScriptImporter::CalculateHash(Buffer, BufferEnd - Buffer);
|
||||
Result->AssetImportData->Update(FactoryCurrentFilename, Hash);
|
||||
|
||||
// VO assets at import time?
|
||||
if (ShouldGenerateVoiceAssets(LongPackagePath))
|
||||
{
|
||||
FSUDSEditorVoiceOverTools::GenerateAssets(Result, Flags, &Logger);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
GEditor->GetEditorSubsystem<UImportSubsystem>()->BroadcastAssetPostImport(this, Result);
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
bool USUDSScriptFactory::ShouldGenerateVoiceAssets(const FString& PackagePath) const
|
||||
{
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
return Settings->ShouldGenerateVoiceAssets(PackagePath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
UStringTable* USUDSScriptFactory::CreateStringTable(UObject* ScriptParent, FName InName, USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger)
|
||||
{
|
||||
auto Settings = GetDefault<USUDSEditorSettings>();
|
||||
const bool bCreateSeparatePackage = Settings && Settings->bCreateStringTablesAsSeparatePackages;
|
||||
|
||||
const FName StringTableName = FName(InName.ToString() + "Strings");
|
||||
UStringTable* Table = nullptr;
|
||||
auto Registry = &FModuleManager::LoadModuleChecked<FAssetRegistryModule>(AssetRegistryConstants::ModuleName).Get();
|
||||
const FString PackageDir = FPackageName::GetLongPackagePath(Script->GetOuter()->GetOutermost()->GetPathName());
|
||||
const FString PackageName = UPackageTools::SanitizePackageName(PackageDir / StringTableName.ToString());
|
||||
|
||||
if (bCreateSeparatePackage)
|
||||
{
|
||||
|
||||
// Create string table as its own package (.uasset)
|
||||
|
||||
{
|
||||
// Destroy any string table packed together with the script
|
||||
const FString ScriptPackageName = Script->GetPackage()->GetPathName();
|
||||
TArray<FAssetData> ScriptAssets;
|
||||
Registry->GetAssetsByPackageName(*ScriptPackageName, ScriptAssets);
|
||||
TArray<UObject*> StringTableAssets;
|
||||
for (auto& Asset : ScriptAssets)
|
||||
{
|
||||
if (Asset.GetClass() == UStringTable::StaticClass())
|
||||
{
|
||||
StringTableAssets.Add(Asset.GetAsset());
|
||||
}
|
||||
}
|
||||
if (StringTableAssets.Num() > 0)
|
||||
{
|
||||
ObjectTools::ForceDeleteObjects(StringTableAssets, false );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (FPackageName::DoesPackageExist(*PackageName))
|
||||
{
|
||||
// Package already exists, so try and import over the top of it, if it doesn't already have a source file path
|
||||
TArray<FAssetData> Assets;
|
||||
if (Registry->GetAssetsByPackageName(*PackageName, Assets))
|
||||
{
|
||||
if (Assets.Num() > 0)
|
||||
{
|
||||
if (Assets[0].GetAsset()->IsA(UStringTable::StaticClass()))
|
||||
{
|
||||
Table = Cast<UStringTable>(Assets[0].GetAsset());
|
||||
Table->GetMutableStringTable()->ClearSourceStrings();
|
||||
Table->MarkPackageDirty();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Asset %s already exists but is not a String Table! Cannot replace, please move aside and re-import script."),
|
||||
*PackageName);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Table)
|
||||
{
|
||||
// String Table didn't exist (although package might have)
|
||||
// It's safe to call CreatePackage either way, it'll return the existing one if needed
|
||||
UPackage* Package = CreatePackage(*PackageName);
|
||||
if (!ensure(Package))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT("Failed to create/retrieve package for string table %s"),
|
||||
*PackageName);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Logger->Logf(ELogVerbosity::Display, TEXT("Creating string table %s"), *PackageName);
|
||||
|
||||
// This constructor registers the string table with FStringTableRegistry
|
||||
Table = NewObject<UStringTable>(Package, StringTableName, Flags);
|
||||
Package->FullyLoad();
|
||||
Table->MarkPackageDirty();
|
||||
FAssetRegistryModule::AssetCreated(Table);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Destroy separate strings package if it exists (could flip this option off after importing)
|
||||
if (FPackageName::DoesPackageExist(*PackageName))
|
||||
{
|
||||
TArray<FAssetData> Assets;
|
||||
if (Registry->GetAssetsByPackageName(*PackageName, Assets))
|
||||
{
|
||||
if (Assets.Num() > 0)
|
||||
{
|
||||
// Note: we need to force deletion because old string table will be referenced
|
||||
// Unfortunately there is no ObjectTools::ForceDeleteAssets, only the inner
|
||||
// ForceDeleteObjects, so we need to do it ourselves
|
||||
ForceDeleteAssets(Assets);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default route, create string table inside script package
|
||||
Table = NewObject<UStringTable>(ScriptParent, StringTableName, Flags);
|
||||
|
||||
}
|
||||
|
||||
return Table;
|
||||
|
||||
}
|
||||
|
||||
void USUDSScriptFactory::ForceDeleteAssets(const TArray<FAssetData>& AssetsToDelete)
|
||||
{
|
||||
// Copied from ObjectTools::DeleteAssets, except that we're using ForceDeleteObjects so we
|
||||
// don't have to display any confusing messages about moving the string table into the shared asset
|
||||
|
||||
TArray<TWeakObjectPtr<UPackage>> PackageFilesToDelete;
|
||||
TArray<UObject*> ObjectsToDelete;
|
||||
for ( int i = 0; i < AssetsToDelete.Num(); i++ )
|
||||
{
|
||||
const FAssetData& AssetData = AssetsToDelete[i];
|
||||
UObject *ObjectToDelete = AssetData.GetAsset({ ULevel::LoadAllExternalObjectsTag });
|
||||
// Assets can be loaded even when their underlying type/class no longer exists...
|
||||
if ( ObjectToDelete!=nullptr )
|
||||
{
|
||||
ObjectsToDelete.Add( ObjectToDelete );
|
||||
}
|
||||
else if ( AssetData.IsUAsset() )
|
||||
{
|
||||
// ... In this cases there is no underlying asset or type so remove the package itself directly after confirming it's valid to do so.
|
||||
FString PackageFilename;
|
||||
if( !FPackageName::DoesPackageExist( AssetData.PackageName.ToString(), &PackageFilename ) )
|
||||
{
|
||||
// Could not determine filename for package so we can not delete
|
||||
continue;
|
||||
}
|
||||
|
||||
UPackage* Package = FindPackage(nullptr, *AssetData.PackageName.ToString());
|
||||
if ( Package )
|
||||
{
|
||||
PackageFilesToDelete.Add(Package);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int32 NumObjectsToDelete = ObjectsToDelete.Num();
|
||||
if ( NumObjectsToDelete > 0 )
|
||||
{
|
||||
ObjectTools::ForceDeleteObjects( ObjectsToDelete, false );
|
||||
}
|
||||
|
||||
const int32 NumPackagesToDelete = PackageFilesToDelete.Num();
|
||||
if (NumPackagesToDelete > 0)
|
||||
{
|
||||
TArray<UPackage*> PackagePointers;
|
||||
for ( const auto& PkgIt : PackageFilesToDelete )
|
||||
{
|
||||
UPackage* Package = PkgIt.Get();
|
||||
if ( Package )
|
||||
{
|
||||
PackagePointers.Add(Package);
|
||||
}
|
||||
}
|
||||
|
||||
if ( PackagePointers.Num() > 0 )
|
||||
{
|
||||
const bool bPerformReferenceCheck = true;
|
||||
ObjectTools::CleanupAfterSuccessfulDelete(PackagePointers, bPerformReferenceCheck);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2741
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptImporter.cpp
Normal file
2741
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptImporter.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,149 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptReimportFactory.h"
|
||||
|
||||
#include "SUDSEditor.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
#include "EditorFramework/AssetImportData.h"
|
||||
#include "HAL/FileManager.h"
|
||||
#include "Sound/DialogueWave.h"
|
||||
|
||||
USUDSScriptReimportFactory::USUDSScriptReimportFactory()
|
||||
{
|
||||
SupportedClass = USUDSScript::StaticClass();
|
||||
bCreateNew = false;
|
||||
// We need to have a unique priority vs the original factory, so go after
|
||||
ImportPriority = DefaultImportPriority - 1;
|
||||
}
|
||||
|
||||
bool USUDSScriptReimportFactory::CanReimport(UObject* Obj, TArray<FString>& OutFilenames)
|
||||
{
|
||||
USUDSScript* Script = Cast<USUDSScript>(Obj);
|
||||
if (Script && Script->AssetImportData)
|
||||
{
|
||||
Script->AssetImportData->ExtractFilenames(OutFilenames);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void USUDSScriptReimportFactory::SetReimportPaths(UObject* Obj, const TArray<FString>& NewReimportPaths)
|
||||
{
|
||||
USUDSScript* Script = Cast<USUDSScript>(Obj);
|
||||
if (Script && ensure(NewReimportPaths.Num() == 1))
|
||||
{
|
||||
Script->AssetImportData->UpdateFilenameOnly(NewReimportPaths[0]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
EReimportResult::Type USUDSScriptReimportFactory::Reimport(UObject* Obj)
|
||||
{
|
||||
USUDSScript* Script = Cast<USUDSScript>(Obj);
|
||||
if (!Script)
|
||||
{
|
||||
return EReimportResult::Failed;
|
||||
}
|
||||
|
||||
// Make sure file is valid and exists
|
||||
const FString Filename = Script->AssetImportData->GetFirstFilename();
|
||||
if (!Filename.Len() || IFileManager::Get().FileSize(*Filename) == INDEX_NONE)
|
||||
{
|
||||
return EReimportResult::Failed;
|
||||
}
|
||||
|
||||
// When a new script is created, it actually lives at the same address as the incoming one. UE must re-use objects
|
||||
// when you put them back at the same outer & asset name?
|
||||
// This means if we want to preserve anything from the previously imported object, such as generated VO asset links,
|
||||
// we need to copy those out now.
|
||||
TMap<FString, UDialogueVoice*> PrevSpeakerVoices = Script->GetSpeakerVoices();
|
||||
// Store the TextID -> DialogueWave, but also store the line text as well so we can detect whether it matches & warn if not
|
||||
TMap<FString, TPair<FString, UDialogueWave*> > PrevWaves;
|
||||
for (auto Node : Script->GetNodes())
|
||||
{
|
||||
if (auto TN = Cast<USUDSScriptNodeText>(Node))
|
||||
{
|
||||
if (auto W = TN->GetWave())
|
||||
{
|
||||
PrevWaves.Add(TN->GetTextID(), TPair<FString, UDialogueWave*>(TN->GetText().ToString(), W));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run the import again
|
||||
EReimportResult::Type Result = EReimportResult::Failed;
|
||||
bool OutCanceled = false;
|
||||
|
||||
if (ImportObject(Script->GetClass(), Script->GetOuter(), *Script->GetName(), RF_Public | RF_Standalone, Filename, nullptr, OutCanceled) != nullptr)
|
||||
{
|
||||
UE_LOG(LogSUDSEditor, Log, TEXT("Imported successfully"));
|
||||
|
||||
FSUDSMessageLogger Logger;
|
||||
// Now, try to restore the speaker voice / line wave links from before
|
||||
for (auto SpeakerID : Script->GetSpeakers())
|
||||
{
|
||||
if (auto pVoice = PrevSpeakerVoices.Find(SpeakerID))
|
||||
{
|
||||
Script->SetSpeakerVoice(SpeakerID, *pVoice);
|
||||
}
|
||||
}
|
||||
for (auto Node : Script->GetNodes())
|
||||
{
|
||||
if (auto TN = Cast<USUDSScriptNodeText>(Node))
|
||||
{
|
||||
if (auto pWavePair = PrevWaves.Find(TN->GetTextID()))
|
||||
{
|
||||
// Set the wave link either way
|
||||
TN->SetWave(pWavePair->Value);
|
||||
|
||||
// Check that the text is the same; if it's not, then lines have potentially changed
|
||||
// we still live with the assignment we have (might be a minor edit) but user should be aware
|
||||
if (TN->GetText().ToString() != pWavePair->Key)
|
||||
{
|
||||
Logger.Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"TextID %s is linked to Dialogue Wave %s, but text has changed. Check whether this line is linked to the correct wave, and consider Writing String Keys back to script before making more script changes in future."),
|
||||
*TN->GetTextID(),
|
||||
*pWavePair->Value->GetName());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Script->AssetImportData->Update(Filename);
|
||||
|
||||
// Try to find the outer package so we can dirty it up
|
||||
if (Script->GetOuter())
|
||||
{
|
||||
Script->GetOuter()->MarkPackageDirty();
|
||||
}
|
||||
else
|
||||
{
|
||||
Script->MarkPackageDirty();
|
||||
}
|
||||
Result = EReimportResult::Succeeded;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (OutCanceled)
|
||||
{
|
||||
UE_LOG(LogSUDSEditor, Warning, TEXT("-- import canceled"));
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogSUDSEditor, Warning, TEXT("-- import failed"));
|
||||
}
|
||||
|
||||
Result = EReimportResult::Failed;
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
int32 USUDSScriptReimportFactory::GetPriority() const
|
||||
{
|
||||
return ImportPriority;
|
||||
}
|
||||
24
Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditor.h
Normal file
24
Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditor.h
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Modules/ModuleInterface.h"
|
||||
|
||||
class FSUDSScriptActions;
|
||||
class FSlateStyleSet;
|
||||
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogSUDSEditor, Verbose, All);
|
||||
|
||||
class FSUDSEditorModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
|
||||
/** IModuleInterface implementation */
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
|
||||
protected:
|
||||
TSharedPtr<FSUDSScriptActions> ScriptActions;
|
||||
TSharedPtr<FSlateStyleSet> StyleSet;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
class USUDSScript;
|
||||
class USUDSScriptNode;
|
||||
struct FSUDSMessageLogger;
|
||||
|
||||
class FSUDSEditorScriptTools
|
||||
{
|
||||
public:
|
||||
|
||||
static void WriteBackTextIDs(USUDSScript* Script, FSUDSMessageLogger& Logger);
|
||||
static bool WriteBackTextIDsFromNodes(const TArray<USUDSScriptNode*> Nodes, TArray<FString>& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger);
|
||||
static bool WriteBackTextID(const FText& AssetText, int LineNo, TArray<FString>& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger);
|
||||
static bool WriteBackGosubID(const FString& GosubID, int LineNo, TArray<FString>& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger);
|
||||
static bool TextIDCheckMatch(const FText& AssetText, const FString& SourceLine);
|
||||
|
||||
|
||||
};
|
||||
73
Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditorSettings.h
Normal file
73
Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditorSettings.h
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "SUDSEditorSettings.generated.h"
|
||||
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class ESUDSAssetLocation : uint8
|
||||
{
|
||||
/// Use a single flat shared directory
|
||||
SharedDirectory,
|
||||
/// Use a shared base directory, but create subfolders based on the script asset name
|
||||
SharedDirectorySubdir,
|
||||
/// Place asset alongside the script that originated it
|
||||
ScriptDirectory,
|
||||
/// Place asset in a subfolder of the folder containing the script that generated it, named the same as the script
|
||||
ScriptDirectorySubdir
|
||||
};
|
||||
|
||||
class USUDSScript;
|
||||
/**
|
||||
* Settings for editor-specific aspects of SUDS (no effect at runtime)
|
||||
*/
|
||||
UCLASS(config = Editor, defaultconfig, meta=(DisplayName="SUDS Editor"))
|
||||
class SUDSEDITOR_API USUDSEditorSettings : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Where to place Dialogue Voice assets for speakers in scripts when generated", RelativeToGameContentDir, LongPackageName))
|
||||
ESUDSAssetLocation DialogueVoiceAssetLocation = ESUDSAssetLocation::SharedDirectory;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Shared directory for Dialogue Voice assets, if using a shared directory", RelativeToGameContentDir, LongPackageName))
|
||||
FDirectoryPath DialogueVoiceAssetSharedDir;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Where to place Dialogue Wave assets for speaker lines in scripts", RelativeToGameContentDir, LongPackageName))
|
||||
ESUDSAssetLocation DialogueWaveAssetLocation = ESUDSAssetLocation::ScriptDirectorySubdir;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Shared directory for Dialogue Wave assets, if using a shared directory", RelativeToGameContentDir, LongPackageName))
|
||||
FDirectoryPath DialogueWaveAssetSharedDir;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Prefix to give Dialogue Voice assets in front of their SpeakerID", RelativeToGameContentDir, LongPackageName))
|
||||
FString DialogueVoiceAssetPrefix = "DV_";
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Prefix to give Dialogue Wave assets in front of their SpeakerID", RelativeToGameContentDir, LongPackageName))
|
||||
FString DialogueWaveAssetPrefix = "DW_";
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "When generating subdirectories and wave asset names from script names, whether to strip characters before the first '_' to avoid including script prefix", RelativeToGameContentDir, LongPackageName))
|
||||
bool StripScriptPrefixesWhenGeneratingNames = true;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", AdvancedDisplay, meta = (Tooltip = "Whether to auto-generate Dialogue Voice/Wave assets for ALL dialogue scripts on import. Note: you can always generate VO assets manually."))
|
||||
bool AlwaysAutoGenerateVoiceOverAssetsOnImport = false;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", AdvancedDisplay, meta = (Tooltip = "Auto-generate Dialogue Voice/Wave assets for scripts in these directories (and subdirectories) on import. Note: you can always generate VO assets manually.", RelativeToGameContentDir, LongPackageName))
|
||||
TArray<FDirectoryPath> DirectoriesToAutoGenerateVoiceOverAssetsOnImport;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Choices", meta = (Tooltip = "Whether to generate a spoken line for choices (default false)."))
|
||||
bool AlwaysGenerateSpeakerLinesFromChoices = false;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Choices", meta = (Tooltip = "The SpeakerID to use for speaker lines generated from choices"))
|
||||
FString SpeakerIdForGeneratedLinesFromChoices = "Player";
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Assets", AdvancedDisplay, meta = (Tooltip = "Whether to create string tables as a separate package (.uasset) from the SUDS Script, which will cause them to appear separately in the Content Browser (requires script re-import)"))
|
||||
bool bCreateStringTablesAsSeparatePackages = true;
|
||||
|
||||
USUDSEditorSettings() {}
|
||||
|
||||
bool ShouldGenerateVoiceAssets(const FString& PackagePath) const;
|
||||
FString GetVoiceOutputDir(const FString& PackagePath, const FString& ScriptName) const;
|
||||
FString GetWaveOutputDir(const FString& PackagePath, const FString& ScriptName) const;
|
||||
static FString GetOutputDir(ESUDSAssetLocation Location, const FString& SharedPath, const FString& PackagePath, const FString& ScriptName);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
#include "Sound/DialogueVoice.h"
|
||||
|
||||
|
||||
struct FSUDSMessageLogger;
|
||||
class USUDSScript;
|
||||
|
||||
class SUDSEDITOR_API FSUDSEditorVoiceOverTools
|
||||
{
|
||||
public:
|
||||
static void GenerateAssets(USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger);
|
||||
protected:
|
||||
static void GenerateVoiceAssets(USUDSScript* Script,
|
||||
EObjectFlags Flags,
|
||||
FSUDSMessageLogger *Logger,
|
||||
TMap<FString, UDialogueVoice*> &OutCreatedVoices);
|
||||
static void GenerateWaveAssets(USUDSScript* Script, EObjectFlags Flags, TMap<FString, UDialogueVoice*>, FSUDSMessageLogger* Logger);
|
||||
static bool GetSpeakerVoicePackageName(USUDSScript* Script, const FString& SpeakerID, FString& OutPackageName);
|
||||
static bool GetSpeakerVoiceAssetNames(USUDSScript* Script,
|
||||
const FString& SpeakerID,
|
||||
FString& OutPackageName,
|
||||
FString& OutAssetName);
|
||||
static UDialogueVoice* FindSpeakerVoice(USUDSScript* Script, const FString& SpeakerID);
|
||||
static FString GetScriptNameAsPrefix(USUDSScript* Script);
|
||||
|
||||
static FString GetVoiceOutputDir(USUDSScript* Script);
|
||||
static FString GetWaveOutputDir(USUDSScript* Script);
|
||||
|
||||
};
|
||||
60
Plugins/SUDS/Source/SUDSEditor/Public/SUDSMessageLogger.h
Normal file
60
Plugins/SUDS/Source/SUDSEditor/Public/SUDSMessageLogger.h
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Logging/TokenizedMessage.h"
|
||||
|
||||
class FTokenizedMessage;
|
||||
|
||||
struct SUDSEDITOR_API FSUDSMessageLogger
|
||||
{
|
||||
protected:
|
||||
TArray<TSharedRef<FTokenizedMessage>> ErrorMessages;
|
||||
|
||||
bool bWriteToMessageLog = true;
|
||||
public:
|
||||
FSUDSMessageLogger() {}
|
||||
FSUDSMessageLogger(bool bWriteToMsg) : bWriteToMessageLog(bWriteToMsg) {}
|
||||
~FSUDSMessageLogger();
|
||||
|
||||
void SetWriteToMessageLog(bool bWrite) { bWriteToMessageLog = bWrite; }
|
||||
bool HasErrors() const;
|
||||
int NumErrors() const;
|
||||
bool HasWarnings() const;
|
||||
int NumWarnings() const;
|
||||
|
||||
bool GetWriteToMessageLog() const { return bWriteToMessageLog; }
|
||||
void AddMessage(EMessageSeverity::Type Severity, const FText& Text);
|
||||
|
||||
#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 6
|
||||
template <typename... Types>
|
||||
FORCEINLINE void Logf(ELogVerbosity::Type Verbosity, UE::Core::TCheckedFormatString<FString::FmtCharType, Types...> Fmt, Types... Args)
|
||||
#else
|
||||
template <typename FmtType, typename... Types>
|
||||
FORCEINLINE void Logf(ELogVerbosity::Type Verbosity, const FmtType& Fmt, Types... Args)
|
||||
#endif
|
||||
{
|
||||
EMessageSeverity::Type Sev = EMessageSeverity::Info;
|
||||
switch(Verbosity)
|
||||
{
|
||||
case ELogVerbosity::Fatal:
|
||||
case ELogVerbosity::Error:
|
||||
Sev = EMessageSeverity::Error;
|
||||
break;
|
||||
case ELogVerbosity::Warning:
|
||||
Sev = EMessageSeverity::Warning;
|
||||
break;
|
||||
default: ;
|
||||
}
|
||||
AddMessage(Sev, FText::FromString(FString::Printf(Fmt, Args...)));
|
||||
}
|
||||
|
||||
const TArray<TSharedRef<FTokenizedMessage>>& GetErrorMessages() const { return ErrorMessages; }
|
||||
|
||||
/// Clear messages in preparation for an import
|
||||
static void ClearMessages();
|
||||
|
||||
|
||||
|
||||
};
|
||||
37
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptActions.h
Normal file
37
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptActions.h
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "AssetTypeActions_Base.h"
|
||||
#include "SUDSScriptNodeGosub.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
|
||||
class USUDSScriptNode;
|
||||
class USUDSScript;
|
||||
struct FSUDSMessageLogger;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class FSUDSScriptActions : public FAssetTypeActions_Base
|
||||
{
|
||||
public:
|
||||
virtual FText GetName() const override;
|
||||
virtual FString GetObjectDisplayName(UObject* Object) const override;
|
||||
virtual UClass* GetSupportedClass() const override;
|
||||
virtual FColor GetTypeColor() const override;
|
||||
virtual uint32 GetCategories() override;
|
||||
virtual void GetResolvedSourceFilePaths(const TArray<UObject*>& TypeAssets,
|
||||
TArray<FString>& OutSourceFilePaths) const override;
|
||||
virtual bool IsImportedAsset() const override { return true; }
|
||||
virtual void GetActions(const TArray<UObject*>& InObjects, FToolMenuSection& Section) override;
|
||||
virtual bool HasActions(const TArray<UObject*>& InObjects) const override;
|
||||
virtual void OpenAssetEditor(const TArray<UObject*>& InObjects,
|
||||
TSharedPtr<IToolkitHost> EditWithinLevelEditor) override;
|
||||
|
||||
protected:
|
||||
void WriteBackTextIDs(TArray<TWeakObjectPtr<USUDSScript>> Scripts);
|
||||
|
||||
void GenerateVOAssets(TArray<TWeakObjectPtr<USUDSScript>> Scripts);
|
||||
|
||||
};
|
||||
37
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptFactory.h
Normal file
37
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptFactory.h
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSScriptImporter.h"
|
||||
#include "Factories/Factory.h"
|
||||
#include "SUDSScriptFactory.generated.h"
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class SUDSEDITOR_API USUDSScriptFactory : public UFactory
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
USUDSScriptFactory();
|
||||
protected:
|
||||
virtual UObject* FactoryCreateText(UClass* InClass,
|
||||
UObject* InParent,
|
||||
FName InName,
|
||||
EObjectFlags Flags,
|
||||
UObject* Context,
|
||||
const TCHAR* Type,
|
||||
const TCHAR*& Buffer,
|
||||
const TCHAR* BufferEnd,
|
||||
FFeedbackContext* Warn) override;
|
||||
|
||||
bool ShouldGenerateVoiceAssets(const FString& PackagePath) const;
|
||||
FSUDSScriptImporter Importer;
|
||||
|
||||
void ForceDeleteAssets(const TArray<FAssetData>& Assets);
|
||||
UStringTable* CreateStringTable(UObject* ScriptParent, FName InName, USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger);
|
||||
};
|
||||
520
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptImporter.h
Normal file
520
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptImporter.h
Normal file
@@ -0,0 +1,520 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSExpression.h"
|
||||
|
||||
struct FSUDSMessageLogger;
|
||||
class USUDSScript;
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogSUDSImporter, Verbose, All);
|
||||
|
||||
struct SUDSEDITOR_API FSUDSParsedEdge
|
||||
{
|
||||
public:
|
||||
/// Text associated with this edge (if a player choice option)
|
||||
FString Text;
|
||||
/// Identifier of the text, for the string table
|
||||
FString TextID;
|
||||
/// Metadata associated with text, for translator comments
|
||||
TMap<FName, FString> TextMetadata;
|
||||
/// The line this edge was created on
|
||||
int SourceLineNo;
|
||||
/// Condition expression that applies to this edge (for select nodes)
|
||||
FSUDSExpression ConditionExpression;
|
||||
/// Custom metadata for a node which the user can set. Can be used to annotate choice lines
|
||||
/// Could be used to disable a choice if you don't have the stats to take it, or anything else
|
||||
/// where you need a bit of extra information about a line that doesn't suit setting a dialogue variable for.
|
||||
TMap<FName, FSUDSExpression> UserMetadata;
|
||||
|
||||
int SourceNodeIdx = -1;
|
||||
int TargetNodeIdx = -1;
|
||||
|
||||
FSUDSParsedEdge(int LineNo) : SourceLineNo(LineNo){}
|
||||
|
||||
FSUDSParsedEdge(int FromNodeIdx, int ToNodeIdx, int LineNo, const FString& InText, const FString& InTextID, const TMap<FName, FString>& Metadata, const TMap<FName, FSUDSExpression>& UserMeta)
|
||||
: Text(InText),
|
||||
TextID(InTextID),
|
||||
TextMetadata(Metadata),
|
||||
SourceLineNo(LineNo),
|
||||
UserMetadata(UserMeta),
|
||||
SourceNodeIdx(FromNodeIdx),
|
||||
TargetNodeIdx(ToNodeIdx)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedEdge(int FromNodeIdx, int ToNodeIdx, int LineNo)
|
||||
: SourceLineNo(LineNo),
|
||||
SourceNodeIdx(FromNodeIdx),
|
||||
TargetNodeIdx(ToNodeIdx)
|
||||
|
||||
{
|
||||
}
|
||||
|
||||
void Reset()
|
||||
{
|
||||
*this = FSUDSParsedEdge(-1);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
enum class ESUDSParsedNodeType : uint8
|
||||
{
|
||||
/// Text node, displaying a line of dialogue
|
||||
Text,
|
||||
/// Choice node, displaying a series of user choices which navigate to other nodes
|
||||
Choice,
|
||||
/// Select node, automatically selecting one which navigates to another node based on state (also Random)
|
||||
Select,
|
||||
/// Goto node, redirects execution somewhere else
|
||||
/// Gotos are only nodes in the parsing structure, because they need to be discoverable as a fallthrough destination
|
||||
/// When converting to runtime use they just become edges
|
||||
Goto,
|
||||
/// Gosub node, acts like goto except stores return location
|
||||
Gosub,
|
||||
/// Return node, returns from a gosub
|
||||
Return,
|
||||
/// Set variable node
|
||||
SetVariable,
|
||||
/// Event node
|
||||
Event
|
||||
};
|
||||
|
||||
/// Intermediate parsed node from script text
|
||||
/// This will be converted into a final asset later
|
||||
struct SUDSEDITOR_API FSUDSParsedNode
|
||||
{
|
||||
public:
|
||||
ESUDSParsedNodeType NodeType;
|
||||
int OriginalIndent;
|
||||
/// Identifier is speaker ID, goto label, variable name etc
|
||||
FString Identifier;
|
||||
/// Text in native language
|
||||
FString Text;
|
||||
/// Identifier of the text, for the string table
|
||||
FString TextID;
|
||||
/// Metadata associated with text, for translator comments
|
||||
TMap<FName, FString> TextMetadata;
|
||||
/// Expression, for nodes that use it (e.g. set)
|
||||
FSUDSExpression Expression;
|
||||
/// Event arguments, for event nodes
|
||||
TArray<FSUDSExpression> EventArgs;
|
||||
/// Labels which lead to this node
|
||||
TArray<FString> Labels;
|
||||
/// Edges leading to other nodes
|
||||
TArray<FSUDSParsedEdge> Edges;
|
||||
/// The line this node was created on
|
||||
int SourceLineNo;
|
||||
/// Whether this is a valid fall-through target
|
||||
bool AllowFallthrough = true;
|
||||
|
||||
/// Custom metadata for a node which the user can set. Can be used to annotate speaker lines
|
||||
TMap<FName, FSUDSExpression> UserMetadata;
|
||||
|
||||
// Path hierarchy of choice nodes leading to this node, of the form "/C002/C006" etc, not including this node index
|
||||
// This helps us identify valid fallthroughs
|
||||
FString ChoicePath;
|
||||
// Path hierarchy of select nodes leading to this node, of the form "/S002/S006" etc, not including this node index
|
||||
// This helps us identify valid fallthroughs
|
||||
FString ConditionalPath;
|
||||
|
||||
/// Although multiple edges can lead here, this index is for the auto-connected parent (may be nothing)
|
||||
int ParentNodeIdx = -1;
|
||||
|
||||
FSUDSParsedNode(ESUDSParsedNodeType InNodeType, int Indent, int LineNo) : NodeType(InNodeType),
|
||||
OriginalIndent(Indent),
|
||||
SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedNode(const FString& Label,
|
||||
const FString& GosubID,
|
||||
int Indent,
|
||||
int LineNo) : NodeType(ESUDSParsedNodeType::Gosub),
|
||||
OriginalIndent(Indent),
|
||||
Identifier(Label),
|
||||
TextID(GosubID),
|
||||
SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedNode(const FString& InSpeaker, const FString& InText, const FString& InTextID, const TMap<FName, FString>& Metadata, const TMap<FName, FSUDSExpression>& UserMeta, int Indent, int LineNo)
|
||||
: NodeType(ESUDSParsedNodeType::Text),
|
||||
OriginalIndent(Indent),
|
||||
Identifier(InSpeaker),
|
||||
Text(InText),
|
||||
TextID(InTextID),
|
||||
TextMetadata(Metadata),
|
||||
SourceLineNo(LineNo),
|
||||
UserMetadata(UserMeta)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedNode(const FString& GotoLabel, int Indent, int LineNo)
|
||||
: NodeType(ESUDSParsedNodeType::Goto), OriginalIndent(Indent), Identifier(GotoLabel), SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedNode(const FString& VariableName, const FSUDSExpression& InExpr, int Indent, int LineNo)
|
||||
: NodeType(ESUDSParsedNodeType::SetVariable),
|
||||
OriginalIndent(Indent),
|
||||
Identifier(VariableName),
|
||||
Expression(InExpr),
|
||||
SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedNode(const FString& VariableName,
|
||||
const FSUDSExpression& InExpr,
|
||||
const FString& InTextID,
|
||||
int Indent,
|
||||
int LineNo)
|
||||
: NodeType(ESUDSParsedNodeType::SetVariable),
|
||||
OriginalIndent(Indent),
|
||||
Identifier(VariableName),
|
||||
TextID(InTextID),
|
||||
Expression(InExpr),
|
||||
SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
};
|
||||
class SUDSEDITOR_API FSUDSScriptImporter
|
||||
{
|
||||
public:
|
||||
bool ImportFromBuffer(const TCHAR* Buffer, int32 Len, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
void PopulateAsset(USUDSScript* Asset, UStringTable* StringTable);
|
||||
static FMD5Hash CalculateHash(const TCHAR* Buffer, int32 Len);
|
||||
static const FString EndGotoLabel;
|
||||
protected:
|
||||
static const FString TreePathSeparator;
|
||||
|
||||
enum class EConditionalStage : uint8
|
||||
{
|
||||
IfStage,
|
||||
ElseIfStage,
|
||||
ElseStage,
|
||||
RandomStage,
|
||||
RandomOptionStage
|
||||
};
|
||||
enum class EConditionalParent : uint8
|
||||
{
|
||||
Select,
|
||||
ElseIfStage,
|
||||
ElseStage
|
||||
};
|
||||
|
||||
// Struct for tracking if/elseif blocks
|
||||
struct ConditionalContext
|
||||
{
|
||||
/// Index of parent select node where else will be added
|
||||
int SelectNodeIdx = -1;
|
||||
/// Previous block index (parent for nesting)
|
||||
int PreviousBlockIdx = -1;
|
||||
/// Track whether we're in if/elseif/else
|
||||
EConditionalStage Stage;
|
||||
/// String identifying the current condition; for elseif or else contains original "if" context
|
||||
FString ConditionPathElement;
|
||||
|
||||
ConditionalContext(int InSelectNodeIdx, int InPrevBlockIdx, EConditionalStage InStage, const FString& InCondStr) :
|
||||
SelectNodeIdx(InSelectNodeIdx),
|
||||
PreviousBlockIdx(InPrevBlockIdx),
|
||||
Stage(InStage),
|
||||
ConditionPathElement(InCondStr)
|
||||
{
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/// Struct for tracking indents
|
||||
struct IndentContext
|
||||
{
|
||||
public:
|
||||
// The index of the Node which is the parent of this context
|
||||
// This potentially changes every time a sequential text node is encountered in the same context, so it's
|
||||
// always pointing to the last node encountered at this level, for connection
|
||||
int LastNodeIdx = -1;
|
||||
|
||||
/// The outermost indent level where this context lives
|
||||
/// You can indent things that don't create a new context, e.g.
|
||||
/// 1. Indent a text line under another text line: this is the same as no indent, just a continuation
|
||||
/// 2. Indent choices or conditions under a text line
|
||||
/// This is just good for readability, but does not create a new context, it's just a linear sequence
|
||||
/// Therefore the ThresholdIndent tracks the outermost indent relating to the current linear sequence, to know
|
||||
/// when you do in fact need to pop the current context off the stack.
|
||||
int ThresholdIndent = 0;
|
||||
|
||||
int LastTextNodeIdx = -1;
|
||||
|
||||
/// The path entry for this indent, to be combined with all previous levels to provide full path context
|
||||
FString PathEntry;
|
||||
|
||||
IndentContext(int NodeIdx, int Indent, const FString& Path) : LastNodeIdx(NodeIdx), ThresholdIndent(Indent), LastTextNodeIdx(-1), PathEntry(Path) {}
|
||||
|
||||
};
|
||||
|
||||
/// A tree of nodes. Contained to separate header nodes from body nodes
|
||||
struct ParsedTree
|
||||
{
|
||||
public:
|
||||
/// The indent context stack representing where we are in the indentation tree while parsing
|
||||
/// There must always be 1 level (root)
|
||||
TArray<IndentContext> IndentLevelStack;
|
||||
/// When encountering conditions and choice lines, we are building up details for an edge to another node, but
|
||||
/// we currently don't know the target node. We keep these pending details here
|
||||
int EdgeInProgressNodeIdx = -1;
|
||||
int EdgeInProgressEdgeIdx = -1;
|
||||
/// List of all nodes, appended to as parsing progresses
|
||||
/// Ordering is important, these nodes must be in the order encountered in the file
|
||||
TArray<FSUDSParsedNode> Nodes;
|
||||
/// Record of goto labels to node index, built up during parsing (forward refs are OK so not complete until end of parsing)
|
||||
TMap<FString, int> GotoLabelList;
|
||||
/// Goto labels which have been encountered but we haven't found a destination yet
|
||||
TArray<FString> PendingGotoLabels;
|
||||
/// Goto labels that lead directly to another goto and thus are just aliases
|
||||
TMap<FString, FString> AliasedGotoLabels;
|
||||
|
||||
/// Conditional blocks
|
||||
/// "if" creates a new context, uses current as parent
|
||||
/// "elseif" and "else" also create new contexts, but copies parent from current (sibling)
|
||||
/// "endif" ends the context
|
||||
/// You cannot have conditionals that were started in an indent context ending outside it
|
||||
TArray<ConditionalContext> ConditionalBlocks;
|
||||
/// Index of the current conditional block, if any
|
||||
int CurrentConditionalBlockIdx = -1;
|
||||
|
||||
void Reset()
|
||||
{
|
||||
IndentLevelStack.Reset();
|
||||
EdgeInProgressNodeIdx = EdgeInProgressEdgeIdx -1;
|
||||
Nodes.Reset();
|
||||
GotoLabelList.Reset();
|
||||
PendingGotoLabels.Reset();
|
||||
AliasedGotoLabels.Reset();
|
||||
ConditionalBlocks.Reset();
|
||||
CurrentConditionalBlockIdx = -1;
|
||||
}
|
||||
};
|
||||
|
||||
ParsedTree HeaderTree;
|
||||
ParsedTree BodyTree;
|
||||
|
||||
struct ParsedMetadata
|
||||
{
|
||||
public:
|
||||
FName Key;
|
||||
FString Value;
|
||||
int IndentLevel;
|
||||
|
||||
ParsedMetadata(const FName& InKey, const FString& InValue, int InIndentLevel)
|
||||
: Key(InKey),
|
||||
Value(InValue),
|
||||
IndentLevel(InIndentLevel)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/// Text Metadata applied to speaker lines / choices until reset
|
||||
/// For each key there's a stack of metadata, with most indented at the top
|
||||
TMap<FName, TArray<ParsedMetadata>> PersistentMetadata;
|
||||
/// Text Metadata applied just to the next speaker line or choice
|
||||
TMap<FName, ParsedMetadata> TransientMetadata;
|
||||
/// User metadata for the next speaker line or choice
|
||||
TMap<FName, FSUDSExpression> UserMetadata;
|
||||
|
||||
/// List of speakers, detected during parsing of lines of text
|
||||
TArray<FString> ReferencedSpeakers;
|
||||
|
||||
const int TabIndentValue = 4;
|
||||
bool bHeaderDone = false;
|
||||
bool bTooLateForHeader = false;
|
||||
bool bHeaderInProgress = false;
|
||||
TOptional<bool> bOverrideGenerateSpeakerLineForChoice;
|
||||
TOptional<FString> OverrideChoiceSpeakerID;
|
||||
bool bTextInProgress = false;
|
||||
int ChoiceUniqueId = 0;
|
||||
/// For generating text IDs
|
||||
int TextIDHighestNumber = 0;
|
||||
/// For generating gosub IDs
|
||||
int GosubIDHighestNumber = 0;
|
||||
/// Parse a single line
|
||||
bool ParseLine(const FStringView& Line, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool ParseHeaderLine(const FStringView& Line, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool ParseBodyLine(const FStringView& Line, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool ParseCommentMetadataLine(const FStringView& Line, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool IsLastNodeOfType(const ParsedTree& Tree, ESUDSParsedNodeType Type);
|
||||
bool ParseChoiceLine(const FStringView& Line, ParsedTree& Tree, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger*
|
||||
Logger,
|
||||
bool bSilent);
|
||||
FSUDSParsedEdge* GetEdgeInProgress(ParsedTree& Tree);
|
||||
void EnsureChoiceNodeExistsAboveSelect(ParsedTree& Tree, int IndentLevel, int LineNo);
|
||||
static bool IsConditionalLine(const FStringView& Line);
|
||||
bool ParseConditionalLine(const FStringView& Line, ParsedTree& Tree, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool ParseIfLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
const FString& ConditionStr,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseElseIfLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
const FString& ConditionStr,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
|
||||
bool ParseElseLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseEndIfLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger*
|
||||
Logger,
|
||||
bool bSilent);
|
||||
static bool IsRandomLine(const FStringView& Line);
|
||||
bool ParseRandomLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseBeginRandomLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseRandomOptionLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseEndRandomLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseGotoLabelLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseGotoLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseGosubLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseReturnLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseSetLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseEventLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseTextLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseImportSettingLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
TMap<FName, FString> GetTextMetadataForNextEntry(int CurrentLineIndent);
|
||||
TMap<FName, FSUDSExpression> ConsumeUserMetadata();
|
||||
bool IsCommentLine(const FStringView& TrimmedLine);
|
||||
FStringView TrimLine(const FStringView& Line, int& OutIndentLevel) const;
|
||||
int FindChoiceAfterTextNode(const FSUDSScriptImporter::ParsedTree& Tree, int TextNodeIdx, const FString& ConditionalPath);
|
||||
int RecurseChoiceNodeFindDeepest(const ParsedTree& Tree, int FromChoiceIdx, const FString& ConditionalPath);
|
||||
int RecurseSelectNodeFindDeepestChoice(const ParsedTree& Tree, int FromSelectNode, const FString& ConditionalPath);
|
||||
int FindEdge(const FSUDSScriptImporter::ParsedTree& Tree, int ParentNodeIdx, int TargetNodeIndex);
|
||||
FString MakeIfConditionPathElement(int SelectNodeIdx, const FString& ConditionStr);
|
||||
FString MakeElseIfConditionPathElement(int SelectNodeIdx, const FString& ConditionStr);
|
||||
FString MakeElseConditionPathElement(int SelectNodeIdx);
|
||||
int FindLastChoiceNode(const ParsedTree& Tree, int IndentLevel);
|
||||
int FindLastChoiceNode(const ParsedTree& Tree, int IndentLevel, int FromIndex, const FString& ConditionPath);
|
||||
void PopIndent(ParsedTree& Tree);
|
||||
void PushIndent(ParsedTree& Tree, int NodeIdx, int Indent, const FString& Path);
|
||||
FString GetCurrentTreePath(const FSUDSScriptImporter::ParsedTree& Tree);
|
||||
FString GetCurrentTreeConditionalPath(const FSUDSScriptImporter::ParsedTree& Tree);
|
||||
FString GetTreeConditionalPath(const ParsedTree& Tree, int NodeIndex);
|
||||
void SetFallthroughForNewNode(FSUDSScriptImporter::ParsedTree& Tree, FSUDSParsedNode& NewNode);
|
||||
int AppendNode(ParsedTree& Tree, const FSUDSParsedNode& InNode);
|
||||
bool SelectNodeIsMissingElsePath(const FSUDSScriptImporter::ParsedTree& Tree, const FSUDSParsedNode& Node);
|
||||
bool PostImportSanityCheck(const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool ChoiceNodeCheckPaths(const FSUDSParsedNode& ChoiceNode,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool RecurseChoiceNodeCheckPaths(const FSUDSParsedNode& OrigChoiceNode, const FSUDSParsedNode& CurrChoiceNode, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool RecurseChoiceNodeCheckPaths(const FSUDSParsedNode& ChoiceNode, const FSUDSParsedEdge& Edge, const FString& NameForErrors, FSUDSMessageLogger*
|
||||
Logger,
|
||||
bool bSilent);
|
||||
void ConnectRemainingNodes(ParsedTree& Tree, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
void GenerateTextIDs(ParsedTree& BodyTree);
|
||||
int FindFallthroughNodeIndex(ParsedTree& Tree, int StartNodeIndex, const FString& FromChoicePath, const FString& FromConditionalPath);
|
||||
bool RetrieveAndRemoveTextID(FStringView& InOutLine, FString& OutTextID);
|
||||
bool RetrieveAndRemoveGosubID(FStringView& InOutLine, FString& OutTextID);
|
||||
FString GenerateTextID();
|
||||
const FSUDSParsedNode* GetNode(const ParsedTree& Tree, int Index = 0);
|
||||
int GetGotoTargetNodeIndex(const ParsedTree& Tree, const FString& InLabel);
|
||||
void PopulateAssetFromTree(USUDSScript* Asset,
|
||||
const ParsedTree& Tree,
|
||||
TArray<TObjectPtr<class USUDSScriptNode>>* pOutNodes,
|
||||
TMap<FName, int>* pOutLabels,
|
||||
UStringTable* StringTable);
|
||||
|
||||
public:
|
||||
const FSUDSParsedNode* GetNode(int Index = 0);
|
||||
const FSUDSParsedNode* GetHeaderNode(int Index = 0);
|
||||
/// Resolve a goto label to a target index (after import), or -1 if not resolvable
|
||||
int GetGotoTargetNodeIndex(const FString& Label);
|
||||
static bool RetrieveTextIDFromLine(FStringView& InOutLine, FString& OutTextID, int& OutNumber);
|
||||
static bool RetrieveGosubIDFromLine(FStringView& InOutLine, FString& OutID, int& OutNumber);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/ObjectMacros.h"
|
||||
#include "EditorReimportHandler.h"
|
||||
#include "SUDSScriptFactory.h"
|
||||
|
||||
#include "SUDSScriptReimportFactory.generated.h"
|
||||
|
||||
// Reimports a USUDSScriptFactory asset
|
||||
// Necessary to get the import pop-up
|
||||
UCLASS()
|
||||
class USUDSScriptReimportFactory : public USUDSScriptFactory, public FReimportHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
USUDSScriptReimportFactory();
|
||||
|
||||
//~ Begin FReimportHandler Interface
|
||||
virtual bool CanReimport(UObject* Obj, TArray<FString>& OutFilenames) override;
|
||||
virtual void SetReimportPaths(UObject* Obj, const TArray<FString>& NewReimportPaths) override;
|
||||
virtual EReimportResult::Type Reimport(UObject* Obj) override;
|
||||
virtual int32 GetPriority() const override;
|
||||
//~ End FReimportHandler Interface
|
||||
};
|
||||
62
Plugins/SUDS/Source/SUDSEditor/SUDSEditor.Build.cs
Normal file
62
Plugins/SUDS/Source/SUDSEditor/SUDSEditor.Build.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class SUDSEditor : ModuleRules
|
||||
{
|
||||
public SUDSEditor(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add public include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add other private include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PublicDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"Core",
|
||||
"SUDS"
|
||||
// ... add other public dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
"InputCore", // needed by come Slate widgets
|
||||
"Projects", // So that we can use the IPluginManager, required for our custom style
|
||||
"ToolMenus",
|
||||
"MessageLog",
|
||||
"UnrealEd",
|
||||
"EditorStyle"
|
||||
// ... add private dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
DynamicallyLoadedModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
// ... add any modules that your module loads dynamically here ...
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
21
Plugins/SUDS/Source/SUDSTest/Private/SudsTestModule.cpp
Normal file
21
Plugins/SUDS/Source/SUDSTest/Private/SudsTestModule.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#include "SudsTestModule.h"
|
||||
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogSudsTestModule)
|
||||
|
||||
void FSudsTestModule::StartupModule()
|
||||
{
|
||||
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
|
||||
UE_LOG(LogSudsTestModule, Log, TEXT("SUDSTest Module Started"))
|
||||
}
|
||||
|
||||
void FSudsTestModule::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.
|
||||
UE_LOG(LogSudsTestModule, Log, TEXT("SUDSTest Module Stopped"))
|
||||
}
|
||||
|
||||
|
||||
IMPLEMENT_MODULE(FSudsTestModule, SUDSTest)
|
||||
14
Plugins/SUDS/Source/SUDSTest/Private/SudsTestModule.h
Normal file
14
Plugins/SUDS/Source/SUDSTest/Private/SudsTestModule.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
#include "Modules/ModuleInterface.h"
|
||||
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogSudsTestModule, All, All)
|
||||
|
||||
class FSudsTestModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
};
|
||||
274
Plugins/SUDS/Source/SUDSTest/Private/TestChoiceSpeakerLines.cpp
Normal file
274
Plugins/SUDS/Source/SUDSTest/Private/TestChoiceSpeakerLines.cpp
Normal file
@@ -0,0 +1,274 @@
|
||||
#include "SUDSDialogue.h"
|
||||
#include "SUDSLibrary.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptImporter.h"
|
||||
#include "TestParticipant.h"
|
||||
#include "TestUtils.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
UE_DISABLE_OPTIMIZATION
|
||||
|
||||
const FString ChoicesAsSpeakerLineEnableInScriptInput = R"RAWSUD(
|
||||
===
|
||||
[importsetting GenerateSpeakerLinesFromChoices true]
|
||||
===
|
||||
Player: Hello
|
||||
* Choice 1
|
||||
NPC: I see
|
||||
* Choice 1a
|
||||
NPC: Sure
|
||||
*- Not a speaker line!
|
||||
Player: I had to say this separately
|
||||
* Choice 2
|
||||
NPC: Totally
|
||||
|
||||
Player: The end
|
||||
)RAWSUD";
|
||||
|
||||
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestChoiceSpeakerLineInScript,
|
||||
"SUDSTest.TestChoiceSpeakerLineInScript",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestChoiceSpeakerLineInScript::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ChoicesAsSpeakerLineEnableInScriptInput), ChoicesAsSpeakerLineEnableInScriptInput.Len(), "ChoicesAsSpeakerLineEnableInScriptInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
auto Participant = NewObject<UTestParticipant>();
|
||||
Participant->TestNumber = 0;
|
||||
Dlg->AddParticipant(Participant);
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Line 1", Dlg, "Player", "Hello");
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1"));
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Choice 2"));
|
||||
const FText ChoiceText0 = Dlg->GetChoiceText(0);
|
||||
Dlg->Choose(0);
|
||||
// Confirm that we got the choice as a speaker line
|
||||
TestDialogueText(this, "Choice as speaker line 1", Dlg, "Player", "Choice 1");
|
||||
// Confirm that text ID is the same
|
||||
TestTrue("Text should be identical", ChoiceText0.IdenticalTo(Dlg->GetText(), ETextIdenticalModeFlags::DeepCompare));
|
||||
TestEqual("TextIDs should match", SUDS_GET_TEXT_KEY(ChoiceText0), SUDS_GET_TEXT_KEY(Dlg->GetText()));
|
||||
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Regular speaker line", Dlg, "NPC", "I see");
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1a"));
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Not a speaker line!"));
|
||||
|
||||
// Test choice which disables speaker line
|
||||
Dlg->Choose(1);
|
||||
TestDialogueText(this, "Regular speaker line", Dlg, "Player", "I had to say this separately");
|
||||
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
const FString ChoicesAsSpeakerLineSetSpeakerIdInScriptInput = R"RAWSUD(
|
||||
===
|
||||
[importsetting GenerateSpeakerLinesFromChoices true]
|
||||
[importsetting SpeakerIDForGeneratedLinesFromChoices `TheDude`]
|
||||
===
|
||||
Player: Hello
|
||||
* Choice 1
|
||||
NPC: You said that choice
|
||||
* Choice 2
|
||||
NPC: Also that one
|
||||
Player: The end
|
||||
)RAWSUD";
|
||||
|
||||
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestChoiceSpeakerSetSpeakerIdInScriptInput,
|
||||
"SUDSTest.TestChoiceSpeakerSetSpeakerIdInScriptInput",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestChoiceSpeakerSetSpeakerIdInScriptInput::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ChoicesAsSpeakerLineSetSpeakerIdInScriptInput), ChoicesAsSpeakerLineSetSpeakerIdInScriptInput.Len(), "ChoicesAsSpeakerLineSetSpeakerIdInScriptInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
auto Participant = NewObject<UTestParticipant>();
|
||||
Participant->TestNumber = 0;
|
||||
Dlg->AddParticipant(Participant);
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Line 1", Dlg, "Player", "Hello");
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1"));
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Choice 2"));
|
||||
const FText ChoiceText0 = Dlg->GetChoiceText(0);
|
||||
Dlg->Choose(0);
|
||||
// Confirm that text ID is the same
|
||||
TestTrue("Text should be identical", ChoiceText0.IdenticalTo(Dlg->GetText(), ETextIdenticalModeFlags::DeepCompare));
|
||||
TestEqual("TextIDs should match", SUDS_GET_TEXT_KEY(ChoiceText0), SUDS_GET_TEXT_KEY(Dlg->GetText()));
|
||||
|
||||
// Confirm that we got the choice as a speaker line, with custom speaker ID
|
||||
TestDialogueText(this, "Choice as speaker line 1", Dlg, "TheDude", "Choice 1");
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Regular speaker line", Dlg, "NPC", "You said that choice");
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
const FString ChoicesAsSpeakerLineChainChoiceInput = R"RAWSUD(
|
||||
===
|
||||
[importsetting GenerateSpeakerLinesFromChoices true]
|
||||
===
|
||||
Player: Hello
|
||||
* Choice 1
|
||||
* Choice 1a
|
||||
NPC: Hey this worked
|
||||
* Choice 1b
|
||||
* Choice 2
|
||||
NPC: Also that one
|
||||
Player: The end
|
||||
)RAWSUD";
|
||||
|
||||
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestChoiceSpeakerChainedChoiceInput,
|
||||
"SUDSTest.TestChoiceSpeakerChainedChoiceInput",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestChoiceSpeakerChainedChoiceInput::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ChoicesAsSpeakerLineChainChoiceInput), ChoicesAsSpeakerLineChainChoiceInput.Len(), "ChoicesAsSpeakerLineChainChoiceInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
auto Participant = NewObject<UTestParticipant>();
|
||||
Participant->TestNumber = 0;
|
||||
Dlg->AddParticipant(Participant);
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Line 1", Dlg, "Player", "Hello");
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1"));
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Choice 2"));
|
||||
Dlg->Choose(0);
|
||||
// Confirm that we got the choice as a speaker line, with custom speaker ID
|
||||
TestDialogueText(this, "Choice as speaker line 1", Dlg, "Player", "Choice 1");
|
||||
TestEqual("Choice 2nd level text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1a"));
|
||||
TestEqual("Choice 2nd leveltext", Dlg->GetChoiceText(1).ToString(), TEXT("Choice 1b"));
|
||||
Dlg->Choose(0);
|
||||
TestDialogueText(this, "Choice as speaker line 1", Dlg, "Player", "Choice 1a");
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Regular speaker line", Dlg, "NPC", "Hey this worked");
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
const FString ChoicesAsSpeakerLineWithStringKeysInput = R"RAWSUD(
|
||||
===
|
||||
[importsetting GenerateSpeakerLinesFromChoices true]
|
||||
===
|
||||
Player: Hello @0012@
|
||||
* Choice 1 @0016@
|
||||
NPC: I see @0017@
|
||||
* Choice 1a @0018@
|
||||
NPC: Sure
|
||||
*- Not a speaker line!
|
||||
Player: I had to say this separately
|
||||
* Choice 2
|
||||
NPC: Totally
|
||||
|
||||
Player: The end
|
||||
)RAWSUD";
|
||||
|
||||
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestChoicesAsSpeakerLineWithStringKeys,
|
||||
"SUDSTest.TestChoicesAsSpeakerLineWithStringKeys",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestChoicesAsSpeakerLineWithStringKeys::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ChoicesAsSpeakerLineWithStringKeysInput), ChoicesAsSpeakerLineWithStringKeysInput.Len(), "ChoicesAsSpeakerLineWithStringKeysInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
auto Participant = NewObject<UTestParticipant>();
|
||||
Participant->TestNumber = 0;
|
||||
Dlg->AddParticipant(Participant);
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Line 1", Dlg, "Player", "Hello");
|
||||
TestEqual("String key", SUDS_GET_TEXT_KEY(Dlg->GetText()), "@0012@");
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1"));
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Choice 2"));
|
||||
TestEqual("String key", SUDS_GET_TEXT_KEY(Dlg->GetChoiceText(0)), "@0016@");
|
||||
FText ChoiceText0 = Dlg->GetChoiceText(0);
|
||||
Dlg->Choose(0);
|
||||
// Confirm that we got the choice as a speaker line
|
||||
TestDialogueText(this, "Choice as speaker line 1", Dlg, "Player", "Choice 1");
|
||||
// Confirm that text ID is the same
|
||||
TestTrue("Text should be identical", ChoiceText0.IdenticalTo(Dlg->GetText(), ETextIdenticalModeFlags::DeepCompare));
|
||||
TestEqual("TextIDs should match", SUDS_GET_TEXT_KEY(ChoiceText0), SUDS_GET_TEXT_KEY(Dlg->GetText()));
|
||||
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Regular speaker line", Dlg, "NPC", "I see");
|
||||
TestEqual("String key", SUDS_GET_TEXT_KEY(Dlg->GetText()), "@0017@");
|
||||
TestEqual("String key", SUDS_GET_TEXT_KEY(Dlg->GetChoiceText(0)), "@0018@");
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1a"));
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Not a speaker line!"));
|
||||
|
||||
ChoiceText0 = Dlg->GetChoiceText(0);
|
||||
Dlg->Choose(0);
|
||||
TestDialogueText(this, "Duplicated speaker line", Dlg, "Player", "Choice 1a");
|
||||
|
||||
// Confirm that text ID is the same
|
||||
TestTrue("Text should be identical", ChoiceText0.IdenticalTo(Dlg->GetText(), ETextIdenticalModeFlags::DeepCompare));
|
||||
TestEqual("TextIDs should match", SUDS_GET_TEXT_KEY(ChoiceText0), SUDS_GET_TEXT_KEY(Dlg->GetText()));
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
UE_ENABLE_OPTIMIZATION
|
||||
1045
Plugins/SUDS/Source/SUDSTest/Private/TestConditionals.cpp
Normal file
1045
Plugins/SUDS/Source/SUDSTest/Private/TestConditionals.cpp
Normal file
File diff suppressed because it is too large
Load Diff
20
Plugins/SUDS/Source/SUDSTest/Private/TestEventSub.cpp
Normal file
20
Plugins/SUDS/Source/SUDSTest/Private/TestEventSub.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include "TestEventSub.h"
|
||||
|
||||
#include "SUDSDialogue.h"
|
||||
|
||||
void UTestEventSub::Init(USUDSDialogue* Dlg)
|
||||
{
|
||||
Dlg->OnEvent.AddDynamic(this, &UTestEventSub::OnEvent);
|
||||
Dlg->OnVariableChanged.AddDynamic(this, &UTestEventSub::OnVariableChanged);
|
||||
|
||||
}
|
||||
|
||||
void UTestEventSub::OnEvent(USUDSDialogue* Dlg, FName EventName, const TArray<FSUDSValue>& Args)
|
||||
{
|
||||
EventRecords.Add(FEventRecord { EventName, Args });
|
||||
}
|
||||
|
||||
void UTestEventSub::OnVariableChanged(USUDSDialogue* Dlg, FName VarName, const FSUDSValue& Value, bool bFromScript)
|
||||
{
|
||||
SetVarRecords.Add(FSetVarRecord { VarName, Value, bFromScript });
|
||||
}
|
||||
39
Plugins/SUDS/Source/SUDSTest/Private/TestEventSub.h
Normal file
39
Plugins/SUDS/Source/SUDSTest/Private/TestEventSub.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSValue.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "TestEventSub.generated.h"
|
||||
|
||||
class USUDSDialogue;
|
||||
UCLASS()
|
||||
class SUDSTEST_API UTestEventSub : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
void Init(USUDSDialogue* Dlg);
|
||||
|
||||
struct FEventRecord
|
||||
{
|
||||
FName Name;
|
||||
TArray<FSUDSValue> Args;
|
||||
};
|
||||
struct FSetVarRecord
|
||||
{
|
||||
FName Name;
|
||||
FSUDSValue Value;
|
||||
bool bFromScript;
|
||||
};
|
||||
|
||||
TArray<FEventRecord> EventRecords;
|
||||
TArray<FSetVarRecord> SetVarRecords;
|
||||
|
||||
UFUNCTION()
|
||||
void OnEvent(USUDSDialogue* Dlg, FName EventName, const TArray<FSUDSValue>& Args);
|
||||
|
||||
UFUNCTION()
|
||||
void OnVariableChanged(USUDSDialogue* Dlg, FName VarName, const FSUDSValue& Value, bool bFromScript);
|
||||
|
||||
|
||||
};
|
||||
278
Plugins/SUDS/Source/SUDSTest/Private/TestEvents.cpp
Normal file
278
Plugins/SUDS/Source/SUDSTest/Private/TestEvents.cpp
Normal file
@@ -0,0 +1,278 @@
|
||||
#include "SUDSDialogue.h"
|
||||
#include "SUDSLibrary.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptImporter.h"
|
||||
#include "TestEventSub.h"
|
||||
#include "TestParticipant.h"
|
||||
#include "TestUtils.h"
|
||||
|
||||
UE_DISABLE_OPTIMIZATION
|
||||
|
||||
const FString EventParsingInput = R"RAWSUD(
|
||||
Player: Ow do?
|
||||
[set IntVar 2]
|
||||
[set FloatVar 66.67]
|
||||
[set StringVar "Ey up"]
|
||||
[event SummatHappened "2 penneth", 99.99, masculine, {IntVar}, 42, false]
|
||||
NPC: Alreet chook
|
||||
[event Well.Blow.Me.Down {FloatVar}, true, {StringVar}]
|
||||
[event Calculated {FloatVar} + 10, true or false, {StringVar}]
|
||||
[event Actor.Emote `Player`, `Question`]
|
||||
Player: Tara
|
||||
)RAWSUD";
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestEvents,
|
||||
"SUDSTest.TestEvents",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
bool FTestEvents::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(EventParsingInput), EventParsingInput.Len(), "EventParsingInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
|
||||
// Subscriber
|
||||
auto EvtSub = NewObject<UTestEventSub>();
|
||||
EvtSub->Init(Dlg);
|
||||
|
||||
// Participant
|
||||
auto Participant = NewObject<UTestParticipant>();
|
||||
Participant->TestNumber = 0;
|
||||
Dlg->AddParticipant(Participant);
|
||||
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Line 1", Dlg, "Player", "Ow do?");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
|
||||
// Should have had an event
|
||||
if (TestEqual("Event sub should have received", EvtSub->EventRecords.Num(), 1))
|
||||
{
|
||||
TestEqual("Event name", EvtSub->EventRecords[0].Name.ToString(), "SummatHappened");
|
||||
if (TestEqual("Event sub arg count", EvtSub->EventRecords[0].Args.Num(), 6))
|
||||
{
|
||||
TestEqual("Event sub arg 0 type", EvtSub->EventRecords[0].Args[0].GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Event sub arg 0 value", EvtSub->EventRecords[0].Args[0].GetTextValue().ToString(), "2 penneth");
|
||||
TestEqual("Event sub arg 1 type", EvtSub->EventRecords[0].Args[1].GetType(), ESUDSValueType::Float);
|
||||
TestEqual("Event sub arg 1 value", EvtSub->EventRecords[0].Args[1].GetFloatValue(), 99.99f);
|
||||
TestEqual("Event sub arg 2 type", EvtSub->EventRecords[0].Args[2].GetType(), ESUDSValueType::Gender);
|
||||
TestEqual("Event sub arg 2 value", EvtSub->EventRecords[0].Args[2].GetGenderValue(), ETextGender::Masculine);
|
||||
TestEqual("Event sub arg 3 type", EvtSub->EventRecords[0].Args[3].GetType(), ESUDSValueType::Int);
|
||||
TestEqual("Event sub arg 3 value", EvtSub->EventRecords[0].Args[3].GetIntValue(), 2);
|
||||
TestEqual("Event sub arg 4 type", EvtSub->EventRecords[0].Args[4].GetType(), ESUDSValueType::Int);
|
||||
TestEqual("Event sub arg 4 value", EvtSub->EventRecords[0].Args[4].GetIntValue(), 42);
|
||||
TestEqual("Event sub arg 5 type", EvtSub->EventRecords[0].Args[5].GetType(), ESUDSValueType::Boolean);
|
||||
TestEqual("Event sub arg 5 value", EvtSub->EventRecords[0].Args[5].GetBooleanValue(), false);
|
||||
}
|
||||
}
|
||||
if (TestEqual("Participant should have received", Participant->EventRecords.Num(), 1))
|
||||
{
|
||||
TestEqual("Event name", Participant->EventRecords[0].Name.ToString(), "SummatHappened");
|
||||
if (TestEqual("Participant arg count", Participant->EventRecords[0].Args.Num(), 6))
|
||||
{
|
||||
TestEqual("Participant arg 0 type", Participant->EventRecords[0].Args[0].GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Participant arg 0 value", Participant->EventRecords[0].Args[0].GetTextValue().ToString(), "2 penneth");
|
||||
TestEqual("Participant arg 1 type", Participant->EventRecords[0].Args[1].GetType(), ESUDSValueType::Float);
|
||||
TestEqual("Participant arg 1 value", Participant->EventRecords[0].Args[1].GetFloatValue(), 99.99f);
|
||||
TestEqual("Participant arg 2 type", Participant->EventRecords[0].Args[2].GetType(), ESUDSValueType::Gender);
|
||||
TestEqual("Participant arg 2 value", Participant->EventRecords[0].Args[2].GetGenderValue(), ETextGender::Masculine);
|
||||
TestEqual("Participant arg 3 type", Participant->EventRecords[0].Args[3].GetType(), ESUDSValueType::Int);
|
||||
TestEqual("Participant arg 3 value", Participant->EventRecords[0].Args[3].GetIntValue(), 2);
|
||||
TestEqual("Participant arg 4 type", Participant->EventRecords[0].Args[4].GetType(), ESUDSValueType::Int);
|
||||
TestEqual("Participant arg 4 value", Participant->EventRecords[0].Args[4].GetIntValue(), 42);
|
||||
TestEqual("Participant arg 5 type", Participant->EventRecords[0].Args[5].GetType(), ESUDSValueType::Boolean);
|
||||
TestEqual("Participant arg 5 value", Participant->EventRecords[0].Args[5].GetBooleanValue(), false);
|
||||
|
||||
}
|
||||
}
|
||||
if (TestEqual("Event var sub should have received", EvtSub->SetVarRecords.Num(), 10))
|
||||
{
|
||||
// First 7 are from the participant setting variables at startup
|
||||
TestEqual("Event var sub arg 0 name", EvtSub->SetVarRecords[0].Name.ToString(), "SpeakerName.Player");
|
||||
TestEqual("Event var sub arg 0 type", EvtSub->SetVarRecords[0].Value.GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Event var sub arg 0 value", EvtSub->SetVarRecords[0].Value.GetTextValue().ToString(), "Protagonist");
|
||||
TestFalse("Event var sub arg 0 fromscript", EvtSub->SetVarRecords[0].bFromScript);
|
||||
|
||||
TestEqual("Event var sub arg 1 name", EvtSub->SetVarRecords[1].Name.ToString(), "SpeakerName.NPC");
|
||||
TestEqual("Event var sub arg 1 type", EvtSub->SetVarRecords[1].Value.GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Event var sub arg 1 value", EvtSub->SetVarRecords[1].Value.GetTextValue().ToString(), "An NPC");
|
||||
TestFalse("Event var sub arg 1 fromscript", EvtSub->SetVarRecords[1].bFromScript);
|
||||
|
||||
TestEqual("Event var sub arg 2 name", EvtSub->SetVarRecords[2].Name.ToString(), "NumCats");
|
||||
TestEqual("Event var sub arg 2 type", EvtSub->SetVarRecords[2].Value.GetType(), ESUDSValueType::Int);
|
||||
TestEqual("Event var sub arg 2 value", EvtSub->SetVarRecords[2].Value.GetIntValue(), 3);
|
||||
TestFalse("Event var sub arg 2 fromscript", EvtSub->SetVarRecords[2].bFromScript);
|
||||
|
||||
TestEqual("Event var sub arg 3 name", EvtSub->SetVarRecords[3].Name.ToString(), "FriendName");
|
||||
TestEqual("Event var sub arg 3 type", EvtSub->SetVarRecords[3].Value.GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Event var sub arg 3 value", EvtSub->SetVarRecords[3].Value.GetTextValue().ToString(), "Susan");
|
||||
TestFalse("Event var sub arg 3 fromscript", EvtSub->SetVarRecords[3].bFromScript);
|
||||
|
||||
TestEqual("Event var sub arg 4 name", EvtSub->SetVarRecords[4].Name.ToString(), "Gender");
|
||||
TestEqual("Event var sub arg 4 type", EvtSub->SetVarRecords[4].Value.GetType(), ESUDSValueType::Gender);
|
||||
TestEqual("Event var sub arg 4 value", EvtSub->SetVarRecords[4].Value.GetGenderValue(), ETextGender::Feminine);
|
||||
TestFalse("Event var sub arg 4 fromscript", EvtSub->SetVarRecords[4].bFromScript);
|
||||
|
||||
TestEqual("Event var sub arg 5 name", EvtSub->SetVarRecords[5].Name.ToString(), "FloatVal");
|
||||
TestEqual("Event var sub arg 5 type", EvtSub->SetVarRecords[5].Value.GetType(), ESUDSValueType::Float);
|
||||
TestEqual("Event var sub arg 5 value", EvtSub->SetVarRecords[5].Value.GetFloatValue(), 12.567f);
|
||||
TestFalse("Event var sub arg 5 fromscript", EvtSub->SetVarRecords[5].bFromScript);
|
||||
|
||||
TestEqual("Event var sub arg 6 name", EvtSub->SetVarRecords[6].Name.ToString(), "BoolVal");
|
||||
TestEqual("Event var sub arg 6 type", EvtSub->SetVarRecords[6].Value.GetType(), ESUDSValueType::Boolean);
|
||||
TestEqual("Event var sub arg 6 value", EvtSub->SetVarRecords[6].Value.GetBooleanValue(), true);
|
||||
TestFalse("Event var sub arg 6 fromscript", EvtSub->SetVarRecords[6].bFromScript);
|
||||
|
||||
// Next 3 are from script
|
||||
TestEqual("Event var sub arg 7 name", EvtSub->SetVarRecords[7].Name.ToString(), "IntVar");
|
||||
TestEqual("Event var sub arg 7 type", EvtSub->SetVarRecords[7].Value.GetType(), ESUDSValueType::Int);
|
||||
TestEqual("Event var sub arg 7 value", EvtSub->SetVarRecords[7].Value.GetIntValue(), 2);
|
||||
TestTrue("Event var sub arg 7 fromscript", EvtSub->SetVarRecords[7].bFromScript);
|
||||
|
||||
TestEqual("Event var sub arg 8 name", EvtSub->SetVarRecords[8].Name.ToString(), "FloatVar");
|
||||
TestEqual("Event var sub arg 8 type", EvtSub->SetVarRecords[8].Value.GetType(), ESUDSValueType::Float);
|
||||
TestEqual("Event var sub arg 8 value", EvtSub->SetVarRecords[8].Value.GetFloatValue(), 66.67f);
|
||||
TestTrue("Event var sub arg 8 fromscript", EvtSub->SetVarRecords[8].bFromScript);
|
||||
|
||||
TestEqual("Event var sub arg 9 name", EvtSub->SetVarRecords[9].Name.ToString(), "StringVar");
|
||||
TestEqual("Event var sub arg 9 type", EvtSub->SetVarRecords[9].Value.GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Event var sub arg 9 value", EvtSub->SetVarRecords[9].Value.GetTextValue().ToString(), "Ey up");
|
||||
TestTrue("Event var sub arg 9 fromscript", EvtSub->SetVarRecords[9].bFromScript);
|
||||
}
|
||||
if (TestEqual("Participant var should have received", Participant->SetVarRecords.Num(), 10))
|
||||
{
|
||||
// First 7 are from the participant setting variables at startup
|
||||
TestEqual("Participant var arg 0 name", Participant->SetVarRecords[0].Name.ToString(), "SpeakerName.Player");
|
||||
TestEqual("Participant var arg 0 type", Participant->SetVarRecords[0].Value.GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Participant var arg 0 value", Participant->SetVarRecords[0].Value.GetTextValue().ToString(), "Protagonist");
|
||||
TestFalse("Participant var arg 0 fromscript", Participant->SetVarRecords[0].bFromScript);
|
||||
|
||||
TestEqual("Participant var arg 1 name", Participant->SetVarRecords[1].Name.ToString(), "SpeakerName.NPC");
|
||||
TestEqual("Participant var arg 1 type", Participant->SetVarRecords[1].Value.GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Participant var arg 1 value", Participant->SetVarRecords[1].Value.GetTextValue().ToString(), "An NPC");
|
||||
TestFalse("Participant var arg 1 fromscript", Participant->SetVarRecords[1].bFromScript);
|
||||
|
||||
TestEqual("Participant var arg 2 name", Participant->SetVarRecords[2].Name.ToString(), "NumCats");
|
||||
TestEqual("Participant var arg 2 type", Participant->SetVarRecords[2].Value.GetType(), ESUDSValueType::Int);
|
||||
TestEqual("Participant var arg 2 value", Participant->SetVarRecords[2].Value.GetIntValue(), 3);
|
||||
TestFalse("Participant var arg 2 fromscript", Participant->SetVarRecords[2].bFromScript);
|
||||
|
||||
TestEqual("Participant var arg 3 name", Participant->SetVarRecords[3].Name.ToString(), "FriendName");
|
||||
TestEqual("Participant var arg 3 type", Participant->SetVarRecords[3].Value.GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Participant var arg 3 value", Participant->SetVarRecords[3].Value.GetTextValue().ToString(), "Susan");
|
||||
TestFalse("Participant var arg 3 fromscript", Participant->SetVarRecords[3].bFromScript);
|
||||
|
||||
TestEqual("Participant var arg 4 name", Participant->SetVarRecords[4].Name.ToString(), "Gender");
|
||||
TestEqual("Participant var arg 4 type", Participant->SetVarRecords[4].Value.GetType(), ESUDSValueType::Gender);
|
||||
TestEqual("Participant var arg 4 value", Participant->SetVarRecords[4].Value.GetGenderValue(), ETextGender::Feminine);
|
||||
TestFalse("Participant var arg 4 fromscript", Participant->SetVarRecords[4].bFromScript);
|
||||
|
||||
TestEqual("Participant var arg 5 name", Participant->SetVarRecords[5].Name.ToString(), "FloatVal");
|
||||
TestEqual("Participant var arg 5 type", Participant->SetVarRecords[5].Value.GetType(), ESUDSValueType::Float);
|
||||
TestEqual("Participant var arg 5 value", Participant->SetVarRecords[5].Value.GetFloatValue(), 12.567f);
|
||||
TestFalse("Participant var arg 5 fromscript", Participant->SetVarRecords[5].bFromScript);
|
||||
|
||||
TestEqual("Participant var arg 6 name", Participant->SetVarRecords[6].Name.ToString(), "BoolVal");
|
||||
TestEqual("Participant var arg 6 type", Participant->SetVarRecords[6].Value.GetType(), ESUDSValueType::Boolean);
|
||||
TestEqual("Participant var arg 6 value", Participant->SetVarRecords[6].Value.GetBooleanValue(), true);
|
||||
TestFalse("Participant var arg 6 fromscript", Participant->SetVarRecords[6].bFromScript);
|
||||
|
||||
// Next 3 are from script
|
||||
TestEqual("Participant var arg 7 name", Participant->SetVarRecords[7].Name.ToString(), "IntVar");
|
||||
TestEqual("Participant var arg 7 type", Participant->SetVarRecords[7].Value.GetType(), ESUDSValueType::Int);
|
||||
TestEqual("Participant var arg 7 value", Participant->SetVarRecords[7].Value.GetIntValue(), 2);
|
||||
TestTrue("Participant var arg 7 fromscript", Participant->SetVarRecords[7].bFromScript);
|
||||
|
||||
TestEqual("Participant var arg 8 name", Participant->SetVarRecords[8].Name.ToString(), "FloatVar");
|
||||
TestEqual("Participant var arg 8 type", Participant->SetVarRecords[8].Value.GetType(), ESUDSValueType::Float);
|
||||
TestEqual("Participant var arg 8 value", Participant->SetVarRecords[8].Value.GetFloatValue(), 66.67f);
|
||||
TestTrue("Participant var arg 8 fromscript", Participant->SetVarRecords[8].bFromScript);
|
||||
|
||||
TestEqual("Participant var arg 9 name", Participant->SetVarRecords[9].Name.ToString(), "StringVar");
|
||||
TestEqual("Participant var arg 9 type", Participant->SetVarRecords[9].Value.GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Participant var arg 9 value", Participant->SetVarRecords[9].Value.GetTextValue().ToString(), "Ey up");
|
||||
TestTrue("Participant var arg 9 fromscript", Participant->SetVarRecords[9].bFromScript);
|
||||
}
|
||||
|
||||
TestDialogueText(this, "Line 2", Dlg, "NPC", "Alreet chook");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
|
||||
if (TestEqual("Event sub should have received", EvtSub->EventRecords.Num(), 4))
|
||||
{
|
||||
TestEqual("Event name", EvtSub->EventRecords[1].Name.ToString(), "Well.Blow.Me.Down");
|
||||
if (TestEqual("Event sub arg count", EvtSub->EventRecords[1].Args.Num(), 3))
|
||||
{
|
||||
TestEqual("Event sub arg 0 type", EvtSub->EventRecords[1].Args[0].GetType(), ESUDSValueType::Float);
|
||||
TestEqual("Event sub arg 0 value", EvtSub->EventRecords[1].Args[0].GetFloatValue(), 66.67f);
|
||||
TestEqual("Event sub arg 1 type", EvtSub->EventRecords[1].Args[1].GetType(), ESUDSValueType::Boolean);
|
||||
TestEqual("Event sub arg 1 value", EvtSub->EventRecords[1].Args[1].GetBooleanValue(), true);
|
||||
TestEqual("Event sub arg 2 type", EvtSub->EventRecords[1].Args[2].GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Event sub arg 2 value", EvtSub->EventRecords[1].Args[2].GetTextValue().ToString(), "Ey up");
|
||||
}
|
||||
TestEqual("Event name", EvtSub->EventRecords[2].Name.ToString(), "Calculated");
|
||||
if (TestEqual("Event sub arg count", EvtSub->EventRecords[2].Args.Num(), 3))
|
||||
{
|
||||
TestEqual("Event sub arg 0 type", EvtSub->EventRecords[2].Args[0].GetType(), ESUDSValueType::Float);
|
||||
TestEqual("Event sub arg 0 value", EvtSub->EventRecords[2].Args[0].GetFloatValue(), 76.67f);
|
||||
TestEqual("Event sub arg 2 type", EvtSub->EventRecords[2].Args[1].GetType(), ESUDSValueType::Boolean);
|
||||
TestEqual("Event sub arg 2 value", EvtSub->EventRecords[2].Args[1].GetBooleanValue(), true);
|
||||
TestEqual("Event sub arg 2 type", EvtSub->EventRecords[2].Args[2].GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Event sub arg 2 value", EvtSub->EventRecords[2].Args[2].GetTextValue().ToString(), "Ey up");
|
||||
}
|
||||
TestEqual("Event name", EvtSub->EventRecords[3].Name.ToString(), "Actor.Emote");
|
||||
if (TestEqual("Event sub arg count", EvtSub->EventRecords[3].Args.Num(), 2))
|
||||
{
|
||||
TestEqual("Event sub arg 0 type", EvtSub->EventRecords[3].Args[0].GetType(), ESUDSValueType::Name);
|
||||
TestEqual("Event sub arg 0 value", EvtSub->EventRecords[3].Args[0].GetNameValue(), FName("Player"));
|
||||
TestEqual("Event sub arg 2 type", EvtSub->EventRecords[3].Args[1].GetType(), ESUDSValueType::Name);
|
||||
TestEqual("Event sub arg 2 value", EvtSub->EventRecords[3].Args[1].GetNameValue(), FName("Question"));
|
||||
}
|
||||
}
|
||||
if (TestEqual("Participant should have received", Participant->EventRecords.Num(), 4))
|
||||
{
|
||||
TestEqual("Event name", Participant->EventRecords[1].Name.ToString(), "Well.Blow.Me.Down");
|
||||
if (TestEqual("Participant arg count", Participant->EventRecords[1].Args.Num(), 3))
|
||||
{
|
||||
TestEqual("Participant arg 0 type", Participant->EventRecords[1].Args[0].GetType(), ESUDSValueType::Float);
|
||||
TestEqual("Participant arg 0 value", Participant->EventRecords[1].Args[0].GetFloatValue(), 66.67f);
|
||||
TestEqual("Participant arg 1 type", Participant->EventRecords[1].Args[1].GetType(), ESUDSValueType::Boolean);
|
||||
TestEqual("Participant arg 1 value", Participant->EventRecords[1].Args[1].GetBooleanValue(), true);
|
||||
TestEqual("Participant arg 2 type", Participant->EventRecords[1].Args[2].GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Participant arg 2 value", Participant->EventRecords[1].Args[2].GetTextValue().ToString(), "Ey up");
|
||||
}
|
||||
TestEqual("Event name", Participant->EventRecords[2].Name.ToString(), "Calculated");
|
||||
if (TestEqual("Event sub arg count", Participant->EventRecords[2].Args.Num(), 3))
|
||||
{
|
||||
TestEqual("Event sub arg 0 type", Participant->EventRecords[2].Args[0].GetType(), ESUDSValueType::Float);
|
||||
TestEqual("Event sub arg 0 value", Participant->EventRecords[2].Args[0].GetFloatValue(), 76.67f);
|
||||
TestEqual("Event sub arg 2 type", Participant->EventRecords[2].Args[1].GetType(), ESUDSValueType::Boolean);
|
||||
TestEqual("Event sub arg 2 value", Participant->EventRecords[2].Args[1].GetBooleanValue(), true);
|
||||
TestEqual("Event sub arg 2 type", Participant->EventRecords[2].Args[2].GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Event sub arg 2 value", Participant->EventRecords[2].Args[2].GetTextValue().ToString(), "Ey up");
|
||||
}
|
||||
TestEqual("Event name", EvtSub->EventRecords[3].Name.ToString(), "Actor.Emote");
|
||||
if (TestEqual("Event sub arg count", EvtSub->EventRecords[3].Args.Num(), 2))
|
||||
{
|
||||
TestEqual("Event sub arg 0 type", EvtSub->EventRecords[3].Args[0].GetType(), ESUDSValueType::Name);
|
||||
TestEqual("Event sub arg 0 value", EvtSub->EventRecords[3].Args[0].GetNameValue(), FName("Player"));
|
||||
TestEqual("Event sub arg 2 type", EvtSub->EventRecords[3].Args[1].GetType(), ESUDSValueType::Name);
|
||||
TestEqual("Event sub arg 2 value", EvtSub->EventRecords[3].Args[1].GetNameValue(), FName("Question"));
|
||||
}
|
||||
}
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
UE_ENABLE_OPTIMIZATION
|
||||
209
Plugins/SUDS/Source/SUDSTest/Private/TestExpressions.cpp
Normal file
209
Plugins/SUDS/Source/SUDSTest/Private/TestExpressions.cpp
Normal file
@@ -0,0 +1,209 @@
|
||||
#include "SUDSExpression.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
UE_DISABLE_OPTIMIZATION
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestExpressions,
|
||||
"SUDSTest.TestExpressions",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestExpressions::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSExpression Expr;
|
||||
TMap<FName, FSUDSValue> Variables;
|
||||
TMap<FName, FSUDSValue> GlobalVariables;
|
||||
|
||||
Variables.Add("Six", 6);
|
||||
TestTrue("SimpleVarParse", Expr.ParseFromString("3 + 4 * {Six} + 1", nullptr));
|
||||
TestEqual("Eval", Expr.Evaluate(Variables, GlobalVariables).GetIntValue(), 28);
|
||||
|
||||
auto& RPN = Expr.GetQueue();
|
||||
if (TestEqual("Queue len", RPN.Num(), 7))
|
||||
{
|
||||
// RPN should be 3 4 6 * + 1 +
|
||||
TestEqual("Queue 0", RPN[0].GetOperandValue().GetIntValue(), 3);
|
||||
TestEqual("Queue 1", RPN[1].GetOperandValue().GetIntValue(), 4);
|
||||
TestEqual("Queue 2", RPN[2].GetOperandValue().GetVariableNameValue().ToString(), "Six");
|
||||
TestEqual("Queue 3", RPN[3].GetType(), ESUDSExpressionItemType::Multiply);
|
||||
TestEqual("Queue 4", RPN[4].GetType(), ESUDSExpressionItemType::Add);
|
||||
TestEqual("Queue 5", RPN[5].GetOperandValue().GetIntValue(), 1);
|
||||
TestEqual("Queue 6", RPN[6].GetType(), ESUDSExpressionItemType::Add);
|
||||
}
|
||||
if (TestEqual("Variable count", Expr.GetVariableNames().Num(), 1))
|
||||
{
|
||||
TestEqual("Variable name", Expr.GetVariableNames()[0].ToString(), "Six");
|
||||
}
|
||||
|
||||
TestTrue("Arithmetic", Expr.ParseFromString("-6.7 * 2 + (21.3 - 8) * 5", nullptr));
|
||||
TestEqual("Eval", Expr.Evaluate(Variables, GlobalVariables).GetFloatValue(), 53.1f);
|
||||
|
||||
// Modulo operator
|
||||
TestTrue("ModuloIntOperator", Expr.ParseFromString("11 % 5", nullptr));
|
||||
TestEqual("Eval", Expr.Evaluate(Variables, GlobalVariables).GetIntValue(), 1);
|
||||
TestTrue("ModuloFloatOperator", Expr.ParseFromString("7.25 % 3.0", nullptr));
|
||||
TestEqual("Eval", Expr.Evaluate(Variables, GlobalVariables).GetFloatValue(), 1.25f);
|
||||
|
||||
// Explicit FSUDSValue(true) needed to avoid it using the int conversion by default
|
||||
Variables.Add("IsATest", FSUDSValue(true));
|
||||
TestTrue("BoolSingleValueParse", Expr.ParseFromString("{IsATest}", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
if (TestEqual("Variable count", Expr.GetVariableNames().Num(), 1))
|
||||
{
|
||||
TestEqual("Variable name", Expr.GetVariableNames()[0].ToString(), "IsATest");
|
||||
}
|
||||
|
||||
Variables.Add("SomethingFalse", FSUDSValue(false));
|
||||
Variables.Add("SomethingTrue", FSUDSValue(true));
|
||||
Variables.Add("SomethingElseFalse", FSUDSValue(false));
|
||||
TestTrue("BoolCompound1", Expr.ParseFromString("!{SomethingFalse} && {SomethingTrue}", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
if (TestEqual("Variable count", Expr.GetVariableNames().Num(), 2))
|
||||
{
|
||||
TestEqual("Variable name", Expr.GetVariableNames()[0].ToString(), "SomethingFalse");
|
||||
TestEqual("Variable name", Expr.GetVariableNames()[1].ToString(), "SomethingTrue");
|
||||
}
|
||||
TestTrue("BoolCompound2", Expr.ParseFromString("{SomethingFalse} || {SomethingTrue}", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("BoolCompound3", Expr.ParseFromString("{SomethingFalse} or {SomethingTrue}", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
// Test parentheses changing precedence & result
|
||||
// True result for successful parsing, but false for Eval unless we parenthesise
|
||||
TestTrue("BoolCompound4", Expr.ParseFromString("!{SomethingFalse} && {SomethingElseFalse} && {SomethingTrue}", nullptr));
|
||||
TestFalse("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
if (TestEqual("Variable count", Expr.GetVariableNames().Num(), 3))
|
||||
{
|
||||
TestEqual("Variable name", Expr.GetVariableNames()[0].ToString(), "SomethingFalse");
|
||||
TestEqual("Variable name", Expr.GetVariableNames()[1].ToString(), "SomethingElseFalse");
|
||||
TestEqual("Variable name", Expr.GetVariableNames()[2].ToString(), "SomethingTrue");
|
||||
}
|
||||
TestTrue("BoolCompound5", Expr.ParseFromString("!({SomethingFalse} && {SomethingElseFalse}) && {SomethingTrue}", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("BoolCompound6", Expr.ParseFromString("not {SomethingFalse} and {SomethingElseFalse} and {SomethingTrue}", nullptr));
|
||||
TestFalse("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("BoolCompound7", Expr.ParseFromString("not ({SomethingFalse} and {SomethingElseFalse}) and {SomethingTrue}", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
|
||||
Variables.Add("Seven", 7);
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{Six} == 6", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{Six} = 6", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{Six} >= 6", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{Six} > 6", nullptr));
|
||||
TestFalse("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{Six} < 6", nullptr));
|
||||
TestFalse("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{Six} <= 6", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{Six} < {Seven}", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{Seven} > {Six}", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{Seven} != {Six}", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{Seven} != {Seven}", nullptr));
|
||||
TestFalse("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
|
||||
// Mixed float/int comparisons
|
||||
Variables.Add("EightFloat", 8.1f);
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{EightFloat} > 8", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{EightFloat} > {Seven}", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{Six} < {EightFloat}", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
// Fuzzy float comparisons
|
||||
Variables.Add("EightFloatPlusMargin", 8.1000002f);
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{EightFloat} == 8.1", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{EightFloat} == 8.15", nullptr));
|
||||
TestFalse("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{EightFloatPlusMargin} == 8.1", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{EightFloat} == 8.1000005", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{EightFloatPlusMargin} == 8.1000005", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
|
||||
// Other type comparisons
|
||||
Variables.Add("SomeText", FText::FromString("Hello"));
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{SomeText} == \"Hello\"", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{SomeText} == \"Hi\"", nullptr));
|
||||
TestFalse("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
|
||||
Variables.Add("Male", ETextGender::Masculine);
|
||||
Variables.Add("Female", ETextGender::Feminine);
|
||||
Variables.Add("AlsoFemale", ETextGender::Feminine);
|
||||
Variables.Add("Neuter", ETextGender::Neuter);
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{Male} == masculine", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{Male} == Masculine", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{Male} == feminine", nullptr));
|
||||
TestFalse("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{Male} == Feminine", nullptr));
|
||||
TestFalse("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{Female} == Feminine", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{Female} != feminine", nullptr));
|
||||
TestFalse("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{Female} == {AlsoFemale}", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{Female} != {Neuter}", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{Neuter} == neuter", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("Comparisons", Expr.ParseFromString("{Neuter} == Neuter", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
|
||||
// Test local / global by defining the same variable
|
||||
GlobalVariables.Add("GlobalLocalTestInt", 3);
|
||||
Variables.Add("GlobalLocalTestInt", 20);
|
||||
TestTrue("LocalTest", Expr.ParseFromString("{GlobalLocalTestInt} == 20", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
TestTrue("GlobalTest", Expr.ParseFromString("{global.GlobalLocalTestInt} == 3", nullptr));
|
||||
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
|
||||
|
||||
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestBadExpressions,
|
||||
"SUDSTest.TestBadExpressions",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestBadExpressions::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSExpression Expr;
|
||||
TMap<FName, FSUDSValue> Variables;
|
||||
|
||||
FString ParseError;
|
||||
|
||||
TestFalse("Missing operand", Expr.ParseFromString(" + 1", &ParseError));
|
||||
TestTrue("Correct error", ParseError.Contains("Bad expression"));
|
||||
TestFalse("Missing operand", Expr.ParseFromString("1 * ", &ParseError));
|
||||
TestTrue("Correct error", ParseError.Contains("Bad expression"));
|
||||
TestFalse("Missing parenthesis", Expr.ParseFromString("(3 + 1", &ParseError));
|
||||
TestTrue("Correct error", ParseError.Contains("Mismatched parentheses"));
|
||||
TestFalse("Missing parenthesis", Expr.ParseFromString("3 + 1)", &ParseError));
|
||||
TestTrue("Correct error", ParseError.Contains("Mismatched parentheses"));
|
||||
TestFalse("Invalid symbol", Expr.ParseFromString("something + 1", &ParseError));
|
||||
TestTrue("Correct error", ParseError.Contains("Bad expression"));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
UE_ENABLE_OPTIMIZATION
|
||||
417
Plugins/SUDS/Source/SUDSTest/Private/TestGotoGosub.cpp
Normal file
417
Plugins/SUDS/Source/SUDSTest/Private/TestGotoGosub.cpp
Normal file
@@ -0,0 +1,417 @@
|
||||
#include "SUDSDialogue.h"
|
||||
#include "SUDSLibrary.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptImporter.h"
|
||||
#include "TestUtils.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
UE_DISABLE_OPTIMIZATION
|
||||
|
||||
const FString GotoGosubInput = R"RAWSUD(
|
||||
Player: Hello there
|
||||
NPC: Hello
|
||||
:choice
|
||||
* Actually bye
|
||||
NPC: How rude
|
||||
[goto end]
|
||||
* Reused
|
||||
NPC: This is going to re-used dialogue
|
||||
[gosub subroutine]
|
||||
NPC: And now we're back
|
||||
[goto goodbye]
|
||||
* Gosub choice
|
||||
NPC: This is going to return to the choice
|
||||
[gosub subroutine]
|
||||
* Choice After
|
||||
NPC: It's a choice after a gosub!
|
||||
[goto goodbye]
|
||||
* Choice After 2
|
||||
NPC: Not really much difference eh
|
||||
[goto goodbye]
|
||||
* Gosub choice via goto
|
||||
NPC: This is going to return to the choice via goto
|
||||
[gosub subroutine]
|
||||
[goto choice]
|
||||
|
||||
:subroutine
|
||||
Player: Some reused discussion
|
||||
NPC: Yep, sure is
|
||||
[return]
|
||||
:goodbye
|
||||
NPC: Bye!
|
||||
)RAWSUD";
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestGotoGosub,
|
||||
"SUDSTest.TestGotoGosub",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestGotoGosub::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(GotoGosubInput), GotoGosubInput.Len(), "GotoGosubInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
Dlg->Start();
|
||||
|
||||
|
||||
TestDialogueText(this, "Start node", Dlg, "Player", "Hello there");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Next", Dlg, "NPC", "Hello");
|
||||
TestEqual("Choices", Dlg->GetNumberOfChoices(), 4);
|
||||
TestEqual("Choice 0 text", Dlg->GetChoiceText(0).ToString(), "Actually bye");
|
||||
TestTrue("Choose 0", Dlg->Choose(0));
|
||||
TestDialogueText(this, "Next", Dlg, "NPC", "How rude");
|
||||
TestFalse("End", Dlg->Continue());
|
||||
|
||||
Dlg->Restart();
|
||||
TestDialogueText(this, "Start node", Dlg, "Player", "Hello there");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Next", Dlg, "NPC", "Hello");
|
||||
TestTrue("Choose 1", Dlg->Choose(1));
|
||||
TestDialogueText(this, "Next", Dlg, "NPC", "This is going to re-used dialogue");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Next", Dlg, "Player", "Some reused discussion");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Next", Dlg, "NPC", "Yep, sure is");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Next", Dlg, "NPC", "And now we're back");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Next", Dlg, "NPC", "Bye!");
|
||||
TestFalse("Continue", Dlg->Continue());
|
||||
|
||||
Dlg->Restart();
|
||||
TestDialogueText(this, "Start node", Dlg, "Player", "Hello there");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Next", Dlg, "NPC", "Hello");
|
||||
TestTrue("Choose 2", Dlg->Choose(2));
|
||||
TestDialogueText(this, "Next", Dlg, "NPC", "This is going to return to the choice");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Next", Dlg, "Player", "Some reused discussion");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Next", Dlg, "NPC", "Yep, sure is");
|
||||
// Should have a choice at the end of it this time
|
||||
TestEqual("Choices", Dlg->GetNumberOfChoices(), 2);
|
||||
TestEqual("Choice 0 text", Dlg->GetChoiceText(0).ToString(), "Choice After");
|
||||
TestEqual("Choice 1 text", Dlg->GetChoiceText(1).ToString(), "Choice After 2");
|
||||
TestTrue("Choose 1", Dlg->Choose(1));
|
||||
TestDialogueText(this, "Next", Dlg, "NPC", "Not really much difference eh");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Next", Dlg, "NPC", "Bye!");
|
||||
TestFalse("Continue", Dlg->Continue());
|
||||
|
||||
Dlg->Restart();
|
||||
TestDialogueText(this, "Start node", Dlg, "Player", "Hello there");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Next", Dlg, "NPC", "Hello");
|
||||
TestTrue("Choose 2", Dlg->Choose(3));
|
||||
TestDialogueText(this, "Next", Dlg, "NPC", "This is going to return to the choice via goto");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Next", Dlg, "Player", "Some reused discussion");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Next", Dlg, "NPC", "Yep, sure is");
|
||||
// Should have a choice at the end of it this time
|
||||
TestEqual("Choices", Dlg->GetNumberOfChoices(), 4);
|
||||
TestEqual("Choice 0 text", Dlg->GetChoiceText(0).ToString(), "Actually bye");
|
||||
TestTrue("Choose 0", Dlg->Choose(0));
|
||||
TestDialogueText(this, "Next", Dlg, "NPC", "How rude");
|
||||
TestFalse("End", Dlg->Continue());
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
const FString NestedGosubInput = R"RAWSUD(
|
||||
Player: Hello there
|
||||
[gosub sub1]
|
||||
NPC: Back at level 0
|
||||
[goto goodbye]
|
||||
|
||||
:sub1
|
||||
Player: This is level 1
|
||||
[gosub sub2_1]
|
||||
[gosub sub2_2]
|
||||
Player: End of level 1
|
||||
[return]
|
||||
|
||||
:sub2_1
|
||||
Player: This is level 2, sub 1
|
||||
[return]
|
||||
|
||||
|
||||
:sub2_2
|
||||
Player: This is level 2, sub 2
|
||||
NPC: We have to go deeper
|
||||
[gosub sub3]
|
||||
Player: End of level 2, sub 2
|
||||
[return]
|
||||
|
||||
:sub3
|
||||
Player: This is level 3
|
||||
[return]
|
||||
|
||||
:goodbye
|
||||
NPC: Bye!
|
||||
)RAWSUD";
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestNestedGosub,
|
||||
"SUDSTest.TestNestedGosub",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestNestedGosub::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(NestedGosubInput), NestedGosubInput.Len(), "NestedGosubInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
Dlg->Start();
|
||||
|
||||
|
||||
TestDialogueText(this, "Start node", Dlg, "Player", "Hello there");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Sub", Dlg, "Player", "This is level 1");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Sub", Dlg, "Player", "This is level 2, sub 1");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Sub", Dlg, "Player", "This is level 2, sub 2");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Sub", Dlg, "NPC", "We have to go deeper");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Sub", Dlg, "Player", "This is level 3");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Sub", Dlg, "Player", "End of level 2, sub 2");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Sub", Dlg, "Player", "End of level 1");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Sub", Dlg, "NPC", "Back at level 0");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Bye", Dlg, "NPC", "Bye!");
|
||||
TestFalse("Continue", Dlg->Continue());
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const FString GotoBetweenSpeakerAndChoiceInput = R"RAWSUD(
|
||||
Player: Hello there
|
||||
[if {SkipChoices}]
|
||||
[goto end]
|
||||
[endif]
|
||||
* Option A
|
||||
* Option B
|
||||
)RAWSUD";
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestGotoBetweenSpeakerAndChoice1,
|
||||
"SUDSTest.TestGotoBetweenSpeakerAndChoice1",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestGotoBetweenSpeakerAndChoice1::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(GotoBetweenSpeakerAndChoiceInput), GotoBetweenSpeakerAndChoiceInput.Len(), "GotoBetweenSpeakerAndChoiceInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
Dlg->Start();
|
||||
|
||||
|
||||
if (TestEqual("Choice Count", Dlg->GetNumberOfChoices(), 2))
|
||||
{
|
||||
TestEqual("Choice 1", Dlg->GetChoiceText(0).ToString(), "Option A");
|
||||
TestEqual("Choice 2", Dlg->GetChoiceText(1).ToString(), "Option B");
|
||||
}
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestGotoBetweenSpeakerAndChoice2,
|
||||
"SUDSTest.TestGotoBetweenSpeakerAndChoice2",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestGotoBetweenSpeakerAndChoice2::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(GotoBetweenSpeakerAndChoiceInput), GotoBetweenSpeakerAndChoiceInput.Len(), "GotoBetweenSpeakerAndChoiceInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
Dlg->SetVariableBoolean("SkipChoices", true);
|
||||
Dlg->Start();
|
||||
|
||||
|
||||
TestDialogueText(this, "Start node", Dlg, "Player", "Hello there");
|
||||
TestTrue("Plain continue", Dlg->IsSimpleContinue());
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
const FString GosubBetweenSpeakerAndChoiceInput1 = R"RAWSUD(
|
||||
Player: Hello there
|
||||
[gosub MaybeSkipChoices]
|
||||
* Option A
|
||||
* Option B
|
||||
[goto end]
|
||||
|
||||
:MaybeSkipChoices
|
||||
[if {SkipChoices}]
|
||||
[goto end]
|
||||
[endif]
|
||||
[return]
|
||||
)RAWSUD";
|
||||
|
||||
const FString GosubBetweenSpeakerAndChoiceInput2 = R"RAWSUD(
|
||||
Player: Hello there
|
||||
[gosub PrintDebug]
|
||||
[gosub MaybeSkipChoices]
|
||||
* Option A
|
||||
* Option B
|
||||
[goto end]
|
||||
|
||||
:PrintDebug
|
||||
Debug: SkipChoices is {SkipChoices}
|
||||
[return]
|
||||
|
||||
:MaybeSkipChoices
|
||||
[if {SkipChoices}]
|
||||
[goto end]
|
||||
[endif]
|
||||
Speaker: Another sentence.
|
||||
[return]
|
||||
)RAWSUD";
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestGosubBetweenSpeakerAndChoice1,
|
||||
"SUDSTest.TestGosubBetweenSpeakerAndChoice1",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestGosubBetweenSpeakerAndChoice1::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(GosubBetweenSpeakerAndChoiceInput1), GosubBetweenSpeakerAndChoiceInput1.Len(), "GosubBetweenSpeakerAndChoiceInput1", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
Dlg->Start();
|
||||
|
||||
|
||||
TestDialogueText(this, "Start node", Dlg, "Player", "Hello there");
|
||||
if (TestEqual("Choice Count", Dlg->GetNumberOfChoices(), 2))
|
||||
{
|
||||
TestEqual("Choice 1", Dlg->GetChoiceText(0).ToString(), "Option A");
|
||||
TestEqual("Choice 2", Dlg->GetChoiceText(1).ToString(), "Option B");
|
||||
}
|
||||
|
||||
// Now test the true case
|
||||
Dlg->SetVariableBoolean("SkipChoices", true);
|
||||
Dlg->Restart(false);
|
||||
TestDialogueText(this, "Start node", Dlg, "Player", "Hello there");
|
||||
TestTrue("Plain continue", Dlg->IsSimpleContinue());
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestGosubBetweenSpeakerAndChoice2,
|
||||
"SUDSTest.TestGosubBetweenSpeakerAndChoice2",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestGosubBetweenSpeakerAndChoice2::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(GosubBetweenSpeakerAndChoiceInput2), GosubBetweenSpeakerAndChoiceInput2.Len(), "GosubBetweenSpeakerAndChoiceInput2", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
Dlg->Start();
|
||||
|
||||
|
||||
TestDialogueText(this, "Start node", Dlg, "Player", "Hello there");
|
||||
// in this case, there's another speaker line in both nested gosubs so the choices are actually associated with the
|
||||
// last of those.
|
||||
TestTrue("Plain continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Gosub 1 speaker node", Dlg, "Debug", "SkipChoices is {SkipChoices}");
|
||||
TestTrue("Plain continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Gosub 2 speaker node", Dlg, "Speaker", "Another sentence.");
|
||||
|
||||
if (TestEqual("Choice Count", Dlg->GetNumberOfChoices(), 2))
|
||||
{
|
||||
TestEqual("Choice 1", Dlg->GetChoiceText(0).ToString(), "Option A");
|
||||
TestEqual("Choice 2", Dlg->GetChoiceText(1).ToString(), "Option B");
|
||||
}
|
||||
|
||||
// Now test the true case
|
||||
Dlg->SetVariableBoolean("SkipChoices", true);
|
||||
Dlg->Restart(false);
|
||||
TestDialogueText(this, "Start node", Dlg, "Player", "Hello there");
|
||||
TestTrue("Plain continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Gosub 1 speaker node", Dlg, "Debug", "SkipChoices is 1");
|
||||
TestTrue("Plain continue", Dlg->IsSimpleContinue());
|
||||
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
UE_ENABLE_OPTIMIZATION
|
||||
133
Plugins/SUDS/Source/SUDSTest/Private/TestMetadata.cpp
Normal file
133
Plugins/SUDS/Source/SUDSTest/Private/TestMetadata.cpp
Normal file
@@ -0,0 +1,133 @@
|
||||
#include "SUDSDialogue.h"
|
||||
#include "SUDSLibrary.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptImporter.h"
|
||||
#include "TestUtils.h"
|
||||
#include "Internationalization/StringTableCore.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
UE_DISABLE_OPTIMIZATION
|
||||
|
||||
// Assign known keys to the strings so we can detect
|
||||
const FString MetadataInput = R"RAWSUD(
|
||||
# No metadata
|
||||
Player: Hello there @001@
|
||||
#= Transient metadata for next line
|
||||
#= TransientData: Something here
|
||||
NPC: Hello @002@
|
||||
# Should have no metadata anymore
|
||||
Player: Well this is nice @003@
|
||||
#+ Persistent metadata
|
||||
#+ PersistentData: Something longer lived
|
||||
NPC: Isn't it though @004@
|
||||
# Should still apply here
|
||||
Player: Indeed @005@
|
||||
#= Test overriding temporarily
|
||||
#= PersistentData: This should override
|
||||
#= ExtraTransient: This should be new only for next line
|
||||
NPC: Well well @006@
|
||||
Player: Metadata should have gone back now @007@
|
||||
#+ Persistent metadata changed now
|
||||
Player: Persistent change @008@
|
||||
* Some choice @009@
|
||||
NPC: How rude @010@
|
||||
[goto end]
|
||||
#= Temporary comment
|
||||
* Another choice @011@
|
||||
NPC: This should be back now @012@
|
||||
#+ Persistent indented change 1
|
||||
* One more choice @013@
|
||||
#+ Even more indented
|
||||
#+ NestedKey: This will disappear fast even though persistent
|
||||
NPC: Ooops @014@
|
||||
* Final choice @015@
|
||||
NPC: This is going to return to the choice via goto @016@
|
||||
# Nested meta should have been lost but outer meta should be back
|
||||
Player: Something something @017@
|
||||
)RAWSUD";
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestMetadata,
|
||||
"SUDSTest.TestMetadata",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestMetadata::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(MetadataInput), MetadataInput.Len(), "MetadataInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// In this test we're only interested in the string table metadata
|
||||
// Have to get the mutable string table to get at metadata
|
||||
auto StrTable = StringTableHolder.StringTable->GetMutableStringTable();
|
||||
|
||||
TestEqual("Line 1 Comment", StrTable->GetMetaData(FTextKey("@001@"), FName("Comment")), "");
|
||||
TestEqual("Line 1 Speaker", StrTable->GetMetaData(FTextKey("@001@"), FName("Speaker")), "Player");
|
||||
TestEqual("Line 2 Comment", StrTable->GetMetaData(FTextKey("@002@"), FName("Comment")), "Transient metadata for next line");
|
||||
TestEqual("Line 2 Speaker", StrTable->GetMetaData(FTextKey("@002@"), FName("Speaker")), "NPC");
|
||||
TestEqual("Line 2 Custom", StrTable->GetMetaData(FTextKey("@002@"), FName("TransientData")), "Something here");
|
||||
TestEqual("Line 3 Comment (should have reset)", StrTable->GetMetaData(FTextKey("@003@"), FName("Comment")), "");
|
||||
TestEqual("Line 3 Speaker", StrTable->GetMetaData(FTextKey("@003@"), FName("Speaker")), "Player");
|
||||
TestEqual("Line 3 Custom (should have reset)", StrTable->GetMetaData(FTextKey("@003@"), FName("TransientData")), "");
|
||||
TestEqual("Line 4 Comment", StrTable->GetMetaData(FTextKey("@004@"), FName("Comment")), "Persistent metadata");
|
||||
TestEqual("Line 4 Speaker", StrTable->GetMetaData(FTextKey("@004@"), FName("Speaker")), "NPC");
|
||||
TestEqual("Line 4 Custom", StrTable->GetMetaData(FTextKey("@004@"), FName("PersistentData")), "Something longer lived");
|
||||
TestEqual("Line 5 Comment", StrTable->GetMetaData(FTextKey("@005@"), FName("Comment")), "Persistent metadata");
|
||||
TestEqual("Line 5 Speaker", StrTable->GetMetaData(FTextKey("@005@"), FName("Speaker")), "Player");
|
||||
TestEqual("Line 5 Custom", StrTable->GetMetaData(FTextKey("@005@"), FName("PersistentData")), "Something longer lived");
|
||||
TestEqual("Line 6 Comment", StrTable->GetMetaData(FTextKey("@006@"), FName("Comment")), "Test overriding temporarily");
|
||||
TestEqual("Line 6 Speaker", StrTable->GetMetaData(FTextKey("@006@"), FName("Speaker")), "NPC");
|
||||
TestEqual("Line 6 Custom", StrTable->GetMetaData(FTextKey("@006@"), FName("PersistentData")), "This should override");
|
||||
TestEqual("Line 6 Custom 2", StrTable->GetMetaData(FTextKey("@006@"), FName("ExtraTransient")), "This should be new only for next line");
|
||||
TestEqual("Line 7 Comment", StrTable->GetMetaData(FTextKey("@007@"), FName("Comment")), "Persistent metadata");
|
||||
TestEqual("Line 7 Speaker", StrTable->GetMetaData(FTextKey("@007@"), FName("Speaker")), "Player");
|
||||
TestEqual("Line 7 Custom", StrTable->GetMetaData(FTextKey("@007@"), FName("PersistentData")), "Something longer lived");
|
||||
TestEqual("Line 8 Comment", StrTable->GetMetaData(FTextKey("@008@"), FName("Comment")), "Persistent metadata changed now");
|
||||
TestEqual("Line 8 Speaker", StrTable->GetMetaData(FTextKey("@008@"), FName("Speaker")), "Player");
|
||||
TestEqual("Line 8 Custom", StrTable->GetMetaData(FTextKey("@008@"), FName("PersistentData")), "Something longer lived");
|
||||
TestEqual("Line 9 Comment", StrTable->GetMetaData(FTextKey("@009@"), FName("Comment")), "Persistent metadata changed now");
|
||||
TestEqual("Line 9 Speaker", StrTable->GetMetaData(FTextKey("@009@"), FName("Speaker")), "Player (Choice)");
|
||||
TestEqual("Line 9 Custom", StrTable->GetMetaData(FTextKey("@009@"), FName("PersistentData")), "Something longer lived");
|
||||
TestEqual("Line 10 Comment", StrTable->GetMetaData(FTextKey("@010@"), FName("Comment")), "Persistent metadata changed now");
|
||||
TestEqual("Line 10 Speaker", StrTable->GetMetaData(FTextKey("@010@"), FName("Speaker")), "NPC");
|
||||
TestEqual("Line 10 Custom", StrTable->GetMetaData(FTextKey("@010@"), FName("PersistentData")), "Something longer lived");
|
||||
TestEqual("Line 11 Comment", StrTable->GetMetaData(FTextKey("@011@"), FName("Comment")), "Temporary comment");
|
||||
TestEqual("Line 11 Speaker", StrTable->GetMetaData(FTextKey("@011@"), FName("Speaker")), "Player (Choice)");
|
||||
TestEqual("Line 11 Custom", StrTable->GetMetaData(FTextKey("@011@"), FName("PersistentData")), "Something longer lived");
|
||||
TestEqual("Line 12 Comment", StrTable->GetMetaData(FTextKey("@012@"), FName("Comment")), "Persistent metadata changed now");
|
||||
TestEqual("Line 12 Speaker", StrTable->GetMetaData(FTextKey("@012@"), FName("Speaker")), "NPC");
|
||||
TestEqual("Line 12 Custom", StrTable->GetMetaData(FTextKey("@012@"), FName("PersistentData")), "Something longer lived");
|
||||
TestEqual("Line 13 Comment", StrTable->GetMetaData(FTextKey("@013@"), FName("Comment")), "Persistent indented change 1");
|
||||
TestEqual("Line 13 Speaker", StrTable->GetMetaData(FTextKey("@013@"), FName("Speaker")), "Player (Choice)");
|
||||
TestEqual("Line 13 Custom", StrTable->GetMetaData(FTextKey("@013@"), FName("PersistentData")), "Something longer lived");
|
||||
TestEqual("Line 14 Comment", StrTable->GetMetaData(FTextKey("@014@"), FName("Comment")), "Even more indented");
|
||||
TestEqual("Line 14 Speaker", StrTable->GetMetaData(FTextKey("@014@"), FName("Speaker")), "NPC");
|
||||
TestEqual("Line 14 Custom", StrTable->GetMetaData(FTextKey("@014@"), FName("PersistentData")), "Something longer lived");
|
||||
TestEqual("Line 14 Custom 2", StrTable->GetMetaData(FTextKey("@014@"), FName("NestedKey")), "This will disappear fast even though persistent");
|
||||
TestEqual("Line 15 Comment", StrTable->GetMetaData(FTextKey("@015@"), FName("Comment")), "Persistent indented change 1"); // should have gone back
|
||||
TestEqual("Line 15 Speaker", StrTable->GetMetaData(FTextKey("@015@"), FName("Speaker")), "Player (Choice)");
|
||||
TestEqual("Line 15 Custom", StrTable->GetMetaData(FTextKey("@015@"), FName("PersistentData")), "Something longer lived");
|
||||
TestEqual("Line 15 Custom 2", StrTable->GetMetaData(FTextKey("@015@"), FName("NestedKey")), "");
|
||||
TestEqual("Line 16 Comment", StrTable->GetMetaData(FTextKey("@016@"), FName("Comment")), "Persistent indented change 1"); // should have gone back
|
||||
TestEqual("Line 16 Speaker", StrTable->GetMetaData(FTextKey("@016@"), FName("Speaker")), "NPC");
|
||||
TestEqual("Line 16 Custom", StrTable->GetMetaData(FTextKey("@016@"), FName("PersistentData")), "Something longer lived");
|
||||
TestEqual("Line 16 Custom 2", StrTable->GetMetaData(FTextKey("@016@"), FName("NestedKey")), "");
|
||||
TestEqual("Line 17 Comment", StrTable->GetMetaData(FTextKey("@017@"), FName("Comment")), "Persistent metadata changed now"); // should have gone back to top level
|
||||
TestEqual("Line 17 Speaker", StrTable->GetMetaData(FTextKey("@017@"), FName("Speaker")), "Player");
|
||||
TestEqual("Line 17 Custom", StrTable->GetMetaData(FTextKey("@017@"), FName("PersistentData")), "Something longer lived");
|
||||
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
UE_ENABLE_OPTIMIZATION
|
||||
147
Plugins/SUDS/Source/SUDSTest/Private/TestParameters.cpp
Normal file
147
Plugins/SUDS/Source/SUDSTest/Private/TestParameters.cpp
Normal file
@@ -0,0 +1,147 @@
|
||||
#include "SUDSDialogue.h"
|
||||
#include "SUDSLibrary.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptImporter.h"
|
||||
#include "TestParticipant.h"
|
||||
#include "TestUtils.h"
|
||||
#include "Internationalization/Internationalization.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
UE_DISABLE_OPTIMIZATION
|
||||
|
||||
const FString ParamsInput = R"RAWSUD(
|
||||
Player: Hello, I'm {SpeakerName.Player}
|
||||
NPC: Greetings, {SpeakerName.Player}, my name is {SpeakerName.NPC}
|
||||
Player: My friend's name is {FriendName}, {Gender}|gender(he,she,they) {Gender}|gender(has,has,have) {NumCats} {NumCats}|plural(one=cat,other=cats)
|
||||
NPC: Floating point {FloatVal} format test
|
||||
Player: Boolean test {BoolVal}?
|
||||
* Choose, {SpeakerName.Player}!
|
||||
* Is {NumCats} {NumCats}|plural(one=cat,other=cats) too many?
|
||||
NPC: No, {numcats} is fine
|
||||
)RAWSUD";
|
||||
|
||||
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestParameters,
|
||||
"SUDSTest.TestParameters",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestParameters::RunTest(const FString& Parameters)
|
||||
{
|
||||
// Number and plural formatting are locale-specific, so we must set it to a predefined value to produce the expected output
|
||||
FInternationalization::FCultureStateSnapshot CultureStateSnapshot;
|
||||
FInternationalization::Get().BackupCultureState(CultureStateSnapshot);
|
||||
FInternationalization::Get().SetCurrentCulture(TEXT("en-US"));
|
||||
ON_SCOPE_EXIT
|
||||
{
|
||||
FInternationalization::Get().RestoreCultureState(CultureStateSnapshot);
|
||||
};
|
||||
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ParamsInput), ParamsInput.Len(), "ParamsInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
auto Participant = NewObject<UTestParticipant>();
|
||||
Participant->TestNumber = 0;
|
||||
Dlg->AddParticipant(Participant);
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Line 1", Dlg, "Player", "Hello, I'm Protagonist");
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Line 2", Dlg, "NPC", "Greetings, Protagonist, my name is An NPC");
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Line 3", Dlg, "Player", "My friend's name is Susan, she has 3 cats");
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Line 4", Dlg, "NPC", "Floating point 12.567 format test");
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Line 5", Dlg, "Player", "Boolean test 1?");
|
||||
if (TestEqual("Number of choices", Dlg->GetNumberOfChoices(), 2))
|
||||
{
|
||||
TestEqual("Choice text 1", Dlg->GetChoiceText(0).ToString(), "Choose, Protagonist!");
|
||||
TestEqual("Choice text 2", Dlg->GetChoiceText(1).ToString(), "Is 3 cats too many?");
|
||||
|
||||
}
|
||||
|
||||
// test case insensitivity, this next line uses {numcats} instead of {NumCats}
|
||||
Dlg->Choose(1);
|
||||
TestDialogueText(this, "Line 6", Dlg, "NPC", "No, 3 is fine");
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestParametersPriority,
|
||||
"SUDSTest.TestParametersPriority",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestParametersPriority::RunTest(const FString& Parameters)
|
||||
{
|
||||
// Number and plural formatting are locale-specific, so we must set it to a predefined value to produce the expected output
|
||||
FInternationalization::FCultureStateSnapshot CultureStateSnapshot;
|
||||
FInternationalization::Get().BackupCultureState(CultureStateSnapshot);
|
||||
FInternationalization::Get().SetCurrentCulture(TEXT("en-US"));
|
||||
ON_SCOPE_EXIT
|
||||
{
|
||||
FInternationalization::Get().RestoreCultureState(CultureStateSnapshot);
|
||||
};
|
||||
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ParamsInput), ParamsInput.Len(), "ParamsInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
auto Participant1 = NewObject<UTestParticipant>();
|
||||
Participant1->TestNumber = 0; // priority 0
|
||||
auto Participant2 = NewObject<UTestParticipant>();
|
||||
Participant2->TestNumber = 1; // priority 100
|
||||
auto Participant3 = NewObject<UTestParticipant>();
|
||||
Participant1->TestNumber = 2; // priority -200
|
||||
Dlg->AddParticipant(Participant1);
|
||||
Dlg->AddParticipant(Participant2);
|
||||
Dlg->AddParticipant(Participant3);
|
||||
Dlg->Start();
|
||||
|
||||
// Ordering of the participants should be from low to high, so variables from higher priority value participant should
|
||||
// have overridden lower priority ones
|
||||
|
||||
// All of these are set by Participant 2 who is higher priority
|
||||
TestDialogueText(this, "Line 1", Dlg, "Player", "Hello, I'm Hero");
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Line 2", Dlg, "NPC", "Greetings, Hero, my name is Bob The NPC");
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Line 3", Dlg, "Player", "My friend's name is Derek, he has 5 cats");
|
||||
Dlg->Continue();
|
||||
|
||||
// These are set by Participant1 and unchanged by 2. 3 has tried to set float but should have been overridden
|
||||
TestDialogueText(this, "Line 4", Dlg, "NPC", "Floating point 12.567 format test");
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Line 5", Dlg, "Player", "Boolean test 1?");
|
||||
|
||||
// Check that there's a variable from Participant2 which no-one else set
|
||||
TestEqual("Participant3 should have set something", Dlg->GetVariableInt("SomethingUniqueTo3"), 120);
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
UE_ENABLE_OPTIMIZATION
|
||||
1189
Plugins/SUDS/Source/SUDSTest/Private/TestParsing.cpp
Normal file
1189
Plugins/SUDS/Source/SUDSTest/Private/TestParsing.cpp
Normal file
File diff suppressed because it is too large
Load Diff
68
Plugins/SUDS/Source/SUDSTest/Private/TestParticipant.cpp
Normal file
68
Plugins/SUDS/Source/SUDSTest/Private/TestParticipant.cpp
Normal file
@@ -0,0 +1,68 @@
|
||||
|
||||
#include "TestParticipant.h"
|
||||
|
||||
#include "SUDSDialogue.h"
|
||||
|
||||
void UTestParticipant::OnDialogueStarting_Implementation(USUDSDialogue* Dialogue, FName AtLabel)
|
||||
{
|
||||
switch(TestNumber)
|
||||
{
|
||||
default:
|
||||
case 0:
|
||||
Dialogue->SetVariable("SpeakerName.Player", FText::FromString("Protagonist"));
|
||||
Dialogue->SetVariable("SpeakerName.NPC", FText::FromString("An NPC"));
|
||||
Dialogue->SetVariable("NumCats", 3);
|
||||
Dialogue->SetVariableText("FriendName", FText::FromString("Susan"));
|
||||
Dialogue->SetVariable("Gender", ETextGender::Feminine);
|
||||
Dialogue->SetVariableFloat("FloatVal", 12.567);
|
||||
Dialogue->SetVariableBoolean("BoolVal", true);
|
||||
break;
|
||||
case 1:
|
||||
Dialogue->SetVariable("SpeakerName.Player", FText::FromString("Hero"));
|
||||
Dialogue->SetVariable("SpeakerName.NPC", FText::FromString("Bob The NPC"));
|
||||
Dialogue->SetVariableText("FriendName", FText::FromString("Derek"));
|
||||
Dialogue->SetVariable("Gender", ETextGender::Masculine);
|
||||
Dialogue->SetVariable("NumCats", 5);
|
||||
break;
|
||||
case 2:
|
||||
// these will all be overridden by higher priorities
|
||||
Dialogue->SetVariable("SpeakerName.Player", FText::FromString("Dweeb"));
|
||||
Dialogue->SetVariable("SpeakerName.NPC", FText::FromString("Imaginary Friend"));
|
||||
Dialogue->SetVariable("NumCats", 10);
|
||||
Dialogue->SetVariableFloat("FloatVal", 0.002);
|
||||
// This one will be unique and so will still get through
|
||||
Dialogue->SetVariableInt("SomethingUniqueTo3", 120);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int UTestParticipant::GetDialogueParticipantPriority_Implementation() const
|
||||
{
|
||||
switch(TestNumber)
|
||||
{
|
||||
default:
|
||||
case 0:
|
||||
return 0;
|
||||
case 1:
|
||||
return 100;
|
||||
case 2:
|
||||
return -200;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void UTestParticipant::OnDialogueEvent_Implementation(USUDSDialogue* Dialogue,
|
||||
FName EventName,
|
||||
const TArray<FSUDSValue>& Arguments)
|
||||
{
|
||||
EventRecords.Add(FEventRecord { EventName, Arguments });
|
||||
}
|
||||
|
||||
void UTestParticipant::OnDialogueVariableChanged_Implementation(USUDSDialogue* Dialogue,
|
||||
FName VariableName,
|
||||
const FSUDSValue& Value,
|
||||
bool bFromScript)
|
||||
{
|
||||
SetVarRecords.Add(FSetVarRecord { VariableName, Value, bFromScript });
|
||||
}
|
||||
|
||||
45
Plugins/SUDS/Source/SUDSTest/Private/TestParticipant.h
Normal file
45
Plugins/SUDS/Source/SUDSTest/Private/TestParticipant.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSParticipant.h"
|
||||
#include "SUDSValue.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "TestParticipant.generated.h"
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class SUDSTEST_API UTestParticipant : public UObject, public ISUDSParticipant
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
int TestNumber = 0;
|
||||
|
||||
struct FEventRecord
|
||||
{
|
||||
FName Name;
|
||||
TArray<FSUDSValue> Args;
|
||||
};
|
||||
struct FSetVarRecord
|
||||
{
|
||||
FName Name;
|
||||
FSUDSValue Value;
|
||||
bool bFromScript;
|
||||
};
|
||||
|
||||
TArray<FEventRecord> EventRecords;
|
||||
TArray<FSetVarRecord> SetVarRecords;
|
||||
|
||||
|
||||
virtual void OnDialogueStarting_Implementation(USUDSDialogue* Dialogue, FName AtLabel) override;
|
||||
virtual int GetDialogueParticipantPriority_Implementation() const override;
|
||||
virtual void OnDialogueEvent_Implementation(USUDSDialogue* Dialogue,
|
||||
FName EventName,
|
||||
const TArray<FSUDSValue>& Arguments) override;
|
||||
virtual void OnDialogueVariableChanged_Implementation(USUDSDialogue* Dialogue,
|
||||
FName VariableName,
|
||||
const FSUDSValue& Value,
|
||||
bool bFromScript) override;
|
||||
};
|
||||
342
Plugins/SUDS/Source/SUDSTest/Private/TestRandom.cpp
Normal file
342
Plugins/SUDS/Source/SUDSTest/Private/TestRandom.cpp
Normal file
@@ -0,0 +1,342 @@
|
||||
#include "SUDSLibrary.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptImporter.h"
|
||||
#include "TestUtils.h"
|
||||
|
||||
UE_DISABLE_OPTIMIZATION
|
||||
|
||||
const FString BasicRandomInput = R"RAWSUD(
|
||||
Player: Hello
|
||||
:start
|
||||
[random]
|
||||
NPC: Reply when random == 0
|
||||
[or]
|
||||
NPC: Reply when random == 1
|
||||
[or]
|
||||
NPC: Reply when random == 2
|
||||
[endrandom]
|
||||
Player: OK
|
||||
[goto start]
|
||||
)RAWSUD";
|
||||
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestRandomBasics,
|
||||
"SUDSTest.TestRandomBasics",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
bool FTestRandomBasics::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(BasicRandomInput), BasicRandomInput.Len(), "BasicRandomInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
|
||||
// Seed random so we have consistent results
|
||||
FMath::SRandInit(34);
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Text node", Dlg, "Player", "Hello");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Random node", Dlg, "NPC", "Reply when random == 2");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Final node", Dlg, "Player", "OK");
|
||||
|
||||
// Should loop, run random again
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Random node", Dlg, "NPC", "Reply when random == 1");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Final node", Dlg, "Player", "OK");
|
||||
|
||||
// Should loop, run random again
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Random node", Dlg, "NPC", "Reply when random == 0");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Final node", Dlg, "Player", "OK");
|
||||
|
||||
// Should loop, run random again
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Random node", Dlg, "NPC", "Reply when random == 1");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Final node", Dlg, "Player", "OK");
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
const FString NestedRandomInput = R"RAWSUD(
|
||||
Player: Hello
|
||||
:start
|
||||
[random]
|
||||
NPC: Reply when random == 0
|
||||
[or]
|
||||
[random]
|
||||
NPC: Reply when random == 1 && subrandom == 0
|
||||
[or]
|
||||
NPC: Reply when random == 1 && subrandom == 1
|
||||
[endrandom]
|
||||
[or]
|
||||
NPC: Reply when random == 2
|
||||
[endrandom]
|
||||
Player: OK
|
||||
[goto start]
|
||||
)RAWSUD";
|
||||
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestRandomNested,
|
||||
"SUDSTest.TestRandomNested",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
bool FTestRandomNested::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(NestedRandomInput), NestedRandomInput.Len(), "NestedRandomInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
|
||||
// Seed random so we have consistent results
|
||||
FMath::SRandInit(785);
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Text node", Dlg, "Player", "Hello");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
|
||||
TestDialogueText(this, "Random node", Dlg, "NPC", "Reply when random == 0");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Final node", Dlg, "Player", "OK");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
|
||||
// Restart
|
||||
TestDialogueText(this, "Random node", Dlg, "NPC", "Reply when random == 1 && subrandom == 1");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Final node", Dlg, "Player", "OK");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
|
||||
// Restart
|
||||
TestDialogueText(this, "Random node", Dlg, "NPC", "Reply when random == 1 && subrandom == 0");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Final node", Dlg, "Player", "OK");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
|
||||
// Restart
|
||||
TestDialogueText(this, "Random node", Dlg, "NPC", "Reply when random == 2");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Final node", Dlg, "Player", "OK");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
|
||||
// Restart
|
||||
TestDialogueText(this, "Random node", Dlg, "NPC", "Reply when random == 0");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Final node", Dlg, "Player", "OK");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
const FString SiblingRandomInput = R"RAWSUD(
|
||||
Player: Hello
|
||||
:start
|
||||
[random]
|
||||
NPC: Reply when random == 0
|
||||
[or]
|
||||
NPC: Reply when random == 1
|
||||
[or]
|
||||
NPC: Reply when random == 2
|
||||
[endrandom]
|
||||
[random]
|
||||
NPC: Second random == 0
|
||||
[or]
|
||||
NPC: Second random == 1
|
||||
[or]
|
||||
NPC: Second random == 2
|
||||
[endrandom]
|
||||
Player: OK
|
||||
[goto start]
|
||||
)RAWSUD";
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestRandomSibling,
|
||||
"SUDSTest.TestRandomSibling",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
bool FTestRandomSibling::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(SiblingRandomInput), SiblingRandomInput.Len(), "SiblingRandomInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
|
||||
// Seed random so we have consistent results
|
||||
FMath::SRandInit(2376);
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Text node", Dlg, "Player", "Hello");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
|
||||
|
||||
TestDialogueText(this, "Text node", Dlg, "NPC", "Reply when random == 1");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Text node", Dlg, "NPC", "Second random == 0");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Text node", Dlg, "Player", "OK");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
|
||||
// Restart
|
||||
TestDialogueText(this, "Text node", Dlg, "NPC", "Reply when random == 0");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Text node", Dlg, "NPC", "Second random == 2");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Text node", Dlg, "Player", "OK");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
|
||||
// Restart
|
||||
TestDialogueText(this, "Text node", Dlg, "NPC", "Reply when random == 1");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Text node", Dlg, "NPC", "Second random == 2");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Text node", Dlg, "Player", "OK");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
|
||||
// Restart
|
||||
TestDialogueText(this, "Text node", Dlg, "NPC", "Reply when random == 0");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Text node", Dlg, "NPC", "Second random == 0");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Text node", Dlg, "Player", "OK");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
|
||||
// Restart
|
||||
TestDialogueText(this, "Text node", Dlg, "NPC", "Reply when random == 2");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Text node", Dlg, "NPC", "Second random == 2");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Text node", Dlg, "Player", "OK");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
|
||||
const FString MixedConditionalChoiceAndRandomInput = R"RAWSUD(
|
||||
Player: Hello
|
||||
* First choice
|
||||
Player: I took the 1.1 choice
|
||||
[if {x} > 0]
|
||||
* Second choice (conditional)
|
||||
[random]
|
||||
Player: I took the 1.2 choice, random == 0
|
||||
[or]
|
||||
Player: I took the 1.2 choice, random == 1
|
||||
[endif]
|
||||
* Third choice (conditional)
|
||||
Player: I took the 1.3 choice
|
||||
[else]
|
||||
* Second Alt Choice
|
||||
[random]
|
||||
Player: I took the alt 1.2 choice, random == 0
|
||||
[or]
|
||||
Player: I took the alt 1.2 choice, random == 1
|
||||
[endif]
|
||||
[endif]
|
||||
* Common last choice
|
||||
Player: I took the 1.4 choice
|
||||
|
||||
|
||||
)RAWSUD";
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestRandomMixed,
|
||||
"SUDSTest.TestRandomMixed",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
bool FTestRandomMixed::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(MixedConditionalChoiceAndRandomInput), MixedConditionalChoiceAndRandomInput.Len(), "MixedRandomInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
|
||||
// Seed random so we have consistent results
|
||||
FMath::SRandInit(999);
|
||||
Dlg->SetVariableInt("x", 5);
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Start node", Dlg, "Player", "Hello");
|
||||
// 2 choices from conditional, 2 common
|
||||
if (TestEqual("Choice count when x=5", Dlg->GetNumberOfChoices(), 4))
|
||||
{
|
||||
TestTrue("Choose 1", Dlg->Choose(1));
|
||||
TestDialogueText(this, "Text node", Dlg, "Player", "I took the 1.2 choice, random == 0");
|
||||
}
|
||||
|
||||
// Restart, same again but different result if random
|
||||
Dlg->Restart(false);
|
||||
TestDialogueText(this, "Start node", Dlg, "Player", "Hello");
|
||||
// 2 choices from conditional, 2 common
|
||||
if (TestEqual("Choice count when x=5", Dlg->GetNumberOfChoices(), 4))
|
||||
{
|
||||
TestTrue("Choose 1", Dlg->Choose(1));
|
||||
TestDialogueText(this, "Text node", Dlg, "Player", "I took the 1.2 choice, random == 1");
|
||||
}
|
||||
|
||||
// Now change to other conditional path
|
||||
Dlg->SetVariableInt("x", 0);
|
||||
|
||||
Dlg->Restart(false);
|
||||
TestDialogueText(this, "Start node", Dlg, "Player", "Hello");
|
||||
if (TestEqual("Choice count when x=0", Dlg->GetNumberOfChoices(), 3))
|
||||
{
|
||||
TestTrue("Choose 1", Dlg->Choose(1));
|
||||
TestDialogueText(this, "Text node", Dlg, "Player", "I took the alt 1.2 choice, random == 1");
|
||||
}
|
||||
Dlg->Restart(false);
|
||||
TestDialogueText(this, "Start node", Dlg, "Player", "Hello");
|
||||
if (TestEqual("Choice count when x=0", Dlg->GetNumberOfChoices(), 3))
|
||||
{
|
||||
TestTrue("Choose 1", Dlg->Choose(1));
|
||||
TestDialogueText(this, "Text node", Dlg, "Player", "I took the alt 1.2 choice, random == 0");
|
||||
}
|
||||
|
||||
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
UE_ENABLE_OPTIMIZATION
|
||||
492
Plugins/SUDS/Source/SUDSTest/Private/TestRunning.cpp
Normal file
492
Plugins/SUDS/Source/SUDSTest/Private/TestRunning.cpp
Normal file
@@ -0,0 +1,492 @@
|
||||
#include "SUDSDialogue.h"
|
||||
#include "SUDSLibrary.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptImporter.h"
|
||||
#include "SUDSSubsystem.h"
|
||||
#include "TestUtils.h"
|
||||
#include "Internationalization/Internationalization.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
UE_DISABLE_OPTIMIZATION
|
||||
|
||||
const FString SimpleRunnerInput = R"RAWSUD(
|
||||
:start
|
||||
Player: Hello there
|
||||
NPC: Salutations fellow human
|
||||
:choice
|
||||
* Actually no
|
||||
NPC: How rude, bye then
|
||||
[goto end]
|
||||
* Nested option
|
||||
:nestedstart
|
||||
NPC: Some nesting
|
||||
* Actually bye
|
||||
Player: Gotta go!
|
||||
[go to goodbye]
|
||||
* A fallthrough choice
|
||||
NPC: This should fall through to latterhalf
|
||||
* A goto choice
|
||||
[goto latterhalf]
|
||||
* Another option
|
||||
Player: What now?
|
||||
NPC: This is another fallthrough
|
||||
:latterhalf
|
||||
Player: This is the latter half of the discussion
|
||||
NPC: Yep, sure is
|
||||
* Go back to choice
|
||||
NPC: Okay!
|
||||
[goto choice]
|
||||
* Return to the start
|
||||
NPC: Gotcha
|
||||
[goto start]
|
||||
* Continue
|
||||
Player: OK I'd like to carry on now
|
||||
NPC: Right you are guv, falling through
|
||||
:goodbye
|
||||
NPC: Bye!
|
||||
)RAWSUD";
|
||||
|
||||
const FString SetVariableRunnerInput = R"RAWSUD(
|
||||
===
|
||||
# Set some vars in header
|
||||
# Text var with an existing localised ID
|
||||
[set SpeakerName.Player "Protagonist"] @12345@
|
||||
# Text var no localised ID
|
||||
[set ValetName "Bob"]
|
||||
// Also test with equals signs
|
||||
[set SomeFloat = 12.5]
|
||||
[set SomeCalculatedInt = (3 + 4) * 2]
|
||||
[set SomeCalculatedBoolean = true or false]
|
||||
// Test global vars
|
||||
[set global.SomeGlobalInt 3]
|
||||
[set global.SomeGlobalFloat = 12.5 * 3]
|
||||
===
|
||||
|
||||
Player: Hello
|
||||
[set SomeInt 99]
|
||||
# Test that we can use variables in set and that ordering works
|
||||
[set SomeOtherFloat {SomeFloat} + 10]
|
||||
[set SomeFloat 43.754]
|
||||
# Test global values
|
||||
NPC: Wotcha
|
||||
Player: Values are: {global.SomeGlobalInt}, {global.SomeGlobalFloat}, {global.GlobalIntSetOutside}
|
||||
# Test that inserting a set node in between text and choice doesn't break link
|
||||
[set SomeGender masculine]
|
||||
* Choice 1
|
||||
[set SomeBoolean True]
|
||||
NPC: Truth
|
||||
* Choice 2
|
||||
NPC: Surprise
|
||||
[set ValetName "Kate"]
|
||||
[set SomeGender feminine]
|
||||
[set someint 101]
|
||||
Player: Well
|
||||
|
||||
)RAWSUD";
|
||||
|
||||
const FString FallthroughEdgeCaseInput = R"RAWSUD(
|
||||
NPC: First line
|
||||
* Option 1
|
||||
NPC: Fallthrough via goto
|
||||
[goto secondchoice]
|
||||
* Option 2
|
||||
NPC: Fallthrough implicitly
|
||||
* Option 3
|
||||
NPC: Fallthrough implicitly again
|
||||
|
||||
:secondchoice
|
||||
* Fallthrough 1
|
||||
NPC: text 1
|
||||
* Fallthrough 2
|
||||
NPC: text 2
|
||||
)RAWSUD";
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestSimpleRunning,
|
||||
"SUDSTest.TestSimpleRunning",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestSimpleRunning::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(SimpleRunnerInput), SimpleRunnerInput.Len(), "SimpleRunnerInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "First node", Dlg, "Player", "Hello there");
|
||||
TestEqual("First node choices", Dlg->GetNumberOfChoices(), 1);
|
||||
TestTrue("First node choice text", Dlg->GetChoiceText(0).IsEmpty());
|
||||
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
|
||||
TestDialogueText(this, "Node 2", Dlg, "NPC", "Salutations fellow human");
|
||||
TestEqual("Node 2 choices", Dlg->GetNumberOfChoices(), 3);
|
||||
TestEqual("Node 2 choice text 0", Dlg->GetChoiceText(0).ToString(), "Actually no");
|
||||
TestEqual("Node 2 choice text 1", Dlg->GetChoiceText(1).ToString(), "Nested option");
|
||||
TestEqual("Node 2 choice text 2", Dlg->GetChoiceText(2).ToString(), "Another option");
|
||||
|
||||
TestTrue("Choice 1", Dlg->Choose(0));
|
||||
TestDialogueText(this, "Choice 1 Text", Dlg, "NPC", "How rude, bye then");
|
||||
// Goes straight to end
|
||||
TestFalse("Choice 1 Follow On", Dlg->Continue());
|
||||
TestTrue("Should be at end", Dlg->IsEnded());
|
||||
|
||||
// Start again
|
||||
Dlg->Restart();
|
||||
TestDialogueText(this, "First node", Dlg, "Player", "Hello there");
|
||||
TestEqual("First node choices", Dlg->GetNumberOfChoices(), 1);
|
||||
TestTrue("First node choice text", Dlg->GetChoiceText(0).IsEmpty());
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Node 2", Dlg, "NPC", "Salutations fellow human");
|
||||
|
||||
// Confirm we previously chose 0
|
||||
TestTrue("Choice 0 taken previously", Dlg->HasChoiceIndexBeenTakenPreviously(0));
|
||||
|
||||
TestTrue("Choice 2", Dlg->Choose(1));
|
||||
TestDialogueText(this, "Choice 2 Text", Dlg, "NPC", "Some nesting");
|
||||
TestEqual("Choice 2 nested choices", Dlg->GetNumberOfChoices(), 3);
|
||||
TestEqual("Choice 2 nested choice text 0", Dlg->GetChoiceText(0).ToString(), "Actually bye");
|
||||
TestEqual("Choice 2 nested choice text 1", Dlg->GetChoiceText(1).ToString(), "A fallthrough choice");
|
||||
TestEqual("Choice 2 nested choice text 2", Dlg->GetChoiceText(2).ToString(), "A goto choice");
|
||||
|
||||
TestTrue("Nested choice made", Dlg->Choose(0));
|
||||
TestDialogueText(this, "Nested choice made text", Dlg, "Player", "Gotta go!");
|
||||
TestTrue("Nested choice follow On", Dlg->Continue());
|
||||
TestDialogueText(this, "Nested choice follow on text", Dlg, "NPC", "Bye!");
|
||||
TestFalse("Nested choice follow On 2", Dlg->Continue());
|
||||
TestTrue("Should be at end", Dlg->IsEnded());
|
||||
|
||||
// Start again, this time from nested choice, and reset all state
|
||||
Dlg->Restart(true, "nestedstart");
|
||||
TestDialogueText(this, "nestedchoice restart Text", Dlg, "NPC", "Some nesting");
|
||||
TestTrue("Nested choice made", Dlg->Choose(1));
|
||||
TestDialogueText(this, "Nested choice 2 Text", Dlg, "NPC", "This should fall through to latterhalf");
|
||||
TestTrue("Nested choice 2 follow On", Dlg->Continue());
|
||||
// Should have fallen through
|
||||
TestDialogueText(this, "Fallthrough Text", Dlg, "Player", "This is the latter half of the discussion");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Fallthrough Text 2", Dlg, "NPC", "Yep, sure is");
|
||||
TestEqual("Fallthrough choices", Dlg->GetNumberOfChoices(), 3);
|
||||
TestEqual("Fallthrough choice text 0", Dlg->GetChoiceText(0).ToString(), "Go back to choice");
|
||||
TestEqual("Fallthrough choice text 1", Dlg->GetChoiceText(1).ToString(), "Return to the start");
|
||||
TestEqual("Fallthrough choice text 2", Dlg->GetChoiceText(2).ToString(), "Continue");
|
||||
|
||||
// Go back to choice
|
||||
TestTrue("Fallthrough choice made", Dlg->Choose(0));
|
||||
TestDialogueText(this, "Fallthrough Choice Text", Dlg, "NPC", "Okay!");
|
||||
// The Goto choice should have collapsed the choices such that we can get them immediately
|
||||
TestEqual("Fallthrough then goto choices", Dlg->GetNumberOfChoices(), 3);
|
||||
TestEqual("Fallthrough then goto choice text 0", Dlg->GetChoiceText(0).ToString(), "Actually no");
|
||||
TestEqual("Fallthrough then goto choice text 1", Dlg->GetChoiceText(1).ToString(), "Nested option");
|
||||
TestEqual("Fallthrough then goto choice text 2", Dlg->GetChoiceText(2).ToString(), "Another option");
|
||||
|
||||
// Restart to test another path
|
||||
Dlg->Restart(true, "nestedstart");
|
||||
TestDialogueText(this, "nestedchoice restart Text", Dlg, "NPC", "Some nesting");
|
||||
TestTrue("Nested choice made", Dlg->Choose(2));
|
||||
// This should be a direct goto to latterhalf
|
||||
TestDialogueText(this, "Direct goto", Dlg, "Player", "This is the latter half of the discussion");
|
||||
|
||||
|
||||
Dlg->Restart(true);
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestTrue("Choice 3", Dlg->Choose(2));
|
||||
TestDialogueText(this, "Choice 3 Text", Dlg, "Player", "What now?");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Choice 3 Text 2", Dlg, "NPC", "This is another fallthrough");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
// Should have fallen through
|
||||
TestDialogueText(this, "Direct goto", Dlg, "Player", "This is the latter half of the discussion");
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestSetVariableRunning,
|
||||
"SUDSTest.TestSetVariableRunning",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestSetVariableRunning::RunTest(const FString& Parameters)
|
||||
{
|
||||
// Number and plural formatting are locale-specific, so we must set it to a predefined value to produce the expected output
|
||||
FInternationalization::FCultureStateSnapshot CultureStateSnapshot;
|
||||
FInternationalization::Get().BackupCultureState(CultureStateSnapshot);
|
||||
FInternationalization::Get().SetCurrentCulture(TEXT("en-US"));
|
||||
ON_SCOPE_EXIT
|
||||
{
|
||||
FInternationalization::Get().RestoreCultureState(CultureStateSnapshot);
|
||||
};
|
||||
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(SetVariableRunnerInput), SetVariableRunnerInput.Len(), "SetVariableRunnerInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// We need to use dummy global vars since subsystem doesn't exist
|
||||
USUDSSubsystem::Test_DummyGlobalVariables.Empty();
|
||||
USUDSSubsystem::Test_DummyGlobalVariables.Add("GlobalIntSetOutside", 245);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
Dlg->Start();
|
||||
|
||||
// Check headers have run & initial variables are set
|
||||
TestEqual("Header: Player name", Dlg->GetVariableText("SpeakerName.Player").ToString(), "Protagonist");
|
||||
TestEqual("Header: Valet name", Dlg->GetVariableText("ValetName").ToString(), "Bob");
|
||||
TestEqual("Header: Some float", Dlg->GetVariableFloat("SomeFloat"), 12.5f);
|
||||
|
||||
// Check initial values
|
||||
TestEqual("Initial: Some int", Dlg->GetVariableInt("SomeInt"), 0);
|
||||
TestEqual("Initial: Some boolean", Dlg->GetVariableBoolean("SomeBoolean"), false);
|
||||
TestEqual("Initial: Some gender", Dlg->GetVariableGender("SomeGender"), ETextGender::Neuter);
|
||||
TestEqual("Initial: calculated int", Dlg->GetVariableInt("SomeCalculatedInt"), 14);
|
||||
TestTrue("Initial: calculated bool", Dlg->GetVariableBoolean("SomeCalculatedBoolean"));
|
||||
|
||||
|
||||
// Test globals
|
||||
TestFalse("Test not setting local var", Dlg->IsVariableSet("global.SomeGlobalInt"));
|
||||
TestFalse("Test not setting local var", Dlg->IsVariableSet("global.SomeGlobalFloat"));
|
||||
TestTrue("Test setting global var", USUDSSubsystem::Test_DummyGlobalVariables.Contains("SomeGlobalInt"));
|
||||
TestTrue("Test setting global var", USUDSSubsystem::Test_DummyGlobalVariables.Contains("SomeGlobalFloat"));
|
||||
|
||||
TestDialogueText(this, "Node 1", Dlg, "Player", "Hello");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
// Set node should have run
|
||||
TestEqual("Initial: Some int", Dlg->GetVariableInt("SomeInt"), 99);
|
||||
TestDialogueText(this, "Node 2", Dlg, "NPC", "Wotcha");
|
||||
|
||||
// Test that setting a new variable from another variable + 10 worked
|
||||
TestEqual("Some copied float", Dlg->GetVariableFloat("SomeOtherFloat"), 22.5f);
|
||||
TestEqual("Original float", Dlg->GetVariableFloat("SomeFloat"), 43.754f);
|
||||
|
||||
// Test globals
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Global text", Dlg, "Player", "Values are: 3, 37.5, 245");
|
||||
|
||||
TestEqual("Choices count", Dlg->GetNumberOfChoices(), 2);
|
||||
TestEqual("Choice 1 text", Dlg->GetChoiceText(0).ToString(), "Choice 1");
|
||||
TestEqual("Choice 2 text", Dlg->GetChoiceText(1).ToString(), "Choice 2");
|
||||
TestTrue("Choose 1", Dlg->Choose(0));
|
||||
TestEqual("Gender should be set", Dlg->GetVariableGender("SomeGender"), ETextGender::Masculine);
|
||||
TestEqual("Some boolean should be set", Dlg->GetVariableBoolean("SomeBoolean"), true);
|
||||
TestEqual("Valet name should not have changed", Dlg->GetVariableText("ValetName").ToString(), "Bob");
|
||||
TestEqual("Gender should not have changed", Dlg->GetVariableGender("SomeGender"), ETextGender::Masculine);
|
||||
TestDialogueText(this, "Choice end text", Dlg, "NPC", "Truth");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Final node", Dlg, "Player", "Well");
|
||||
TestFalse("Continue", Dlg->Continue());
|
||||
TestTrue("At end", Dlg->IsEnded());
|
||||
|
||||
// Restart and DON'T reset state
|
||||
Dlg->Restart(false);
|
||||
|
||||
// Variables should be the same
|
||||
// Except for the headers, which will have run again
|
||||
TestEqual("Player name should have been set again", Dlg->GetVariableText("SpeakerName.Player").ToString(), "Protagonist");
|
||||
TestEqual("Valet name should have been set again", Dlg->GetVariableText("ValetName").ToString(), "Bob");
|
||||
TestEqual("Some float should have been set again", Dlg->GetVariableFloat("SomeFloat"), 12.5f);
|
||||
TestEqual("Int should still be set", Dlg->GetVariableInt("SomeInt"), 99);
|
||||
TestEqual("Gender should still be set", Dlg->GetVariableGender("SomeGender"), ETextGender::Masculine);
|
||||
TestEqual("Some boolean should still be set", Dlg->GetVariableBoolean("SomeBoolean"), true);
|
||||
|
||||
// Restart and DO reset state
|
||||
Dlg->Restart(true);
|
||||
TestEqual("Player name should have been set again", Dlg->GetVariableText("SpeakerName.Player").ToString(), "Protagonist");
|
||||
TestEqual("Valet name should have been set again", Dlg->GetVariableText("ValetName").ToString(), "Bob");
|
||||
TestEqual("Some float should have been set again", Dlg->GetVariableFloat("SomeFloat"), 12.5f);
|
||||
TestEqual("Int should have been reset", Dlg->GetVariableInt("SomeInt"), 0);
|
||||
TestEqual("Gender should have been reset", Dlg->GetVariableGender("SomeGender"), ETextGender::Neuter);
|
||||
TestEqual("Some boolean should have been reset", Dlg->GetVariableBoolean("SomeBoolean"), false);
|
||||
|
||||
// Try the other path
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestTrue("Choose 2", Dlg->Choose(1));
|
||||
TestDialogueText(this, "Choice 2 text", Dlg, "NPC", "Surprise");
|
||||
TestEqual("Gender should not be changed yet", Dlg->GetVariableGender("SomeGender"), ETextGender::Masculine);
|
||||
TestEqual("Valet name should not be changed yet", Dlg->GetVariableText("ValetName").ToString(), "Bob");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Final node", Dlg, "Player", "Well");
|
||||
TestEqual("Gender should have changed", Dlg->GetVariableGender("SomeGender"), ETextGender::Feminine);
|
||||
TestEqual("Valet name should have changed", Dlg->GetVariableText("ValetName").ToString(), "Kate");
|
||||
TestEqual("Case insensitive set test", Dlg->GetVariableInt("SomeInt"), 101);
|
||||
|
||||
TestFalse("Continue", Dlg->Continue());
|
||||
TestTrue("At end", Dlg->IsEnded());
|
||||
|
||||
USUDSSubsystem::Test_DummyGlobalVariables.Empty();
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
|
||||
const FString SpeakerNamesInput = R"RAWSUD(
|
||||
===
|
||||
[set SpeakerName.Player "Protagonist"]
|
||||
[set SpeakerName.NPC "Just Some Guy"]
|
||||
===
|
||||
:start
|
||||
Player: Hello there
|
||||
NPC: Salutations fellow human
|
||||
ThirdGuy: Sup
|
||||
[set SpeakerName.NPC "Actually A Villain"]
|
||||
NPC: Aha!
|
||||
)RAWSUD";
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestSpeakerNames,
|
||||
"SUDSTest.TestSpeakerNames",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
bool FTestSpeakerNames::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(SpeakerNamesInput), SpeakerNamesInput.Len(), "SpeakerNamesInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
Dlg->Start();
|
||||
|
||||
// Speaker ID test
|
||||
TestDialogueText(this, "Initial text", Dlg, "Player", "Hello there");
|
||||
TestEqual("Initial player speaker name", Dlg->GetSpeakerDisplayName().ToString(), "Protagonist");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Text 2", Dlg, "NPC", "Salutations fellow human");
|
||||
TestEqual("Initial NPC speaker name", Dlg->GetSpeakerDisplayName().ToString(), "Just Some Guy");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Text 3", Dlg, "ThirdGuy", "Sup");
|
||||
TestEqual("Initial ThirdGuy speaker name", Dlg->GetSpeakerDisplayName().ToString(), "ThirdGuy");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Text 4", Dlg, "NPC", "Aha!");
|
||||
TestEqual("NPC speaker name should have changed", Dlg->GetSpeakerDisplayName().ToString(), "Actually A Villain");
|
||||
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestFallthroughEdgeCase,
|
||||
"SUDSTest.TestFallthroughEdgeCase",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestFallthroughEdgeCase::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(FallthroughEdgeCaseInput), FallthroughEdgeCaseInput.Len(), "FallthroughEdgeCaseInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Initial text", Dlg, "NPC", "First line");
|
||||
TestEqual("Initial num choices", Dlg->GetNumberOfChoices(), 3);
|
||||
TestTrue("Choice 1", Dlg->Choose(0));
|
||||
TestDialogueText(this, "Second text", Dlg, "NPC", "Fallthrough via goto");
|
||||
if (TestEqual("Second num choices", Dlg->GetNumberOfChoices(), 2))
|
||||
{
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), "Fallthrough 1");
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), "Fallthrough 2");
|
||||
}
|
||||
|
||||
// Now prove that implicit fallthrough works in middle option
|
||||
Dlg->Restart();
|
||||
TestTrue("Choice 2", Dlg->Choose(1));
|
||||
TestDialogueText(this, "Second text", Dlg, "NPC", "Fallthrough implicitly");
|
||||
if (TestEqual("Second num choices", Dlg->GetNumberOfChoices(), 2))
|
||||
{
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), "Fallthrough 1");
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), "Fallthrough 2");
|
||||
}
|
||||
|
||||
// Now prove that implicit fallthrough works in last option
|
||||
Dlg->Restart();
|
||||
TestTrue("Choice 2", Dlg->Choose(2));
|
||||
TestDialogueText(this, "Second text", Dlg, "NPC", "Fallthrough implicitly again");
|
||||
if (TestEqual("Second num choices", Dlg->GetNumberOfChoices(), 2))
|
||||
{
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), "Fallthrough 1");
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), "Fallthrough 2");
|
||||
}
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
const FString LinesBetweenTextAndChoiceInput = R"RAWSUD(
|
||||
NPC: Hello
|
||||
NPC: Here's some choices
|
||||
[event SomeEvent]
|
||||
* Option 1
|
||||
NPC: This is option 1
|
||||
* Option 2
|
||||
NPC: This is option 2
|
||||
)RAWSUD";
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestLinesBetweenTextAndChoice,
|
||||
"SUDSTest.TestLinesBetweenTextAndChoiceInput",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestLinesBetweenTextAndChoice::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(LinesBetweenTextAndChoiceInput), LinesBetweenTextAndChoiceInput.Len(), "LinesBetweenTextAndChoiceInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Initial text", Dlg, "NPC", "Hello");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
TestDialogueText(this, "Initial text", Dlg, "NPC", "Here's some choices");
|
||||
TestEqual("Initial num choices", Dlg->GetNumberOfChoices(), 2);
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
UE_ENABLE_OPTIMIZATION
|
||||
146
Plugins/SUDS/Source/SUDSTest/Private/TestSaveState.cpp
Normal file
146
Plugins/SUDS/Source/SUDSTest/Private/TestSaveState.cpp
Normal file
@@ -0,0 +1,146 @@
|
||||
#include "SUDSDialogue.h"
|
||||
#include "SUDSLibrary.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptImporter.h"
|
||||
#include "TestUtils.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
UE_DISABLE_OPTIMIZATION
|
||||
|
||||
const FString SaveStateInput = R"RAWSUD(
|
||||
===
|
||||
# To confirm that when resuming we don't run headers again
|
||||
[set y = -2.4]
|
||||
===
|
||||
[if {alreadyvisited}]
|
||||
[goto secondvisit]
|
||||
[endif]
|
||||
|
||||
NPC: Hello
|
||||
* First choice
|
||||
Player: I took the 1.1 choice
|
||||
[if {x} > 0]
|
||||
* Second choice (conditional)
|
||||
Player: I took the 1.2 choice
|
||||
* Third choice (conditional)
|
||||
Player: I took the 1.3 choice
|
||||
[else]
|
||||
* Second Alt Choice
|
||||
Player: I took the alt 1.2 choice
|
||||
[endif]
|
||||
* Common last choice
|
||||
Player: I took the 1.4 choice
|
||||
|
||||
[goto goodbye]
|
||||
|
||||
:secondvisit
|
||||
|
||||
NPC: Hello again you!
|
||||
|
||||
[if {y} < 0]
|
||||
Player: Y is less than 0
|
||||
NPC: How interesting
|
||||
* You don't sound that interested
|
||||
NPC: Well I was trying to be nice
|
||||
[if {x} == 0]
|
||||
* Also x is zero
|
||||
NPC: Fascinating
|
||||
[endif]
|
||||
* Well, better be off
|
||||
[goto goodbye]
|
||||
[elseif {y} == 0]
|
||||
Player: Y is zero
|
||||
NPC: So is my interest level
|
||||
Player: Rude
|
||||
|
||||
[else]
|
||||
Player: Who knows what Y is anyway
|
||||
[if {ponderous}]
|
||||
* Who knows what anything is?
|
||||
NPC: Get out
|
||||
[endif]
|
||||
[if {y} > 4.99]
|
||||
* It's more than I can count on one hand
|
||||
NPC: Well, that helps
|
||||
[else]
|
||||
* It's kind of small though
|
||||
NPC: Much like your brain
|
||||
[endif]
|
||||
* I'm done with this
|
||||
[endif]
|
||||
|
||||
Player: This is some fallthrough text
|
||||
|
||||
:goodbye
|
||||
NPC: Bye
|
||||
)RAWSUD";
|
||||
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestSaveState,
|
||||
"SUDSTest.TestSaveState",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestSaveState::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(SaveStateInput), SaveStateInput.Len(), "SaveStateInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
// Set x before start
|
||||
// No point settings y because that gets changed by headers
|
||||
Dlg->SetVariableInt("x", 5);
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Text Node", Dlg, "NPC", "Hello");
|
||||
if (!TestEqual("Num choices", Dlg->GetNumberOfChoices(), 4))
|
||||
return true;
|
||||
|
||||
TestEqual("ChoiceText", Dlg->GetChoiceText(0).ToString(), "First choice");
|
||||
TestEqual("ChoiceText", Dlg->GetChoiceText(1).ToString(), "Second choice (conditional)");
|
||||
TestEqual("ChoiceText", Dlg->GetChoiceText(2).ToString(), "Third choice (conditional)");
|
||||
TestEqual("ChoiceText", Dlg->GetChoiceText(3).ToString(), "Common last choice");
|
||||
TestTrue("Choose", Dlg->Choose(2));
|
||||
TestDialogueText(this, "Text node", Dlg, "Player", "I took the 1.3 choice");
|
||||
|
||||
// Set value of y to test it's retained, and not reset by running headers
|
||||
Dlg->SetVariableFloat("y", 23.5f);
|
||||
|
||||
// Save it here
|
||||
auto SaveState = Dlg->GetSavedState();
|
||||
|
||||
// Re-construct the dialogue & restore
|
||||
auto Dlg2 = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
Dlg2->RestoreSavedState(SaveState);
|
||||
|
||||
// We should be back at the same point
|
||||
TestDialogueText(this, "Text node", Dlg2, "Player", "I took the 1.3 choice");
|
||||
// Check vars
|
||||
TestEqual("x value", Dlg2->GetVariableInt("x"), 5);
|
||||
TestEqual("y value", Dlg2->GetVariableFloat("y"), 23.5f);
|
||||
TestTrue("Continue", Dlg2->Continue());
|
||||
TestDialogueText(this, "Text node", Dlg2, "NPC", "Bye");
|
||||
|
||||
// Restart to check choices were remembered
|
||||
Dlg2->Restart();
|
||||
TestDialogueText(this, "Text Node", Dlg2, "NPC", "Hello");
|
||||
TestFalse("Choice not taken", Dlg2->HasChoiceIndexBeenTakenPreviously(0));
|
||||
TestFalse("Choice not taken", Dlg2->HasChoiceIndexBeenTakenPreviously(1));
|
||||
TestTrue("Choice not taken", Dlg2->HasChoiceIndexBeenTakenPreviously(2));
|
||||
TestFalse("Choice not taken", Dlg2->HasChoiceIndexBeenTakenPreviously(3));
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
UE_ENABLE_OPTIMIZATION
|
||||
124
Plugins/SUDS/Source/SUDSTest/Private/TestUserMetadata.cpp
Normal file
124
Plugins/SUDS/Source/SUDSTest/Private/TestUserMetadata.cpp
Normal file
@@ -0,0 +1,124 @@
|
||||
#include "SUDSDialogue.h"
|
||||
#include "SUDSLibrary.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptImporter.h"
|
||||
#include "TestUtils.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
UE_DISABLE_OPTIMIZATION
|
||||
|
||||
// Assign known keys to the strings so we can detect
|
||||
const FString UserMetadataInput = R"RAWSUD(
|
||||
# No metadata
|
||||
Player: Good day sir
|
||||
#% IsGreeting = true
|
||||
NPC: Salutations to you too
|
||||
#% IntValue = 1
|
||||
#% Politeness 3.142
|
||||
Player: What are we doing today?
|
||||
# Test a variable-derived, expression metadata
|
||||
[set IntVariable 2]
|
||||
#% IntValue = {IntVariable} + 3
|
||||
#% LookMumNoEquals `OK_Dear`
|
||||
NPC: Looks like we're doing some testing. How about a choice?
|
||||
#% RequirePoshness = 10
|
||||
* Choice 1
|
||||
Player: Capital, old chap
|
||||
#% RequirePoshness = 15
|
||||
#% RequireTopHat = true
|
||||
* Choice 2
|
||||
Player: Top hole, dear boy
|
||||
* Choice 3
|
||||
Player: OK then
|
||||
)RAWSUD";
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestUserMetadata,
|
||||
"SUDSTest.TestUserMetadata",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestUserMetadata::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(UserMetadataInput), UserMetadataInput.Len(), "UserMetadataInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "First node", Dlg, "Player", "Good day sir");
|
||||
TestEqual("Check metadata empty", Dlg->GetAllSpeakerLineUserMetadata().Num(), 0);
|
||||
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Next", Dlg, "NPC", "Salutations to you too");
|
||||
FSUDSValue Actual = Dlg->GetSpeakerLineUserMetadata(FName("IsGreeting"));
|
||||
if (TestTrue("Boolean test", Actual.GetType() == ESUDSValueType::Boolean))
|
||||
{
|
||||
TestEqual("Boolean test", Actual.GetBooleanValue(), true);
|
||||
}
|
||||
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Next", Dlg, "Player", "What are we doing today?");
|
||||
Actual = Dlg->GetSpeakerLineUserMetadata(FName("IntValue"));
|
||||
if (TestTrue("Int test", Actual.GetType() == ESUDSValueType::Int))
|
||||
{
|
||||
TestEqual("Int test", Actual.GetIntValue(), 1);
|
||||
}
|
||||
Actual = Dlg->GetSpeakerLineUserMetadata(FName("Politeness"));
|
||||
if (TestTrue("Float test", Actual.GetType() == ESUDSValueType::Float))
|
||||
{
|
||||
TestEqual("Float test", Actual.GetFloatValue(), 3.142f);
|
||||
}
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Next", Dlg, "NPC", "Looks like we're doing some testing. How about a choice?");
|
||||
Actual = Dlg->GetSpeakerLineUserMetadata(FName("IntValue"));
|
||||
if (TestTrue("Expression test", Actual.GetType() == ESUDSValueType::Int))
|
||||
{
|
||||
TestEqual("Expression test", Actual.GetIntValue(), 5);
|
||||
}
|
||||
Actual = Dlg->GetSpeakerLineUserMetadata(FName("LookMumNoEquals"));
|
||||
if (TestTrue("Name & no equals test", Actual.GetType() == ESUDSValueType::Name))
|
||||
{
|
||||
TestEqual("Name & no equals test", Actual.GetNameValue(), FName("OK_Dear"));
|
||||
}
|
||||
|
||||
if (TestEqual("Choice count", Dlg->GetNumberOfChoices(), 3))
|
||||
{
|
||||
Actual = Dlg->GetChoiceUserMetadata(0, FName("RequirePoshness"));
|
||||
if (TestTrue("Choice 0 test", Actual.GetType() == ESUDSValueType::Int))
|
||||
{
|
||||
TestEqual("Choice 0 test", Actual.GetIntValue(), 10);
|
||||
}
|
||||
Actual = Dlg->GetChoiceUserMetadata(1, FName("RequirePoshness"));
|
||||
if (TestTrue("Choice 1 test A", Actual.GetType() == ESUDSValueType::Int))
|
||||
{
|
||||
TestEqual("Choice 1 test A", Actual.GetIntValue(), 15);
|
||||
}
|
||||
Actual = Dlg->GetChoiceUserMetadata(1, FName("RequireTopHat"));
|
||||
if (TestTrue("Choice 1 test B", Actual.GetType() == ESUDSValueType::Boolean))
|
||||
{
|
||||
TestEqual("Choice 1 test B", Actual.GetBooleanValue(), true);
|
||||
}
|
||||
auto Choice1All = Dlg->GetAllChoiceUserMetadata(1);
|
||||
TestEqual("Choice 1 test count", Choice1All.Num(), 2);
|
||||
TestEqual("Choice 2 test", Dlg->GetAllChoiceUserMetadata(2).Num(), 0);
|
||||
|
||||
}
|
||||
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
UE_ENABLE_OPTIMIZATION
|
||||
297
Plugins/SUDS/Source/SUDSTest/Private/TestUtils.h
Normal file
297
Plugins/SUDS/Source/SUDSTest/Private/TestUtils.h
Normal file
@@ -0,0 +1,297 @@
|
||||
#pragma once
|
||||
#include "SUDSDialogue.h"
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
#include "Internationalization/StringTable.h"
|
||||
#include "Internationalization/StringTableRegistry.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "UObject/Package.h"
|
||||
|
||||
FORCEINLINE void TestDialogueText(FAutomationTestBase* T, const FString& NameForTest, USUDSDialogue* D, const FString& SpeakerID, const FString& Text)
|
||||
{
|
||||
T->TestEqual(NameForTest, D->GetSpeakerID(), SpeakerID);
|
||||
T->TestEqual(NameForTest, D->GetText().ToString(), Text);
|
||||
|
||||
}
|
||||
|
||||
FORCEINLINE bool TestParsedText(FAutomationTestBase* T, const FString& NameForTest, const FSUDSParsedNode* Node, const FString& Speaker, const FString& Text)
|
||||
{
|
||||
if (T->TestNotNull(NameForTest, Node))
|
||||
{
|
||||
T->TestEqual(NameForTest, Node->NodeType, ESUDSParsedNodeType::Text);
|
||||
T->TestEqual(NameForTest, Node->Identifier, Speaker);
|
||||
T->TestEqual(NameForTest, Node->Text, Text);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FORCEINLINE bool TestArgValue(FAutomationTestBase* T, const FString& NameForTest, const FSUDSValue& Actual, int Expected)
|
||||
{
|
||||
if (T->TestEqual(NameForTest, Actual.GetType(), ESUDSValueType::Int))
|
||||
{
|
||||
return T->TestEqual(NameForTest, Actual.GetIntValue(), Expected);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
FORCEINLINE bool TestArgValue(FAutomationTestBase* T, const FString& NameForTest, const FSUDSValue& Actual, float Expected)
|
||||
{
|
||||
if (T->TestEqual(NameForTest, Actual.GetType(), ESUDSValueType::Float))
|
||||
{
|
||||
return T->TestEqual(NameForTest, Actual.GetFloatValue(), Expected);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
FORCEINLINE bool TestArgValue(FAutomationTestBase* T, const FString& NameForTest, const FSUDSValue& Actual, ETextGender Expected)
|
||||
{
|
||||
if (T->TestEqual(NameForTest, Actual.GetType(), ESUDSValueType::Gender))
|
||||
{
|
||||
return T->TestEqual(NameForTest, Actual.GetGenderValue(), Expected);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
FORCEINLINE bool TestArgValue(FAutomationTestBase* T, const FString& NameForTest, const FSUDSValue& Actual, const FString& Expected)
|
||||
{
|
||||
if (T->TestEqual(NameForTest, Actual.GetType(), ESUDSValueType::Text))
|
||||
{
|
||||
return T->TestEqual(NameForTest, Actual.GetTextValue().ToString(), Expected);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename V>
|
||||
FORCEINLINE bool TestParsedSetLiteral(FAutomationTestBase* T, const FString& NameForTest, const FSUDSParsedNode* Node, const FString& VarName, V Literal)
|
||||
{
|
||||
if (T->TestNotNull(NameForTest, Node))
|
||||
{
|
||||
T->TestEqual(NameForTest, Node->NodeType, ESUDSParsedNodeType::SetVariable);
|
||||
T->TestEqual(NameForTest, Node->Identifier, VarName);
|
||||
if (T->TestTrue(NameForTest, Node->Expression.IsLiteral()))
|
||||
{
|
||||
TestArgValue(T, NameForTest,Node->Expression.GetLiteralValue(), Literal);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
// Explicit bool version of the above since otherwise it gets converted to int & fails
|
||||
FORCEINLINE bool TestParsedSetLiteral(FAutomationTestBase* T, const FString& NameForTest, const FSUDSParsedNode* Node, const FString& VarName, bool Literal)
|
||||
{
|
||||
if (T->TestNotNull(NameForTest, Node))
|
||||
{
|
||||
T->TestEqual(NameForTest, Node->NodeType, ESUDSParsedNodeType::SetVariable);
|
||||
T->TestEqual(NameForTest, Node->Identifier, VarName);
|
||||
if (T->TestTrue(NameForTest, Node->Expression.IsLiteral()))
|
||||
{
|
||||
T->TestEqual(NameForTest,Node->Expression.GetBooleanLiteralValue(), Literal);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
FORCEINLINE bool TestParsedSetLiteral(FAutomationTestBase* T, const FString& NameForTest, const FSUDSParsedNode* Node, const FString& VarName, const FName& Name)
|
||||
{
|
||||
if (T->TestNotNull(NameForTest, Node))
|
||||
{
|
||||
T->TestEqual(NameForTest, Node->NodeType, ESUDSParsedNodeType::SetVariable);
|
||||
T->TestEqual(NameForTest, Node->Identifier, VarName);
|
||||
if (T->TestTrue(NameForTest, Node->Expression.IsLiteral()))
|
||||
{
|
||||
T->TestEqual(NameForTest,Node->Expression.GetNameLiteralValue(), Name);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
FORCEINLINE bool TestGetParsedNextNode(FAutomationTestBase* T, const FString& NameForTest, const FSUDSParsedNode* Node, FSUDSScriptImporter& Importer, bool bIsHeader, const FSUDSParsedNode** OutNextNode)
|
||||
{
|
||||
if (T->TestNotNull(NameForTest, Node))
|
||||
{
|
||||
if (T->TestEqual(NameForTest, Node->Edges.Num(), 1))
|
||||
{
|
||||
const int NextNodeIdx = Node->Edges[0].TargetNodeIdx;
|
||||
*OutNextNode = bIsHeader ? Importer.GetHeaderNode(NextNodeIdx) : Importer.GetNode(NextNodeIdx);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
*OutNextNode = nullptr;
|
||||
return false;
|
||||
}
|
||||
FORCEINLINE bool TestParsedChoice(FAutomationTestBase* T, const FString& NameForTest, const FSUDSParsedNode* Node, int ExpectedNumChoices)
|
||||
{
|
||||
if (T->TestNotNull(NameForTest, Node))
|
||||
{
|
||||
T->TestEqual(NameForTest, Node->NodeType, ESUDSParsedNodeType::Choice);
|
||||
T->TestEqual(NameForTest, Node->Edges.Num(), ExpectedNumChoices);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
FORCEINLINE bool TestParsedSelect(FAutomationTestBase* T, const FString& NameForTest, const FSUDSParsedNode* Node, int ExpectedNumEdges)
|
||||
{
|
||||
if (T->TestNotNull(NameForTest, Node))
|
||||
{
|
||||
T->TestEqual(NameForTest, Node->NodeType, ESUDSParsedNodeType::Select);
|
||||
T->TestEqual(NameForTest, Node->Edges.Num(), ExpectedNumEdges);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FORCEINLINE bool TestParsedChoiceEdge(FAutomationTestBase* T, const FString& NameForTest, const FSUDSParsedNode* Node, int EdgeIndex, const FString& Text, FSUDSScriptImporter& Importer, const FSUDSParsedNode** OutNode)
|
||||
{
|
||||
*OutNode = nullptr;
|
||||
if (Node && Node->Edges.Num() > EdgeIndex)
|
||||
{
|
||||
auto& Edge = Node->Edges[EdgeIndex];
|
||||
T->TestEqual(NameForTest, Edge.Text, Text);
|
||||
const int Idx = Edge.TargetNodeIdx;
|
||||
*OutNode = Importer.GetNode(Idx);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
FORCEINLINE bool TestParsedSelectEdge(FAutomationTestBase* T, const FString& NameForTest, const FSUDSParsedNode* Node, int EdgeIndex, const FString& ConditionStr, FSUDSScriptImporter& Importer, const FSUDSParsedNode** OutNode)
|
||||
{
|
||||
*OutNode = nullptr;
|
||||
if (Node && Node->Edges.Num() > EdgeIndex)
|
||||
{
|
||||
auto& Edge = Node->Edges[EdgeIndex];
|
||||
T->TestEqual(NameForTest, Edge.ConditionExpression.GetSourceString(), ConditionStr);
|
||||
const int Idx = Edge.TargetNodeIdx;
|
||||
*OutNode = Importer.GetNode(Idx);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
FORCEINLINE bool TestParsedGoto(FAutomationTestBase* T, const FString& NameForTest, const FSUDSParsedNode* Node, FSUDSScriptImporter& Importer, const FSUDSParsedNode** OutNode)
|
||||
{
|
||||
if (T->TestNotNull(NameForTest, Node))
|
||||
{
|
||||
T->TestEqual(NameForTest, Node->NodeType, ESUDSParsedNodeType::Goto);
|
||||
const int Target = Importer.GetGotoTargetNodeIndex(Node->Identifier);
|
||||
*OutNode = Importer.GetNode(Target);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FORCEINLINE bool TestTextNode(FAutomationTestBase* T, const FString& NameForTest, const USUDSScriptNode* Node, const FString& Speaker, const FString& Text)
|
||||
{
|
||||
if (T->TestNotNull(NameForTest, Node))
|
||||
{
|
||||
T->TestEqual(NameForTest, Node->GetNodeType(), ESUDSScriptNodeType::Text);
|
||||
if (auto TextNode = Cast<USUDSScriptNodeText>(Node))
|
||||
{
|
||||
T->TestEqual(NameForTest, TextNode->GetSpeakerID(), Speaker);
|
||||
T->TestEqual(NameForTest, TextNode->GetText().ToString(), Text);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FORCEINLINE bool TestEdge(FAutomationTestBase* T, const FString& NameForTest, USUDSScriptNode* Node, int EdgeIndex, USUDSScriptNode** OutNode)
|
||||
{
|
||||
*OutNode = nullptr;
|
||||
if (Node && Node->GetEdgeCount() > EdgeIndex)
|
||||
{
|
||||
if (auto Edge = Node->GetEdge(EdgeIndex))
|
||||
{
|
||||
*OutNode = Edge->GetTargetNode().Get();
|
||||
return T->TestNotNull(NameForTest, *OutNode);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
FORCEINLINE bool TestChoiceNode(FAutomationTestBase* T, const FString& NameForTest, const USUDSScriptNode* Node, int NumChoices)
|
||||
{
|
||||
if (T->TestNotNull(NameForTest, Node))
|
||||
{
|
||||
T->TestEqual(NameForTest, Node->GetNodeType(), ESUDSScriptNodeType::Choice);
|
||||
return T->TestEqual(NameForTest, Node->GetEdgeCount(), NumChoices);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FORCEINLINE bool TestSelectNode(FAutomationTestBase* T, const FString& NameForTest, const USUDSScriptNode* Node, int NumEdges)
|
||||
{
|
||||
if (T->TestNotNull(NameForTest, Node))
|
||||
{
|
||||
T->TestEqual(NameForTest, Node->GetNodeType(), ESUDSScriptNodeType::Select);
|
||||
return T->TestEqual(NameForTest, Node->GetEdgeCount(), NumEdges);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FORCEINLINE bool TestChoiceEdge(FAutomationTestBase* T, const FString& NameForTest, USUDSScriptNode* Node, int EdgeIndex, const FString& Text, USUDSScriptNode** OutNode)
|
||||
{
|
||||
*OutNode = nullptr;
|
||||
if (Node && Node->GetEdgeCount() > EdgeIndex)
|
||||
{
|
||||
if (auto Edge = Node->GetEdge(EdgeIndex))
|
||||
{
|
||||
T->TestEqual(NameForTest, Edge->GetText().ToString(), Text);
|
||||
*OutNode = Edge->GetTargetNode().Get();
|
||||
return T->TestNotNull(NameForTest, *OutNode);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
FORCEINLINE bool TestSelectEdge(FAutomationTestBase* T, const FString& NameForTest, USUDSScriptNode* Node, int EdgeIndex, const FString& ConditionStr, USUDSScriptNode** OutNode)
|
||||
{
|
||||
*OutNode = nullptr;
|
||||
if (Node && Node->GetEdgeCount() > EdgeIndex)
|
||||
{
|
||||
if (auto Edge = Node->GetEdge(EdgeIndex))
|
||||
{
|
||||
T->TestEqual(NameForTest, Edge->GetCondition().GetSourceString(), ConditionStr);
|
||||
*OutNode = Edge->GetTargetNode().Get();
|
||||
return T->TestNotNull(NameForTest, *OutNode);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
// Helper to provide a string table just in scope
|
||||
struct ScopedStringTableHolder
|
||||
{
|
||||
public:
|
||||
|
||||
UStringTable* StringTable;
|
||||
|
||||
ScopedStringTableHolder()
|
||||
{
|
||||
StringTable = NewObject<UStringTable>(GetTransientPackage(), "TestStrings");
|
||||
}
|
||||
|
||||
~ScopedStringTableHolder()
|
||||
{
|
||||
// Tidy up string table
|
||||
// UStringTable constructor registered this table
|
||||
FStringTableRegistry::Get().UnregisterStringTable(StringTable->GetStringTableId());
|
||||
}
|
||||
|
||||
};
|
||||
30
Plugins/SUDS/Source/SUDSTest/SUDSTest.Build.cs
Normal file
30
Plugins/SUDS/Source/SUDSTest/SUDSTest.Build.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class SUDSTest : ModuleRules
|
||||
{
|
||||
public SUDSTest(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"SUDS",
|
||||
"SUDSEditor"
|
||||
}
|
||||
);
|
||||
|
||||
// Uncomment if you are using Slate UI
|
||||
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
|
||||
|
||||
// Uncomment if you are using online features
|
||||
// PrivateDependencyModuleNames.Add("OnlineSubsystem");
|
||||
|
||||
// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user