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;
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user