diff --git a/Plugins/SUDS/Content/Editor/Slate/Icons/SUDSScript_16x.png b/Plugins/SUDS/Content/Editor/Slate/Icons/SUDSScript_16x.png deleted file mode 100644 index 088330f9..00000000 --- a/Plugins/SUDS/Content/Editor/Slate/Icons/SUDSScript_16x.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:22dbadfb392e532abb4d49760463c8c3b91de07c13f43e374ccd0119d1e886e2 -size 814 diff --git a/Plugins/SUDS/Content/Editor/Slate/Icons/SUDSScript_64x.png b/Plugins/SUDS/Content/Editor/Slate/Icons/SUDSScript_64x.png deleted file mode 100644 index e95a9474..00000000 --- a/Plugins/SUDS/Content/Editor/Slate/Icons/SUDSScript_64x.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c2e765a7c9d335d2203ebca354e1eb8419c7bc105aa265be8f5d4d24f7c8a3ca -size 5615 diff --git a/Plugins/SUDS/Extras/PygmentsLexer/.vscode/settings.json b/Plugins/SUDS/Extras/PygmentsLexer/.vscode/settings.json deleted file mode 100644 index 50b05174..00000000 --- a/Plugins/SUDS/Extras/PygmentsLexer/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "cSpell.words": [ - "speakerline", - "squarebrackets" - ] -} \ No newline at end of file diff --git a/Plugins/SUDS/Extras/PygmentsLexer/setup.py b/Plugins/SUDS/Extras/PygmentsLexer/setup.py deleted file mode 100644 index c8b594c3..00000000 --- a/Plugins/SUDS/Extras/PygmentsLexer/setup.py +++ /dev/null @@ -1,12 +0,0 @@ -from setuptools import setup, find_packages - -setup ( - name='sudslexer', - packages=find_packages(), - version="0.4.0", - entry_points = - """ - [pygments.lexers] - sudslexer = sudslexer.lexer:SudsLexer - """, -) \ No newline at end of file diff --git a/Plugins/SUDS/Extras/PygmentsLexer/sudslexer/__init__.py b/Plugins/SUDS/Extras/PygmentsLexer/sudslexer/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/Plugins/SUDS/Extras/PygmentsLexer/sudslexer/lexer.py b/Plugins/SUDS/Extras/PygmentsLexer/sudslexer/lexer.py deleted file mode 100644 index 66eec43e..00000000 --- a/Plugins/SUDS/Extras/PygmentsLexer/sudslexer/lexer.py +++ /dev/null @@ -1,71 +0,0 @@ -from pygments.lexer import RegexLexer, bygroups -from pygments.token import * - -class SudsLexer(RegexLexer): - name = 'SUDS' - aliases = ['suds'] - filenames = ['*.sud'] - - tokens = { - 'root': [ - (r'===\s*\n', Generic.Heading), - # Choices - (r'\s*\*\-?\s+[^\@\n]+[\@\n]', Generic.Subheading), - (r'\s*#.*\n', Comment), - # Speaker lines - (r'\s*\S+\:', Name.Class, 'speakerline'), - # Open brackets for special functions - (r'\[', Operator, 'squarebrackets'), - - # Variables - (r'(\{)([\w\.]+)(\})', bygroups(Operator, Name.Variable, Operator)), - (r'(\|)(plural|gender)(\()(.*?)(\))', bygroups(Operator, Keyword, Operator, Keyword, Operator)), - (r'\b([tT]rue|[fF]alse|[mM]asculine|[fF]eminine|[nN]euter)\b', Name.Constant), - # Line IDs - (r'\@[0-9a-fA-F]+\@', Comment.Special), - # Embedded markup - (r'\<\w+\>', Name.Decorator), - (r'\<\/\>', Name.Decorator), - # Goto labels - (r'\s*:\S+\n', Name.Label), - # Comments - (r'\s*\#[\=\+\%]?.*\n', Comment.Special), - # Fallback for all other text - # Needs an optional \n on the end to finish lines correctly - (r'\s+[\n]?', Text), - (r'\S+?[\n]?', Text), # non-greedy so we don't consume all non-whitespace - - - ], - # While in a speaker line, we ignore everything else except variables & markup - 'speakerline' : [ - # Variables - (r'(\{)([\w\.]+)(\})', bygroups(Operator, Name.Variable, Operator)), - (r'(\|)(plural|gender)(\()(.*?)(\))', bygroups(Operator, Keyword, Operator, Keyword, Operator)), - (r'[\@\n]', Text, '#pop'), - # Embedded markup - (r'\<\w+\>', Name.Decorator), - (r'\<\/\>', Name.Decorator), - (r'[^\@\n\<\{]+', Text), - - ], - # While in a [] block, highlight operators etc (don't do it elsewhere) - 'squarebrackets' : [ - # Close bracket finishes - (r'\]\s*[\n]?', Operator, '#pop'), - # Variables - (r'(\{)([\w\.]+)(\})', bygroups(Operator, Name.Variable, Operator)), - (r'\+\/\-\*\!\%', Operator), - (r'\"[^\"]*\"', String.Double), - (r'\`[^\`]*\`', String.Escape), - (r'\d+(\.\d+)?', Number), - (r'\b([tT]rue|[fF]alse|[mM]asculine|[fF]eminine|[nN]euter)\b', Name.Constant), - # Set, event commands so we can highlight variable/event differently - (r'\s*(set|event)(\s+)([^\]\s]+)', bygroups(Keyword, Text, Name.Variable)), - (r'\s*(if|else|elseif|endif|event|return|goto|gosub|go to|go sub|random|endrandom)\b', Keyword), - (r'\b(and|or|&&|\|\||not)\b', Operator), - (r'[,]', Punctuation), - (r'\s+', Text), # whitespace OK - - ] - } \ No newline at end of file diff --git a/Plugins/SUDS/License.txt b/Plugins/SUDS/License.txt deleted file mode 100644 index 2e0d6a72..00000000 --- a/Plugins/SUDS/License.txt +++ /dev/null @@ -1,18 +0,0 @@ -The MIT License (MIT) Copyright © 2022 Steve Streeting - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the “Software”), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Plugins/SUDS/Resources/Icon128.png b/Plugins/SUDS/Resources/Icon128.png deleted file mode 100644 index 8fda0ada..00000000 --- a/Plugins/SUDS/Resources/Icon128.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4d56a57fca5beb2d44bd0367dda7b67607685698704df37b4d7a9843c254c05d -size 13715 diff --git a/Plugins/SUDS/Resources/logo.afdesign b/Plugins/SUDS/Resources/logo.afdesign deleted file mode 100644 index f50f54b4..00000000 Binary files a/Plugins/SUDS/Resources/logo.afdesign and /dev/null differ diff --git a/Plugins/SUDS/SUDS.uplugin b/Plugins/SUDS/SUDS.uplugin deleted file mode 100644 index b8256bab..00000000 --- a/Plugins/SUDS/SUDS.uplugin +++ /dev/null @@ -1,33 +0,0 @@ -{ - "FileVersion": 3, - "Version": 1, - "VersionName": "1.0", - "FriendlyName": "SUDS", - "Description": "Steve's Unreal Dialogue System", - "Category": "Other", - "CreatedBy": "Steve Streeting", - "CreatedByURL": "", - "DocsURL": "", - "MarketplaceURL": "", - "CanContainContent": true, - "IsBetaVersion": false, - "IsExperimentalVersion": false, - "Installed": false, - "Modules": [ - { - "Name": "SUDS", - "Type": "Runtime", - "LoadingPhase": "Default" - }, - { - "Name": "SUDSEditor", - "Type": "Editor", - "LoadingPhase": "Default" - }, - { - "Name": "SUDSTest", - "Type": "Editor", - "LoadingPhase": "Default" - } - ] -} \ No newline at end of file diff --git a/Plugins/SUDS/Source/SUDS/Private/SUDS.cpp b/Plugins/SUDS/Source/SUDS/Private/SUDS.cpp deleted file mode 100644 index 61f5ef77..00000000 --- a/Plugins/SUDS/Source/SUDS/Private/SUDS.cpp +++ /dev/null @@ -1,21 +0,0 @@ -// 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) diff --git a/Plugins/SUDS/Source/SUDS/Private/SUDSCommon.cpp b/Plugins/SUDS/Source/SUDS/Private/SUDSCommon.cpp deleted file mode 100644 index 55c2c062..00000000 --- a/Plugins/SUDS/Source/SUDS/Private/SUDSCommon.cpp +++ /dev/null @@ -1,4 +0,0 @@ -#include "SUDSCommon.h" - -const FName FSUDSConstants::RandomItemSelectIndexVarName(SUDS_RANDOMITEM_VAR); - diff --git a/Plugins/SUDS/Source/SUDS/Private/SUDSDialogue.cpp b/Plugins/SUDS/Source/SUDS/Private/SUDSDialogue.cpp deleted file mode 100644 index 0784eb99..00000000 --- a/Plugins/SUDS/Source/SUDS/Private/SUDSDialogue.cpp +++ /dev/null @@ -1,1379 +0,0 @@ -// Copyright Steve Streeting 2022 -// Released under the MIT license https://opensource.org/license/MIT/ -#include "SUDSDialogue.h" - -#include "SUDSInternal.h" -#include "SUDSLibrary.h" -#include "SUDSParticipant.h" -#include "SUDSScript.h" -#include "SUDSScriptNode.h" -#include "SUDSScriptNodeEvent.h" -#include "SUDSScriptNodeGosub.h" -#include "SUDSScriptNodeSet.h" -#include "SUDSScriptNodeText.h" -#include "SUDSSubsystem.h" -#include "Kismet/GameplayStatics.h" -#include "Sound/DialogueSoundWaveProxy.h" -#include "Sound/DialogueWave.h" - -DEFINE_LOG_CATEGORY(LogSUDSDialogue); - -const FText USUDSDialogue::DummyText = FText::FromString("INVALID"); -const FString USUDSDialogue::DummyString = "INVALID"; - - - -FArchive& operator<<(FArchive& Ar, FSUDSDialogueState& Value) -{ - Ar << Value.TextNodeID; - Ar << Value.Variables; - Ar << Value.ChoicesTaken; - Ar << Value.ReturnStack; - - return Ar; -} - -void operator<<(FStructuredArchive::FSlot Slot, FSUDSDialogueState& Value) -{ - FStructuredArchive::FRecord Record = Slot.EnterRecord(); - Record - << SA_VALUE(TEXT("TextNodeID"), Value.TextNodeID) - << SA_VALUE(TEXT("Variables"), Value.Variables) - << SA_VALUE(TEXT("ChoicesTaken"), Value.ChoicesTaken) - << SA_VALUE(TEXT("ReturnStack"), Value.ReturnStack); - -} - -USUDSDialogue::USUDSDialogue(): BaseScript(nullptr), - CurrentSpeakerNode(nullptr), - CurrentRootChoiceNode(nullptr), - bParamNamesExtracted(false), - CurrentSourceLineNo(0) -{ -} - -void USUDSDialogue::Initialise(const USUDSScript* Script) -{ - BaseScript = Script; - CurrentSpeakerNode = nullptr; - - InitVariables(); - - CurrentSpeakerNode = nullptr; - -} - -void USUDSDialogue::InitVariables() -{ - VariableState.Empty(); - // Run header nodes immediately (only set nodes) - RunUntilNextSpeakerNodeOrEnd(BaseScript->GetHeaderNode(), false); -} - -void USUDSDialogue::Start(FName Label) -{ - // Only start if not already on a speaker node - // This makes the restore sequence easier, you don't have to test IsEnded - if (!IsValid(CurrentSpeakerNode)) - { - // Note that we don't reset state by default here. This is to allow long-term memory on dialogue, such as - // knowing whether you've met a character before etc. - // We also don't re-run headers here since they will have been run on Initialise() - // This is to allow callers to set variables before Start() that override headers - Restart(false, Label, false); - } -} - -void USUDSDialogue::SetParticipants(const TArray& InParticipants) -{ - // Protect against null participants - // Since our stable sort dereferences - Participants.Empty(); - for (auto P : InParticipants) - { - if (IsValid(P)) - { - Participants.AddUnique(P); - } - } - SortParticipants(); -} - -void USUDSDialogue::AddParticipant(UObject* Participant) -{ - if (IsValid(Participant)) - { - Participants.AddUnique(Participant); - SortParticipants(); - } -} - -void USUDSDialogue::SortParticipants() -{ - if (!Participants.IsEmpty()) - { - // We order by ascending priority so that higher priority values are later in the list - // Which means they're called last and get to override values set by earlier ones - // We'll do a stable sort so that otherwise order is maintained - Participants.StableSort([](const UObject& A, const UObject& B) - { - if (A.Implements() && B.Implements()) - { - return ISUDSParticipant::Execute_GetDialogueParticipantPriority(&A) < - ISUDSParticipant::Execute_GetDialogueParticipantPriority(&B); - } - // Be deterministic - return &A < &B; - }); - } -} - -void USUDSDialogue::RunUntilNextSpeakerNodeOrEnd(USUDSScriptNode* NextNode, bool bRaiseAtEnd) -{ - // We run through nodes which don't require a speaker line prompt - // E.g. set nodes, select nodes which are all automatically resolved - // Starting with this node - while (NextNode && !IsChoiceOrTextNode(NextNode->GetNodeType())) - { - NextNode = RunNode(NextNode); - } - - if (NextNode) - { - if (NextNode->GetNodeType() == ESUDSScriptNodeType::Text) - { - SetCurrentSpeakerNode(Cast(NextNode), false); - } - else - { - // This can happen if for example user creates a choice node as the first thing - UE_LOG(LogSUDSDialogue, - Error, - TEXT("Error in %s line %d: Tried to run to next speaker node but encountered unexpected node of type %s"), - *BaseScript->GetName(), - NextNode->GetSourceLineNo(), - *(StaticEnum()->GetValueAsString(NextNode->GetNodeType())) - ); - } - } - else - { - End(!bRaiseAtEnd); - } - -} - -USUDSScriptNode* USUDSDialogue::RunNode(USUDSScriptNode* Node) -{ - CurrentSourceLineNo = Node->GetSourceLineNo(); - switch (Node->GetNodeType()) - { - case ESUDSScriptNodeType::Select: - return RunSelectNode(Node); - case ESUDSScriptNodeType::SetVariable: - return RunSetVariableNode(Node); - case ESUDSScriptNodeType::Event: - return RunEventNode(Node); - case ESUDSScriptNodeType::Gosub: - return RunGosubNode(Node); - case ESUDSScriptNodeType::Return: - return RunReturnNode(Node); - default: ; - } - - UE_LOG(LogSUDSDialogue, - Error, - TEXT("Error in %s line %d: Attempted to run non-runnable node type %s"), - *BaseScript->GetName(), - Node->GetSourceLineNo(), - *(StaticEnum()->GetValueAsString(Node->GetNodeType())) - ) - return nullptr; -} - -USUDSScriptNode* USUDSDialogue::RunSelectNode(USUDSScriptNode* Node) -{ - // Define internal random selection variable (used in random selects) - if (Node->IsRandomSelect()) - { - // Random picker - // Could try to NOT pick the same ones we already picked, but this would require some additional state, similar - // to "ChoicesTaken" state but for random text nodes already chosen. For now, keep it simple - - const int OptCount = Node->GetEdgeCount(); - // Use SRand() so can be seeded if required - const int RandChoice = FMath::Min(OptCount-1, FMath::TruncToInt(FMath::SRand() * (float)OptCount)); - - SetVariableInt(FSUDSConstants::RandomItemSelectIndexVarName, RandChoice); - } - - for (auto& Edge : Node->GetEdges()) - { - if (Edge.GetCondition().IsValid()) - { - // use the first satisfied edge - RaiseExpressionVariablesRequested(Edge.GetCondition(), Edge.GetSourceLineNo()); - const bool bSuccess = Edge.GetCondition().EvaluateBoolean(VariableState, GetGlobalVariables(), BaseScript->GetName()); -#if WITH_EDITOR - { - FString ExprStr = Edge.GetCondition().GetSourceString(); - if (ExprStr.IsEmpty()) - { - // Lack of condition is an else / final random option - ExprStr = "else"; - } - InternalOnSelectEval.ExecuteIfBound(this, ExprStr, bSuccess, Edge.GetSourceLineNo()); - } -#endif - - if (bSuccess) - { - return Edge.GetTargetNode().Get(); - } - } - } - // NOTE: if no valid path, go to end - // We've already created fall-through else nodes if possible - return nullptr; -} - -USUDSScriptNode* USUDSDialogue::RunEventNode(USUDSScriptNode* Node) -{ - if (USUDSScriptNodeEvent* EvtNode = Cast(Node)) - { - // Build a resolved args list, because we need to evaluate expressions - TArray ArgsResolved; - - for (auto& Expr : EvtNode->GetArgs()) - { - RaiseExpressionVariablesRequested(Expr, EvtNode->GetSourceLineNo()); - ArgsResolved.Add(Expr.Evaluate(VariableState, GetGlobalVariables())); - } - - for (const auto& P : Participants) - { - if (P->GetClass()->ImplementsInterface(USUDSParticipant::StaticClass())) - { - ISUDSParticipant::Execute_OnDialogueEvent(P, this, EvtNode->GetEventName(), ArgsResolved); - } - } - OnEvent.Broadcast(this, EvtNode->GetEventName(), ArgsResolved); -#if WITH_EDITOR - InternalOnEvent.ExecuteIfBound(this, EvtNode->GetEventName(), ArgsResolved, EvtNode->GetSourceLineNo()); -#endif - } - return GetNextNode(Node); -} - -USUDSScriptNode* USUDSDialogue::RunGosubNode(USUDSScriptNode* Node) -{ - if (USUDSScriptNodeGosub* GosubNode = Cast(Node)) - { - if (auto TargetNode = BaseScript->GetNodeByLabel(GosubNode->GetLabelName())) - { - // Push this gosub node to the return stack, then jump - GosubReturnStack.Push(GosubNode); - return TargetNode; - } - else - { - UE_LOG(LogSUDSDialogue, - Error, - TEXT("Error in %s: Cannot gosub to label '%s', was not found"), - *BaseScript->GetName(), - *GosubNode->GetLabelName().ToString()); - - } - } - return GetNextNode(Node); -} - -USUDSScriptNode* USUDSDialogue::RunReturnNode(USUDSScriptNode* Node) -{ - if (GosubReturnStack.Num() > 0) - { - // We return to the next node after the gosub, which temporarily redirected - const auto GoSubNode = GosubReturnStack.Pop(); - return GetNextNode(GoSubNode); - } - else - { - UE_LOG(LogSUDSDialogue, - Error, - TEXT("Attempted to return at %s:%d but there was no previous gosub to return to"), - *BaseScript->GetName(), - Node->GetSourceLineNo()); - return nullptr; - - } -} - -USUDSScriptNode* USUDSDialogue::RunSetVariableNode(USUDSScriptNode* Node) -{ - if (USUDSScriptNodeSet* SetNode = Cast(Node)) - { - if (SetNode->GetExpression().IsValid()) - { - RaiseExpressionVariablesRequested(SetNode->GetExpression(), SetNode->GetSourceLineNo()); - FSUDSValue Value = SetNode->GetExpression().Evaluate(VariableState, GetGlobalVariables()); - FName Identifier; - if (USUDSLibrary::IsDialogueVariableGlobal(SetNode->GetIdentifier(), Identifier)) - { - InternalSetGlobalVariable(this->GetWorld(), Identifier, Value, true, BaseScript->GetName(), SetNode->GetSourceLineNo()); - } - else - { - SetVariableImpl(SetNode->GetIdentifier(), Value, true, SetNode->GetSourceLineNo()); - } -#if WITH_EDITOR - // We do this here so that we have access to the expression - InternalOnSetVar.ExecuteIfBound(this, - SetNode->GetIdentifier(), - Value, - SetNode->GetExpression().IsLiteral() - ? "" - : SetNode->GetExpression().GetSourceString(), - SetNode->GetSourceLineNo()); -#endif - } - } - - // Always one edge - return GetNextNode(Node); - -} - -void USUDSDialogue::RaiseVariableChange(const FName& VarName, const FSUDSValue& Value, bool bFromScript, int LineNo) -{ - for (const auto& P : Participants) - { - if (P->GetClass()->ImplementsInterface(USUDSParticipant::StaticClass())) - { - ISUDSParticipant::Execute_OnDialogueVariableChanged(P, this, VarName, Value, bFromScript); - } - } - OnVariableChanged.Broadcast(this, VarName, Value, bFromScript); -#if WITH_EDITOR - if (!bFromScript) - { - // Script setting is raised in SetNode so we have access to expressions - InternalOnSetVarByCode.ExecuteIfBound(this, VarName, Value); - } -#endif - -} - -void USUDSDialogue::RaiseVariableRequested(const FName& VarName, int LineNo) -{ - // Because variables set by participants should "win", raise event first - OnVariableRequested.Broadcast(this, VarName); - for (const auto& P : Participants) - { - if (P->GetClass()->ImplementsInterface(USUDSParticipant::StaticClass())) - { - ISUDSParticipant::Execute_OnDialogueVariableRequested(P, this, VarName); - } - } -} - -void USUDSDialogue::RaiseExpressionVariablesRequested(const FSUDSExpression& Expression, int LineNo) -{ - for (auto& Var : Expression.GetVariableNames()) - { - RaiseVariableRequested(Var, LineNo); - } -} - -const TMap& USUDSDialogue::GetGlobalVariables() const -{ - return InternalGetGlobalVariables(this->GetWorld()); -} - -void USUDSDialogue::SetCurrentSpeakerNode(USUDSScriptNodeText* Node, bool bQuietly) -{ - CurrentSpeakerNode = Node; - - CurrentSpeakerDisplayName = FText::GetEmpty(); - bParamNamesExtracted = false; - if (Node) - { - CurrentSourceLineNo = Node->GetSourceLineNo(); - } - else - { - CurrentSourceLineNo = 0; - } - UpdateChoices(); - - if (!bQuietly) - { - if (CurrentSpeakerNode) - RaiseNewSpeakerLine(); - else - RaiseFinished(); - } - -} - -FText USUDSDialogue::ResolveParameterisedText(const TArray Params, const FTextFormat& TextFormat, int LineNo) -{ - for (const auto& P : Params) - { - RaiseVariableRequested(P, LineNo); - } - // Need to make a temp arg list for compatibility - // Also lets us just set the ones we need to - FFormatNamedArguments Args; - GetTextFormatArgs(Params, Args); - return FText::Format(TextFormat, Args); - -} - -void USUDSDialogue::GetTextFormatArgs(const TArray& ArgNames, FFormatNamedArguments& OutArgs) const -{ - for (auto& Name : ArgNames) - { - FName GlobalName; - if (USUDSLibrary::IsDialogueVariableGlobal(Name, GlobalName)) - { - auto& Globals = InternalGetGlobalVariables(this->GetWorld()); - if (const FSUDSValue* Value = Globals.Find(GlobalName)) - { - // Add to format args using name with prefix - OutArgs.Add(Name.ToString(), Value->ToFormatArg()); - } - } - else if (const FSUDSValue* Value = VariableState.Find(Name)) - { - // Use the operator conversion - OutArgs.Add(Name.ToString(), Value->ToFormatArg()); - } - } -} - -FText USUDSDialogue::GetText() -{ - if (CurrentSpeakerNode) - { - if (CurrentSpeakerNode->HasParameters()) - { - return ResolveParameterisedText(CurrentSpeakerNode->GetParameterNames(), - CurrentSpeakerNode->GetTextFormat(), - CurrentSpeakerNode->GetSourceLineNo()); - } - else - { - return CurrentSpeakerNode->GetText(); - } - } - return DummyText; -} - -UDialogueWave* USUDSDialogue::GetWave() const -{ - if (CurrentSpeakerNode) - { - return CurrentSpeakerNode->GetWave(); - } - - return nullptr; -} - -bool USUDSDialogue::IsCurrentLineVoiced() const -{ - if (CurrentSpeakerNode) - { - return IsValid(CurrentSpeakerNode->GetWave()); - } - - return false; -} - -const FString& USUDSDialogue::GetSpeakerID() const -{ - if (CurrentSpeakerNode) - return CurrentSpeakerNode->GetSpeakerID(); - - return DummyString; -} - -FText USUDSDialogue::GetSpeakerDisplayName() const -{ - if (CurrentSpeakerDisplayName.IsEmpty()) - { - // Derive speaker display name - // Is just a special variable "SpeakerName.SpeakerID" - // or just the SpeakerID if none specified - static const FString SpeakerIDPrefix = "SpeakerName."; - FName Key(SpeakerIDPrefix + GetSpeakerID()); - if (auto Arg = VariableState.Find(Key)) - { - if (Arg->GetType() == ESUDSValueType::Text) - { - CurrentSpeakerDisplayName = Arg->GetTextValue(); - } - else - { - UE_LOG(LogSUDSDialogue, - Error, - TEXT("Error in %s: %s was set to a value that was not text, cannot use"), - *BaseScript->GetName(), - *Key.ToString()); - } - } - if (CurrentSpeakerDisplayName.IsEmpty()) - { - // If no display name was specified, use the (non-localised) speaker ID - CurrentSpeakerDisplayName = FText::FromString(GetSpeakerID()); - } - } - return CurrentSpeakerDisplayName; -} - -UDialogueVoice* USUDSDialogue::GetSpeakerVoice() const -{ - if (CurrentSpeakerNode) - { - return GetVoice(CurrentSpeakerNode->GetSpeakerID()); - } - return nullptr; -} - -UDialogueVoice* USUDSDialogue::GetVoice(FString Name) const -{ - return BaseScript->GetSpeakerVoice(Name); -} - -UDialogueVoice* USUDSDialogue::GetTargetVoice() const -{ - if (CurrentSpeakerNode) - { - // Assume that target is the first party that's NOT speaking - for (auto& Name : BaseScript->GetSpeakers()) - { - if (Name != CurrentSpeakerNode->GetSpeakerID()) - { - return BaseScript->GetSpeakerVoice(Name); - } - } - } - return nullptr; - -} - -USoundBase* USUDSDialogue::GetSoundForCurrentLine(bool bAllowAnyTarget) const -{ - // UDialogueWave's contexts have both speakers and targets, but the GetWaveFromContext method is too restrictive - // Instead we'll search the contexts ourselves and be more fuzzy - if (auto Wave = GetWave()) - { - auto SpeakerVoice = GetSpeakerVoice(); - auto TargetVoice = GetTargetVoice(); - for (auto& Ctx : Wave->ContextMappings) - { - // Match specific target voice first, and unspecified targets - if (Ctx.Context.Speaker == SpeakerVoice) - { - if (Ctx.Context.Targets.Contains(TargetVoice)) - { - // Need to use the proxy according to DialogueWave - return Ctx.Proxy; - } - } - } - // If we got here, match more leniently - if (bAllowAnyTarget) - { - for (auto& Ctx : Wave->ContextMappings) - { - // Match specific target voice first, and unspecified targets - if (Ctx.Context.Speaker == SpeakerVoice) - { - // Need to use the proxy according to DialogueWave - return Ctx.Proxy; - } - } - - } - } - - return nullptr; -} - -USoundConcurrency* USUDSDialogue::GetVoiceSoundConcurrency() const -{ - return GetSUDSSubsystem(this->GetWorld())->GetVoicedLineConcurrency(); -} - -void USUDSDialogue::PlayVoicedLine2D(float VolumeMultiplier, float PitchMultiplier, bool bLooselyMatchTarget) -{ - if (auto Sound = GetSoundForCurrentLine(bLooselyMatchTarget)) - { - UGameplayStatics::PlaySound2D(this, Sound, VolumeMultiplier, PitchMultiplier, 0, GetVoiceSoundConcurrency()); - } -} - -void USUDSDialogue::PlayVoicedLineAtLocation(FVector Location, - FRotator Rotation, - float VolumeMultiplier, - float PitchMultiplier, - USoundAttenuation* AttenuationSettings, bool bLooselyMatchTarget) -{ - if (auto Sound = GetSoundForCurrentLine(bLooselyMatchTarget)) - { - UGameplayStatics::PlaySoundAtLocation(this, - Sound, - Location, - Rotation, - VolumeMultiplier, - PitchMultiplier, - 0, - AttenuationSettings, - GetVoiceSoundConcurrency()); - } -} - -UAudioComponent* USUDSDialogue::SpawnVoicedLine2D(float VolumeMultiplier, float PitchMultiplier, bool bLooselyMatchTarget) -{ - if (auto Sound = GetSoundForCurrentLine(bLooselyMatchTarget)) - { - return UGameplayStatics::SpawnSound2D(this, - Sound, - VolumeMultiplier, - PitchMultiplier, - 0, - GetVoiceSoundConcurrency()); - } - - return nullptr; -} - -UAudioComponent* USUDSDialogue::SpawnVoicedLineAtLocation(FVector Location, - FRotator Rotation, - float VolumeMultiplier, - float PitchMultiplier, - USoundAttenuation* AttenuationSettings, - bool bLooselyMatchTarget) -{ - if (auto Sound = GetSoundForCurrentLine(bLooselyMatchTarget)) - { - return UGameplayStatics::SpawnSoundAtLocation(this, - Sound, - Location, - Rotation, - VolumeMultiplier, - PitchMultiplier, - 0, - AttenuationSettings, - GetVoiceSoundConcurrency()); - } - - return nullptr; -} - -USoundBase* USUDSDialogue::GetVoicedLineSound(bool bLooselyMatchTarget) -{ - return GetSoundForCurrentLine(bLooselyMatchTarget); -} - -USUDSScriptNode* USUDSDialogue::GetNextNode(USUDSScriptNode* Node) -{ - // In the case of select or random, we need to evaluate to get the next node - if (Node->GetNodeType() == ESUDSScriptNodeType::Select) - { - return RunSelectNode(Node); - } - else - { - return BaseScript->GetNextNode(Node); - } -} - -bool USUDSDialogue::IsChoiceOrTextNode(ESUDSScriptNodeType Type) -{ - return Type == ESUDSScriptNodeType::Text || Type == ESUDSScriptNodeType::Choice; -} - -const USUDSScriptNode* USUDSDialogue::WalkToNextChoiceNode(USUDSScriptNode* FromNode, bool bExecute) -{ - if (FromNode && FromNode->GetEdgeCount() == 1) - { - const auto NextNode = GetNextNode(FromNode); - TArray> TempGosubStack; - if (!bExecute) - { - // Make a copy of the gosub stack so we can safely explore gosubs - TempGosubStack.Append(GosubReturnStack); - } - - const auto ResultNode = RecurseWalkToNextChoiceOrTextNode(NextNode, bExecute, bExecute ? GosubReturnStack : TempGosubStack); - if (ResultNode && ResultNode->GetNodeType() == ESUDSScriptNodeType::Choice) - { - return ResultNode; - } - } - return nullptr; -} - -USUDSScriptNode* USUDSDialogue::RecurseWalkToNextChoiceOrTextNode(USUDSScriptNode* Node, bool bExecute, TArray>& LocalGosubStack) -{ - auto NextNode = Node; - while (NextNode && !IsChoiceOrTextNode(NextNode->GetNodeType())) - { - // Special case gosub/return in non-execute mode, since only RunNode will explore them - if (!bExecute) - { - if (NextNode->GetNodeType() == ESUDSScriptNodeType::Gosub) - { - // We need to special case Gosubs, since to find the choice we have to go into them and potentially out again - if (USUDSScriptNodeGosub* GosubNode = Cast(NextNode)) - { - if (auto SubNode = BaseScript->GetNodeByLabel(GosubNode->GetLabelName())) - { - LocalGosubStack.Add(GosubNode); - NextNode = RecurseWalkToNextChoiceOrTextNode(SubNode, bExecute, LocalGosubStack); - continue; - } - } - - } - else if (NextNode->GetNodeType() == ESUDSScriptNodeType::Return) - { - if (LocalGosubStack.Num() > 0) - { - // We try to find the next choice node after the gosub, which temporarily redirected - const auto GoSubNode = LocalGosubStack.Pop(); - NextNode = RecurseWalkToNextChoiceOrTextNode(GetNextNode(GoSubNode), bExecute, LocalGosubStack); - continue; - } - else - { - return nullptr; - } - } - } - - if (bExecute) - { - NextNode = RunNode(NextNode); - } - else - { - NextNode = GetNextNode(NextNode); - } - } - - return NextNode; -} - -const USUDSScriptNode* USUDSDialogue::RunUntilNextChoiceNode(USUDSScriptNode* FromNode) -{ - return WalkToNextChoiceNode(FromNode, true); -} -const USUDSScriptNode* USUDSDialogue::FindNextChoiceNode(USUDSScriptNode* FromNode) -{ - return WalkToNextChoiceNode(FromNode, false); -} - -const TArray& USUDSDialogue::GetChoices() const -{ - return CurrentChoices; -} - -void USUDSDialogue::RecurseAppendChoices(const USUDSScriptNode* Node, TArray& OutChoices) -{ - if (!Node) - return; - - // We only cascade into choices or selects - if(Node->GetNodeType() != ESUDSScriptNodeType::Choice && - Node->GetNodeType() != ESUDSScriptNodeType::Select) - { - return; - } - - for (auto& Edge : Node->GetEdges()) - { - switch (Edge.GetType()) - { - case ESUDSEdgeType::Decision: - OutChoices.Add(Edge); - break; - case ESUDSEdgeType::Condition: - // Conditional edges are under selects - if (Edge.GetCondition().IsValid()) - { - RaiseExpressionVariablesRequested(Edge.GetCondition(), Edge.GetSourceLineNo()); - if (Edge.GetCondition().EvaluateBoolean(VariableState, GetGlobalVariables(), BaseScript->GetName())) - { - RecurseAppendChoices(Edge.GetTargetNode().Get(), OutChoices); - // When we choose a path on a select, we don't check the other paths, we can only go down one - return; - } - } - break; - case ESUDSEdgeType::Chained: - RecurseAppendChoices(Edge.GetTargetNode().Get(), OutChoices); - break; - default: - case ESUDSEdgeType::Continue: - UE_LOG(LogSUDSDialogue, Fatal, TEXT("Should not have encountered invalid edge in RecurseAppendChoices")) - break; - }; - - } -} - -void USUDSDialogue::UpdateChoices() -{ - CurrentChoices.Reset(); - CurrentRootChoiceNode = nullptr; - if (CurrentSpeakerNode) - { - // If we've either found choices through static checking (on one or other select paths), we look for them now - // We also check if we're inside a gosub, since the call site changes whether there may be choices or not - if (CurrentSpeakerNode->MayHaveChoices() || - GosubReturnStack.Num() > 0) - { - // We MIGHT have a choice; conditionals can result in HasChoices() being true but the current state not actually - // taking us to a choice path - CurrentRootChoiceNode = FindNextChoiceNode(CurrentSpeakerNode); - if (CurrentRootChoiceNode) - { - // Run any e.g. set nodes between text and choice - // These can be set nodes directly under the text and before the first choice, which get run for all choices - RunUntilNextChoiceNode(CurrentSpeakerNode); - - // Once we've found & run up to the root choice, there can be potentially a tree of mixed choice/select nodes - // for supporting conditional choices - RecurseAppendChoices(CurrentRootChoiceNode, CurrentChoices); - } - } - - if (CurrentChoices.Num() == 0) - { - if (auto Edge = CurrentSpeakerNode->GetEdge(0)) - { - // Simple no-choice progression - // May occur if HasChoices was true but in current state no choice was found - CurrentChoices.Add(*Edge); - } - } - } -} - - -int USUDSDialogue::GetNumberOfChoices() const -{ - return CurrentChoices.Num(); -} - -bool USUDSDialogue::IsSimpleContinue() const -{ - return CurrentChoices.Num() == 1 && CurrentChoices[0].GetText().IsEmpty(); -} - -FText USUDSDialogue::GetChoiceText(int Index) -{ - - if (CurrentChoices.IsValidIndex(Index)) - { - auto& Choice = CurrentChoices[Index]; - if (Choice.HasParameters()) - { - return ResolveParameterisedText(Choice.GetParameterNames(), Choice.GetTextFormat(), Choice.GetSourceLineNo()); - } - else - { - return Choice.GetText(); - } - } - else - { - UE_LOG(LogSUDSDialogue, Error, TEXT("Invalid choice index %d on node %s"), Index, *GetText().ToString()); - } - - return DummyText; -} - -bool USUDSDialogue::HasChoiceIndexBeenTakenPreviously(int Index) -{ - if (CurrentChoices.IsValidIndex(Index)) - { - return HasChoiceBeenTakenPreviously(CurrentChoices[Index]); - } - return false; -} - -bool USUDSDialogue::HasChoiceBeenTakenPreviously(const FSUDSScriptEdge& Choice) -{ - return ChoicesTaken.Contains(Choice.GetTextID()); -} - -bool USUDSDialogue::Continue() -{ - if (GetNumberOfChoices() == 1) - { - return Choose(0); - } - return !IsEnded(); -} - -bool USUDSDialogue::Choose(int Index) -{ - if (CurrentChoices.IsValidIndex(Index)) - { - // ONLY run to choice node if there is one! - // This method is called for Continue() too, which has no choice node - if (CurrentNodeHasChoices()) - { - const auto& Choice = CurrentChoices[Index]; - ChoicesTaken.Add(Choice.GetTextID()); - - RaiseChoiceMade(Index, Choice.GetSourceLineNo()); - RaiseProceeding(); - } - else - { - RaiseProceeding(); - } - // Then choose path - RunUntilNextSpeakerNodeOrEnd(CurrentChoices[Index].GetTargetNode().Get(), true); - return !IsEnded(); - } - else - { - UE_LOG(LogSUDSDialogue, Error, TEXT("Invalid choice index %d on node %s"), Index, *GetText().ToString()); - } - return false; -} - -bool USUDSDialogue::CurrentNodeHasChoices() const -{ - return CurrentRootChoiceNode != nullptr; -} - -bool USUDSDialogue::IsEnded() const -{ - return CurrentSpeakerNode == nullptr; -} - -bool USUDSDialogue::IsFinalLine() const -{ - return CurrentSpeakerNode && CurrentChoices.Num() == 1 && CurrentChoices[0].GetTargetNode() == nullptr; -} - -void USUDSDialogue::End(bool bQuietly) -{ - SetCurrentSpeakerNode(nullptr, bQuietly); -} - -int USUDSDialogue::GetCurrentSourceLine() const -{ - return CurrentSourceLineNo; -} - -void USUDSDialogue::ResetState(bool bResetVariables, bool bResetPosition, bool bResetVisited) -{ - if (bResetVariables) - InitVariables(); - if (bResetPosition) - SetCurrentSpeakerNode(nullptr, true); - if (bResetVisited) - ChoicesTaken.Reset(); -} - -FSUDSDialogueState USUDSDialogue::GetSavedState() const -{ - const FString CurrentNodeId = CurrentSpeakerNode - ? SUDS_GET_TEXT_KEY(CurrentSpeakerNode->GetText()) - : FString(); - - TArray ExportReturnStack; - for (auto Node : GosubReturnStack) - { - if (auto GN = Cast(Node)) - { - ExportReturnStack.Add(GN->GetGosubID()); - } - - } - return FSUDSDialogueState(CurrentNodeId, VariableState, ChoicesTaken, ExportReturnStack); - -} - -void USUDSDialogue::RestoreSavedState(const FSUDSDialogueState& State) -{ - // Don't just empty variables - // Re-run init to ensure header state is initialised then merge; important for it script is altered since state saved - InitVariables(); - VariableState.Append(State.GetVariables()); - ChoicesTaken.Empty(); - ChoicesTaken.Append(State.GetChoicesTaken()); - GosubReturnStack.Empty(); - for (auto ID : State.GetReturnStack()) - { - USUDSScriptNodeGosub* Node = BaseScript->GetNodeByGosubID(ID); - if (!Node) - { - UE_LOG(LogSUDSDialogue, Error, TEXT("Restore: Can't find Gosub with ID %s, returns referencing it will go to end"), *ID); - } - // Add anyway, will just go to end - GosubReturnStack.Add(Node); - } - - // If not found this will be null - if (!State.GetTextNodeID().IsEmpty()) - { - USUDSScriptNodeText* Node = BaseScript->GetNodeByTextID(State.GetTextNodeID()); - SetCurrentSpeakerNode(Node, true); - } - else - { - SetCurrentSpeakerNode(nullptr, true); - } -} - -void USUDSDialogue::Restart(bool bResetState, FName StartLabel, bool bReRunHeader) -{ - if (bResetState) - { - ResetState(); - } - // Always reset return stack - GosubReturnStack.Empty(); - CurrentSourceLineNo = 0; - RaiseStarting(StartLabel); - - if (!bResetState && bReRunHeader) - { - // Run header nodes but don't re-init - RunUntilNextSpeakerNodeOrEnd(BaseScript->GetHeaderNode(), false); - } - - if (StartLabel != NAME_None) - { - // Check that StartLabel leads to a text node - // Labels can lead to choices or select nodes for looping, but there has to be a text node to start with. - auto StartNode = BaseScript->GetNodeByLabel(StartLabel); - if (!StartNode) - { - UE_LOG(LogSUDSDialogue, Error, TEXT("No start label called %s in dialogue %s"), *StartLabel.ToString(), *BaseScript->GetName()); - StartNode = BaseScript->GetFirstNode(); - } - else if (StartNode->GetNodeType() == ESUDSScriptNodeType::Choice) - { - UE_LOG(LogSUDSDialogue, - Error, - TEXT("Label %s in dialogue %s cannot be used as a start point, points to a choice."), - *StartLabel.ToString(), - *BaseScript->GetName()); - StartNode = BaseScript->GetFirstNode(); - } - RunUntilNextSpeakerNodeOrEnd(StartNode, true); - } - else - { - RunUntilNextSpeakerNodeOrEnd(BaseScript->GetFirstNode(), true); - } - -} - - -TSet USUDSDialogue::GetParametersInUse() -{ - // Build on demand, may not be needed - if (!bParamNamesExtracted) - { - CurrentRequestedParamNames.Reset(); - if (CurrentSpeakerNode && CurrentSpeakerNode->HasParameters()) - { - CurrentRequestedParamNames.Append(CurrentSpeakerNode->GetParameterNames()); - } - for (auto& Choice : CurrentChoices) - { - if (Choice.HasParameters()) - { - CurrentRequestedParamNames.Append(Choice.GetParameterNames()); - } - } - bParamNamesExtracted = true; - } - - return CurrentRequestedParamNames; - -} - -void USUDSDialogue::RaiseStarting(FName StartLabel) -{ - for (const auto& P : Participants) - { - if (P->GetClass()->ImplementsInterface(USUDSParticipant::StaticClass())) - { - ISUDSParticipant::Execute_OnDialogueStarting(P, this, StartLabel); - } - } - OnStarting.Broadcast(this, StartLabel); -#if WITH_EDITOR - InternalOnStarting.ExecuteIfBound(this, StartLabel); -#endif -} - -void USUDSDialogue::RaiseFinished() -{ - for (const auto& P : Participants) - { - if (P->GetClass()->ImplementsInterface(USUDSParticipant::StaticClass())) - { - ISUDSParticipant::Execute_OnDialogueFinished(P, this); - } - } - OnFinished.Broadcast(this); -#if WITH_EDITOR - InternalOnFinished.ExecuteIfBound(this); -#endif - -} - -void USUDSDialogue::RaiseNewSpeakerLine() -{ - for (const auto& P : Participants) - { - if (P->GetClass()->ImplementsInterface(USUDSParticipant::StaticClass())) - { - ISUDSParticipant::Execute_OnDialogueSpeakerLine(P, this); - } - } - - // Event listeners get it after - OnSpeakerLine.Broadcast(this); -#if WITH_EDITOR - InternalOnSpeakerLine.ExecuteIfBound(this, GetCurrentSourceLine()); -#endif -} - -void USUDSDialogue::RaiseChoiceMade(int Index, int LineNo) -{ - for (const auto& P : Participants) - { - if (P->GetClass()->ImplementsInterface(USUDSParticipant::StaticClass())) - { - ISUDSParticipant::Execute_OnDialogueChoiceMade(P, this, Index); - } - } - // Event listeners get it after - OnChoice.Broadcast(this, Index); -#if WITH_EDITOR - InternalOnChoice.ExecuteIfBound(this, Index, LineNo); -#endif -} - -void USUDSDialogue::RaiseProceeding() -{ - for (const auto& P : Participants) - { - if (P->GetClass()->ImplementsInterface(USUDSParticipant::StaticClass())) - { - ISUDSParticipant::Execute_OnDialogueProceeding(P, this); - } - } - // Event listeners get it after - OnProceeding.Broadcast(this); -#if WITH_EDITOR - InternalOnProceeding.ExecuteIfBound(this); -#endif -} - -FText USUDSDialogue::GetVariableText(FName Name) const -{ - if (const auto Arg = VariableState.Find(Name)) - { - if (Arg->GetType() == ESUDSValueType::Text) - { - return Arg->GetTextValue(); - } - else - { - UE_LOG(LogSUDSDialogue, Error, TEXT("Requested variable %s of type text but was not a compatible type"), *Name.ToString()); - } - } - return FText(); -} - -void USUDSDialogue::SetVariableInt(FName Name, int32 Value) -{ - SetVariable(Name, Value); -} - -int USUDSDialogue::GetVariableInt(FName Name) const -{ - if (const auto Arg = VariableState.Find(Name)) - { - switch (Arg->GetType()) - { - case ESUDSValueType::Int: - return Arg->GetIntValue(); - case ESUDSValueType::Float: - UE_LOG(LogSUDSDialogue, 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(LogSUDSDialogue, Error, TEXT("Variable %s is not a compatible integer type"), *Name.ToString()); - } - } - return 0; -} - -void USUDSDialogue::SetVariableFloat(FName Name, float Value) -{ - SetVariable(Name, Value); -} - -float USUDSDialogue::GetVariableFloat(FName Name) const -{ - if (const auto Arg = VariableState.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(LogSUDSDialogue, Error, TEXT("Variable %s is not a compatible float type"), *Name.ToString()); - } - } - return 0; -} - -void USUDSDialogue::SetVariableGender(FName Name, ETextGender Value) -{ - SetVariable(Name, Value); -} - -ETextGender USUDSDialogue::GetVariableGender(FName Name) const -{ - if (const auto Arg = VariableState.Find(Name)) - { - switch (Arg->GetType()) - { - case ESUDSValueType::Gender: - return Arg->GetGenderValue(); - default: - case ESUDSValueType::Int: - case ESUDSValueType::Float: - case ESUDSValueType::Text: - UE_LOG(LogSUDSDialogue, Error, TEXT("Variable %s is not a compatible gender type"), *Name.ToString()); - } - } - return ETextGender::Neuter; -} - -void USUDSDialogue::SetVariableBoolean(FName Name, bool Value) -{ - // Use explicit FSUDSValue constructor to avoid default int conversion - SetVariable(Name, FSUDSValue(Value)); -} - -bool USUDSDialogue::GetVariableBoolean(FName Name) const -{ - if (const auto Arg = VariableState.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(LogSUDSDialogue, Error, TEXT("Variable %s is not a compatible boolean type"), *Name.ToString()); - } - } - return false; -} - -void USUDSDialogue::SetVariableName(FName Name, FName Value) -{ - SetVariable(Name, FSUDSValue(Value, false)); -} - -FName USUDSDialogue::GetVariableName(FName Name) const -{ - if (const auto Arg = VariableState.Find(Name)) - { - if (Arg->GetType() == ESUDSValueType::Name) - { - return Arg->GetNameValue(); - } - else - { - UE_LOG(LogSUDSDialogue, Error, TEXT("Requested variable %s of type text but was not a compatible type"), *Name.ToString()); - } - } - return NAME_None; -} - -void USUDSDialogue::UnSetVariable(FName Name) -{ - VariableState.Remove(Name); -} - -FSUDSValue USUDSDialogue::GetSpeakerLineUserMetadata(FName Key) const -{ - if (CurrentSpeakerNode) - { - if (auto pExpr = CurrentSpeakerNode->GetUserMetadata().Find(Key)) - { - return pExpr->Evaluate(VariableState, GetGlobalVariables()); - } - } - return FSUDSValue(); -} - -TMap USUDSDialogue::GetAllSpeakerLineUserMetadata() const -{ - TMap Ret; - if (CurrentSpeakerNode) - { - const TMap& GlobalVariables = GetGlobalVariables(); - const auto& InMeta = CurrentSpeakerNode->GetUserMetadata(); - for (const auto& Pair : InMeta) - { - Ret.Add(Pair.Key, Pair.Value.Evaluate(VariableState, GlobalVariables)); - } - } - return Ret; -} - -FSUDSValue USUDSDialogue::GetChoiceUserMetadata(int Index, FName Key) const -{ - if (CurrentChoices.IsValidIndex(Index)) - { - if (auto pExpr = CurrentChoices[Index].GetUserMetadata().Find(Key)) - { - return pExpr->Evaluate(VariableState, GetGlobalVariables()); - } - } - return FSUDSValue(); -} - -TMap USUDSDialogue::GetAllChoiceUserMetadata(int Index) const -{ - TMap Ret; - if (CurrentChoices.IsValidIndex(Index)) - { - const TMap& GlobalVariables = GetGlobalVariables(); - const auto& InMeta = CurrentChoices[Index].GetUserMetadata(); - for (const auto& Pair : InMeta) - { - Ret.Add(Pair.Key, Pair.Value.Evaluate(VariableState, GlobalVariables)); - } - } - return Ret; -} diff --git a/Plugins/SUDS/Source/SUDS/Private/SUDSExpression.cpp b/Plugins/SUDS/Source/SUDS/Private/SUDSExpression.cpp deleted file mode 100644 index ec94ef46..00000000 --- a/Plugins/SUDS/Source/SUDS/Private/SUDSExpression.cpp +++ /dev/null @@ -1,465 +0,0 @@ -// 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 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(OperatorStack.Top()) < static_cast(OpType) || - (static_cast(OperatorStack.Top()) <= static_cast(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 EvalStack; - const TMap 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& Variables, const TMap& 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 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& Variables, const TMap& 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& Variables, - const TMap& 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& Variables, - const TMap& 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; -} diff --git a/Plugins/SUDS/Source/SUDS/Private/SUDSInternal.h b/Plugins/SUDS/Source/SUDS/Private/SUDSInternal.h deleted file mode 100644 index cb34a459..00000000 --- a/Plugins/SUDS/Source/SUDS/Private/SUDSInternal.h +++ /dev/null @@ -1,41 +0,0 @@ -// 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& 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 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 - } -} - diff --git a/Plugins/SUDS/Source/SUDS/Private/SUDSLibrary.cpp b/Plugins/SUDS/Source/SUDS/Private/SUDSLibrary.cpp deleted file mode 100644 index 0812e68a..00000000 --- a/Plugins/SUDS/Source/SUDS/Private/SUDSLibrary.cpp +++ /dev/null @@ -1,154 +0,0 @@ -// 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(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& 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(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 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; - } -} diff --git a/Plugins/SUDS/Source/SUDS/Private/SUDSParticipant.cpp b/Plugins/SUDS/Source/SUDS/Private/SUDSParticipant.cpp deleted file mode 100644 index 9daf12d7..00000000 --- a/Plugins/SUDS/Source/SUDS/Private/SUDSParticipant.cpp +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright Steve Streeting 2022 -// Released under the MIT license https://opensource.org/license/MIT/ -#include "SUDSParticipant.h" - - diff --git a/Plugins/SUDS/Source/SUDS/Private/SUDSScript.cpp b/Plugins/SUDS/Source/SUDS/Private/SUDSScript.cpp deleted file mode 100644 index b6be5d43..00000000 --- a/Plugins/SUDS/Source/SUDS/Private/SUDSScript.cpp +++ /dev/null @@ -1,276 +0,0 @@ -// 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>** ppNodes, - TArray>** ppHeaderNodes, - TMap** ppLabelList, - TMap** ppHeaderLabelList, - TArray** 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(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(Node)) - { - TextNode->NotifyMayHaveChoices(); - } - break; - } - case ESUDSScriptNodeType::Gosub: - { - if (auto GosubNode = Cast(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(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(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(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& 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(this, TEXT("AssetImportData")); - } -} -#endif diff --git a/Plugins/SUDS/Source/SUDS/Private/SUDSScriptEdge.cpp b/Plugins/SUDS/Source/SUDS/Private/SUDSScriptEdge.cpp deleted file mode 100644 index c95e5a7a..00000000 --- a/Plugins/SUDS/Source/SUDS/Private/SUDSScriptEdge.cpp +++ /dev/null @@ -1,79 +0,0 @@ -// 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 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& InTargetNode) -{ - TargetNode = InTargetNode; -} - -const FTextFormat& FSUDSScriptEdge::GetTextFormat() const -{ - if (!bFormatExtracted) - { - ExtractFormat(); - } - return TextFormat; - -} - -const TArray& FSUDSScriptEdge::GetParameterNames() const -{ - if (!bFormatExtracted) - { - ExtractFormat(); - } - return ParameterNames; - -} - -bool FSUDSScriptEdge::HasParameters() const -{ - if (!bFormatExtracted) - { - ExtractFormat(); - } - return !ParameterNames.IsEmpty(); - -} diff --git a/Plugins/SUDS/Source/SUDS/Private/SUDSScriptNode.cpp b/Plugins/SUDS/Source/SUDS/Private/SUDSScriptNode.cpp deleted file mode 100644 index 6a7b33ce..00000000 --- a/Plugins/SUDS/Source/SUDS/Private/SUDSScriptNode.cpp +++ /dev/null @@ -1,38 +0,0 @@ -// 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); -} - diff --git a/Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeEvent.cpp b/Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeEvent.cpp deleted file mode 100644 index de209333..00000000 --- a/Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeEvent.cpp +++ /dev/null @@ -1,12 +0,0 @@ -// 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& InArgs, int LineNo) -{ - NodeType = ESUDSScriptNodeType::Event; - EventName = FName(EvtName); - Args = InArgs; - SourceLineNo = LineNo; - -} diff --git a/Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeSet.cpp b/Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeSet.cpp deleted file mode 100644 index e614726f..00000000 --- a/Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeSet.cpp +++ /dev/null @@ -1,11 +0,0 @@ -// 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; -} diff --git a/Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeText.cpp b/Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeText.cpp deleted file mode 100644 index 4edbf34d..00000000 --- a/Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeText.cpp +++ /dev/null @@ -1,62 +0,0 @@ -// 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& 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 TextParams; - TextFormat.GetFormatArgumentNames(TextParams); - for (auto Param : TextParams) - { - ParameterNames.Add(FName(Param)); - } - bFormatExtracted = true; -} diff --git a/Plugins/SUDS/Source/SUDS/Private/SUDSSubsystem.cpp b/Plugins/SUDS/Source/SUDS/Private/SUDSSubsystem.cpp deleted file mode 100644 index 4e7cbde4..00000000 --- a/Plugins/SUDS/Source/SUDS/Private/SUDSSubsystem.cpp +++ /dev/null @@ -1,199 +0,0 @@ -// 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 USUDSSubsystem::Test_DummyGlobalVariables; -#endif - -void USUDSSubsystem::Initialize(FSubsystemCollectionBase& Collection) -{ - Super::Initialize(Collection); - - // Default to a single voice line being played at once - VoiceConcurrency = NewObject(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); -} diff --git a/Plugins/SUDS/Source/SUDS/Private/SUDSValue.cpp b/Plugins/SUDS/Source/SUDS/Private/SUDSValue.cpp deleted file mode 100644 index 0782e31f..00000000 --- a/Plugins/SUDS/Source/SUDS/Private/SUDSValue.cpp +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright Steve Streeting 2022 -// Released under the MIT license https://opensource.org/license/MIT/ -#include "SUDSValue.h" - -#include "UObject/Class.h" -#include "Internationalization/Text.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(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: -#if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8 - return LexToString(GetGenderValue()); -#else - return *StaticEnum()->GetNameStringByValue((int64)GetGenderValue()); -#endif - 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()->GetDisplayValueAsText(Type).ToString(), *ToString()); - return true; - } - - // Use the default for everything else - return false; - -} diff --git a/Plugins/SUDS/Source/SUDS/Public/SUDS.h b/Plugins/SUDS/Source/SUDS/Public/SUDS.h deleted file mode 100644 index 589f92d3..00000000 --- a/Plugins/SUDS/Source/SUDS/Public/SUDS.h +++ /dev/null @@ -1,15 +0,0 @@ -// 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; -}; diff --git a/Plugins/SUDS/Source/SUDS/Public/SUDSCommon.h b/Plugins/SUDS/Source/SUDS/Public/SUDSCommon.h deleted file mode 100644 index 793c18f1..00000000 --- a/Plugins/SUDS/Source/SUDS/Public/SUDSCommon.h +++ /dev/null @@ -1,23 +0,0 @@ -// 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 \ No newline at end of file diff --git a/Plugins/SUDS/Source/SUDS/Public/SUDSDialogue.h b/Plugins/SUDS/Source/SUDS/Public/SUDSDialogue.h deleted file mode 100644 index 96c38ba7..00000000 --- a/Plugins/SUDS/Source/SUDS/Public/SUDSDialogue.h +++ /dev/null @@ -1,694 +0,0 @@ -// 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&, 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& /*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 Variables; - - UPROPERTY(BlueprintReadOnly, SaveGame, Category="SUDS|Dialogue") - TArray ChoicesTaken; - - UPROPERTY(BlueprintReadOnly, SaveGame, Category="SUDS|Dialogue") - TArray ReturnStack; - -public: - FSUDSDialogueState() {} - - FSUDSDialogueState(const FString& TxtID, - const TMap& InVars, - const TSet& InChoices, - const TArray& InReturnStack) : TextNodeID(TxtID), - Variables(InVars), - ChoicesTaken(InChoices.Array()), - ReturnStack(InReturnStack) - { - } - - const FString& GetTextNodeID() const { return TextNodeID; } - const TMap& GetVariables() const { return Variables; } - const TArray& GetChoicesTaken() const { return ChoicesTaken; } - const TArray& 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 BaseScript; - UPROPERTY() - TObjectPtr CurrentSpeakerNode; - UPROPERTY() - TObjectPtr CurrentRootChoiceNode; - - /// External objects which want to closely participate in the dialogue (not just listen to events) - UPROPERTY() - TArray> 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 FSUDSValueMap; - FSUDSValueMap VariableState; - - /// Stack of Gosub nodes to return to - UPROPERTY() - TArray> GosubReturnStack; - - /// Set of all the TextIDs of choices taken already in this dialogue - TSet ChoicesTaken; - - TSet CurrentRequestedParamNames; - bool bParamNamesExtracted; - - /// Cached derived info - mutable FText CurrentSpeakerDisplayName; - /// All valid choices - TArray 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>& 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& 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& OutChoices); - USoundBase* GetSoundForCurrentLine(bool bAllowAnyTarget) const; - UDialogueVoice* GetTargetVoice() const; - class USoundConcurrency* GetVoiceSoundConcurrency() const; - - FText ResolveParameterisedText(const TArray Params, const FTextFormat& TextFormat, int LineNo); - void GetTextFormatArgs(const TArray& 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& 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& 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& 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 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& 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 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 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 -}; diff --git a/Plugins/SUDS/Source/SUDS/Public/SUDSExpression.h b/Plugins/SUDS/Source/SUDS/Public/SUDSExpression.h deleted file mode 100644 index 91258966..00000000 --- a/Plugins/SUDS/Source/SUDS/Public/SUDSExpression.h +++ /dev/null @@ -1,234 +0,0 @@ -// 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(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 Queue; - - /// Whether the tree is valid to execute - UPROPERTY(BlueprintReadOnly, Category="SUDS|Expression") - bool bIsValid; - - UPROPERTY() - TArray 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& Variables, - const TMap& GlobalVariables) const; - FSUDSValue EvaluateOperand(const FSUDSValue& Operand, const TMap& Variables, const TMap& 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& Variables, const TMap& GlobalVariables) const; - - /// Evaluate the expression and return the result as a boolean, using a given variable state - bool EvaluateBoolean(const TMap& Variables, const TMap& 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& 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& 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(); - } - - -}; - diff --git a/Plugins/SUDS/Source/SUDS/Public/SUDSLibrary.h b/Plugins/SUDS/Source/SUDS/Public/SUDSLibrary.h deleted file mode 100644 index b1f8aead..00000000 --- a/Plugins/SUDS/Source/SUDS/Public/SUDSLibrary.h +++ /dev/null @@ -1,145 +0,0 @@ -// 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& 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); -}; diff --git a/Plugins/SUDS/Source/SUDS/Public/SUDSParticipant.h b/Plugins/SUDS/Source/SUDS/Public/SUDSParticipant.h deleted file mode 100644 index 91bf6e5e..00000000 --- a/Plugins/SUDS/Source/SUDS/Public/SUDSParticipant.h +++ /dev/null @@ -1,129 +0,0 @@ -// 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& 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; - -}; - - diff --git a/Plugins/SUDS/Source/SUDS/Public/SUDSScript.h b/Plugins/SUDS/Source/SUDS/Public/SUDSScript.h deleted file mode 100644 index f6c3b81e..00000000 --- a/Plugins/SUDS/Source/SUDS/Public/SUDSScript.h +++ /dev/null @@ -1,114 +0,0 @@ -// 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> Nodes; - - /// Map of labels to nodes - UPROPERTY(BlueprintReadOnly, VisibleDefaultsOnly, Category="SUDS") - TMap LabelList; - - // Header equivalents for startup - UPROPERTY(BlueprintReadOnly, Category="SUDS") - TArray> HeaderNodes; - - UPROPERTY(BlueprintReadOnly, Category="SUDS") - TMap HeaderLabelList; - - /// Array of all speaker IDs found in this script - UPROPERTY(BlueprintReadOnly, VisibleDefaultsOnly, Category="SUDS") - TArray Speakers; - - /// When using VO, Dialogue Voice assets are associated with speaker IDs - UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category="SUDS") - TMap> SpeakerVoices; - - bool DoesAnyPathAfterLeadToChoice(USUDSScriptNode* FromNode); - int RecurseLookForChoice(USUDSScriptNode* CurrNode); - -public: - void StartImport(TArray>** Nodes, - TArray>** HeaderNodes, - TMap** LabelList, - TMap** ppHeaderLabelList, - TArray** SpeakerList); - void FinishImport(); - - const TArray& GetNodes() const { return ObjectPtrDecay(Nodes); } - const TArray& GetHeaderNodes() const { return ObjectPtrDecay(HeaderNodes); } - const TMap& GetLabelList() const { return LabelList; } - const TMap& 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& 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& GetSpeakerVoices() const { return ObjectPtrDecay(SpeakerVoices); } - -#if WITH_EDITORONLY_DATA - // Import data for this - UPROPERTY(VisibleAnywhere, Instanced, Category=ImportSettings) - TObjectPtr 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& OutTags) const override; -#endif - virtual void Serialize(FArchive& Ar) override; - // End of UObject interface -#endif - -}; diff --git a/Plugins/SUDS/Source/SUDS/Public/SUDSScriptEdge.h b/Plugins/SUDS/Source/SUDS/Public/SUDSScriptEdge.h deleted file mode 100644 index c615b817..00000000 --- a/Plugins/SUDS/Source/SUDS/Public/SUDSScriptEdge.h +++ /dev/null @@ -1,88 +0,0 @@ -// 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 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 UserMetadata; - - - mutable bool bFormatExtracted = false; - mutable TArray 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 GetTargetNode() const { return TargetNode; } - const FSUDSExpression& GetCondition() const { return Condition; } - int GetSourceLineNo() const { return SourceLineNo; } - const TMap& GetUserMetadata() const { return UserMetadata; } - - void SetText(const FText& Text); - void SetType(ESUDSEdgeType InType) { Type = InType; } - void SetTargetNode(const TWeakObjectPtr& InTargetNode); - void SetCondition(const FSUDSExpression& InCondition) { Condition = InCondition; } - void SetUserMetadata(const TMap& Meta) { UserMetadata = Meta; } - - const FTextFormat& GetTextFormat() const; - const TArray& GetParameterNames() const; - bool HasParameters() const; -}; \ No newline at end of file diff --git a/Plugins/SUDS/Source/SUDS/Public/SUDSScriptNode.h b/Plugins/SUDS/Source/SUDS/Public/SUDSScriptNode.h deleted file mode 100644 index d2d5c5a0..00000000 --- a/Plugins/SUDS/Source/SUDS/Public/SUDSScriptNode.h +++ /dev/null @@ -1,81 +0,0 @@ -// 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 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& 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; -}; diff --git a/Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeEvent.h b/Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeEvent.h deleted file mode 100644 index ab5a242a..00000000 --- a/Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeEvent.h +++ /dev/null @@ -1,33 +0,0 @@ -// 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 Args; - -public: - - void Init(const FString& EvtName, const TArray& InArgs, int LineNo); - FName GetEventName() const { return EventName; } - const TArray& GetArgs() const { return Args; } - - -}; diff --git a/Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeGosub.h b/Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeGosub.h deleted file mode 100644 index c0b09c51..00000000 --- a/Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeGosub.h +++ /dev/null @@ -1,49 +0,0 @@ -// 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; } - -}; diff --git a/Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeSet.h b/Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeSet.h deleted file mode 100644 index f7bb92e5..00000000 --- a/Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeSet.h +++ /dev/null @@ -1,33 +0,0 @@ -// 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; } - -}; diff --git a/Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeText.h b/Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeText.h deleted file mode 100644 index d94306f9..00000000 --- a/Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeText.h +++ /dev/null @@ -1,72 +0,0 @@ -// 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 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 UserMetadata; - - mutable bool bFormatExtracted = false; - mutable TArray 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& GetParameterNames() const; - bool HasParameters() const; - - void NotifyMayHaveChoices() { bHasChoices = true; } - - const TMap& GetUserMetadata() const { return UserMetadata; } - void SetUserMetadata(const TMap& Meta) { UserMetadata = Meta; } - - -}; diff --git a/Plugins/SUDS/Source/SUDS/Public/SUDSSubsystem.h b/Plugins/SUDS/Source/SUDS/Public/SUDSSubsystem.h deleted file mode 100644 index d3bbef6c..00000000 --- a/Plugins/SUDS/Source/SUDS/Public/SUDSSubsystem.h +++ /dev/null @@ -1,292 +0,0 @@ -// 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 GlobalVariables; - -public: - FSUDSGlobalState() {} - - FSUDSGlobalState(const TMap& InGlobalVars) : GlobalVariables(InGlobalVars) - { - } - - const TMap& 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 VoiceConcurrency; - - /// Global variable state - TMap 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& 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 Test_DummyGlobalVariables; -#endif - - - -}; - -inline USUDSSubsystem* GetSUDSSubsystem(UWorld* WorldContext) -{ - if (IsValid(WorldContext) && WorldContext->IsGameWorld()) - { - auto GI = WorldContext->GetGameInstance(); - if (IsValid(GI)) - return GI->GetSubsystem(); - } - - return nullptr; -} diff --git a/Plugins/SUDS/Source/SUDS/Public/SUDSValue.h b/Plugins/SUDS/Source/SUDS/Public/SUDSValue.h deleted file mode 100644 index 05f0a0ac..00000000 --- a/Plugins/SUDS/Source/SUDS/Public/SUDSValue.h +++ /dev/null @@ -1,416 +0,0 @@ -// 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() / GetSUDSValue() -/// 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 TextValue; - // Used for variables and name values - TOptional 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(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()->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()->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()->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()->GetValueAsString(Type)) - - return static_cast(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()->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()->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()->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 : public TStructOpsTypeTraitsBase2 -{ - enum - { - WithSerializer = true, - WithExportTextItem = true - }; -}; - - - diff --git a/Plugins/SUDS/Source/SUDS/SUDS.Build.cs b/Plugins/SUDS/Source/SUDS/SUDS.Build.cs deleted file mode 100644 index b0e14a14..00000000 --- a/Plugins/SUDS/Source/SUDS/SUDS.Build.cs +++ /dev/null @@ -1,53 +0,0 @@ -// 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 ... - } - ); - } -} diff --git a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditor.cpp b/Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditor.cpp deleted file mode 100644 index b4018894..00000000 --- a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditor.cpp +++ /dev/null @@ -1,65 +0,0 @@ -// 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(); - 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("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("Settings"); - if (SettingsModule) - { - ISettingsSectionPtr SettingsSection = SettingsModule->RegisterSettings("Project", "Plugins", "SUDS Editor", - LOCTEXT("SUDSEditorSettingsName", "SUDS Editor"), - LOCTEXT("SUDSEditorSettingsDescription", "Configure the editor parts of SUDS."), - GetMutableDefault() - ); - } - - 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); diff --git a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorScriptTools.cpp b/Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorScriptTools.cpp deleted file mode 100644 index 45216d7b..00000000 --- a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorScriptTools.cpp +++ /dev/null @@ -1,280 +0,0 @@ -// 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 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 Nodes, TArray& 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(N)) - { - bAnyChanges = WriteBackTextID(TN->GetText(), TN->GetSourceLineNo(), Lines, NameForErrors, Logger) || bAnyChanges; - } - } - else if (N->GetNodeType() == ESUDSScriptNodeType::SetVariable) - { - if (const auto* SN = Cast(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(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& 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& 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; - -} diff --git a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorSettings.cpp b/Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorSettings.cpp deleted file mode 100644 index e1552c54..00000000 --- a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorSettings.cpp +++ /dev/null @@ -1,48 +0,0 @@ -// 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); - } -} diff --git a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.cpp b/Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.cpp deleted file mode 100644 index 8b61ff5a..00000000 --- a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.cpp +++ /dev/null @@ -1,1590 +0,0 @@ -// Copyright Steve Streeting 2022 -// Released under the MIT license https://opensource.org/license/MIT/ -#include "SUDSEditorToolkit.h" - -#include "EditorReimportHandler.h" -#include "IDetailRootObjectCustomization.h" -#include "IDocumentation.h" -#include "SUDSDialogue.h" -#include "SUDSEditorScriptTools.h" -#include "SUDSEditorVoiceOverTools.h" -#include "SUDSLibrary.h" -#include "SUDSMessageLogger.h" -#include "SUDSScript.h" -#include "SUDSScriptNodeText.h" -#include "SUDSSubsystem.h" -#include "Framework/Text/SlateTextRun.h" -#include "Misc/MessageDialog.h" -#include "Widgets/Docking/SDockTab.h" -#include "Widgets/Input/SButton.h" -#include "Widgets/Input/SCheckBox.h" -#include "Widgets/Input/SComboButton.h" -#include "Widgets/Input/SEditableTextBox.h" -#include "Widgets/Input/SMultiLineEditableTextBox.h" -#include "Widgets/Input/SNumericEntryBox.h" -#include "Widgets/SToolTip.h" -#include "Widgets/Views/SListView.h" -#include "Toolkits/AssetEditorToolkit.h" -#include "PropertyEditorModule.h" - -const FName NAME_SpeakerLine("SpeakerLine"); -const FName NAME_Choice("Choice"); -const FName NAME_VariableSet("VariableSet"); -const FName NAME_VariableEdit("VariableEdit"); -const FName NAME_SelectEval("Condition"); -const FName NAME_RandomEval("Random"); -const FName NAME_Event("Event"); -const FName NAME_Start("Start"); -const FName NAME_Finish("Finish"); - -FName FSUDSEditorToolkit::DialogueOutputTabName("SUDSDialogueTab"); -FName FSUDSEditorToolkit::VariablesTabName("SUDSVariablesTab"); -FName FSUDSEditorToolkit::DetailsTabName("SUDSDetailsTab"); -FName FSUDSEditorToolkit::LogTabName("SUDSLogTab"); - - -class FSUDSDetailRootObjectCustomization : public IDetailRootObjectCustomization -{ -public: - virtual TSharedPtr CustomizeObjectHeader(const FDetailsObjectSet& InRootObjectSet, - const TSharedPtr& InTableRow) override - { - auto Ret = SNew(STextBlock) - .Margin(FMargin(10,5)) - .TextStyle(&FAppStyle::Get().GetWidgetStyle("SmallText")); - if (auto TN = Cast(InRootObjectSet.RootObjects[0])) - { - Ret->SetText(FText::FromString(FString::Printf(TEXT("Speaker Line - ID: %s Source Line: %d"), *TN->GetTextID(), TN->GetSourceLineNo()))); - } - return Ret; - } - - virtual bool ShouldDisplayHeader(const FDetailsObjectSet& InRootObjectSet) const override - { - return InRootObjectSet.RootObjects[0]->IsA(USUDSScriptNodeText::StaticClass()); - } -}; - -void FSUDSEditorToolkit::InitEditor(const TArray& InObjects) -{ - if (InObjects.Num() > 0) - { - Script = Cast(InObjects[0]); - - ReimportDelegateHandle = FReimportManager::Instance()->OnPostReimport().AddRaw(this, &FSUDSEditorToolkit::OnPostReimport); - - const TSharedRef Layout = FTabManager::NewLayout("SUDSEditorLayout_v3") - ->AddArea - ( - FTabManager::NewPrimaryArea()->SetOrientation(Orient_Horizontal) - ->Split - ( - FTabManager::NewSplitter() - ->SetSizeCoefficient(0.75f) - ->SetOrientation(Orient_Horizontal) - ->Split - ( - FTabManager::NewSplitter() - ->SetSizeCoefficient(0.6f) - ->SetOrientation(Orient_Vertical) - ->Split - ( - - - FTabManager::NewStack() - ->SetSizeCoefficient(0.8f) - ->AddTab("SUDSDialogueTab", ETabState::OpenedTab) - ) - ->Split - ( - FTabManager::NewStack() - ->SetSizeCoefficient(0.4f) - ->AddTab("SUDSLogTab", ETabState::OpenedTab) - ) - ) - ->Split - ( - FTabManager::NewSplitter() - ->SetSizeCoefficient(0.25f) - ->SetOrientation(Orient_Vertical) - ->Split - ( - FTabManager::NewStack() - ->SetSizeCoefficient(0.6f) - ->AddTab("SUDSVariablesTab", ETabState::OpenedTab) - ) - ->Split - ( - FTabManager::NewStack() - ->SetSizeCoefficient(0.4f) - ->AddTab("SUDSDetailsTab", ETabState::OpenedTab) - ) - ) - ) - ); - FAssetEditorToolkit::InitAssetEditor(EToolkitMode::Standalone, {}, "SUDSEditor", Layout, true, true, InObjects); - - } -} - -void FSUDSEditorToolkit::RegisterTabSpawners(const TSharedRef& InTabManager) -{ - FAssetEditorToolkit::RegisterTabSpawners(InTabManager); - - WorkspaceMenuCategory = InTabManager->AddLocalWorkspaceMenuCategory(INVTEXT("SUDS Editor")); - - InTabManager->RegisterTabSpawner(DialogueOutputTabName, FOnSpawnTab::CreateLambda([this](const FSpawnTabArgs&) - { - OutputListView = SNew(SListView>) -#if ENGINE_MINOR_VERSION < 5 - .ItemHeight(24) -#endif - .SelectionMode(ESelectionMode::None) - .ListItemsSource(&OutputRows) - .OnGenerateRow(this, &FSUDSEditorToolkit::OnGenerateRowForOutput) - .HeaderRow( - SNew(SHeaderRow) - + SHeaderRow::Column("PrefixHeader") - .FillSized(PrefixColumnWidth) - [ - SNew(SHorizontalBox) - +SHorizontalBox::Slot() - .AutoWidth() - .VAlign(VAlign_Center) - [ - SNew( STextBlock ) - .Text( INVTEXT("") ) - ] - ] - + SHeaderRow::Column("LineHeader") - .FillWidth(1.0f) - [ - SNew(SHorizontalBox) - +SHorizontalBox::Slot() - .AutoWidth() - .VAlign(VAlign_Center) - [ - SNew( STextBlock ) - .Text( INVTEXT("") ) - ] - ] - ); - - - ChoicesBox = SNew(SVerticalBox); - - return SNew(SDockTab) - [ - SNew(SVerticalBox) - +SVerticalBox::Slot() - .FillHeight(1) - [ - OutputListView.ToSharedRef() - ] - +SVerticalBox::Slot() - .AutoHeight() - .HAlign(HAlign_Left) - .Padding(30, 15, 30, 15) - [ - ChoicesBox.ToSharedRef() - ] - ]; - })) - .SetDisplayName(INVTEXT("Dialogue Output")) - .SetGroup(WorkspaceMenuCategory.ToSharedRef()); - - InTabManager->RegisterTabSpawner(VariablesTabName, FOnSpawnTab::CreateLambda([this](const FSpawnTabArgs&) - { - VariablesListView = SNew(SListView>) -#if ENGINE_MINOR_VERSION < 5 - .ItemHeight(28) -#endif - .SelectionMode(ESelectionMode::None) - .ListItemsSource(&VariableRows) - .OnGenerateRow(this, &FSUDSEditorToolkit::OnGenerateRowForVariable) - .HeaderRow( - SNew(SHeaderRow) - + SHeaderRow::Column("NameHeader") - .FillSized(VarColumnWidth) - .VAlignCell(VAlign_Center) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .AutoWidth() - .VAlign(VAlign_Center) - [ - SNew(STextBlock) - .Text(INVTEXT("Name")) - ] - ] - + SHeaderRow::Column("ValueHeader") - .FillWidth(1.0f) - .VAlignCell(VAlign_Fill) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .AutoWidth() - .VAlign(VAlign_Center) - [ - SNew(STextBlock) - .Text(INVTEXT("Value")) - ] - ] - - ); - - // To ensure globals are pre-populated - UpdateVariables(); - - // Possibly use a SPropertyTable with a custom IPropertyTable to implement variable binding - return SNew(SDockTab) - [ - SNew(SVerticalBox) - + SVerticalBox::Slot() - .VAlign(VAlign_Top) - .AutoHeight() - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .HAlign(HAlign_Right) - .Padding(2) - [ - SNew(SButton) - .Text(INVTEXT("Add Variable")) - .OnClicked(this, &FSUDSEditorToolkit::AddVariableClicked) - ] - ] - + SVerticalBox::Slot() - .FillHeight(1.0) - [ - VariablesListView.ToSharedRef() - ] - ]; - })) - .SetDisplayName(INVTEXT("Variables")) - .SetGroup(WorkspaceMenuCategory.ToSharedRef()); - - InTabManager->RegisterTabSpawner(DetailsTabName, FOnSpawnTab::CreateLambda([this](const FSpawnTabArgs&) - { - FPropertyEditorModule& PropertyEditorModule = FModuleManager::Get().LoadModuleChecked("PropertyEditor"); - FDetailsViewArgs DetailsViewArgs; - DetailsViewArgs.bShowOptions = false; - DetailsViewArgs.bUpdatesFromSelection = false; - DetailsViewArgs.bShowPropertyMatrixButton = false; - DetailsViewArgs.bAllowSearch = false; - DetailsViewArgs.bAllowMultipleTopLevelObjects = true; - DetailsViewArgs.bShowObjectLabel = false; - DetailsViewArgs.bHideSelectionTip = true; - auto DetailView = PropertyEditorModule.CreateDetailView(DetailsViewArgs); - DetailView->SetRootObjectCustomizationInstance(MakeShareable(new FSUDSDetailRootObjectCustomization())); - TArray ObjectsInDetailView; - ObjectsInDetailView.Add(Script); - for (auto Node : Script->GetNodes()) - { - if (auto TNode = Cast(Node)) - { - ObjectsInDetailView.Add(TNode); - } - } - DetailView->SetObjects(ObjectsInDetailView, true, true); - // Possibly use a SPropertyTable with a custom IPropertyTable to implement variable binding - return SNew(SDockTab) - [ - SNew(SVerticalBox) - + SVerticalBox::Slot() - .VAlign(VAlign_Top) - [ - DetailView - ] - ]; - })) - .SetDisplayName(INVTEXT("Details")) - .SetGroup(WorkspaceMenuCategory.ToSharedRef()); - - - InTabManager->RegisterTabSpawner(LogTabName, FOnSpawnTab::CreateLambda([this](const FSpawnTabArgs&) - { - TraceLog = SNew(SSUDSTraceLog); - return SNew(SDockTab) - [ - TraceLog.ToSharedRef() - ]; - - })) - .SetDisplayName(INVTEXT("Trace Log")) - .SetGroup(WorkspaceMenuCategory.ToSharedRef()); - - - // Set up the toolbar - FSUDSToolbarCommands::Register(); - - const TWeakPtr WeakToolkit = this->AsShared(); - - TSharedRef ToolbarExtender = MakeShareable(new FExtender); - - ToolbarExtender->AddToolBarExtension( - "Asset", - EExtensionHook::After, - GetToolkitCommands(), - FToolBarExtensionDelegate::CreateRaw(this, &FSUDSEditorToolkit::ExtendToolbar, TWeakPtr(TabManager->GetOwnerTab()))); - AddToolbarExtender(ToolbarExtender); - - GetToolkitCommands()->MapAction(FSUDSToolbarCommands::Get().StartDialogue, - FExecuteAction::CreateSP(this, &FSUDSEditorToolkit::StartDialogue), - FCanExecuteAction()); - GetToolkitCommands()->MapAction(FSUDSToolbarCommands::Get().WriteBackTextIDs, - FExecuteAction::CreateSP(this, &FSUDSEditorToolkit::WriteBackTextIDs), - FCanExecuteAction()); - GetToolkitCommands()->MapAction(FSUDSToolbarCommands::Get().GenerateVOAssets, - FExecuteAction::CreateSP(this, &FSUDSEditorToolkit::GenerateVOAssets), - FCanExecuteAction()); - - //RegenerateMenusAndToolbars(); - - -} - -void FSUDSEditorToolkit::EnsureTabsVisible() -{ - // Hard requirement for the dialogue output tab to be open, so restore it if the user had - // closed it (probably by accident) - if (!TabManager->FindExistingLiveTab(DialogueOutputTabName)) - { - TabManager->TryInvokeTab(DialogueOutputTabName); - } - -} - -void FSUDSEditorToolkit::ExtendToolbar(FToolBarBuilder& ToolbarBuilder, TWeakPtr Tab) -{ - if (!Tab.IsValid()) - { - return; - } - - ToolbarBuilder.BeginSection("CachedState"); - { - ToolbarBuilder.AddToolBarButton(FSUDSToolbarCommands::Get().StartDialogue, - NAME_None, TAttribute(), TAttribute(), - FSlateIcon( -#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0 - FAppStyle::GetAppStyleSetName(), -#else - FEditorStyle::GetStyleSetName(), -#endif - TEXT("Icons.Toolbar.Play"))); - - TSharedRef LabelSelectionBox = SNew(SComboButton) - .OnGetMenuContent(this, &FSUDSEditorToolkit::GetStartLabelMenu) - .ButtonContent() - [ - SNew(STextBlock) - .ToolTipText( INVTEXT("Choose where to start dialogue from") ) - .Text(this, &FSUDSEditorToolkit::GetSelectedStartLabel ) - ]; - - ToolbarBuilder.AddWidget(LabelSelectionBox); - - ToolbarBuilder.AddWidget(SNew(STextBlock) - .Text(FText::FromString(TEXT("Reset Variables:"))) - .Margin(FMargin(10, 7, 10, 5))); - TSharedPtr ResetTooltip = IDocumentation::Get()->CreateToolTip( - FText::FromString(TEXT( - "Whether to reset variable state created by the script (note: vars manually created in this editor are never reset, delete them yourself)")), - NULL, - "", - TEXT("ResetVars")); - TSharedRef ResetVarsCheckbox = SNew(SCheckBox) - .ToolTip(ResetTooltip) - .IsChecked(this, &FSUDSEditorToolkit::GetResetVarsCheckState) - .OnCheckStateChanged(this, &FSUDSEditorToolkit::OnResetVarsCheckStateChanged); - ToolbarBuilder.AddWidget(ResetVarsCheckbox); - - } - ToolbarBuilder.EndSection(); - - ToolbarBuilder.AddSeparator(); - - ToolbarBuilder.BeginSection("CachedState"); - { - ToolbarBuilder.AddToolBarButton(FSUDSToolbarCommands::Get().WriteBackTextIDs, - NAME_None, TAttribute(), TAttribute(), - FSlateIcon( -#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0 - FAppStyle::GetAppStyleSetName(), -#else - FEditorStyle::GetStyleSetName(), -#endif - TEXT("Icons.Toolbar.Details"))); - - ToolbarBuilder.AddToolBarButton(FSUDSToolbarCommands::Get().GenerateVOAssets, - NAME_None, TAttribute(), TAttribute(), - FSlateIcon( -#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0 - FAppStyle::GetAppStyleSetName(), -#else - FEditorStyle::GetStyleSetName(), -#endif - TEXT("Icons.Toolbar.Export"))); - - } - ToolbarBuilder.EndSection(); - -} - -TSharedRef FSUDSEditorToolkit::GetStartLabelMenu() -{ - FMenuBuilder MenuBuilder(true, NULL); - - const FUIAction StartAction(FExecuteAction::CreateSP(this, &FSUDSEditorToolkit::OnStartLabelSelected, FName(NAME_None))); - MenuBuilder.AddMenuEntry(INVTEXT("Beginning"), INVTEXT("Start from the beginning of the dialogue"), FSlateIcon(), StartAction); - MenuBuilder.AddSeparator(); - - if (IsValid(Script)) - { - for (auto& LabelPair : Script->GetLabelList()) - { - FUIAction LabelAction(FExecuteAction::CreateSP(this, &FSUDSEditorToolkit::OnStartLabelSelected, LabelPair.Key)); - MenuBuilder.AddMenuEntry(FText::FromName(LabelPair.Key), FText(), FSlateIcon(), LabelAction); - } - } - - - return MenuBuilder.MakeWidget(); -} - -void FSUDSEditorToolkit::OnStartLabelSelected(FName Label) -{ - StartLabel = Label; -} - -FText FSUDSEditorToolkit::GetSelectedStartLabel() const -{ - return FText::Format(INVTEXT("From: {0}"), - StartLabel.IsNone() ? INVTEXT("Beginning") : FText::FromName(StartLabel)); -} - -ECheckBoxState FSUDSEditorToolkit::GetResetVarsCheckState() const -{ - return bResetVarsOnStart ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; -} - -void FSUDSEditorToolkit::OnResetVarsCheckStateChanged(ECheckBoxState NewState) -{ - bResetVarsOnStart = NewState == ECheckBoxState::Checked; -} - -void FSUDSEditorToolkit::UnregisterTabSpawners(const TSharedRef& InTabManager) -{ - FAssetEditorToolkit::UnregisterTabSpawners(InTabManager); - - InTabManager->UnregisterTabSpawner("SUDSDialogueTab"); - InTabManager->UnregisterTabSpawner("SUDSVariablesTab"); - InTabManager->UnregisterTabSpawner("SUDSLogTab"); -} - -FText FSUDSEditorToolkit::GetBaseToolkitName() const -{ - return INVTEXT("SUDS Script Editor"); -} - -FName FSUDSEditorToolkit::GetToolkitFName() const -{ - return "SUDSScriptEditor"; -} - -FLinearColor FSUDSEditorToolkit::GetWorldCentricTabColorScale() const -{ - return FLinearColor(); -} - -FString FSUDSEditorToolkit::GetWorldCentricTabPrefix() const -{ - return "SUDS Script "; -} - -void FSUDSEditorToolkit::UserEditVariable(const FName& Name, FSUDSValue Value) -{ - FName GlobalName; - if (USUDSLibrary::IsDialogueVariableGlobal(Name, GlobalName)) - { - USUDSSubsystem::Test_DummyGlobalVariables.Add(GlobalName, Value); - OnDialogueUserEditedVar(nullptr, Name, Value); - } - else - { - // Update manual overrides if it's one of these - if (ManualOverrideVariables.Contains(Name)) - { - ManualOverrideVariables[Name] = Value; - } - - // This will cause a refresh - if (Dialogue) - Dialogue->SetVariable(Name, Value); - else - { - // This won't get called if dialogue is not initialised, so call it to get log & refresh - OnDialogueUserEditedVar(nullptr, Name, Value); - } - } -} - -void FSUDSEditorToolkit::DeleteVariable(const FName& Name) -{ - FName GlobalName; - if (USUDSLibrary::IsDialogueVariableGlobal(Name, GlobalName)) - { - USUDSSubsystem::Test_DummyGlobalVariables.Remove(GlobalName); - } - else - { - ManualOverrideVariables.Remove(Name); - if (Dialogue) - Dialogue->UnSetVariable(Name); - } - - UpdateVariables(); -} - -void FSUDSEditorToolkit::OnClose() -{ - FAssetEditorToolkit::OnClose(); - FReimportManager::Instance()->OnPostReimport().Remove(ReimportDelegateHandle); - ReimportDelegateHandle.Reset(); - - DestroyDialogue(); - -} - -void FSUDSEditorToolkit::StartDialogue() -{ - // In case the user closed important tabs - EnsureTabsVisible(); - Clear(); - if (!Dialogue) - { - // Custom creation of USUDSDialogue, we want it to exist only for this window. - // Mark as a root object (we'll clear it on close) - { - // Have to guard vs GC - FGCScopeGuard GCGuard; - const FName Name = MakeUniqueObjectName(GetTransientPackage(), - USUDSDialogue::StaticClass(), - Script->GetFName()); - Dialogue = NewObject(GetTransientPackage(), - Name, RF_Transient | RF_MarkAsRootSet); - } - Dialogue->Initialise(Script); - - Dialogue->InternalOnChoice.BindSP(this, &FSUDSEditorToolkit::OnDialogueChoice); - Dialogue->InternalOnEvent.BindSP(this, &FSUDSEditorToolkit::OnDialogueEvent); - Dialogue->InternalOnFinished.BindSP(this, &FSUDSEditorToolkit::OnDialogueFinished); - Dialogue->InternalOnProceeding.BindSP(this, &FSUDSEditorToolkit::OnDialogueProceeding); - Dialogue->InternalOnStarting.BindSP(this, &FSUDSEditorToolkit::OnDialogueStarting); - Dialogue->InternalOnSpeakerLine.BindSP(this, &FSUDSEditorToolkit::OnDialogueSpeakerLine); - Dialogue->InternalOnSetVar.BindSP(this, &FSUDSEditorToolkit::OnDialogueSetVar); - Dialogue->InternalOnSetVarByCode.BindSP(this, &FSUDSEditorToolkit::OnDialogueUserEditedVar); - Dialogue->InternalOnSelectEval.BindSP(this, &FSUDSEditorToolkit::OnDialogueSelectEval); - - - } - else - { - // Reset things manually so we can set manual override vars - if (bResetVarsOnStart) - Dialogue->ResetState(); - } - - // Set manual override vars before we start - for (auto& Pair : ManualOverrideVariables) - { - Dialogue->SetVariable(Pair.Key, Pair.Value); - } - - Dialogue->Restart(false, StartLabel); - - - UpdateVariables(); - -} - -void FSUDSEditorToolkit::Clear() -{ - OutputRows.Empty(); - if (OutputListView.IsValid()) - { - OutputListView->RequestListRefresh(); - } - VariableRows.Empty(); - if (VariablesListView.IsValid()) - { - VariablesListView->RequestListRefresh(); - } - //TraceLog->ClearMessages(); - -} - -void FSUDSEditorToolkit::DestroyDialogue() -{ - if (Dialogue) - { - // Handle garbage collection of our UObject - { - FGCScopeGuard GCGuard; - Dialogue->RemoveFromRoot(); - Dialogue->MarkAsGarbage(); - } - Dialogue = nullptr; - CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); - } -} - -void FSUDSEditorToolkit::UpdateOutput() -{ - if (OutputListView.IsValid()) - { - OutputListView->RequestListRefresh(); - OutputListView->ScrollToBottom(); - } -} - -void FSUDSEditorToolkit::UpdateChoiceButtons() -{ - USUDSDialogue* D = Dialogue; - ChoicesBox->ClearChildren(); - if (!D->IsEnded()) - { - if (D->IsSimpleContinue()) - { - ChoicesBox->AddSlot() - .Padding(0, 0 ,0 , 5) - [ - SNew(SButton) - .HAlign(HAlign_Left) - .Text(INVTEXT("(Continue...)")) - .OnClicked_Lambda([D]() - { - D->Continue(); - return FReply::Handled(); - }) - ]; - } - else - { - for (int i = 0; i < D->GetNumberOfChoices(); ++i) - { - ChoicesBox->AddSlot() - .Padding(0, 0 ,0 , 5) - [ - SNew(SButton) - .HAlign(HAlign_Left) - .Text(D->GetChoiceText(i)) - .OnClicked_Lambda([D, i]() - { - D->Choose(i); - return FReply::Handled(); - }) - ]; - } - - } - } - -} - -void FSUDSEditorToolkit::OnDialogueChoice(USUDSDialogue* D, int ChoiceIndex, int LineNo) -{ - if (!D->IsSimpleContinue()) - { - AddDialogueStep(NAME_Choice, LineNo, - FText::Format(INVTEXT("[{1}] {0}"), D->GetChoiceText(ChoiceIndex), ChoiceIndex), - INVTEXT("Choice")); - - } -} - -void FSUDSEditorToolkit::OnDialogueEvent(USUDSDialogue* D, FName EventName, const TArray& Args, int LineNo) -{ -#if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8 - TStringBuilder<256> B; -#else - FStringBuilderBase B; -#endif - if (Args.Num() > 0) - { - for (auto& Arg : Args) - { - if (B.Len() > 0) - { - B.Appendf(TEXT(", %s"), *Arg.ToString()); - } - else - { - B.Append("( "); - B.Append(Arg.ToString()); - } - } - B.Append(" )"); - } - FText ArgText = FText::FromString(B.ToString()); - AddDialogueStep(NAME_Event, LineNo, - FText::FormatOrdered(INVTEXT("{0} {1}"), FText::FromName(EventName), ArgText), - INVTEXT("Event")); - -} - -void FSUDSEditorToolkit::OnDialogueFinished(USUDSDialogue* D) -{ - AddDialogueStep(NAME_Finish, 0, - INVTEXT("Dialogue Finished"), - INVTEXT("Finish")); - UpdateChoiceButtons(); -} - -void FSUDSEditorToolkit::OnDialogueProceeding(USUDSDialogue* D) -{ -} - -void FSUDSEditorToolkit::OnDialogueStarting(USUDSDialogue* D, FName LabelName) -{ - FString LabelStr = LabelName.IsNone() ? FString("beginning") : LabelName.ToString(); - AddDialogueStep(NAME_Start, 0, - FText::Format(INVTEXT("Starting from {0}"), FText::FromString(LabelStr)), - INVTEXT("Start")); -} - -void FSUDSEditorToolkit::OnDialogueSpeakerLine(USUDSDialogue* D, int LineNo) -{ - AddDialogueStep(NAME_SpeakerLine, LineNo , D->GetText(),D->GetSpeakerDisplayName()); - UpdateChoiceButtons(); - -} - -void FSUDSEditorToolkit::AddOutputRow(const FText& Prefix, - const FText& Line, - const FSlateColor& PrefixColour, - const FSlateColor& LineColour) -{ - // Stupid table changes any colours I give it, white = dark grey - // Can't figure out where it's coming from, I f**king hate Slate - const FSlateColor BgColour = (OutputRows.Num() % 2) > 0 ? FSlateColor(FLinearColor::White) : FSlateColor(FLinearColor(0.9f, 0.9f, 0.9f, 1)); - OutputRows.Add(MakeShareable( - new FSUDSEditorOutputRow(Prefix, Line, PrefixColour, LineColour, BgColour))); - UpdateOutput(); - -} - -void FSUDSEditorToolkit::AddTraceLogRow(const FName& Category, int SourceLineNo, const FString& Message) -{ - FSlateColor Colour = GetColourForCategory(Category); - - if (TraceLog) - { - TraceLog->AppendMessage(Category, SourceLineNo, Message, Colour); - } -} - -FSlateColor FSUDSEditorToolkit::GetColourForCategory(const FName& Category) -{ - if (Category == NAME_SpeakerLine) - { - return SpeakerColour; - } - else if (Category == NAME_Choice) - { - return ChoiceColour; - } - else if (Category == NAME_Event) - { - return EventColour; - } - else if (Category == NAME_VariableSet) - { - return VarSetColour; - } - else if (Category == NAME_VariableEdit) - { - return VarEditColour; - } - else if (Category == NAME_Start) - { - return StartColour; - } - else if (Category == NAME_Finish) - { - return FinishColour; - } - else if (Category == NAME_SelectEval) - { - return SelectColour; - } - - return FSlateColor::UseForeground(); - -} - -void FSUDSEditorToolkit::AddDialogueStep(const FName& Category, int SourceLineNo, const FText& Description, const FText& Prefix) -{ - if (Category == NAME_SpeakerLine) - { - AddOutputRow(Prefix, Description, SpeakerColour, FSlateColor::UseForeground()); - AddTraceLogRow(Category, SourceLineNo, FString::Printf(TEXT("%s: %s"), *Prefix.ToString(), *Description.ToString())); - } - else if (Category == NAME_Choice) - { - AddOutputRow(Prefix, Description, ChoiceColour, ChoiceColour); - AddTraceLogRow(Category, SourceLineNo, Description.ToString()); - } - else if (Category == NAME_Start) - { - AddOutputRow(Prefix, Description, StartColour, StartColour); - AddTraceLogRow(Category, SourceLineNo, Description.ToString()); - } - else if (Category == NAME_Finish) - { - AddOutputRow(Prefix, Description, FinishColour, FinishColour); - AddTraceLogRow(Category, SourceLineNo, Description.ToString()); - } - else - { - AddTraceLogRow(Category, SourceLineNo, Description.ToString()); - } -} - -FReply FSUDSEditorToolkit::AddVariableClicked() -{ - TSharedRef Window = SNew(SWindow) - .Title(INVTEXT("Add Variable")) - .IsPopupWindow(true) - .SupportsMaximize(false) - .SupportsMinimize(false) - .CreateTitleBar(true) - .SizingRule(ESizingRule::Autosized) - .FocusWhenFirstShown(true) - .MinWidth(400) - .ActivationPolicy(EWindowActivationPolicy::FirstShown); - - bool bConfirmed = false; - ESUDSValueType ValType = ESUDSValueType::Int; - FText VarNameText = FText::FromString("NewVar"); - - Window->SetContent( - SNew(SVerticalBox) - + SVerticalBox::Slot() - .AutoHeight() - .Padding(5) - [ - SNew(SHorizontalBox) - +SHorizontalBox::Slot() - .FillWidth(0.4) - [ - SNew(STextBlock) - .Text(INVTEXT("Name")) - ] - +SHorizontalBox::Slot() - .FillWidth(0.6) - [ - SNew(SEditableTextBox) - .MinDesiredWidth(150) - .SelectAllTextWhenFocused(true) - .Text(VarNameText) - .OnTextCommitted_Lambda([&VarNameText](const FText& NewText, ETextCommit::Type CommitType) - { - VarNameText = NewText; - }) - ] - - ] - + SVerticalBox::Slot() - .AutoHeight() - .Padding(5) - [ - SNew(SHorizontalBox) - +SHorizontalBox::Slot() - .FillWidth(0.4) - [ - SNew(STextBlock) - .Text(INVTEXT("Type")) - ] - +SHorizontalBox::Slot() - .FillWidth(0.6) - [ - SNew(SComboButton) - .OnGetMenuContent_Lambda([&ValType] - { - FMenuBuilder MenuBuilder(true, NULL); - - // NumEnums() - 1 because the last value is the autogen _MAX value - for (int i = 0; i < StaticEnum()->NumEnums() - 1; ++i) - { - ESUDSValueType Val = (ESUDSValueType)StaticEnum()->GetValueByIndex(i); - if (Val != ESUDSValueType::Empty && Val != ESUDSValueType::Variable) - { - const FUIAction Action(FExecuteAction::CreateLambda([Val, &ValType]() - { - ValType = Val; - })); - MenuBuilder.AddMenuEntry(FText::FromString(StaticEnum()->GetNameStringByIndex(i)), - FText(), - FSlateIcon(), - Action); - } - - } - - return MenuBuilder.MakeWidget(); - - }) - .ButtonContent() - [ - SNew(STextBlock) - .Text_Lambda([&ValType] - { - return FText::FromString(StaticEnum()->GetNameStringByValue((int64)ValType)); - }) - ] - ] - ] - + SVerticalBox::Slot() - .AutoHeight() - .Padding(0, 5, 0, 3) - [ - SNew(SHorizontalBox) - +SHorizontalBox::Slot() - .HAlign(HAlign_Right) - .Padding(2) - [ - SNew(SButton) - .Text(INVTEXT("OK")) - .OnClicked_Lambda([&bConfirmed, Window, VarNameText]() - { - if (!VarNameText.IsEmptyOrWhitespace()) - { - bConfirmed = true; - Window->RequestDestroyWindow(); - } - return FReply::Handled(); - }) - ] - +SHorizontalBox::Slot() - .HAlign(HAlign_Right) - .AutoWidth() - .Padding(2) - [ - SNew(SButton) - .Text(INVTEXT("Cancel")) - .OnClicked_Lambda([&bConfirmed, Window]() - { - bConfirmed = false; - Window->RequestDestroyWindow(); - return FReply::Handled(); - }) - ] - - ] - ); - - FVector2D Pos = FSlateApplication::Get().GetCursorPos(); - Pos.X -= 230; - Pos.Y += 100; - Window->MoveWindowTo(Pos); - - // This doesn't return until closed - GEditor->EditorAddModalWindow(Window); - - if (bConfirmed) - { - FName VarName(VarNameText.ToString().TrimStartAndEnd()); - const FSUDSValue Val(ValType); - FName GlobalVarName; - if (USUDSLibrary::IsDialogueVariableGlobal(VarName, GlobalVarName)) - { - USUDSSubsystem::Test_DummyGlobalVariables.Add(GlobalVarName, Val); - OnDialogueUserEditedVar(nullptr, VarName, Val); - } - else - { - ManualOverrideVariables.Add(VarName, Val); - if (Dialogue) - { - // This will cause a refresh - Dialogue->SetVariable(VarName, Val); - } - else - { - // This won't get called if dialogue is not initialised, so call it to get log & refresh - OnDialogueUserEditedVar(nullptr, VarName, Val); - } - } - - } - return FReply::Handled(); - -} - - - -void FSUDSEditorToolkit::UpdateVariables() -{ - VariableRows.Empty(); - // From dialogue (will contain manual overrides already) - if (Dialogue) - { - for (auto& Pair : Dialogue->GetVariables()) - { - VariableRows.Add(MakeShareable( - new FSUDSEditorVariableRow(Pair.Key, Pair.Value, ManualOverrideVariables.Contains(Pair.Key)))); - } - } - else - { - // Manual overrides can occur before dialogue is started - // Otherwise, they will be in the dialogue - for (auto& Pair : ManualOverrideVariables) - { - VariableRows.Add(MakeShareable(new FSUDSEditorVariableRow(Pair.Key, Pair.Value, true))); - } - } - // Also do global vars - for (auto& Pair : USUDSSubsystem::Test_DummyGlobalVariables) - { - VariableRows.Add(MakeShareable( - new FSUDSEditorVariableRow( - // We need to insert the "global." prefix back in since it's removed in the global var list - FName(FString::Printf(TEXT("global.%s"), *Pair.Key.ToString())), - Pair.Value, - true))); - } - - VariableRows.Sort(); - - if (VariablesListView.IsValid()) - { - VariablesListView->RequestListRefresh(); - } - -} - - -void FSUDSEditorToolkit::OnPostReimport(UObject* Object, bool bSuccess) -{ - if (Object == Script) - { - // Destroy any dialogue instance, will not be valid post-import - DestroyDialogue(); - Clear(); - } -} - -void FSUDSEditorToolkit::OnDialogueSetVar(USUDSDialogue* D, - FName VariableName, - const FSUDSValue& ToValue, - const FString& ExpressionStr, - int LineNo) -{ - FText Description; - if (ExpressionStr.Len() > 0) - { - Description = FText::Format(INVTEXT("{0} = {1} = {2}"), - FText::FromName(VariableName), - FText::FromString(ExpressionStr), - FText::FromString(ToValue.ToString())); - } - else - { - Description = FText::Format(INVTEXT("{0} = {1}"), - FText::FromName(VariableName), - FText::FromString(ToValue.ToString())); - - } - AddDialogueStep(NAME_VariableSet, - LineNo, - Description, - INVTEXT("Set Variable")); - UpdateVariables(); -} - -void FSUDSEditorToolkit::OnDialogueUserEditedVar(USUDSDialogue* D, FName VariableName, const FSUDSValue& ToValue) -{ - // User-edited value - const FText Description = FText::Format(INVTEXT("{0} = {1}"), - FText::FromName(VariableName), - FText::FromString(ToValue.ToString())); - AddDialogueStep(NAME_VariableEdit, - 0, - Description, - INVTEXT("Edit Variable")); - UpdateVariables(); - -} - -void FSUDSEditorToolkit::OnDialogueSelectEval(USUDSDialogue* D, - const FString& ExpressionStr, - bool bSuccess, - int LineNo) -{ - AddDialogueStep(NAME_SelectEval, - LineNo, - FText::Format(INVTEXT("{0} = {1}"), - FText::FromString(ExpressionStr), - bSuccess ? INVTEXT("true") : INVTEXT("false")), - INVTEXT("Conditional")); -} - -void FSUDSEditorToolkit::OnDialogueRandomEval(USUDSDialogue* D, - const int RandomOutcome, - int LineNo) -{ - AddDialogueStep(NAME_RandomEval, - LineNo, - FText::Format(INVTEXT(": {0}"), - RandomOutcome), - INVTEXT("Random")); -} - -TSharedRef FSUDSEditorToolkit::OnGenerateRowForOutput( - TSharedPtr Row, - const TSharedRef& OwnerTable) -{ - return SNew( SSUDSEditorOutputItem, OwnerTable ) - .Prefix(Row->Prefix) - .PrefixColour(Row->PrefixColour) - .Line(Row->Line) - .LineColour(Row->LineColour) - .BgColour(Row->BgColour) - .InitialWidth( PrefixColumnWidth ); -} - -TSharedRef FSUDSEditorToolkit::OnGenerateRowForVariable(TSharedPtr Row, - const TSharedRef& Table) -{ - - return SNew( SSUDSEditorVariableItem, Table ) - .VariableName(Row->Name) - .VariableValue(Row->Value) - .bIsManualOverride(Row->bIsManualOverride) - .Parent(this) - .InitialWidth( VarColumnWidth ); - - -} - -void FSUDSEditorToolkit::WriteBackTextIDs() -{ - if (FMessageDialog::Open(EAppMsgType::YesNo, - FText::FromString( - "Are you sure you want to write string keys back to this script?")) - == EAppReturnType::Yes) - { - FSUDSMessageLogger::ClearMessages(); - FSUDSMessageLogger Logger; - FSUDSEditorScriptTools::WriteBackTextIDs(Script, Logger); - } - -} - -void FSUDSEditorToolkit::GenerateVOAssets() -{ - if (FMessageDialog::Open(EAppMsgType::YesNo, - FText::FromString( - "Are you sure you want to generate Dialogue Voice / Dialogue Wave assets for this script?")) - == EAppReturnType::Yes) - { - EObjectFlags Flags = RF_Public | RF_Standalone | RF_Transactional; - FSUDSMessageLogger::ClearMessages(); - FSUDSMessageLogger Logger; - FSUDSEditorVoiceOverTools::GenerateAssets(Script, Flags, &Logger); - } - -} - -void SSUDSEditorVariableItem::Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView) -{ - InitialWidth = InArgs._InitialWidth; - VariableName = InArgs._VariableName; - VariableValue = InArgs._VariableValue; - bIsManualOverride = InArgs._bIsManualOverride; - Parent = InArgs._Parent; - - SMultiColumnTableRow< TSharedPtr< FString > >::Construct( SMultiColumnTableRow< TSharedPtr< FString > >::FArguments(), InOwnerTableView ); -} - -TSharedRef SSUDSEditorVariableItem::GenerateWidgetForColumn(const FName& ColumnName) -{ -#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0 - const FSlateFontInfo PropertyFont = FAppStyle::GetFontStyle(TEXT("PropertyWindow.NormalFont")); - const FSlateBrush* BorderBrush = FAppStyle::GetBrush("ToolPanel.GroupBorder"); -#else - const FSlateFontInfo PropertyFont = FEditorStyle::GetFontStyle(TEXT("PropertyWindow.NormalFont")); - const FSlateBrush* BorderBrush = FEditorStyle::GetBrush("ToolPanel.GroupBorder"); -#endif - const FSlateColor NormalBgColour = FSlateColor(FLinearColor::White); - const FSlateColor ManualOverrideBgColour = FSlateColor(FLinearColor(0.9f,0.7f,1,1)); - - if (ColumnName == "NameHeader") - { - const FMargin BoxMargin(0, 2, 0, 1.333f); - return SNew(SBorder) - .BorderImage(BorderBrush) - .BorderBackgroundColor(bIsManualOverride ? ManualOverrideBgColour : NormalBgColour) - .Padding(BoxMargin) - [ - SNew(SHorizontalBox) - - + SHorizontalBox::Slot() - .FillWidth(1.0) - .Padding(10, 5, 5, 5) - .VAlign(VAlign_Center) - [ - SNew(STextBlock) - .Font(PropertyFont) - .Text(FText::FromName(VariableName)) - ] - ]; - - } - else - { - TSharedPtr ValueWidget; - - FMargin InnerPadding = FMargin(5, 2, 5, 2); - switch (VariableValue.GetType()) - { - case ESUDSValueType::Int: - ValueWidget = SNew(SNumericEntryBox) - .Value(VariableValue.GetIntValue()) - .OnValueCommitted_Lambda([this] (const int32 InValue, ETextCommit::Type) - { - Parent->UserEditVariable(VariableName, InValue); - }) - .MinDesiredValueWidth(60) - .Font(PropertyFont); - break; - case ESUDSValueType::Float: - ValueWidget = SNew(SNumericEntryBox) - .Value(VariableValue.GetFloatValue()) - .OnValueCommitted_Lambda([this] (const float InValue, ETextCommit::Type) - { - Parent->UserEditVariable(VariableName, InValue); - }) - .MinDesiredValueWidth(60) - .Font(PropertyFont); - break; - case ESUDSValueType::Boolean: - ValueWidget = SNew(SCheckBox) - .IsChecked(VariableValue.GetBooleanValue() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked) - .OnCheckStateChanged_Lambda([this] (ECheckBoxState NewState) - { - Parent->UserEditVariable(VariableName, NewState == ECheckBoxState::Checked); - }); - InnerPadding.Top = InnerPadding.Bottom = 2; - break; - case ESUDSValueType::Gender: - ValueWidget = SNew(SComboButton) - .OnGetMenuContent(this, &SSUDSEditorVariableItem::GetGenderMenu) - .ButtonContent() - [ - SNew(STextBlock) - .Text(FText::FromString(VariableValue.ToString())) - .Font(PropertyFont) - ]; - InnerPadding.Top = InnerPadding.Bottom = 1; - - break; - case ESUDSValueType::Text: - ValueWidget = SNew(SEditableTextBox) - .IsReadOnly(false) - .Text(VariableValue.GetTextValue()) - .Font(PropertyFont) - .OnTextCommitted_Lambda([this] (const FText& InValue, ETextCommit::Type) - { - Parent->UserEditVariable(VariableName, InValue); - }); - break; - case ESUDSValueType::Name: - ValueWidget = SNew(SEditableTextBox) - .IsReadOnly(false) - .Text(FText::FromString(VariableValue.GetNameValue().ToString())) - .Font(PropertyFont) - .OnTextCommitted_Lambda([this] (const FText& InValue, ETextCommit::Type) - { - Parent->UserEditVariable(VariableName, FSUDSValue(FName(InValue.ToString()), false)); - }); - break; - case ESUDSValueType::Variable: - case ESUDSValueType::Empty: - default: - ValueWidget = SNew(STextBlock); - break; - - }; - - const FMargin BoxMargin(2, 1, 0, 1); - return SNew(SBox) - .HeightOverride(26) - .Padding(BoxMargin) - [ - SNew(SBorder) - .BorderImage(BorderBrush) - .BorderBackgroundColor(bIsManualOverride ? ManualOverrideBgColour : NormalBgColour) - .VAlign(VAlign_Center) - [ - - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .Padding(InnerPadding) - .AutoWidth() - [ - ValueWidget.ToSharedRef() - ] - + SHorizontalBox::Slot() - .Padding(5,2,5,2) - .FillWidth(1.0) - .HAlign(HAlign_Right) - [ - SNew(SButton) - .ButtonStyle( FAppStyle::Get(), "SimpleButton" ) - .OnClicked_Lambda([this] - { - // Delete click - Parent->DeleteVariable(VariableName); - return FReply::Handled(); - } ) - .ContentPadding(0) - .IsFocusable(false) - [ - SNew( SImage ) -#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0 - .Image( FAppStyle::GetBrush("Icons.Delete") ) -#else - .Image( FEditorStyle::GetBrush("Icons.Delete") ) -#endif - .ColorAndOpacity( FSlateColor::UseForeground() ) - ] - - ] - ] - ]; - - - } - -} - -TSharedRef SSUDSEditorVariableItem::GetGenderMenu() -{ - FMenuBuilder MenuBuilder(true, NULL); - -#if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8 - // StaticEnum() doesn't exist anymore so we have to do this manually - const FUIAction Action0(FExecuteAction::CreateSP(this, &SSUDSEditorVariableItem::OnGenderSelected, ETextGender::Feminine)); - MenuBuilder.AddMenuEntry(FText::FromString(LexToString(ETextGender::Feminine)), - FText(), - FSlateIcon(), - Action0); - const FUIAction Action1(FExecuteAction::CreateSP(this, &SSUDSEditorVariableItem::OnGenderSelected, ETextGender::Masculine)); - MenuBuilder.AddMenuEntry(FText::FromString(LexToString(ETextGender::Masculine)), - FText(), - FSlateIcon(), - Action1); - const FUIAction Action2(FExecuteAction::CreateSP(this, &SSUDSEditorVariableItem::OnGenderSelected, ETextGender::Neuter)); - MenuBuilder.AddMenuEntry(FText::FromString(LexToString(ETextGender::Neuter)), - FText(), - FSlateIcon(), - Action2); -#else - // NumEnums() - 1 because the last value is the autogen _MAX value - for (int i = 0; i < StaticEnum()->NumEnums() - 1; ++i) - { - ETextGender Val = (ETextGender)StaticEnum()->GetValueByIndex(i); - const FUIAction Action(FExecuteAction::CreateSP(this, &SSUDSEditorVariableItem::OnGenderSelected, Val)); - MenuBuilder.AddMenuEntry(FText::FromString(StaticEnum()->GetNameStringByIndex(i)), - FText(), - FSlateIcon(), - Action); - - } -#endif - - return MenuBuilder.MakeWidget(); - -} - -void SSUDSEditorVariableItem::OnGenderSelected(ETextGender NewGender) -{ - Parent->UserEditVariable(VariableName, NewGender); -} - -FVector2D SSUDSEditorVariableItem::ComputeDesiredSize(float X) const -{ - // ItemHeight(28) at table level is totally not working for some reason, so fudge it here - FVector2D Desired = SMultiColumnTableRow>::ComputeDesiredSize(X); - Desired.Y = 28; - return Desired; -} - -void SSUDSEditorOutputItem::Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView) -{ - InitialWidth = InArgs._InitialWidth; - Prefix = InArgs._Prefix; - PrefixColour = InArgs._PrefixColour; - Line = InArgs._Line; - LineColour = InArgs._LineColour; - - SetBorderBackgroundColor(InArgs._BgColour); - - SMultiColumnTableRow< TSharedPtr< FString > >::Construct( SMultiColumnTableRow< TSharedPtr< FString > >::FArguments(), InOwnerTableView ); -} - -TSharedRef SSUDSEditorOutputItem::GenerateWidgetForColumn(const FName& ColumnName) -{ - if (ColumnName == "PrefixHeader") - { - return SNew(SHorizontalBox) - - + SHorizontalBox::Slot() - .FillWidth(1.0) - .Padding(10, 5, 10, 5) - [ - SNew(STextBlock) - .Text(Prefix) - .ColorAndOpacity(PrefixColour) - ]; - } - else - { - return SNew(SHorizontalBox) - - + SHorizontalBox::Slot() - .FillWidth(1.0) - .Padding(10, 5, 10, 5) - [ - SNew(STextBlock) - .Text(Line) - .AutoWrapText(true) - .ColorAndOpacity(LineColour) - ]; - } - - -} - -FSUDSTraceLogMarshaller::FSUDSTraceLogMarshaller() -{ -} - -void FSUDSTraceLogMarshaller::SetText(const FString& SourceString, FTextLayout& TargetTextLayout) -{ - - static const FName LogNormalStyle(TEXT("Log.Normal")); -#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0 - const FTextBlockStyle& OrigStyle = FAppStyle::Get().GetWidgetStyle(LogNormalStyle); -#else - const FTextBlockStyle& OrigStyle = FEditorStyle::Get().GetWidgetStyle(LogNormalStyle); -#endif - - for (const auto& Msg : Messages) - { - // Get base style & copy - FTextBlockStyle Style = OrigStyle; - Style.ColorAndOpacity = Msg->Colour; - - TArray> Runs; - Runs.Add(FSlateTextRun::Create(FRunInfo(), Msg->Message, Style)); - - TargetTextLayout.AddLine(FTextLayout::FNewLineData(Msg->Message, Runs)); - } - -} - -void FSUDSTraceLogMarshaller::GetText(FString& TargetString, const FTextLayout& SourceTextLayout) -{ - SourceTextLayout.GetAsText(TargetString); -} - -void FSUDSTraceLogMarshaller::AppendMessage(FName InCategory, int LineNo, const FString& Message, const FSlateColor& Colour) -{ - const int MinCategorySize = 12; - const FString CatStr = InCategory.ToString(); - int CatLen = CatStr.Len(); - const FString CatPadding = CatLen < MinCategorySize ? FString::ChrN(MinCategorySize - CatLen, ' ') : ""; - const FString LineNoStr = LineNo > 0 ? FString::Printf(TEXT("L%04d"), LineNo) : " "; - const FString ConcatLine = FString::Printf(TEXT("%s[%s] %s %s"), *CatPadding, *CatStr, *LineNoStr, *Message); - Messages.Add(MakeShareable(new FSUDSTraceLogMessage(InCategory, ConcatLine, Colour))); - MakeDirty(); -} - -void FSUDSTraceLogMarshaller::ClearMessages() -{ - Messages.Empty(); - MakeDirty(); -} - -void SSUDSTraceLog::Construct(const FArguments& InArgs) -{ - TraceLogMarshaller = MakeShareable(new FSUDSTraceLogMarshaller()); - TraceLogTextBox = SNew(SMultiLineEditableTextBox) -#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0 - .Style(FAppStyle::Get(), "Log.TextBox") -#else - .Style(FEditorStyle::Get(), "Log.TextBox") - .TextStyle(FEditorStyle::Get(), "Log.Normal") -#endif - .Marshaller(TraceLogMarshaller) - .IsReadOnly(true) - .AutoWrapText(true) - .OnVScrollBarUserScrolled(this, &SSUDSTraceLog::OnUserScrolled) - .AlwaysShowScrollbars(true); - - ChildSlot - .Padding(3) - [ - SNew(SVerticalBox) - - // could add a filter here like output log - - // log area - +SVerticalBox::Slot() - .FillHeight(1) - [ - TraceLogTextBox.ToSharedRef() - ] - - ]; - - -} - -void SSUDSTraceLog::OnUserScrolled(float X) -{ - // revert to auto scroll when near the bottom - bIsUserScrolled = X < (1.0 - SMALL_NUMBER); -} - -void SSUDSTraceLog::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) -{ - SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); - - // We seem to have to do this in Tick(), trying to do it after appending never works - if (!bIsUserScrolled) - { - ScrollToEnd(); - } -} - -void SSUDSTraceLog::AppendMessage(FName InCategory, int LineNo, const FString& Message, const FSlateColor& Colour) -{ - TraceLogMarshaller->AppendMessage(InCategory, LineNo, Message, Colour); -} - -void SSUDSTraceLog::ClearMessages() -{ - TraceLogMarshaller->ClearMessages(); -} - -void SSUDSTraceLog::ScrollToEnd() -{ - TraceLogTextBox->ScrollTo(ETextLocation::EndOfDocument); - bIsUserScrolled = false; -} diff --git a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.h b/Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.h deleted file mode 100644 index 4a22e545..00000000 --- a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.h +++ /dev/null @@ -1,343 +0,0 @@ -// 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 -{ -public: - - FSUDSToolbarCommands() - : TCommands( - "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 StartDialogue; - TSharedPtr WriteBackTextIDs; - TSharedPtr 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 > -{ -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& 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 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& Lhs, const TSharedPtr& RHS) - { - return *Lhs < *RHS; - } - - - -}; - -class SSUDSEditorVariableItem : public SMultiColumnTableRow< TSharedPtr > -{ -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& 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 GenerateWidgetForColumn( const FName& ColumnName ) override; -protected: - float InitialWidth = 70; - FName VariableName; - FSUDSValue VariableValue; - bool bIsManualOverride = false; - class FSUDSEditorToolkit* Parent = nullptr; - - TSharedRef GetGenderMenu(); - void OnGenderSelected(ETextGender TextGender); - virtual FVector2D ComputeDesiredSize(float) const override; - -}; - -struct FSUDSTraceLogMessage -{ - TSharedRef Message; - FName Category; - FSlateColor Colour; - - FSUDSTraceLogMessage(FName InCategory, const FString& InMessage, const FSlateColor& InColour) - : Message(MakeShared(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 > 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 TraceLogMarshaller; - TSharedPtr TraceLogTextBox; - - void OnUserScrolled(float X); - -}; - -class FSUDSEditorToolkit : public FAssetEditorToolkit -{ -public: - void InitEditor(const TArray& InObjects); - - virtual void RegisterTabSpawners(const TSharedRef& InTabManager) override; - virtual void UnregisterTabSpawners(const TSharedRef& 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>> OutputListView; - TArray> OutputRows; - TSharedPtr ChoicesBox; - TSharedPtr>> VariablesListView; - TArray> VariableRows; - TSharedPtr TraceLog; - TMap 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 Tab); - TSharedRef 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& 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 OnGenerateRowForOutput( - TSharedPtr FsudsEditorDialogueRow, - const TSharedRef& TableViewBase); - TSharedRef OnGenerateRowForVariable(TSharedPtr Row, - const TSharedRef& Table); - FSlateColor GetColourForCategory(const FName& Category); - void OnPostReimport(UObject* Object, bool bSuccess); - void Clear(); - void WriteBackTextIDs(); - void GenerateVOAssets(); - -}; - -#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorVoiceOverTools.cpp b/Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorVoiceOverTools.cpp deleted file mode 100644 index 99728c37..00000000 --- a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorVoiceOverTools.cpp +++ /dev/null @@ -1,381 +0,0 @@ -// 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()) - { - 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 UnsavedVoices; - GenerateVoiceAssets(Script, Flags, Logger, UnsavedVoices); - GenerateWaveAssets(Script, Flags, UnsavedVoices, Logger); -} - -void FSUDSEditorVoiceOverTools::GenerateVoiceAssets(USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger, TMap &OutCreatedVoices) -{ - auto Registry = &FModuleManager::LoadModuleChecked(AssetRegistryConstants::ModuleName).Get(); - - FString Prefix; - FString ParentDir; - if (auto Settings = GetDefault()) - { - 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 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(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(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()) - { - 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(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 Assets; - if (Registry->GetAssetsByPackageName(*PackageName, Assets)) - { - if (Assets.Num() > 0) - { - if (Assets[0].GetAsset()->IsA(UDialogueVoice::StaticClass())) - { - return Cast(Assets[0].GetAsset()); - } - } - } - } - } - return nullptr; -} - -void FSUDSEditorVoiceOverTools::GenerateWaveAssets(USUDSScript* Script, EObjectFlags Flags, TMap 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(AssetRegistryConstants::ModuleName).Get(); - - FString Prefix; - FString ParentDir; - if (auto Settings = GetDefault()) - { - 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(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 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(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(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()); - - // 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()) - { - 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()) - { - 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()) - { - if (Settings->StripScriptPrefixesWhenGeneratingNames) - { - int32 Index = INDEX_NONE; - if (Name.FindChar('_', Index)) - { - if (Index < Name.Len() - 1) - { - Name = Name.RightChop(Index + 1); - } - } - } - } - return Name; -} diff --git a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSMessageLogger.cpp b/Plugins/SUDS/Source/SUDSEditor/Private/SUDSMessageLogger.cpp deleted file mode 100644 index c0914ef8..00000000 --- a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSMessageLogger.cpp +++ /dev/null @@ -1,93 +0,0 @@ -// 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("MessageLog"); - TSharedPtr 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& Msg : ErrorMessages) - { - if (Msg->GetSeverity() == EMessageSeverity::Error) - { - return true; - } - } - return false; -} - -int FSUDSMessageLogger::NumErrors() const -{ - int Errs = 0; - for (const TSharedRef& Msg : ErrorMessages) - { - if (Msg->GetSeverity() == EMessageSeverity::Error) - { - ++Errs; - } - } - return Errs; -} - -bool FSUDSMessageLogger::HasWarnings() const -{ - for (const TSharedRef& Msg : ErrorMessages) - { - if (Msg->GetSeverity() == EMessageSeverity::Warning) - { - return true; - } - } - return false; -} - -int FSUDSMessageLogger::NumWarnings() const -{ - int Count = 0; - for (const TSharedRef& 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("MessageLog"); - TSharedPtr LogListing = MessageLogModule.GetLogListing(LogTitle); - LogListing->SetLabel(FText::FromString("SUDS")); - LogListing->ClearMessages(); -} - diff --git a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptActions.cpp b/Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptActions.cpp deleted file mode 100644 index c84038ec..00000000 --- a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptActions.cpp +++ /dev/null @@ -1,134 +0,0 @@ -// 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& TypeAssets, - TArray& OutSourceFilePaths) const -{ - for (auto& Asset : TypeAssets) - { - const auto Script = CastChecked(Asset); - if (Script->AssetImportData) - { - Script->AssetImportData->ExtractFilenames(OutSourceFilePaths); - } - } -} - -bool FSUDSScriptActions::HasActions(const TArray& InObjects) const -{ - return true; -} - -void FSUDSScriptActions::OpenAssetEditor(const TArray& InObjects, - TSharedPtr EditWithinLevelEditor) -{ - MakeShared()->InitEditor(InObjects); -} - -void FSUDSScriptActions::GetActions(const TArray& InObjects, FToolMenuSection& Section) -{ - const auto Scripts = GetTypedWeakObjectPtrs(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> 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> 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); - } - } - } -} diff --git a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptFactory.cpp b/Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptFactory.cpp deleted file mode 100644 index e57660f0..00000000 --- a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptFactory.cpp +++ /dev/null @@ -1,262 +0,0 @@ -// 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()->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(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()->BroadcastAssetPostImport(this, Result); - - return Result; -} - -bool USUDSScriptFactory::ShouldGenerateVoiceAssets(const FString& PackagePath) const -{ - if (auto Settings = GetDefault()) - { - return Settings->ShouldGenerateVoiceAssets(PackagePath); - } - return false; -} - -UStringTable* USUDSScriptFactory::CreateStringTable(UObject* ScriptParent, FName InName, USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger) -{ - auto Settings = GetDefault(); - const bool bCreateSeparatePackage = Settings && Settings->bCreateStringTablesAsSeparatePackages; - - const FName StringTableName = FName(InName.ToString() + "Strings"); - UStringTable* Table = nullptr; - auto Registry = &FModuleManager::LoadModuleChecked(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 ScriptAssets; - Registry->GetAssetsByPackageName(*ScriptPackageName, ScriptAssets); - TArray 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 Assets; - if (Registry->GetAssetsByPackageName(*PackageName, Assets)) - { - if (Assets.Num() > 0) - { - if (Assets[0].GetAsset()->IsA(UStringTable::StaticClass())) - { - Table = Cast(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(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 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(ScriptParent, StringTableName, Flags); - - } - - return Table; - -} - -void USUDSScriptFactory::ForceDeleteAssets(const TArray& 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> PackageFilesToDelete; - TArray 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 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); - } - } -} - diff --git a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptImporter.cpp b/Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptImporter.cpp deleted file mode 100644 index cf1d9126..00000000 --- a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptImporter.cpp +++ /dev/null @@ -1,2765 +0,0 @@ -// Copyright Steve Streeting 2022 -// Released under the MIT license https://opensource.org/license/MIT/ -#include "SUDSScriptImporter.h" - -#include "SUDSEditorSettings.h" -#include "SUDSExpression.h" -#include "SUDSMessageLogger.h" -#include "SUDSScript.h" -#include "SUDSScriptNode.h" -#include "SUDSScriptNodeEvent.h" -#include "SUDSScriptNodeGosub.h" -#include "SUDSScriptNodeSet.h" -#include "SUDSScriptNodeText.h" -#include "Internationalization/Regex.h" -#include "Internationalization/StringTable.h" -#include "Internationalization/StringTableCore.h" - -class USUDSEditorSettings; -const FString FSUDSScriptImporter::EndGotoLabel = "end"; -const FString FSUDSScriptImporter::TreePathSeparator = "/"; - -DEFINE_LOG_CATEGORY(LogSUDSImporter) - -bool FSUDSScriptImporter::ImportFromBuffer(const TCHAR *Start, int32 Length, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent) -{ - static const TCHAR* LineEndings[] = - { - TEXT("\r\n"), - TEXT("\r"), - TEXT("\n"), - }; - constexpr int32 NumDelims = UE_ARRAY_COUNT(LineEndings); - - int LineNumber = 1; - HeaderTree.Reset(); - BodyTree.Reset(); - PersistentMetadata.Empty(); - TransientMetadata.Empty(); - UserMetadata.Empty(); - bHeaderDone = false; - bHeaderInProgress = false; - bTooLateForHeader = false; - bool bImportedOK = true; - ChoiceUniqueId = 0; - TextIDHighestNumber = 0; - bOverrideGenerateSpeakerLineForChoice.Reset(); - OverrideChoiceSpeakerID.Reset(); - ReferencedSpeakers.Empty(); - if (Start) - { - int32 SubstringBeginIndex = 0; - - // Iterate through string. - for(int32 i = 0; i < Length;) - { - int32 SubstringEndIndex = INDEX_NONE; - int32 DelimiterLength = 0; - - // Attempt each delimiter. - for(int32 DelimIndex = 0; DelimIndex < NumDelims; ++DelimIndex) - { - DelimiterLength = FCString::Strlen(LineEndings[DelimIndex]); - - // If we found a delimiter... - if (FCString::Strncmp(Start + i, LineEndings[DelimIndex], DelimiterLength) == 0) - { - // Mark the end of the substring. - SubstringEndIndex = i; - break; - } - } - - if (SubstringEndIndex != INDEX_NONE) - { - const int32 SubstringLength = SubstringEndIndex - SubstringBeginIndex; - FStringView Line = FStringView(Start + SubstringBeginIndex, SubstringLength); - if (!ParseLine(Line, LineNumber++, NameForErrors, Logger, bSilent)) - { - // Abort, error - bImportedOK = false; - break; - } - - - // Next substring begins at the end of the discovered delimiter. - SubstringBeginIndex = SubstringEndIndex + DelimiterLength; - i = SubstringBeginIndex; - } - else - { - ++i; - } - } - - // Add any remaining characters after the last delimiter. - const int32 SubstringLength = Length - SubstringBeginIndex; - const FStringView Line = FStringView(Start + SubstringBeginIndex, SubstringLength); - bImportedOK = ParseLine(Line, LineNumber++, NameForErrors, Logger, bSilent) && bImportedOK; - } - - ConnectRemainingNodes(HeaderTree, NameForErrors, Logger, bSilent); - ConnectRemainingNodes(BodyTree, NameForErrors, Logger, bSilent); - GenerateTextIDs(HeaderTree); - GenerateTextIDs(BodyTree); - - bImportedOK = PostImportSanityCheck(NameForErrors, Logger, bSilent) && bImportedOK; - - return bImportedOK; - -} - -bool FSUDSScriptImporter::ParseLine(const FStringView& Line, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent) -{ - // Trim off any whitespace, but record how much of it there is since it can be relevant - int IndentLevel; - const FStringView TrimmedLine = TrimLine(Line, IndentLevel); - - if (TrimmedLine.Len() == 0 && !bTextInProgress) - { - // We will skip any blank lines that aren't inside text - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:00: BLANK %s"), LineNo, *FString(Line)); - return true; - } - - if (IsCommentLine(TrimmedLine)) - { - // Skip over comment lines - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: COMMENT %s"), LineNo, IndentLevel, *FString(Line)); - - // May be metadata in the comment though - ParseCommentMetadataLine(TrimmedLine, IndentLevel, LineNo, NameForErrors, Logger, bSilent); - - return true; - } - - // Check for headers - static const FStringView HeaderPrefix(TEXT("===")); - if (TrimmedLine.StartsWith(HeaderPrefix)) - { - if (bHeaderDone) - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Failed to parse %s Line %d: Duplicate header section"), *NameForErrors, LineNo); - return false; - } - else if (bTooLateForHeader) - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Failed to parse %s Line %d: Header section must be at start"), *NameForErrors, LineNo); - return false; - } - - if (bHeaderInProgress) - { - // End of header - bHeaderInProgress = false; - bHeaderDone = true; - } - else - { - bHeaderInProgress = true; - } - return true; - } - else if (bHeaderInProgress) - { - return ParseHeaderLine(TrimmedLine, IndentLevel, LineNo, NameForErrors, Logger, bSilent); - } - - // Process body - return ParseBodyLine(TrimmedLine, IndentLevel, LineNo, NameForErrors, Logger, bSilent); - -} - -bool FSUDSScriptImporter::ParseCommentMetadataLine(const FStringView& Line, - int IndentLevel, - int LineNo, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - // Translator comments - // Comment metadata starts with: - // #= [Key:] Single Use Metadata (next line only) - // #+ [Key:] Persistent Metadata (apply to all lines until reset) - // [Key:] is optional; if omitted the key is "Comment" - // Persistent Metadata is reset when: - // - The same key is set again (can be set to blank to reset to empty) - // - A line that is more outdented than the source of the key is encountered - - if (Line.StartsWith(TEXT("#=")) || Line.StartsWith(TEXT("#+"))) - { - FString LineStr(Line); - const FRegexPattern MetaPattern(TEXT("^#([\\=\\+])\\s*(?:(\\S*)\\s*:\\s*)?(.*)$")); - FRegexMatcher MetaRegex(MetaPattern, LineStr); - if (MetaRegex.FindNext()) - { - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: META : %s"), LineNo, IndentLevel, *FString(Line)); - - const bool bIsPersistent = MetaRegex.GetCaptureGroup(1) == "+"; - // There is no "count" of capture groups, test highest to detect if key used - FString KeyStr = MetaRegex.GetCaptureGroup(2); - const FName Key = FName(KeyStr.IsEmpty() ? "Comment" :KeyStr); - const FString Value = MetaRegex.GetCaptureGroup(3).TrimStartAndEnd(); - - if (bIsPersistent) - { - TArray* pStack = PersistentMetadata.Find(Key); - if (!pStack) - { - // Only bother creating if non-empty - // If we find a blank and there's a stack there already, we do add an entry since blank overrides others in scope - if (!Value.IsEmpty()) - { - pStack = &PersistentMetadata.Add(Key); - } - } - - if (pStack) - { - // First we need to check if this line is less or equal indented; if so we have to strip out existing stack items - while (!pStack->IsEmpty() && IndentLevel <= pStack->Top().IndentLevel) - { - pStack->Pop(); - } - pStack->Push(ParsedMetadata(Key, Value, IndentLevel)); - } - } - else - { - if (Value.IsEmpty()) - { - // Reset - TransientMetadata.Remove(Key); - } - else - { - TransientMetadata.Add(Key, ParsedMetadata(Key, Value, IndentLevel)); - } - - } - return true; - } - else - { - Logger->Logf(ELogVerbosity::Warning, - TEXT( - "%s: Malformed translator comment on line %d, ignoring"), - *NameForErrors, - LineNo); - return false; - } - } - else if (Line.StartsWith(TEXT("#%"))) - { - // User custom metadata - always applies to next line only - // Similar syntax to set lines, except text isn't allowed (this is not player visible) - // #% Key = Value - // #% Key Value - FString LineStr(Line); - const FRegexPattern UserMetaPattern(TEXT("^#\\%\\s+(\\S+)\\s+(?:=\\s+)?(\\S.*)$")); - FRegexMatcher UserMetaRegex(UserMetaPattern, LineStr); - if (UserMetaRegex.FindNext()) - { - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: USERMETA : %s"), LineNo, IndentLevel, *FString(Line)); - - FString Name = UserMetaRegex.GetCaptureGroup(1); - FString ExprStr = UserMetaRegex.GetCaptureGroup(2).TrimStartAndEnd(); // trim because capture accepts spaces in quotes - - FSUDSExpression Expr; - { - FString ParseError; - if (Expr.ParseFromString(ExprStr, &ParseError)) - { - if (Expr.IsTextLiteral()) - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Warning, TEXT("Error in %s line %d: Text values not allowed in user metadata"), *NameForErrors, LineNo); - return false; - } - else - { - UserMetadata.Add(FName(Name), Expr); - return true; - } - } - else - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Warning, TEXT("Error in %s line %d: %s"), *NameForErrors, LineNo, *ParseError); - return false; - } - } - } - else - { - // Only a warning, in case other types of comment accidentally clash - Logger->Logf(ELogVerbosity::Warning, - TEXT( - "%s: Malformed user metadata comment on line %d, ignoring"), - *NameForErrors, - LineNo); - return false; - } - } - - return false; - -} - -TMap FSUDSScriptImporter::GetTextMetadataForNextEntry(int CurrentLineIndent) -{ - TMap Ret; - - // For each key - for (auto It = PersistentMetadata.CreateIterator(); It; ++It) - { - auto& Stack = It->Value; - // Use top of stack, so long as equally or less indented than current line - while (!Stack.IsEmpty() && CurrentLineIndent < Stack.Top().IndentLevel) - { - Stack.Pop(); - } - if (Stack.IsEmpty()) - { - // Remove key entry if there's no values left on the stack - It.RemoveCurrent(); - } - else - { - Ret.Add(It->Key, Stack.Top().Value); - } - } - - // Add transient after so they override persistent - for (auto& Pair: TransientMetadata) - { - // Always apply transient ones to next case, don't check indent - Ret.Add(Pair.Key, Pair.Value.Value); - } - TransientMetadata.Empty(); - - return Ret; -} - -TMap FSUDSScriptImporter::ConsumeUserMetadata() -{ - TMap Ret = UserMetadata; - UserMetadata.Empty(); - return Ret; -} - - -bool FSUDSScriptImporter::ParseHeaderLine(const FStringView& Line, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent) -{ - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:00: HEADER: %s"), LineNo, *FString(Line)); - - // Header can still have indenting, only a very limited set of functions though (conditionals) - while (HeaderTree.IndentLevelStack.Num() > 1 && - IndentLevel < HeaderTree.IndentLevelStack.Top().ThresholdIndent) - { - // Pop as much from the stack as necessary to return to this indent level - PopIndent(HeaderTree); - } - - if (HeaderTree.IndentLevelStack.IsEmpty()) - { - // Must be the first body line encountered. Add 1 indent level for the root - PushIndent(HeaderTree, -1, 0, ""); - } - - // Header lines include set / conditionals - // We ignore every other type of line - if (Line.StartsWith(TEXT('['))) - { - bool bParsed = ParseSetLine(Line, HeaderTree, 0, LineNo, NameForErrors, Logger, bSilent); - if (!bParsed) - bParsed = ParseImportSettingLine(Line, HeaderTree, 0, LineNo, NameForErrors, Logger, bSilent);; - } - - return true; -} - -bool FSUDSScriptImporter::ParseBodyLine(const FStringView& Line, - int IndentLevel, - int LineNo, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - // Once we've had anything other than comments or blanks and non-headers, it's too late for headers - bTooLateForHeader = true; - - // Body indenting matters - // If less than "threshold indent", pop contexts from stack until that's no longer the case - // If more than threshold indent, may be a child of previous line. Increase threshold indent to this level - // ONLY a new context level IF previous was condition or choice. If previous was text then just a continuation - // This is why "threshold indent" remains as the outermost indent in this context - // If same as threshold indent, continuation of current context - - while (BodyTree.IndentLevelStack.Num() > 1 && - IndentLevel < BodyTree.IndentLevelStack.Top().ThresholdIndent) - { - // Pop as much from the stack as necessary to return to this indent level - PopIndent(BodyTree); - } - - if (BodyTree.IndentLevelStack.IsEmpty()) - { - // Must be the first body line encountered. Add 1 indent level for the root - PushIndent(BodyTree, -1, 0, ""); - } - - if (Line.StartsWith(TEXT('*'))) - { - return ParseChoiceLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent); - } - else if (Line.StartsWith(TEXT(':'))) - { - return ParseGotoLabelLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent); - } - else if (Line.StartsWith(TEXT('['))) - { - bool bParsed = ParseConditionalLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent); - if (!bParsed) - bParsed = ParseGotoLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent); - if (!bParsed) - bParsed = ParseSetLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent); - if (!bParsed) - bParsed = ParseEventLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent); - if (!bParsed) - bParsed = ParseGosubLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent); - if (!bParsed) - bParsed = ParseReturnLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent); - if (!bParsed) - bParsed = ParseImportSettingLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent); - if (!bParsed) - bParsed = ParseRandomLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent); - - if (!bParsed) - { - if (!bSilent) - { - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: CMD : %s"), LineNo, IndentLevel, *FString(Line)); - Logger->Logf(ELogVerbosity::Warning, TEXT("%s Line %d: Unrecognised command. Ignoring!"), *NameForErrors, LineNo); - } - // We still return true because we don't want to fail the entire import - } - return true; - } - else - { - return ParseTextLine(Line, BodyTree, IndentLevel, LineNo, NameForErrors, Logger, bSilent); - } -} - -bool FSUDSScriptImporter::IsLastNodeOfType(const FSUDSScriptImporter::ParsedTree& Tree, ESUDSParsedNodeType Type) -{ - auto& Ctx = Tree.IndentLevelStack.Top(); - return Tree.Nodes.IsValidIndex(Ctx.LastNodeIdx) && Tree.Nodes[Ctx.LastNodeIdx].NodeType == Type; -} - -int FSUDSScriptImporter::FindLastChoiceNode(const ParsedTree& Tree, int IndentLevel) -{ - int Ret = FindLastChoiceNode(Tree, IndentLevel,Tree.Nodes.Num() - 1, GetCurrentTreeConditionalPath(Tree)); - // if (Ret == -1) - // { - // // Fallback to try to find from previous text - // auto& Ctx = Tree.IndentLevelStack.Top(); - // - // if (Tree.Nodes.IsValidIndex(Ctx.LastTextNodeIdx)) - // { - // return FindChoiceAfterTextNode(Tree, Ctx.LastTextNodeIdx); - // } - // } - return Ret; -} - -int FSUDSScriptImporter::FindChoiceAfterTextNode(const FSUDSScriptImporter::ParsedTree& Tree, int TextNodeIdx, const FString& ConditionalPath) -{ - const auto& TextNode = Tree.Nodes[TextNodeIdx]; - - int RootChoiceIdx = -1; - if (TextNode.Edges.Num() == 1 && Tree.Nodes.IsValidIndex(TextNode.Edges[0].TargetNodeIdx)) - { - int NextNodeIdx = TextNode.Edges[0].TargetNodeIdx; - while (Tree.Nodes.IsValidIndex(NextNodeIdx)) - { - auto& Node = Tree.Nodes[NextNodeIdx]; - switch (Node.NodeType) - { - case ESUDSParsedNodeType::Select: - case ESUDSParsedNodeType::Text: - // Didn't find - break; - case ESUDSParsedNodeType::Choice: - RootChoiceIdx = NextNodeIdx; - NextNodeIdx = -1; // to break out of loop - break; - case ESUDSParsedNodeType::SetVariable: - case ESUDSParsedNodeType::Goto: - case ESUDSParsedNodeType::Event: - // Cascade through events to find the first choice - if (Node.Edges.Num() == 1) - { - NextNodeIdx = Node.Edges[0].TargetNodeIdx; - } - else - { - NextNodeIdx = -1; - } - break; - default: ; - }; - } - - if (RootChoiceIdx == -1) - { - return -1; - } - - // Now we've found the first choice after the text node - // BUT if there are conditional choices, there are select nodes underneath this, with - // more choice nodes. We want to find the deepest matching one and use the choice under that. - return RecurseChoiceNodeFindDeepest(Tree, RootChoiceIdx, ConditionalPath); - - } - return -1; - -} - -int FSUDSScriptImporter::RecurseChoiceNodeFindDeepest(const ParsedTree& Tree, - int FromChoiceIdx, - const FString& ConditionalPath) -{ - // We need to recurse from the Choice and any sequence of S->C->S->C is valid - // so long as the condition path matches. The deepest one is the best - // So here we're a Choice, and we can recurse into any Selects, and any Choices under that - // where the edge condition is a subset of ConditionalPath - - int Ret = FromChoiceIdx; - - if (Tree.Nodes.IsValidIndex(FromChoiceIdx)) - { - const auto& Choice = Tree.Nodes[FromChoiceIdx]; - for (int i = 0; i < Choice.Edges.Num(); ++i) - { - const auto& Edge = Choice.Edges[i]; - if (Edge.Text.IsEmpty() && Tree.Nodes.IsValidIndex(Edge.TargetNodeIdx) && Tree.Nodes[Edge.TargetNodeIdx].NodeType == ESUDSParsedNodeType::Select) - { - int ChildIdx = RecurseSelectNodeFindDeepestChoice(Tree, Edge.TargetNodeIdx, ConditionalPath); - // There should be only one that matches conditional - if (ChildIdx != -1) - { - Ret = ChildIdx; - break; - } - } - } - } - return Ret; -} - -int FSUDSScriptImporter::RecurseSelectNodeFindDeepestChoice(const ParsedTree& Tree, - int FromSelectNode, - const FString& ConditionalPath) -{ - // Default to not having found a choice that matches conditional - int Ret = -1; - - if (Tree.Nodes.IsValidIndex(FromSelectNode)) - { - const auto& Select = Tree.Nodes[FromSelectNode]; - for (int i = 0; i < Select.Edges.Num(); ++i) - { - const auto& Edge = Select.Edges[i]; - if (Tree.Nodes.IsValidIndex(Edge.TargetNodeIdx) && Tree.Nodes[Edge.TargetNodeIdx].NodeType == ESUDSParsedNodeType::Choice) - { - // It might be this choice node, IF edge matches the conditional path - FString ChoiceConditionalPath = GetTreeConditionalPath(Tree, Edge.TargetNodeIdx); - - if (ConditionalPath.StartsWith(ChoiceConditionalPath)) - { - // But also we recurse from here because there may be other nested Selects which match closer - int ChildIdx = RecurseChoiceNodeFindDeepest(Tree, Edge.TargetNodeIdx, ConditionalPath); - // There should be only one that matches conditional - if (ChildIdx != -1) - { - Ret = ChildIdx; - break; - } - } - } - } - } - return Ret; - -} - - -int FSUDSScriptImporter::FindLastChoiceNode(const ParsedTree& Tree, int IndentLevel, int FromIndex, const FString& ConditionPath) -{ - // Scan backwards from end of tree looking for a choice node which has the same or higher indent and condition state as current - // But abort if we hit text nodes on that path - - for (int i = FromIndex; i >= 0; --i) - { - auto& Node = Tree.Nodes[i]; - - if (ConditionPath.StartsWith(Node.ConditionalPath) && - Node.NodeType == ESUDSParsedNodeType::Text && - Node.OriginalIndent <= IndentLevel) - { - // We hit a parent text node, we can't go back any further - return -1; - } - // Only consider nodes on the same conditional path - // Note: NOT containing the conditional path. We don't want to skip over an intervening select node - if (ConditionPath == Node.ConditionalPath) - { - // note that we allow indents < as well as == - // This is so that if you choose to aesthetically indent choices it still works - if (Node.NodeType == ESUDSParsedNodeType::Choice && Node.OriginalIndent <= IndentLevel) - { - return i; - } - } - } - return -1; -} - -bool FSUDSScriptImporter::ParseChoiceLine(const FStringView& Line, - FSUDSScriptImporter::ParsedTree& Tree, - int IndentLevel, - int LineNo, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - if (Line.StartsWith('*')) - { - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: CHOICE: %s"), LineNo, IndentLevel, *FString(Line)); - - auto& Ctx = Tree.IndentLevelStack.Top(); - - - // Find a previous choice node to join with - int ChoiceNodeIdx = - 1; - // however, if there are pending goto labels, they MUST split the choice - if (Tree.PendingGotoLabels.Num() == 0) - { - ChoiceNodeIdx = FindLastChoiceNode(Tree, IndentLevel); - } - - // If the current indent context node is NOT a choice, create a choice node, and connect to previous node (using pending edge if needed) - if (ChoiceNodeIdx == -1) - { - // Didn't find a choice node we can connect to, so create one - - // However, before we do that, if our parent is a select node, we must ensure that its parent is a choice node - // This is just so that we have a choice node outside the select that any choices that come after the [endif] - // can attach to. If a non-conditional choice was encountered first, it'll already be there. But if the first - // choice was conditional, the select node will have been created first. - // We still create a choice node here too, underneath the select, to act as a holder for potentially multiple - // choices within this select condition - EnsureChoiceNodeExistsAboveSelect(Tree, IndentLevel, LineNo); - ChoiceNodeIdx = AppendNode(Tree, FSUDSParsedNode(ESUDSParsedNodeType::Choice, IndentLevel, LineNo)); - } - if (Tree.EdgeInProgressNodeIdx != -1) - { - // Must already have been a choice node but previous pending edge wasn't resolved - // This means it's a fallthrough, will be resolved at end - Tree.EdgeInProgressNodeIdx = Tree.EdgeInProgressEdgeIdx = -1; - } - - auto& ChoiceNode = Tree.Nodes[ChoiceNodeIdx]; - - // Inside each choice, everything should be indented at least as much as 1 character inside the * - // We provide the edge with context C001, C002 etc for fallthrough - PushIndent(Tree, ChoiceNodeIdx, IndentLevel + 1, FString::Printf(TEXT("C%03d"), ++ChoiceUniqueId)); - - // Do we want to generate a speaker line for this choice - bool bGenerateSpeakerLine = false; - FString GeneratedSpeakerID; - if (const auto Settings = GetDefault()) - { - bGenerateSpeakerLine = Settings->AlwaysGenerateSpeakerLinesFromChoices; - GeneratedSpeakerID = Settings->SpeakerIdForGeneratedLinesFromChoices; - } - if (bOverrideGenerateSpeakerLineForChoice.IsSet()) - { - bGenerateSpeakerLine = bOverrideGenerateSpeakerLineForChoice.GetValue(); - } - if (OverrideChoiceSpeakerID.IsSet()) - { - GeneratedSpeakerID = OverrideChoiceSpeakerID.GetValue(); - } - int ChoiceTextStart = 1; - if (Line.Len() > 1 && Line[1] == '-') - { - // *- prefix, override speaker line - bGenerateSpeakerLine = false; - ChoiceTextStart = 2; - } - - // Add a pending edge, with the choice text - // Following things fill in the edge details, the next node to be parsed will finalise the destination - FString ChoiceTextID; - auto ChoiceTextView = Line.SubStr(ChoiceTextStart, Line.Len() - ChoiceTextStart).TrimStart(); - // TextID might be blank after this, but that's OK, we fix later after we know what IDs are in use - RetrieveAndRemoveTextID(ChoiceTextView, ChoiceTextID); - const FString ChoiceText = FString(ChoiceTextView); - auto ChoiceTextMeta = GetTextMetadataForNextEntry(IndentLevel); - auto ChoiceUserMeta = ConsumeUserMetadata(); - const int EdgeIdx = ChoiceNode.Edges.Add(FSUDSParsedEdge(ChoiceNodeIdx, -1, LineNo, ChoiceText, ChoiceTextID, ChoiceTextMeta, ChoiceUserMeta)); - Tree.EdgeInProgressNodeIdx = ChoiceNodeIdx; - Tree.EdgeInProgressEdgeIdx = EdgeIdx; - - if (bGenerateSpeakerLine) - { - // We use the same text & ID so this is just one localisation entry - Ctx.LastTextNodeIdx = AppendNode(Tree, FSUDSParsedNode(GeneratedSpeakerID, ChoiceText, ChoiceTextID, ChoiceTextMeta, ChoiceUserMeta, IndentLevel + 1, LineNo)); - ReferencedSpeakers.AddUnique(GeneratedSpeakerID); - } - return true; - } - return false; -} - -FSUDSParsedEdge* FSUDSScriptImporter::GetEdgeInProgress(ParsedTree& Tree) -{ - if (Tree.Nodes.IsValidIndex(Tree.EdgeInProgressNodeIdx)) - { - auto& N = Tree.Nodes[Tree.EdgeInProgressNodeIdx]; - if (N.Edges.IsValidIndex(Tree.EdgeInProgressEdgeIdx)) - { - return &N.Edges[Tree.EdgeInProgressEdgeIdx]; - } - } - return nullptr; -} -void FSUDSScriptImporter::EnsureChoiceNodeExistsAboveSelect(ParsedTree& Tree, int IndentLevel, int LineNo) -{ - auto& Ctx = Tree.IndentLevelStack.Top(); - int ParentIdx = -1; - // Inserting is going to mess up EdgeInProgress - if (auto E = GetEdgeInProgress(Tree)) - { - ParentIdx = E->SourceNodeIdx; - } - else - { - ParentIdx = Ctx.LastNodeIdx; - } - - // Nothing to do if immediate parent isn't select - if (!Tree.Nodes.IsValidIndex(ParentIdx) || Tree.Nodes[ParentIdx].NodeType != ESUDSParsedNodeType::Select) - return; - - int TopSelectIdx = ParentIdx; - // Find the node above the top of potentially nested select chain - while (Tree.Nodes.IsValidIndex(ParentIdx) && Tree.Nodes[ParentIdx].NodeType == ESUDSParsedNodeType::Select) - { - TopSelectIdx = ParentIdx; - ParentIdx = Tree.Nodes[ParentIdx].ParentNodeIdx; - } - - if (Tree.Nodes.IsValidIndex(ParentIdx) && Tree.Nodes[ParentIdx].NodeType == ESUDSParsedNodeType::Choice) - { - // OK we found a choice above the select, this is OK - return; - } - - if (ParentIdx == -1) - { - // This means we hit the top of the chain without finding choice or select - // We have to trust that FindLastChoiceNode will find it & it's valid (previous placement should resolve) - - // Connect top select to choice under last text - if (Tree.Nodes.IsValidIndex(TopSelectIdx) && Tree.Nodes.IsValidIndex(Ctx.LastTextNodeIdx)) - { - // Choice node might not be directly underneath - const int ChoiceIdx = FindChoiceAfterTextNode(Tree, Ctx.LastTextNodeIdx, Tree.Nodes[TopSelectIdx].ConditionalPath); - if (ChoiceIdx != -1) - { - auto& SelNode = Tree.Nodes[TopSelectIdx]; - auto& ChoiceNode = Tree.Nodes[ChoiceIdx]; - FSUDSParsedEdge NewEdge(LineNo); - NewEdge.SourceNodeIdx = ChoiceIdx; - NewEdge.TargetNodeIdx = TopSelectIdx; - ChoiceNode.Edges.Add(NewEdge); - SelNode.ParentNodeIdx = ChoiceIdx; - } - } - return; - } - - // If we got here, we navigated to the point above the select(s) and didn't end up on a root choice - // So we need to insert one, after this index - const int InsertIdx = ParentIdx + 1; // this happens to return 0 for having hit the start of the tree (-1), which is fine - - Tree.Nodes.Insert(FSUDSParsedNode(ESUDSParsedNodeType::Choice, IndentLevel, LineNo), InsertIdx); - auto& NewChoice = Tree.Nodes[InsertIdx]; - auto& SelectNode = Tree.Nodes[InsertIdx + 1]; - // Add edge to the select, fixup the parent nodes for both - NewChoice.Edges.Add(FSUDSParsedEdge(InsertIdx, InsertIdx + 1, LineNo)); - NewChoice.ParentNodeIdx = SelectNode.ParentNodeIdx; - NewChoice.ChoicePath = SelectNode.ChoicePath; - NewChoice.ConditionalPath = SelectNode.ConditionalPath; - - // Now for every other node after this, we have to fix up indexes that are >= InsertIdx - // We don't fix up anything before, because we want things that pointed forward to the select to now point at the choice - for (int i = InsertIdx + 1; i < Tree.Nodes.Num(); ++i) - { - if (i != InsertIdx) - { - auto& N = Tree.Nodes[i]; - if (N.ParentNodeIdx >= InsertIdx) - ++N.ParentNodeIdx; - for (auto& E : N.Edges) - { - if (E.SourceNodeIdx >= InsertIdx) - ++E.SourceNodeIdx; - if (E.TargetNodeIdx >= InsertIdx) - ++E.TargetNodeIdx; - } - } - } - - // Also fix indent contexts - for (auto& I : Tree.IndentLevelStack) - { - if (I.LastNodeIdx >= InsertIdx) - ++I.LastNodeIdx; - if (I.LastTextNodeIdx >= InsertIdx) - ++I.LastTextNodeIdx; - } - // Also conditional blocks - for (auto& CB : Tree.ConditionalBlocks) - { - if (CB.SelectNodeIdx >= InsertIdx) - ++CB.SelectNodeIdx; - } - // Fixup edge in progress - if (Tree.EdgeInProgressNodeIdx >= InsertIdx) - ++Tree.EdgeInProgressNodeIdx; - - - // Manually change the select node parent index afterwards (if we change it before it'll get adjusted again) - SelectNode.ParentNodeIdx = InsertIdx; - - SetFallthroughForNewNode(Tree, NewChoice); - - -} - -bool FSUDSScriptImporter::IsConditionalLine(const FStringView& Line) -{ - return Line.StartsWith(TEXT("[if")) || - Line.StartsWith(TEXT("[else")) || - Line.StartsWith(TEXT("[endif")); -} - -bool FSUDSScriptImporter::ParseElseLine(const FStringView& Line, - FSUDSScriptImporter::ParsedTree& Tree, - int IndentLevel, - int LineNo, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - if (Tree.ConditionalBlocks.IsValidIndex(Tree.CurrentConditionalBlockIdx)) - { - // "else" changes the current "if" or "elseif" block to "else" & creates a new condition-free edge - // Select or choice node should already be there - // Select node may be turned into a choice later if choice is the first thing encountered - - auto& Block = Tree.ConditionalBlocks[Tree.CurrentConditionalBlockIdx]; - if (Block.Stage != EConditionalStage::ElseStage) - { - Block.Stage = EConditionalStage::ElseStage; - // We need to give each else a unique condition string, otherwise sibling else's can be considered - // equivalent, when they in fact originate from different if's - // ID by select node index - Block.ConditionPathElement = MakeElseConditionPathElement(Block.SelectNodeIdx); - const int NodeIdx = Block.SelectNodeIdx; - - auto& SelectNode = Tree.Nodes[NodeIdx]; - const int EdgeIdx = SelectNode.Edges.Add(FSUDSParsedEdge(NodeIdx, -1, LineNo)); - Tree.EdgeInProgressNodeIdx = NodeIdx; - Tree.EdgeInProgressEdgeIdx = EdgeIdx; - // Wind back the last node to select - auto& Ctx = Tree.IndentLevelStack.Top(); - Ctx.LastNodeIdx = NodeIdx; - - - } - else - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: cannot have more than one 'else'"), *NameForErrors, LineNo); - } - } - else - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: 'else' with no matching 'if'"), *NameForErrors, LineNo); - } - - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: ELSE : %s"), LineNo, IndentLevel, *FString(Line)); - return true; - -} - -FString FSUDSScriptImporter::MakeIfConditionPathElement(int SelectNodeIdx, - const FString& ConditionStr) -{ - return ConditionStr; -} - -FString FSUDSScriptImporter::MakeElseIfConditionPathElement(int SelectNodeIdx, - const FString& ConditionStr) -{ - return FString::Printf(TEXT("elseif-%d %s"), SelectNodeIdx, *ConditionStr); -} - -FString FSUDSScriptImporter::MakeElseConditionPathElement(int SelectNodeIdx) -{ - return FString::Printf(TEXT("else-%d"), SelectNodeIdx); -} - -bool FSUDSScriptImporter::ParseEndIfLine(const FStringView& Line, - FSUDSScriptImporter::ParsedTree& Tree, - int IndentLevel, - int LineNo, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - if (Tree.ConditionalBlocks.IsValidIndex(Tree.CurrentConditionalBlockIdx)) - { - // Endif finishes the current block - const auto& Block = Tree.ConditionalBlocks[Tree.CurrentConditionalBlockIdx]; - Tree.CurrentConditionalBlockIdx = Block.PreviousBlockIdx; - // We must also clear the indent last node pointer, because we never want to auto-connect to conditionals - // We'll let the final fallthrough pass connect things - auto& Ctx = Tree.IndentLevelStack.Top(); - Ctx.LastNodeIdx = -1; - } - else - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: 'endif' with no matching 'if'"), *NameForErrors, LineNo); - } - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: ENDIF : %s"), LineNo, IndentLevel, *FString(Line)); - return true; - -} - -bool FSUDSScriptImporter::ParseIfLine(const FStringView& Line, - FSUDSScriptImporter::ParsedTree& Tree, - const FString& ConditionStr, - int IndentLevel, - int LineNo, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: IF : %s"), LineNo, IndentLevel, *FString(Line)); - - // New if level always creates select node - const int NewNodeIdx = AppendNode(Tree, FSUDSParsedNode(ESUDSParsedNodeType::Select, IndentLevel, LineNo)); - auto& SelectNode = Tree.Nodes[NewNodeIdx]; - const int EdgeIdx = SelectNode.Edges.Add(FSUDSParsedEdge(NewNodeIdx, -1, LineNo)); - auto E = &SelectNode.Edges[EdgeIdx]; - { - FString ParseError; - if (!E->ConditionExpression.ParseFromString(ConditionStr, &ParseError)) - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: %s"), *NameForErrors, LineNo, *ParseError); - } - } - Tree.EdgeInProgressNodeIdx = NewNodeIdx; - Tree.EdgeInProgressEdgeIdx = EdgeIdx; - - Tree.CurrentConditionalBlockIdx = Tree.ConditionalBlocks.Add( - ConditionalContext(NewNodeIdx, Tree.CurrentConditionalBlockIdx, EConditionalStage::IfStage, MakeIfConditionPathElement(NewNodeIdx, ConditionStr))); - - return true; - -} - -bool FSUDSScriptImporter::ParseElseIfLine(const FStringView& Line, - ParsedTree& Tree, - const FString& ConditionStr, - int IndentLevel, - int LineNo, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: ELSEIF: %s"), LineNo, IndentLevel, *FString(Line)); - - // "elseif" changes the current block state - // Select or choice node should already be there - // Select node may be turned into a choice later if choice is the first thing encountered - - if (Tree.ConditionalBlocks.IsValidIndex(Tree.CurrentConditionalBlockIdx)) - { - auto& Block = Tree.ConditionalBlocks[Tree.CurrentConditionalBlockIdx]; - if (Block.Stage != EConditionalStage::ElseStage) - { - Block.Stage = EConditionalStage::ElseIfStage; - // For the purposes of the block, the condition isn't just the condition contained here, - // it's also the negation of the original "if". This is to prevent multiple sibling - // elseifs merging if they contain the same condition but are attached to different ifs - Block.ConditionPathElement = MakeElseIfConditionPathElement(Block.SelectNodeIdx, ConditionStr); - const int NodeIdx = Block.SelectNodeIdx; - - auto& SelectOrChoiceNode = Tree.Nodes[NodeIdx]; - const int EdgeIdx = SelectOrChoiceNode.Edges.Add(FSUDSParsedEdge(NodeIdx, -1, LineNo)); - auto E = &SelectOrChoiceNode.Edges[EdgeIdx]; - { - FString ParseError; - if (!E->ConditionExpression.ParseFromString(ConditionStr, &ParseError)) - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: %s"), *NameForErrors, LineNo, *ParseError); - } - } - Tree.EdgeInProgressNodeIdx = NodeIdx; - Tree.EdgeInProgressEdgeIdx = EdgeIdx; - } - else - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: 'elseif' occurs after 'else'"), *NameForErrors, LineNo); - - } - - } - else - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: 'elseif' with no matching 'if'"), *NameForErrors, LineNo); - } - - return true; -} - -bool FSUDSScriptImporter::ParseConditionalLine(const FStringView& Line, - FSUDSScriptImporter::ParsedTree& Tree, - int IndentLevel, - int LineNo, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - - if (!IsConditionalLine(Line)) - return false; - - if (Line.Equals(TEXT("[else]"))) - { - return ParseElseLine(Line, Tree, IndentLevel, LineNo, NameForErrors, Logger, bSilent); - - } - else if (Line.Equals(TEXT("[endif]"))) - { - return ParseEndIfLine(Line, Tree, IndentLevel, LineNo, NameForErrors, Logger, bSilent); - } - else - { - const FString LineStr(Line); - const FRegexPattern IfPattern(TEXT("^\\[if\\s+(.+)\\]$")); - FRegexMatcher IfRegex(IfPattern, LineStr); - if (IfRegex.FindNext()) - { - const FString ConditionStr = IfRegex.GetCaptureGroup(1); - return ParseIfLine(Line, Tree, ConditionStr, IndentLevel, LineNo, NameForErrors, Logger, bSilent); - } - else - { - const FRegexPattern ElseIfPattern(TEXT("^\\[elseif\\s+(.+)\\]$")); - FRegexMatcher ElseIfRegex(ElseIfPattern, LineStr); - if (ElseIfRegex.FindNext()) - { - const FString ConditionStr = ElseIfRegex.GetCaptureGroup(1); - return ParseElseIfLine(Line, Tree, ConditionStr, IndentLevel, LineNo, NameForErrors, Logger, bSilent); - - } - } - } - - return false; -} - -bool FSUDSScriptImporter::ParseRandomLine(const FStringView& Line, - ParsedTree& Tree, - int IndentLevel, - int LineNo, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - if (!IsRandomLine(Line)) - return false; - - else if (Line.Equals(TEXT("[random]"))) - { - return ParseBeginRandomLine(Line, Tree, IndentLevel, LineNo, NameForErrors, Logger, bSilent); - } - else if (Line.Equals(TEXT("[or]"))) - { - return ParseRandomOptionLine(Line, Tree, IndentLevel, LineNo, NameForErrors, Logger, bSilent); - - } - else if (Line.Equals(TEXT("[endrandom]"))) - { - return ParseEndRandomLine(Line, Tree, IndentLevel, LineNo, NameForErrors, Logger, bSilent); - } - - return false; -} - -bool FSUDSScriptImporter::IsRandomLine(const FStringView& Line) -{ - return Line.StartsWith(TEXT("[random")) || - Line.StartsWith(TEXT("[or")) || - Line.StartsWith(TEXT("[endrandom")); -} - -bool FSUDSScriptImporter::ParseBeginRandomLine(const FStringView& Line, - ParsedTree& Tree, - int IndentLevel, - int LineNo, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: RANDOM: %s"), LineNo, IndentLevel, *FString(Line)); - - // New random level always creates node - const int NewNodeIdx = AppendNode(Tree, FSUDSParsedNode(ESUDSParsedNodeType::Select, IndentLevel, LineNo)); - auto& SelectNode = Tree.Nodes[NewNodeIdx]; - const int EdgeIdx = SelectNode.Edges.Add(FSUDSParsedEdge(NewNodeIdx, -1, LineNo)); - FString ConditionStr = FString::Printf(TEXT("{%hs} == 0"), SUDS_RANDOMITEM_VAR); - auto E = &SelectNode.Edges[EdgeIdx]; - { - // Set the condition automatically based on internal random number var - FString ParseError; - if (!E->ConditionExpression.ParseFromString(ConditionStr, &ParseError)) - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: %s"), *NameForErrors, LineNo, *ParseError); - } - } - - Tree.EdgeInProgressNodeIdx = NewNodeIdx; - Tree.EdgeInProgressEdgeIdx = EdgeIdx; - - Tree.CurrentConditionalBlockIdx = Tree.ConditionalBlocks.Add( - ConditionalContext(NewNodeIdx, Tree.CurrentConditionalBlockIdx, EConditionalStage::RandomStage, ConditionStr)); - - return true; - -} - -bool FSUDSScriptImporter::ParseRandomOptionLine(const FStringView& Line, - ParsedTree& Tree, - int IndentLevel, - int LineNo, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - - // This is kind of like an auto-generated elseif - - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: OR: %s"), LineNo, IndentLevel, *FString(Line)); - - // "elseif" changes the current block state - // Select or choice node should already be there - // Select node may be turned into a choice later if choice is the first thing encountered - - if (Tree.ConditionalBlocks.IsValidIndex(Tree.CurrentConditionalBlockIdx)) - { - auto& Block = Tree.ConditionalBlocks[Tree.CurrentConditionalBlockIdx]; - if (Block.Stage == EConditionalStage::RandomStage || - Block.Stage == EConditionalStage::RandomOptionStage) - { - Block.Stage = EConditionalStage::RandomOptionStage; - const int NodeIdx = Block.SelectNodeIdx; - - auto& SelectOrChoiceNode = Tree.Nodes[NodeIdx]; - // Generate condition based on auto-generated random item choice - Block.ConditionPathElement = FString::Printf(TEXT("{%hs} == %d"), SUDS_RANDOMITEM_VAR, SelectOrChoiceNode.Edges.Num()); - const int EdgeIdx = SelectOrChoiceNode.Edges.Add(FSUDSParsedEdge(NodeIdx, -1, LineNo)); - auto E = &SelectOrChoiceNode.Edges[EdgeIdx]; - { - FString ParseError; - if (!E->ConditionExpression.ParseFromString(Block.ConditionPathElement, &ParseError)) - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: %s"), *NameForErrors, LineNo, *ParseError); - } - } - Tree.EdgeInProgressNodeIdx = NodeIdx; - Tree.EdgeInProgressEdgeIdx = EdgeIdx; - } - else - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: 'or' must occur after 'random'"), *NameForErrors, LineNo); - - } - - } - else - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: 'or' with no matching 'random'"), *NameForErrors, LineNo); - } - - return true; -} - -bool FSUDSScriptImporter::ParseEndRandomLine(const FStringView& Line, - ParsedTree& Tree, - int IndentLevel, - int LineNo, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - // Similar to "endif"; except that we need to turn the last option into an "else", otherwise at end of - // parsing we'll add an extra else edge - if (Tree.ConditionalBlocks.IsValidIndex(Tree.CurrentConditionalBlockIdx)) - { - // Endrandom finishes the current block - const auto& Block = Tree.ConditionalBlocks[Tree.CurrentConditionalBlockIdx]; - if (Block.Stage == EConditionalStage::RandomStage || - Block.Stage == EConditionalStage::RandomOptionStage) - { - const int NodeIdx = Block.SelectNodeIdx; - auto& SelectNode = Tree.Nodes[NodeIdx]; - if (SelectNode.Edges.Num() > 0) - { - auto& LastEdge = SelectNode.Edges[SelectNode.Edges.Num()-1]; - LastEdge.ConditionExpression.Reset(); - } - - Tree.CurrentConditionalBlockIdx = Block.PreviousBlockIdx; - // We must also clear the indent last node pointer, because we never want to auto-connect to conditionals - // We'll let the final fallthrough pass connect things - auto& Ctx = Tree.IndentLevelStack.Top(); - Ctx.LastNodeIdx = -1; - } - else - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: 'endrandom' with no matching 'random'"), *NameForErrors, LineNo); - } - } - else - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: 'endrandom' with no matching 'random'"), *NameForErrors, LineNo); - } - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: ENDRANDOM : %s"), LineNo, IndentLevel, *FString(Line)); - return true; - -} - - -bool FSUDSScriptImporter::ParseGotoLabelLine(const FStringView& Line, - FSUDSScriptImporter::ParsedTree& Tree, - int IndentLevel, - int LineNo, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - // We've already established that line starts with ':' - // There should not be any spaces in the label - const FString LineStr(Line); - const FRegexPattern LabelPattern(TEXT("^\\:\\s*(\\w+)$")); - FRegexMatcher LabelRegex(LabelPattern, LineStr); - if (LabelRegex.FindNext()) - { - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: LABEL : %s"), LineNo, IndentLevel, *FString(Line)); - // lowercase goto labels so case insensitive - FString Label = LabelRegex.GetCaptureGroup(1).ToLower(); - if (Label == EndGotoLabel) - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: Label 'end' is reserved and cannot be used in the script, ignoring"), *NameForErrors, LineNo); - } - else - { - Tree.PendingGotoLabels.Add(Label); - // This will be connected to the next node created - // Is a list since multiple labels may resolve to the same place - } - } - else - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Warning, TEXT("Error in %s line %d: Badly formed goto label"), *NameForErrors, LineNo); - } - // Always return true to carry on, may not be used - return true; -} - -bool FSUDSScriptImporter::ParseGotoLine(const FStringView& Line, - FSUDSScriptImporter::ParsedTree& Tree, - int IndentLevel, - int LineNo, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - // Unfortunately FRegexMatcher doesn't support FStringView - const FString LineStr(Line); - // Allow both 'goto' and 'go to' - const FRegexPattern GotoPattern(TEXT("^\\[go[ ]?to\\s+(\\w+)\\s*\\]$")); - FRegexMatcher GotoRegex(GotoPattern, LineStr); - if (GotoRegex.FindNext()) - { - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: GOTO : %s"), LineNo, IndentLevel, *FString(Line)); - // lower case label so case insensitive - const FString Label = GotoRegex.GetCaptureGroup(1).ToLower(); - // note that we do NOT try to resolve the goto label here, to allow forward jumps. - const auto& Ctx = Tree.IndentLevelStack.Top(); - // A goto is an edge from the current node to another node - // That means if we had pending labels that didn't hit a node before now, they're just aliases to THIS label - for (auto PendingLabel : Tree.PendingGotoLabels) - { - Tree.AliasedGotoLabels.Add(PendingLabel, Label); - } - Tree.PendingGotoLabels.Reset(); - AppendNode(Tree, FSUDSParsedNode(Label, IndentLevel, LineNo)); - return true; - } - return false; -} - -bool FSUDSScriptImporter::ParseGosubLine(const FStringView& InLine, - ParsedTree& Tree, - int IndentLevel, - int LineNo, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - // Attempt to find existing ID - // We need these in order to save the return stack - FString GosubID; - FStringView Line = InLine; - // If this is a continuation line, we shouldn't generate one, but we need to trim it off if it's there - bool bFoundID = RetrieveAndRemoveGosubID(Line, GosubID); - - // Unfortunately FRegexMatcher doesn't support FStringView - const FString LineStr(Line); - // Allow both 'gosub' and 'go sub' - const FRegexPattern GosubPattern(TEXT("^\\[go[ ]?sub\\s+(\\w+)\\s*\\]$")); - FRegexMatcher GosubRegex(GosubPattern, LineStr); - if (GosubRegex.FindNext()) - { - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: GOSUB : %s"), LineNo, IndentLevel, *FString(Line)); - // lower case label so case insensitive - const FString Label = GosubRegex.GetCaptureGroup(1).ToLower(); - - // You CANNOT "gosub end" - if (Label == EndGotoLabel) - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: You cannot 'gosub end', will never return. Did you mean goto?"), *NameForErrors, LineNo); - - } - else - { - // note that we do NOT try to resolve the label here, to allow forward jumps. - const auto& Ctx = Tree.IndentLevelStack.Top(); - // A gosub will become a node of its own in the final runtime - // Therefore we don't need to alias labels like we do with gotos - AppendNode(Tree, FSUDSParsedNode(Label, GosubID, IndentLevel, LineNo)); - } - return true; - } - return false; - -} - -bool FSUDSScriptImporter::ParseReturnLine(const FStringView& Line, - ParsedTree& Tree, - int IndentLevel, - int LineNo, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - // Unfortunately FRegexMatcher doesn't support FStringView - const FString LineStr(Line); - const FRegexPattern ReturnPattern(TEXT("^\\[return\\s*\\]$")); - FRegexMatcher ReturnRegex(ReturnPattern, LineStr); - if (ReturnRegex.FindNext()) - { - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: RETURN : %s"), LineNo, IndentLevel, *FString(Line)); - AppendNode(Tree, FSUDSParsedNode(ESUDSParsedNodeType::Return, IndentLevel, LineNo)); - return true; - } - return false; -} - -bool FSUDSScriptImporter::ParseSetLine(const FStringView& InLine, - FSUDSScriptImporter::ParsedTree& Tree, - int IndentLevel, - int LineNo, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - // Attempt to find existing text ID, for string literals - // For multiple lines, may not be present until last line (but in fact can be on any line) - // We generate anyway, because it can be overriden by later lines, but makes sure we have one always - FString TextID; - FStringView Line = InLine; - // TextID may be blank after this, that's OK - we fix at the end once we know what IDs are used - RetrieveAndRemoveTextID(Line, TextID); - - // Unfortunately FRegexMatcher doesn't support FStringView - const FString LineStr(Line); - // Accept forms: - // [set Var Expression] - // [set Var = Expression] (more readable in the case of non-trivial expressions) - const FRegexPattern SetPattern(TEXT("^\\[set\\s+(\\S+)\\s+(?:=\\s+)?([^\\]]+)\\]$")); - FRegexMatcher SetRegex(SetPattern, LineStr); - if (SetRegex.FindNext()) - { - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: SET : %s"), LineNo, IndentLevel, *FString(Line)); - - FString Name = SetRegex.GetCaptureGroup(1); - FString ExprStr = SetRegex.GetCaptureGroup(2).TrimStartAndEnd(); // trim because capture accepts spaces in quotes - - FSUDSExpression Expr; - { - FString ParseError; - if (Expr.ParseFromString(ExprStr, &ParseError)) - { - if (Expr.IsTextLiteral()) - { - AppendNode(Tree, FSUDSParsedNode(Name, Expr, TextID, IndentLevel, LineNo)); - } - else - { - AppendNode(Tree, FSUDSParsedNode(Name, Expr, IndentLevel, LineNo)); - } - return true; - } - else - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: %s"), *NameForErrors, LineNo, *ParseError); - } - } - } - return false; -} - -bool FSUDSScriptImporter::ParseImportSettingLine(const FStringView& Line, - ParsedTree& Tree, - int IndentLevel, - int LineNo, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - // Unfortunately FRegexMatcher doesn't support FStringView - const FString LineStr(Line); - const FRegexPattern ImportSetPattern(TEXT("^\\[importsetting\\s+(\\S+)\\s+(?:=\\s+)?([^\\]]+)\\]$")); - FRegexMatcher ImportSetRegex(ImportSetPattern, LineStr); - if (ImportSetRegex.FindNext()) - { - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: IMPORTSETTING: %s"), LineNo, IndentLevel, *FString(Line)); - - const FString Name = ImportSetRegex.GetCaptureGroup(1); - const FString ExprStr = ImportSetRegex.GetCaptureGroup(2).TrimStartAndEnd(); // trim because capture accepts spaces in quotes - - FSUDSExpression Expr; - { - FString ParseError; - if (Expr.ParseFromString(ExprStr, &ParseError)) - { - if (Expr.IsLiteral()) - { - // Import settings affect importer state directly, no nodes - if (Name.Compare("GenerateSpeakerLinesFromChoices", ESearchCase::IgnoreCase) == 0) - { - if (Expr.GetLiteralValue().GetType() == ESUDSValueType::Boolean) - { - bOverrideGenerateSpeakerLineForChoice = Expr.GetBooleanLiteralValue(); - } - else - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: [importsetting GenerateSpeakerLinesFromChoices ...] requires a boolean literal"), *NameForErrors, LineNo); - } - } - else if (Name.Compare("SpeakerIDForGeneratedLinesFromChoices", ESearchCase::IgnoreCase) == 0) - { - if (Expr.GetLiteralValue().GetType() == ESUDSValueType::Name) - { - // Speaker IDs are strings, but we go via FName to avoid translation - OverrideChoiceSpeakerID = Expr.GetNameLiteralValue().ToString(); - } - else - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: [importsetting SpeakerIDForGeneratedLinesFromChoices ...] requires a Name literal e.g. (`Value`)"), *NameForErrors, LineNo); - } - } - - return true; - } - else - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: importsetting only accepts literal values"), *NameForErrors, LineNo); - } - } - else - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Error, TEXT("Error in %s line %d: %s"), *NameForErrors, LineNo, *ParseError); - } - } - } - return false; - -} - -bool FSUDSScriptImporter::ParseEventLine(const FStringView& Line, - FSUDSScriptImporter::ParsedTree& Tree, - int IndentLevel, - int LineNo, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - const FString LineStr(Line); - const FRegexPattern EventPattern(TEXT("^\\[event\\s+([\\w\\.]+)([^\\]]*)\\]$")); - FRegexMatcher EventRegex(EventPattern, LineStr); - if (EventRegex.FindNext()) - { - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: EVENT : %s"), LineNo, IndentLevel, *FString(Line)); - - FSUDSParsedNode Node(ESUDSParsedNodeType::Event, IndentLevel, LineNo); - - Node.Identifier = EventRegex.GetCaptureGroup(1); - - if (EventRegex.GetCaptureGroupBeginning(2) != INDEX_NONE) - { - // Has arguments, all lumped together - // Capture using a sub-regex which can detect quoted strings, split by commas - FString AllArgs = EventRegex.GetCaptureGroup(2).TrimStartAndEnd(); - const FRegexPattern ArgPattern(TEXT("((\\\"[^\\\"]*\\\"|[^,\\\"]+))")); - FRegexMatcher ArgRegex(ArgPattern, AllArgs); - while (ArgRegex.FindNext()) - { - // then process the quote - FString ArgStr = ArgRegex.GetCaptureGroup(1).TrimStartAndEnd(); - if (ArgStr.Len() == 0) - continue; - - FSUDSExpression Expr; - FString ParseError; - if (Expr.ParseFromString(ArgStr, &ParseError)) - { - // note: no localisation of event literals, they're just strings - // we assume the receiver of the event will set localised text to variables if they want - Node.EventArgs.Add(Expr); - } - else - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Warning, TEXT("Error in %s line %d: Literal argument %d ('%s') invalid: %s"), *NameForErrors, LineNo, Node.EventArgs.Num() + 1, *ArgStr, *ParseError); - } - } - } - - AppendNode(Tree, Node); - - return true; - } - return false; -} - -bool FSUDSScriptImporter::ParseTextLine(const FStringView& InLine, - FSUDSScriptImporter::ParsedTree& Tree, - int IndentLevel, - int LineNo, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - auto& Ctx = Tree.IndentLevelStack.Top(); - - // Attempt to find existing text ID - // For multiple lines, may not be present until last line (but in fact can be on any line) - FString TextID; - FStringView Line = InLine; - // Retrieve, but don't generate text ID at this point - // If this is a continuation line, we shouldn't generate one, but we need to trim it off if it's there - // TextID may be blank after this, that's OK - we fix at the end once we know what IDs are used - RetrieveAndRemoveTextID(Line, TextID); - - const FString LineStr(Line); - const FRegexPattern SpeakerPattern(TEXT("^(\\S+)\\:\\s*(.+)$")); - FRegexMatcher SpeakerRegex(SpeakerPattern, LineStr); - if (SpeakerRegex.FindNext()) - { - // OK this is a speaker line, in which case this is a new text node - const FString Speaker = SpeakerRegex.GetCaptureGroup(1); - const FString Text = SpeakerRegex.GetCaptureGroup(2); - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: TEXT : %s"), LineNo, IndentLevel, *FString(Line)); - // New text node - // Text nodes can never introduce another indent context - // We've already backed out to the outer indent in caller - Ctx.LastTextNodeIdx = AppendNode(Tree, FSUDSParsedNode(Speaker, Text, TextID, GetTextMetadataForNextEntry(IndentLevel), ConsumeUserMetadata(), IndentLevel, LineNo)); - - ReferencedSpeakers.AddUnique(Speaker); - - return true; - } - - // If we fell through, this line is appended to the last text node - if (!bSilent) - UE_LOG(LogSUDSImporter, VeryVerbose, TEXT("%3d:%2d: TEXT+ : %s"), LineNo, IndentLevel, *FString(Line)); - if (Tree.Nodes.IsValidIndex(Ctx.LastNodeIdx)) - { - auto& Node = Tree.Nodes[Ctx.LastNodeIdx]; - if (Node.NodeType == ESUDSParsedNodeType::Text) - { - Node.Text.Appendf(TEXT("\n%s"), *LineStr); - } - else - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Warning, TEXT("Error in %s line %d: Text newline continuation is not immediately after a speaker line. Ignoring."), *NameForErrors, LineNo); - // We still return true to allow continue - } - } - - return true; - - -} - -bool FSUDSScriptImporter::RetrieveAndRemoveTextID(FStringView& InOutLine, FString& OutTextID) -{ - - FString LineWithout; - int Number; - if (RetrieveTextIDFromLine(InOutLine, OutTextID, Number)) - { - TextIDHighestNumber = FMath::Max(TextIDHighestNumber, Number); - return true; - } - - return false; - -} - -bool FSUDSScriptImporter::RetrieveTextIDFromLine(FStringView& InOutLine, FString& OutTextID, int& OutNumber) -{ - const FString LineStr(InOutLine); - const FRegexPattern TextIDPattern(TEXT("(\\@([0-9a-fA-F]+)\\@)")); - FRegexMatcher TextIDRegex(TextIDPattern, LineStr); - if (TextIDRegex.FindNext()) - { - OutTextID = TextIDRegex.GetCaptureGroup(1); - // Chop the incoming string to the left of the TextID - InOutLine = InOutLine.Left(TextIDRegex.GetCaptureGroupBeginning(1)); - // Also trim right - InOutLine = InOutLine.TrimEnd(); - // FDefaultValueHelper::ParseInt requires an "0x" prefix but we're not using that - // Plus does extra checking we don't need - OutNumber = FCString::Strtoi(*TextIDRegex.GetCaptureGroup(2), nullptr, 16); - return true; - } - - return false; - -} - -bool FSUDSScriptImporter::RetrieveAndRemoveGosubID(FStringView& InOutLine, FString& OutTextID) -{ - - FString LineWithout; - int Number; - if (RetrieveGosubIDFromLine(InOutLine, OutTextID, Number)) - { - GosubIDHighestNumber = Number; - return true; - } - - return false; - -} - -bool FSUDSScriptImporter::RetrieveGosubIDFromLine(FStringView& InOutLine, FString& OutID, int& OutNumber) -{ - const FString LineStr(InOutLine); - const FRegexPattern IDPattern(TEXT("(\\@GS([0-9a-fA-F]+)\\@)")); - FRegexMatcher IDRegex(IDPattern, LineStr); - if (IDRegex.FindNext()) - { - OutID = IDRegex.GetCaptureGroup(1); - // Chop the incoming string to the left of the TextID - InOutLine = InOutLine.Left(IDRegex.GetCaptureGroupBeginning(1)); - // Also trim right - InOutLine = InOutLine.TrimEnd(); - // FDefaultValueHelper::ParseInt requires an "0x" prefix but we're not using that - // Plus does extra checking we don't need - OutNumber = FCString::Strtoi(*IDRegex.GetCaptureGroup(2), nullptr, 16); - return true; - } - - return false; - -} - -FString FSUDSScriptImporter::GenerateTextID() -{ - // Generate a new text ID just based on ascending numbers - // We don't actually base this on the line but we have it for future possible use - // Since it's a string, format exactly as in the sud file - return FString::Printf(TEXT("@%04x@"), ++TextIDHighestNumber); -} - -FString FSUDSScriptImporter::GetCurrentTreePath(const FSUDSScriptImporter::ParsedTree& Tree) -{ - // This is just a path of all the choice / select nodes AND their edges leading to this point, for fallthrough - // * Choice (/C000/) - // * Nested choice (/C000/C001/) - // Fallthrough from here - // * Choice (/C002/C003/) - // Do NOT fallthrough to here - // Fallthrough to here instead (/) - -#if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8 - TStringBuilder<256> B; -#else - FStringBuilderBase B; -#endif - for (auto Indent : Tree.IndentLevelStack) - { - B.Appendf(TEXT("%s%s"), *Indent.PathEntry, *TreePathSeparator); - } - return B.ToString(); -} - -FString FSUDSScriptImporter::GetCurrentTreeConditionalPath(const FSUDSScriptImporter::ParsedTree& Tree) -{ - // Like GetCurrentTreePath, but for conditional blocks - // Cannot fall through to blocks that aren't on the same conditional path -#if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8 - TStringBuilder<256> B; -#else - FStringBuilderBase B; -#endif - int BlockIdx = Tree.CurrentConditionalBlockIdx; - B.Append(TreePathSeparator); - // work backwards, hence prepend - while (BlockIdx != -1) - { - const FString& ConditionStr = Tree.ConditionalBlocks[BlockIdx].ConditionPathElement; - // Note: add the "/" even if ConditionStr is empty, because it means it's an else level - // Not including it can cause an if block to fall through to its own else - B.Prepend(FString::Printf(TEXT("%s%s"), *TreePathSeparator, *ConditionStr)); - BlockIdx = Tree.ConditionalBlocks[BlockIdx].PreviousBlockIdx; - } - return B.ToString(); - -} - -FString FSUDSScriptImporter::GetTreeConditionalPath(const FSUDSScriptImporter::ParsedTree& Tree, int NodeIndex) -{ - // Like GetCurrentTreeConditionalPath, but we're not in the block context. Follow the nodes back up -#if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8 - TStringBuilder<256> B; -#else - FStringBuilderBase B; -#endif - B.Append(TreePathSeparator); - - while (Tree.Nodes.IsValidIndex(NodeIndex)) - { - const auto Node = Tree.Nodes[NodeIndex]; - if (Tree.Nodes.IsValidIndex(Node.ParentNodeIdx)) - { - const auto& Parent = Tree.Nodes[Node.ParentNodeIdx]; - if (Parent.NodeType == ESUDSParsedNodeType::Select) - { - const int SelectNodeIdx = Node.ParentNodeIdx; - const int EdgeIdx = FindEdge(Tree, SelectNodeIdx, NodeIndex); - if (Parent.Edges.IsValidIndex(EdgeIdx)) - { - const auto& Edge = Parent.Edges[EdgeIdx]; - const bool bHasCondition = !Edge.ConditionExpression.IsEmpty(); - FString ConditionalPathElem; - if (EdgeIdx == 0) - { - // First edge is always if - ConditionalPathElem = MakeIfConditionPathElement(SelectNodeIdx, Edge.ConditionExpression.GetSourceString()); - } - else - { - if (bHasCondition) - { - // non-zero index edge with a condition is elseif - ConditionalPathElem = MakeElseIfConditionPathElement(SelectNodeIdx, Edge.ConditionExpression.GetSourceString()); - } - else - { - ConditionalPathElem = MakeElseConditionPathElement(SelectNodeIdx); - } - } - // working backwards, hence prepend - B.Prepend(FString::Printf(TEXT("%s%s"), *TreePathSeparator, *ConditionalPathElem)); - } - } - } - NodeIndex = Node.ParentNodeIdx; - } - - return B.ToString(); - -} - -int FSUDSScriptImporter::FindEdge(const FSUDSScriptImporter::ParsedTree& Tree, int ParentNodeIdx, int TargetNodeIndex) -{ - if (Tree.Nodes.IsValidIndex(ParentNodeIdx)) - { - const auto& Parent = Tree.Nodes[ParentNodeIdx]; - for (int i = 0; i < Parent.Edges.Num(); ++i) - { - if (Parent.Edges[i].TargetNodeIdx == TargetNodeIndex) - { - return i; - } - } - } - return -1; -} - -void FSUDSScriptImporter::SetFallthroughForNewNode(FSUDSScriptImporter::ParsedTree& Tree, FSUDSParsedNode& NewNode) -{ - // Choice nodes are allowed to be falled through to now - if (NewNode.NodeType == ESUDSParsedNodeType::Choice) - { - - // But, disable fallthrough for any parent select nodes all the way up the chain - int PrevIdx = NewNode.ParentNodeIdx; - while (Tree.Nodes.IsValidIndex(PrevIdx) && Tree.Nodes[PrevIdx].NodeType == ESUDSParsedNodeType::Select) - { - Tree.Nodes[PrevIdx].AllowFallthrough = false; - PrevIdx = Tree.Nodes[PrevIdx].ParentNodeIdx; - } - } - else - { - if (Tree.Nodes.IsValidIndex(NewNode.ParentNodeIdx)) - { - auto& PrevNode = Tree.Nodes[NewNode.ParentNodeIdx]; - // Do not allow fallthrough to this node for selects that are themselves children of other selects or choices - // It means they're compound conditionals / choices and we should only fall through to the root of that - if (NewNode.NodeType == ESUDSParsedNodeType::Select && - (PrevNode.NodeType == ESUDSParsedNodeType::Choice || PrevNode.NodeType == ESUDSParsedNodeType::Select)) - { - NewNode.AllowFallthrough = false; - } - } - } - -} - -int FSUDSScriptImporter::AppendNode(FSUDSScriptImporter::ParsedTree& Tree, const FSUDSParsedNode& InNode) -{ - auto& Ctx = Tree.IndentLevelStack.Top(); - - const int NewIndex = Tree.Nodes.Add(InNode); - - // Set the tree path of the node (post-add) - auto& NewNode = Tree.Nodes[NewIndex]; - NewNode.ChoicePath = GetCurrentTreePath(Tree); - NewNode.ConditionalPath = GetCurrentTreeConditionalPath(Tree); - - // Use pending edge if present; that could be because this is under a choice node, or a condition - if (auto E = GetEdgeInProgress(Tree)) - { - E->TargetNodeIdx = NewIndex; - - const int PrevNodeIdx = E->SourceNodeIdx; - NewNode.ParentNodeIdx = PrevNodeIdx; - SetFallthroughForNewNode(Tree, NewNode); - Tree.EdgeInProgressNodeIdx = -1; - Tree.EdgeInProgressEdgeIdx = -1; - } - else - { - const int PrevNodeIdx = Ctx.LastNodeIdx; - if (Tree.Nodes.IsValidIndex(PrevNodeIdx)) - { - // Append this node onto the last one - auto& PrevNode = Tree.Nodes[PrevNodeIdx]; - // Auto-connect new nodes to previous nodes - // Valid for nodes with only one output node - // A new node with no pending edge following any other type may be connected via fallthrough at - // the end of parsing - // Don't allow connection of fallthrough nodes to choices, unless they're to another select node - if (PrevNode.NodeType != ESUDSParsedNodeType::Choice || - (NewNode.NodeType == ESUDSParsedNodeType::Select)) - { - PrevNode.Edges.Add(FSUDSParsedEdge(PrevNodeIdx, NewIndex, InNode.SourceLineNo)); - NewNode.ParentNodeIdx = PrevNodeIdx; - } - - // Don't throw an error otherwise, because prev index can be a choice due to fallthrough - // This will be connected up at the end - - } - - SetFallthroughForNewNode(Tree, NewNode); - - } - - // All goto labels in scope at this point now point to this node - for (auto Label : Tree.PendingGotoLabels) - { - Tree.GotoLabelList.Add(Label, NewIndex); - } - Tree.PendingGotoLabels.Reset(); - - Ctx.LastNodeIdx = NewIndex; - Ctx.ThresholdIndent = FMath::Min(Ctx.ThresholdIndent, InNode.OriginalIndent); - - - return NewIndex; -} - -bool FSUDSScriptImporter::SelectNodeIsMissingElsePath(const FSUDSScriptImporter::ParsedTree& Tree, const FSUDSParsedNode& Node) -{ - for (auto& E : Node.Edges) - { - if (E.ConditionExpression.IsEmpty()) - { - // This is an else - return false; - } - } - - // If the first node the select points to (other than a nested select) is a choice, we never add an else - // note: you should never have an if/else where one branch has a set/text node and the other has a choice node, this is badly formed - // perhaps we should validate that - if (Node.Edges.Num() > 0) - { - int NextIdx = Node.Edges[0].TargetNodeIdx; - // While so that we can follow nested selects to first resolved - while (Tree.Nodes.IsValidIndex(NextIdx)) - { - auto N = Tree.Nodes[NextIdx]; - if (N.NodeType == ESUDSParsedNodeType::Select) - { - // Nested select, cascade down - if (N.Edges.Num() > 0) - { - NextIdx = N.Edges[0].TargetNodeIdx; - } - else - { - NextIdx = -1; - } - } - else - { - if (N.NodeType == ESUDSParsedNodeType::Choice) - { - // We never add fallthrough else paths for selection between choices - return false; - } - NextIdx = -1; - } - } - } - - return true; -} - -void FSUDSScriptImporter::PopIndent(FSUDSScriptImporter::ParsedTree& Tree) -{ - Tree.IndentLevelStack.Pop(); - // We *could* reset the pending goto list if there are labels at higher indents than current that never resolved to a node - // but I'm choosing not to right now and letting them fall through -} - -void FSUDSScriptImporter::PushIndent(FSUDSScriptImporter::ParsedTree& Tree, int NodeIdx, int Indent, const FString& Path) -{ - Tree.IndentLevelStack.Push(IndentContext(NodeIdx, Indent, Path)); - -} - -bool FSUDSScriptImporter::IsCommentLine(const FStringView& TrimmedLine) -{ - return TrimmedLine.StartsWith('#'); -} - -FStringView FSUDSScriptImporter::TrimLine(const FStringView& Line, int& OutIndentLevel) const -{ - OutIndentLevel = 0; - int32 SkippedChars = 0; - for (const TCHAR Char : Line) - { - if (!TChar::IsWhitespace(Char)) - { - break; - } - ++SkippedChars; - if (Char == '\t') - OutIndentLevel += TabIndentValue; - else - ++OutIndentLevel; - } - FStringView LeftTrimmed = FStringView(Line.GetData() + SkippedChars, Line.Len() - SkippedChars); - // Trim end, don't need to know how much it was - return LeftTrimmed.TrimEnd(); - -} - -void FSUDSScriptImporter::ConnectRemainingNodes(FSUDSScriptImporter::ParsedTree& Tree, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent) -{ - // Now we go through all nodes, resolving gotos, and finding links that don't go anywhere & making - // them fall through to the next appropriate outdented node (or the end) - - // Firstly turn all the alias gotos into final gotos - for (auto& Alias : Tree.AliasedGotoLabels) - { - if (int* pIdx = Tree.GotoLabelList.Find(Alias.Value)) - { - Tree.GotoLabelList.Add(Alias.Key, *pIdx); - } - } - Tree.AliasedGotoLabels.Reset(); - - - // We go through top-to-bottom, which is the order of lines in the file as well - // We don't need to cascade for this - for (int i = 0; i < Tree.Nodes.Num(); ++i) - { - auto& Node = Tree.Nodes[i]; - // We check for dead-end nodes, and for select nodes with no "else" (in case all conditions fail) - const bool bIsSelectNodeMissingElse = Node.NodeType == ESUDSParsedNodeType::Select && SelectNodeIsMissingElsePath(Tree, Node); - if (Node.Edges.IsEmpty() || bIsSelectNodeMissingElse) - { - if (Node.NodeType == ESUDSParsedNodeType::Goto) - { - // Try to resolve goto now that we've parsed all labels - // We don't actually create edges here, the label is enough so long as it leads somewhere - // Check aliases first - FString Label = Node.Identifier; - // Special case 'end' which needs no further checking - if (Label != EndGotoLabel) - { - const int GotoNodeIdx = GetGotoTargetNodeIndex(Tree, Node.Identifier); - if (GotoNodeIdx == -1) - { - if (!bSilent) - Logger->Logf(ELogVerbosity::Warning, TEXT("Error in %s line %d: Goto label '%s' was not found, references to it will goto End"), *NameForErrors, Node.SourceLineNo, *Node.Identifier); - } - } - } - // Return nodes never fall through, they're always going back - // Gosubs DO come through here though, fallthrough is AFTER the return - else if (Node.NodeType != ESUDSParsedNodeType::Return) - { - // Find the next node which is at a higher indent level than this - // For a select node missing else, treat the indent as 1 inward, since it's really falling through from a nested part of the select - const int IndentLessThan = bIsSelectNodeMissingElse ? Node.OriginalIndent + 1 : Node.OriginalIndent; - const auto FallthroughIdx = FindFallthroughNodeIndex(Tree, i+1, Node.ChoicePath, Node.ConditionalPath); - if (Tree.Nodes.IsValidIndex(FallthroughIdx)) - { - Node.Edges.Add(FSUDSParsedEdge(i, FallthroughIdx, Node.SourceLineNo)); - } - else - { - // If no node to fallthrough to, default will be to end - } - } - } - else - { - for (auto& Edge : Node.Edges) - { - if (!Tree.Nodes.IsValidIndex(Edge.TargetNodeIdx)) - { - // Usually this is a choice line without anything under it, or a condition with nothing in it - const auto FallthroughIdx = FindFallthroughNodeIndex(Tree, i+1, Node.ChoicePath, Node.ConditionalPath); - if (Tree.Nodes.IsValidIndex(FallthroughIdx)) - { - Edge.TargetNodeIdx = FallthroughIdx; - } - else - { - // If no node to fallthrough to, goto end - Edge.TargetNodeIdx = -1; - } - } - } - } - } -} - -void FSUDSScriptImporter::GenerateTextIDs(ParsedTree& Tree) -{ - // This is where we generate any missing TextIDs for Text nodes, Set nodes (optionally) and Choice edges - // We don't want to do it as we go along, because if a script has some lines with TextIDs and new inserted lines - // in between, we won't know from a top-down scan which IDs have already been used. Later explicit TextIDs in - // the script could cause an ID clash with generated ones for inserted lines - - for (auto& Node : Tree.Nodes) - { - switch (Node.NodeType) - { - case ESUDSParsedNodeType::Text: - if (Node.TextID.IsEmpty()) - { - Node.TextID = GenerateTextID(); - } - break; - case ESUDSParsedNodeType::Choice: - // Text for choices is on edges - { - for (auto& Edge : Node.Edges) - { - if (!Edge.Text.IsEmpty() && Edge.TextID.IsEmpty()) - { - Edge.TextID = GenerateTextID(); - } - - // If this choice generated a speaker line, we need to give that the same text ID - // It will always be directly after the choice edge - if (Tree.Nodes.IsValidIndex(Edge.TargetNodeIdx)) - { - auto& NextNode = Tree.Nodes[Edge.TargetNodeIdx]; - if (NextNode.NodeType == ESUDSParsedNodeType::Text && NextNode.TextID.IsEmpty() && - NextNode.Text == Edge.Text) - { - NextNode.TextID = Edge.TextID; - } - } - } - } - break; - case ESUDSParsedNodeType::SetVariable: - if (Node.Expression.IsTextLiteral()) - { - // Text must be localised - if (Node.TextID.IsEmpty()) - { - Node.TextID = GenerateTextID(); - } - } - break; - default: - break; - } - - } - -} - -bool FSUDSScriptImporter::PostImportSanityCheck(const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent) -{ - bool bOK = true; - for (int i = 0; i < BodyTree.Nodes.Num(); ++i) - { - auto& Node = BodyTree.Nodes[i]; - - if (Node.NodeType == ESUDSParsedNodeType::Choice) - { - // Check all of them so we can report all errors, rather than early-out - bOK = ChoiceNodeCheckPaths(Node, NameForErrors, Logger, bSilent) && bOK; - } - } - // check for unfinished conditional blocks - if (BodyTree.ConditionalBlocks.IsValidIndex(BodyTree.CurrentConditionalBlockIdx)) - { - auto& Block = BodyTree.ConditionalBlocks[BodyTree.CurrentConditionalBlockIdx]; - auto SelectNode = BodyTree.Nodes.IsValidIndex(Block.SelectNodeIdx) ? &BodyTree.Nodes[Block.SelectNodeIdx] : nullptr; - if (!bSilent && SelectNode) - { - FString BlockStartDesc; - FString BlockEndDesc; - switch (Block.Stage) - { - case EConditionalStage::IfStage: - case EConditionalStage::ElseIfStage: - case EConditionalStage::ElseStage: - BlockStartDesc = "if"; - BlockEndDesc = "endif"; - break; - case EConditionalStage::RandomStage: - case EConditionalStage::RandomOptionStage: - BlockStartDesc = "random"; - BlockEndDesc = "endrandom"; - break; - } - Logger->Logf(ELogVerbosity::Error, - TEXT( - "%s: '%s' block started on line %d is missing '%s'"), - *NameForErrors, - *BlockStartDesc, - SelectNode->SourceLineNo, - *BlockEndDesc); - } - - } - - // Now check everything is referenced - TArray ReferencedNodes; - ReferencedNodes.SetNumZeroed(BodyTree.Nodes.Num()); - if (!ReferencedNodes.IsEmpty()) - { - // First node is always reachable - ReferencedNodes[0] = true; - } - for (const auto& Goto : BodyTree.GotoLabelList) - { - // Goto creates a reference - ReferencedNodes[Goto.Value] = true; - } - for (const auto& Node : BodyTree.Nodes) - { - for (const auto& Edge : Node.Edges) - { - if (ReferencedNodes.IsValidIndex(Edge.TargetNodeIdx)) - { - ReferencedNodes[Edge.TargetNodeIdx] = true; - } - } - } - - for (int i = 0; i < ReferencedNodes.Num(); ++i) - { - if (!ReferencedNodes[i]) - { - Logger->Logf(ELogVerbosity::Warning, - TEXT( - "%s: Line %d is unreachable. Check your indenting particularly under choice lines."), - *NameForErrors, - BodyTree.Nodes[i].SourceLineNo); - } - } - - return bOK; -} - -bool FSUDSScriptImporter::ChoiceNodeCheckPaths(const FSUDSParsedNode& ChoiceNode, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - return RecurseChoiceNodeCheckPaths(ChoiceNode, ChoiceNode, NameForErrors, Logger, bSilent); -} - -bool FSUDSScriptImporter::RecurseChoiceNodeCheckPaths(const FSUDSParsedNode& OrigChoiceNode, - const FSUDSParsedNode& CurrChoiceNode, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - bool bOK = true; - for (const auto& Edge: CurrChoiceNode.Edges) - { - // We want to make sure that every choice path leads to a speaker line, before it leads to another choice - // A choice that leads directly to another choice can't be properly represented in dialogue; choices have to - // be anchored by speaker lines so proceeding to another choice directly after a choice is made is wrong - // Usually this will be caused by a bad goto but could also be just bad nesting - // Every path has to lead to a speaker node before a choice - bOK = RecurseChoiceNodeCheckPaths(OrigChoiceNode, Edge, NameForErrors, Logger, bSilent) && bOK; - } - return bOK; -} - -bool FSUDSScriptImporter::RecurseChoiceNodeCheckPaths(const FSUDSParsedNode& ChoiceNode, const FSUDSParsedEdge& Edge, - const FString& NameForErrors, - FSUDSMessageLogger* Logger, - bool bSilent) -{ - const FSUDSParsedNode* TargetNode = GetNode(Edge.TargetNodeIdx); - while (TargetNode) - { - switch (TargetNode->NodeType) - { - case ESUDSParsedNodeType::Text: - // We're OK, found a speaker line - return true; - case ESUDSParsedNodeType::Choice: - // Definitely not ok, we found a choice node before a speaker node - Logger->Logf(ELogVerbosity::Error, - TEXT( - "%s: Choice '%s' on line %d needs a speaker line between it and the next choice at line %d. Choices MUST show another speaker line before the next choice."), - *NameForErrors, - *Edge.Text, - Edge.SourceLineNo, - TargetNode->SourceLineNo); - return false; - case ESUDSParsedNodeType::Select: - { - // Recurse selects & randoms; but in this case we can have nested choices underneath - bool bOK = true; - for (const auto& SelEdge : TargetNode->Edges) - { - const auto SelTarget = GetNode(SelEdge.TargetNodeIdx); - if (SelTarget) - { - if (SelTarget->NodeType == ESUDSParsedNodeType::Choice) - { - // First level of nested choices is OK; they will be combined with the original choice - // We don't need to recurse here since this choice will itself - } - else - { - bOK = RecurseChoiceNodeCheckPaths(ChoiceNode, SelEdge, NameForErrors, Logger, bSilent); - } - } - } - return bOK; - } - case ESUDSParsedNodeType::SetVariable: - case ESUDSParsedNodeType::Event: - // Continue (linear) - if (TargetNode->Edges.Num() > 0) - { - TargetNode = GetNode(TargetNode->Edges[0].TargetNodeIdx); - } - else - { - // This can happen if the event is the last line, but also nested in a choice - return true; - } - break; - case ESUDSParsedNodeType::Gosub: - case ESUDSParsedNodeType::Return: - // We can't really check the gosub/return statically, this will be a runtime error - return true; - case ESUDSParsedNodeType::Goto: - { - // Follow the goto (if end, will result in null) - const int GotoIdx = GetGotoTargetNodeIndex(BodyTree, TargetNode->Identifier); - TargetNode = GetNode(GotoIdx); - break; - } - } - } - - return true; -} - -int FSUDSScriptImporter::FindFallthroughNodeIndex(FSUDSScriptImporter::ParsedTree& Tree, - int StartNodeIndex, - const FString& FromChoicePath, - const FString& FromConditionalPath) -{ - // In order to be a valid fallthrough, also needs to be on the same choice (or select) path - // E.g. it's possible to have: - // - // * Choice (C1) - // * Nested choice (C1.1) - // Fallthrough from here (F1) - // * Choice (C2) - // Do NOT fallthrough to here (T1) - // Fallthrough to here instead (T2) - // - // Just testing the indent would fallthrough to T1 which is incorrect because it's not on the same choice path - // We need to check that the fallthrough point is on the same choice path (not just a common parent) - // Fallthrough from is on path /C1/C1.1 - // Point T1 is on /C2 which is NOT a subset of /C1/C1.1 so not OK - // Point T2 is on /, which is a subset of /C1/C1/1 so OK - // - // Also need to deal with this case: - // - // * Choice (C1) - // * Nested choice (C1.1) - // Fallthrough from here (F1) - // Fallthrough to here (T1) then from here (F2) - // * Choice (C2) - // Do NOT fallthrough to here (T2) - // Finally fall through to here (T3) - // - // In this case, fallthrough should first go to T1, then to T3 - // - Fallthrough F1 is on path /C1/C1.1 - // - Point T1 is on /C1 which IS a subset of /C1/C1.1 so OK - // - Fallthrough F2 is on path /C1 - // - Point T2 is on /C2 which is NOT a subset of /C1 so not OK - // - Point T3 is on / which is a subset of /C1 so OK - - // We'll form these paths just from node indexes rather than C1/C2 etc. Nesting can be for a choice or a select - for (int i = StartNodeIndex; i < Tree.Nodes.Num(); ++i) - { - auto N = Tree.Nodes[i]; - // We used to require that N.OriginalIndent < IndentLessThan here - // However, this is actually not needed, since indentation only controls association with choice paths, otherwise - // it's irrelevant. And we already check that things only fall through if they're on the same choice/conditional - // path (or a superset of it). - if (N.AllowFallthrough && - FromChoicePath.StartsWith(N.ChoicePath) && - FromConditionalPath.StartsWith(N.ConditionalPath)) - { - return i; - } - - } - - return -1; -} - -const FSUDSParsedNode* FSUDSScriptImporter::GetNode(const FSUDSScriptImporter::ParsedTree& Tree, int Index) -{ - if (Tree.Nodes.IsValidIndex(Index)) - { - return &Tree.Nodes[Index]; - } - - return nullptr; -} - -const FSUDSParsedNode* FSUDSScriptImporter::GetHeaderNode(int Index) -{ - return GetNode(HeaderTree, Index); -} - -const FSUDSParsedNode* FSUDSScriptImporter::GetNode(int Index) -{ - return GetNode(BodyTree, Index); -} - -int FSUDSScriptImporter::GetGotoTargetNodeIndex(const FString& InLabel) -{ - // Assume Body for this public version - return GetGotoTargetNodeIndex(BodyTree, InLabel); -} - -int FSUDSScriptImporter::GetGotoTargetNodeIndex(const ParsedTree& Tree, const FString& InLabel) -{ - FString Label = InLabel; - if (const FString* AliasLabel = Tree.AliasedGotoLabels.Find(Label)) - { - Label = *AliasLabel; - } - - // Resolve using goto list - if (const int *pGotoIdx = Tree.GotoLabelList.Find(Label)) - { - return *pGotoIdx; - } - - return -1; - -} - -void FSUDSScriptImporter::PopulateAsset(USUDSScript* Asset, UStringTable* StringTable) -{ - // This is only called if the parsing was successful - // Populate the runtime asset - TArray> *pOutNodes = nullptr; - TArray> *pOutHeaderNodes = nullptr; - TMap *pOutLabels = nullptr; - TMap *pOutHeaderLabels = nullptr; - TArray *pOutSpeakers = nullptr; - Asset->StartImport(&pOutNodes, &pOutHeaderNodes, &pOutLabels, &pOutHeaderLabels, &pOutSpeakers); - - pOutSpeakers->Append(ReferencedSpeakers); - - PopulateAssetFromTree(Asset, HeaderTree, pOutHeaderNodes, pOutHeaderLabels, StringTable); - PopulateAssetFromTree(Asset, BodyTree, pOutNodes, pOutLabels, StringTable); - - Asset->FinishImport(); -} - -FMD5Hash FSUDSScriptImporter::CalculateHash(const TCHAR* Buffer, int32 Len) -{ - FMD5Hash Hash; - FMD5 MD5; - - MD5.Update((uint8*)Buffer, Len * sizeof(TCHAR)); - - Hash.Set(MD5); - return Hash; - -} - -void FSUDSScriptImporter::PopulateAssetFromTree(USUDSScript* Asset, - const FSUDSScriptImporter::ParsedTree& Tree, - TArray>* pOutNodes, - TMap* pOutLabels, - UStringTable* StringTable) -{ - if (pOutNodes && pOutLabels) - { - TArray IndexRemap; - int OutIndex = 0; - // First pass, create all the nodes - for (const auto& InNode : Tree.Nodes) - { - // Gotos are dealt with in the node that references them, so ignore them - // We're going to be removing Goto nodes in the parse structure, because they were useful while parsing - // (letting you fallthrough to a goto node) but in the final runtime we just want them to be edges - // So firstly we need to figure out what the indexes of other nodes are going to be with them removed - if (InNode.NodeType == ESUDSParsedNodeType::Goto) - { - // note that this one goes nowhere, and don't increment dest index - IndexRemap.Add(-1); - } - else - { - IndexRemap.Add(OutIndex++); - - USUDSScriptNode* Node = nullptr; - switch (InNode.NodeType) - { - case ESUDSParsedNodeType::Text: - { -#if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8 - StringTable->GetMutableStringTable()->SetSourceString(InNode.TextID, InNode.Text, ""); -#else - StringTable->GetMutableStringTable()->SetSourceString(InNode.TextID, InNode.Text); -#endif - // Always include speaker metadata - StringTable->GetMutableStringTable()->SetMetaData(InNode.TextID, FName("Speaker"), InNode.Identifier); - // Other metadata - for (auto Pair : InNode.TextMetadata) - { - StringTable->GetMutableStringTable()->SetMetaData(InNode.TextID, Pair.Key, Pair.Value); - } - - auto TextNode = NewObject(Asset); - TextNode->Init(InNode.Identifier, FText::FromStringTable (StringTable->GetStringTableId(), InNode.TextID), InNode.SourceLineNo); - TextNode->SetUserMetadata(InNode.UserMetadata); - Node = TextNode; - break; - } - case ESUDSParsedNodeType::Choice: - { - auto ChoiceNode = NewObject(Asset); - ChoiceNode->InitChoice(InNode.SourceLineNo); - Node = ChoiceNode; - break; - } - case ESUDSParsedNodeType::Select: - { - auto SelectNode = NewObject(Asset); - SelectNode->InitSelect(InNode.SourceLineNo); - Node = SelectNode; - break; - } - case ESUDSParsedNodeType::SetVariable: - { - auto SetNode = NewObject(Asset); - // For text literals, re-point to string table - FSUDSExpression Expr = InNode.Expression; - if (Expr.IsTextLiteral()) - { -#if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8 - StringTable->GetMutableStringTable()->SetSourceString(InNode.TextID, Expr.GetTextLiteralValue().ToString(), ""); -#else - StringTable->GetMutableStringTable()->SetSourceString(InNode.TextID, Expr.GetTextLiteralValue().ToString()); -#endif - Expr.SetTextLiteralValue(FText::FromStringTable (StringTable->GetStringTableId(), InNode.TextID)); - } - SetNode->Init(InNode.Identifier, Expr, InNode.SourceLineNo); - Node = SetNode; - break; - } - case ESUDSParsedNodeType::Event: - { - auto EvtNode = NewObject(Asset); - EvtNode->Init(InNode.Identifier, InNode.EventArgs, InNode.SourceLineNo); - Node = EvtNode; - break; - } - case ESUDSParsedNodeType::Gosub: - { - // Validate gosub label at this point - auto GosubNode = NewObject(Asset); - GosubNode->Init(InNode.Identifier, InNode.TextID, InNode.SourceLineNo); - Node = GosubNode; - break; - } - case ESUDSParsedNodeType::Return: - { - auto ReturnNode = NewObject(Asset); - ReturnNode->InitReturn(InNode.SourceLineNo); - Node = ReturnNode; - break; - } - case ESUDSParsedNodeType::Goto: - // Gotos do not become nodes, just fixed edges - default: ; - break; - } - - pOutNodes->Add(Node); - - } - } - - // Second pass, create edges between nodes now that we know where everything is - for (int i = 0; i < Tree.Nodes.Num(); ++i) - { - const FSUDSParsedNode& InNode = Tree.Nodes[i]; - if (InNode.NodeType != ESUDSParsedNodeType::Goto) - { - USUDSScriptNode* Node = (*pOutNodes)[IndexRemap[i]]; - // Edges - if (InNode.Edges.Num() == 0) - { - // This normally happens with the final node in the script - // Make it an edge to nullptr for consistency - Node->AddEdge(FSUDSScriptEdge(nullptr, ESUDSEdgeType::Continue, InNode.SourceLineNo)); - } - else - { - for (auto& InEdge : InNode.Edges) - { - ESUDSEdgeType NewEdgeType; - - const FSUDSParsedNode *InTargetNode = GetNode(Tree, InEdge.TargetNodeIdx); - - switch (InNode.NodeType) - { - case ESUDSParsedNodeType::Text: - if (InTargetNode && InTargetNode->NodeType == ESUDSParsedNodeType::Choice) - { - // Text -> Choice is chained - NewEdgeType = ESUDSEdgeType::Chained; - } - else - { - NewEdgeType = ESUDSEdgeType::Continue; - } - break; - case ESUDSParsedNodeType::Choice: - if (InEdge.Text.IsEmpty() && InTargetNode && InTargetNode->NodeType == ESUDSParsedNodeType::Select) - { - // Choice->Select with no text is chained - NewEdgeType = ESUDSEdgeType::Chained; - } - else - { - NewEdgeType = ESUDSEdgeType::Decision; - } - break; - case ESUDSParsedNodeType::Select: - // All edges under selects are conditions - NewEdgeType = ESUDSEdgeType::Condition; - break; - default: - case ESUDSParsedNodeType::SetVariable: - case ESUDSParsedNodeType::Goto: - case ESUDSParsedNodeType::Event: - NewEdgeType = ESUDSEdgeType::Continue; - break; - }; - - USUDSScriptNode* TargetNode = nullptr; - - if (InTargetNode) - { - - if (InTargetNode->NodeType == ESUDSParsedNodeType::Goto) - { - // Resolve GOTOs immediately, point them directly at node goto points to - int Idx = GetGotoTargetNodeIndex(Tree, InTargetNode->Identifier); - // -1 means "Goto end", leave target null in that case - if (Idx != -1) - { - const int NewTargetIndex = IndexRemap[Idx]; - TargetNode = (*pOutNodes)[NewTargetIndex]; - } - } - else - { - const int NewTargetIndex = IndexRemap[InEdge.TargetNodeIdx]; - TargetNode = (*pOutNodes)[NewTargetIndex]; - } - - } - - FSUDSScriptEdge NewEdge(TargetNode, NewEdgeType, InEdge.SourceLineNo); - NewEdge.SetCondition(InEdge.ConditionExpression); - NewEdge.SetTargetNode(TargetNode); - - if (!InEdge.TextID.IsEmpty() && !InEdge.Text.IsEmpty()) - { -#if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8 - StringTable->GetMutableStringTable()->SetSourceString(InEdge.TextID, InEdge.Text, ""); -#else - StringTable->GetMutableStringTable()->SetSourceString(InEdge.TextID, InEdge.Text); -#endif - NewEdge.SetText(FText::FromStringTable(StringTable->GetStringTableId(), InEdge.TextID)); - // Always include speaker metadata, always the player in a choice - // Identify that it's a choice so translators know that there may be more limited space - StringTable->GetMutableStringTable()->SetMetaData(InEdge.TextID, FName("Speaker"), "Player (Choice)"); - // Other metadata - for (auto Pair : InEdge.TextMetadata) - { - StringTable->GetMutableStringTable()->SetMetaData(InEdge.TextID, Pair.Key, Pair.Value); - } - } - - NewEdge.SetUserMetadata(InEdge.UserMetadata); - Node->AddEdge(NewEdge); - - } - } - } - } - - // Add labels, so that dialogue can be entered at any label - // Aliases have already been resolved - for (auto& Elem : Tree.GotoLabelList) - { - int NewIndex = IndexRemap[Elem.Value]; - pOutLabels->Add(FName(Elem.Key), NewIndex); - } - - } - -} - diff --git a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptReimportFactory.cpp b/Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptReimportFactory.cpp deleted file mode 100644 index c8a2f653..00000000 --- a/Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptReimportFactory.cpp +++ /dev/null @@ -1,149 +0,0 @@ -// 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& OutFilenames) -{ - USUDSScript* Script = Cast(Obj); - if (Script && Script->AssetImportData) - { - Script->AssetImportData->ExtractFilenames(OutFilenames); - return true; - } - return false; -} - -void USUDSScriptReimportFactory::SetReimportPaths(UObject* Obj, const TArray& NewReimportPaths) -{ - USUDSScript* Script = Cast(Obj); - if (Script && ensure(NewReimportPaths.Num() == 1)) - { - Script->AssetImportData->UpdateFilenameOnly(NewReimportPaths[0]); - } - -} - -EReimportResult::Type USUDSScriptReimportFactory::Reimport(UObject* Obj) -{ - USUDSScript* Script = Cast(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 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 > PrevWaves; - for (auto Node : Script->GetNodes()) - { - if (auto TN = Cast(Node)) - { - if (auto W = TN->GetWave()) - { - PrevWaves.Add(TN->GetTextID(), TPair(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(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; -} diff --git a/Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditor.h b/Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditor.h deleted file mode 100644 index e252027d..00000000 --- a/Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditor.h +++ /dev/null @@ -1,24 +0,0 @@ -// 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 ScriptActions; - TSharedPtr StyleSet; -}; diff --git a/Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditorScriptTools.h b/Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditorScriptTools.h deleted file mode 100644 index 3db5fdb0..00000000 --- a/Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditorScriptTools.h +++ /dev/null @@ -1,22 +0,0 @@ -// 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 Nodes, TArray& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger); - static bool WriteBackTextID(const FText& AssetText, int LineNo, TArray& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger); - static bool WriteBackGosubID(const FString& GosubID, int LineNo, TArray& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger); - static bool TextIDCheckMatch(const FText& AssetText, const FString& SourceLine); - - -}; diff --git a/Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditorSettings.h b/Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditorSettings.h deleted file mode 100644 index 5414eee6..00000000 --- a/Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditorSettings.h +++ /dev/null @@ -1,73 +0,0 @@ -// 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 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); -}; diff --git a/Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditorVoiceOverTools.h b/Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditorVoiceOverTools.h deleted file mode 100644 index 47780487..00000000 --- a/Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditorVoiceOverTools.h +++ /dev/null @@ -1,34 +0,0 @@ -// 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 &OutCreatedVoices); - static void GenerateWaveAssets(USUDSScript* Script, EObjectFlags Flags, TMap, 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); - -}; diff --git a/Plugins/SUDS/Source/SUDSEditor/Public/SUDSMessageLogger.h b/Plugins/SUDS/Source/SUDSEditor/Public/SUDSMessageLogger.h deleted file mode 100644 index 0a95431a..00000000 --- a/Plugins/SUDS/Source/SUDSEditor/Public/SUDSMessageLogger.h +++ /dev/null @@ -1,60 +0,0 @@ -// 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> 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 - FORCEINLINE void Logf(ELogVerbosity::Type Verbosity, UE::Core::TCheckedFormatString Fmt, Types... Args) -#else - template - 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>& GetErrorMessages() const { return ErrorMessages; } - - /// Clear messages in preparation for an import - static void ClearMessages(); - - - -}; diff --git a/Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptActions.h b/Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptActions.h deleted file mode 100644 index 01cba953..00000000 --- a/Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptActions.h +++ /dev/null @@ -1,37 +0,0 @@ -// 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& TypeAssets, - TArray& OutSourceFilePaths) const override; - virtual bool IsImportedAsset() const override { return true; } - virtual void GetActions(const TArray& InObjects, FToolMenuSection& Section) override; - virtual bool HasActions(const TArray& InObjects) const override; - virtual void OpenAssetEditor(const TArray& InObjects, - TSharedPtr EditWithinLevelEditor) override; - -protected: - void WriteBackTextIDs(TArray> Scripts); - - void GenerateVOAssets(TArray> Scripts); - -}; diff --git a/Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptFactory.h b/Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptFactory.h deleted file mode 100644 index ba773103..00000000 --- a/Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptFactory.h +++ /dev/null @@ -1,37 +0,0 @@ -// 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& Assets); - UStringTable* CreateStringTable(UObject* ScriptParent, FName InName, USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger); -}; diff --git a/Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptImporter.h b/Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptImporter.h deleted file mode 100644 index 60f213a8..00000000 --- a/Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptImporter.h +++ /dev/null @@ -1,520 +0,0 @@ -// 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 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 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& Metadata, const TMap& 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 TextMetadata; - /// Expression, for nodes that use it (e.g. set) - FSUDSExpression Expression; - /// Event arguments, for event nodes - TArray EventArgs; - /// Labels which lead to this node - TArray Labels; - /// Edges leading to other nodes - TArray 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 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& Metadata, const TMap& 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 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 Nodes; - /// Record of goto labels to node index, built up during parsing (forward refs are OK so not complete until end of parsing) - TMap GotoLabelList; - /// Goto labels which have been encountered but we haven't found a destination yet - TArray PendingGotoLabels; - /// Goto labels that lead directly to another goto and thus are just aliases - TMap 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 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> PersistentMetadata; - /// Text Metadata applied just to the next speaker line or choice - TMap TransientMetadata; - /// User metadata for the next speaker line or choice - TMap UserMetadata; - - /// List of speakers, detected during parsing of lines of text - TArray ReferencedSpeakers; - - const int TabIndentValue = 4; - bool bHeaderDone = false; - bool bTooLateForHeader = false; - bool bHeaderInProgress = false; - TOptional bOverrideGenerateSpeakerLineForChoice; - TOptional 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 GetTextMetadataForNextEntry(int CurrentLineIndent); - TMap 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>* pOutNodes, - TMap* 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); -}; diff --git a/Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptReimportFactory.h b/Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptReimportFactory.h deleted file mode 100644 index b9924991..00000000 --- a/Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptReimportFactory.h +++ /dev/null @@ -1,27 +0,0 @@ -// 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& OutFilenames) override; - virtual void SetReimportPaths(UObject* Obj, const TArray& NewReimportPaths) override; - virtual EReimportResult::Type Reimport(UObject* Obj) override; - virtual int32 GetPriority() const override; - //~ End FReimportHandler Interface -}; \ No newline at end of file diff --git a/Plugins/SUDS/Source/SUDSEditor/SUDSEditor.Build.cs b/Plugins/SUDS/Source/SUDSEditor/SUDSEditor.Build.cs deleted file mode 100644 index 04424f67..00000000 --- a/Plugins/SUDS/Source/SUDSEditor/SUDSEditor.Build.cs +++ /dev/null @@ -1,62 +0,0 @@ -// 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 ... - } - ); - - } - -} diff --git a/Plugins/SUDS/Source/SUDSTest/Private/SudsTestModule.cpp b/Plugins/SUDS/Source/SUDSTest/Private/SudsTestModule.cpp deleted file mode 100644 index 9a934491..00000000 --- a/Plugins/SUDS/Source/SUDSTest/Private/SudsTestModule.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#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) diff --git a/Plugins/SUDS/Source/SUDSTest/Private/SudsTestModule.h b/Plugins/SUDS/Source/SUDSTest/Private/SudsTestModule.h deleted file mode 100644 index 870cc5a6..00000000 --- a/Plugins/SUDS/Source/SUDSTest/Private/SudsTestModule.h +++ /dev/null @@ -1,14 +0,0 @@ -#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; -}; \ No newline at end of file diff --git a/Plugins/SUDS/Source/SUDSTest/Private/TestChoiceSpeakerLines.cpp b/Plugins/SUDS/Source/SUDSTest/Private/TestChoiceSpeakerLines.cpp deleted file mode 100644 index 4c32be66..00000000 --- a/Plugins/SUDS/Source/SUDSTest/Private/TestChoiceSpeakerLines.cpp +++ /dev/null @@ -1,274 +0,0 @@ -#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(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(); - 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(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(); - 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(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(); - 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(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(); - 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 \ No newline at end of file diff --git a/Plugins/SUDS/Source/SUDSTest/Private/TestConditionals.cpp b/Plugins/SUDS/Source/SUDSTest/Private/TestConditionals.cpp deleted file mode 100644 index 8ab76500..00000000 --- a/Plugins/SUDS/Source/SUDSTest/Private/TestConditionals.cpp +++ /dev/null @@ -1,1045 +0,0 @@ -#include "SUDSLibrary.h" -#include "SUDSMessageLogger.h" -#include "SUDSScript.h" -#include "SUDSScriptImporter.h" -#include "TestUtils.h" - -UE_DISABLE_OPTIMIZATION - -const FString BasicConditionalInput = R"RAWSUD( -Player: Hello -[if {x} == 1] - NPC: Reply when x == 1 - [if {y} > 0] - Player: Player text when x ==1 and y > 0 - [endif] -[elseif {x} > 1] - NPC: Reply when x > 1 -[else] - NPC: Reply when x is something else -[endif] -[if {z}] - Player: the end is true -[else] - Player: the end is false -[endif] -NPC: OK -)RAWSUD"; - -const FString ConditionalChoiceInput = R"RAWSUD( -# First test has a regular choice first before conditional -NPC: Hello - * First choice - Player: I took the 1.1 choice -[if {y} == 2] - * 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 -# Second test has conditional choice as the first one -NPC: OK next question -[if {y} == 0] - * First conditional choice - Player: I took the 2.1 choice -[elseif {y} == 1] - * Second conditional choice - Player: I took the 2.2 choice - [if {q} == 10] - * Nested conditional choice - Player: I took the 2.2.1 choice - [endif] -[else] - * Third conditional choice - Player: I took the 2.3 choice -[endif] - * Final common choice - Player: I took the 2.4 choice -NPC: Bye -)RAWSUD"; - -const FString SiblingConditionalChoiceInput = R"RAWSUD( -NPC: Hello -[if {x} == 0] - * First choice - Player: I took the 1.1 choice -[endif] -[if {y} == 1] - * Second choice (conditional) - Player: I took the 1.2 choice -[endif] -[if {z} == 1] - * Third choice (conditional) - Player: I took the 1.3 choice -[endif] - * Second Alt Choice - -NPC: End -)RAWSUD"; - -const FString SiblingConditionalChoiceWithElseInput = R"RAWSUD( -NPC: Hello -[if {x} == 0] - * First choice - Player: I took the 1.1 choice -[elseif {SomeBool}] - * First alt choice elseif - Player: I took the 1.1 elseif choice -[else] - * First alt choice - Player: I took the 1.1b choice -[endif] -[if {y} == 1] - * Second choice (conditional) - Player: I took the 1.2 choice -[elseif {SomeBool}] - * Second alt choice elseif - Player: I took the 1.2 elseif choice -[else] - * Second alt choice - Player: I took the 1.2b choice -[endif] -[if {z} == 1] - * Third choice (conditional) - Player: I took the 1.3 choice -[elseif {SomeBool}] - * Third alt choice elseif - Player: I took the 1.3 elseif choice -[else] - * Third alt choice - Player: I took the 1.3b choice -[endif] -NPC: End -)RAWSUD"; - - -const FString MixedChoiceAndBranchInput = R"RAWSUD( -[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 - -[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} > 5] - * 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(FTestBasicConditionals, - "SUDSTest.TestBasicConditionals", - EAutomationTestFlags::EditorContext | - EAutomationTestFlags::ClientContext | - EAutomationTestFlags::ProductFilter) - - -bool FTestBasicConditionals::RunTest(const FString& Parameters) -{ - FSUDSMessageLogger Logger(false); - FSUDSScriptImporter Importer; - TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(BasicConditionalInput), BasicConditionalInput.Len(), "BasicConditionalInput", &Logger, true)); - - // Test the content of the parsing - auto NextNode = Importer.GetNode(0); - if (!TestNotNull("Root node should exist", NextNode)) - return false; - - TestParsedText(this, "First node", NextNode, "Player", "Hello"); - TestGetParsedNextNode(this, "Get next", NextNode, Importer, false, &NextNode); - - if (TestParsedSelect(this, "First Select node", NextNode, 3)) - { - auto SelectNode = NextNode; - TestParsedSelectEdge(this, "First select edge 1 (if)", SelectNode, 0, "{x} == 1", Importer, &NextNode); - TestParsedText(this, "Nested node 1", NextNode, "NPC", "Reply when x == 1"); - TestGetParsedNextNode(this, "Get next", NextNode, Importer, false, &NextNode); - // Note: even though this is a single "if", there is an implicit "else" edge created - if (TestParsedSelect(this, "Nested Select node", NextNode, 2)) - { - // Nested select - auto SelectNode2 = NextNode; - TestParsedSelectEdge(this, "Nested select edge 1", SelectNode2, 0, "{y} > 0", Importer, &NextNode); - TestParsedText(this, "Nested node edge 1", NextNode, "Player", "Player text when x ==1 and y > 0"); - TestGetParsedNextNode(this, "Get next", NextNode, Importer, false, &NextNode); - if (TestParsedSelect(this, "Fallthrough select", NextNode, 2)) - { - auto SelectNode3 = NextNode; - TestParsedSelectEdge(this, "Final select edge 1", SelectNode3, 0, "{z}", Importer, &NextNode); - TestParsedText(this, "Final select edge 1 text", NextNode, "Player", "the end is true"); - TestGetParsedNextNode(this, "Get next", NextNode, Importer, false, &NextNode); - TestParsedText(this, "Final fallthrough", NextNode, "NPC", "OK"); - - TestParsedSelectEdge(this, "Final select edge 2", SelectNode3, 1, "", Importer, &NextNode); - TestParsedText(this, "Final select edge 2 text", NextNode, "Player", "the end is false"); - TestGetParsedNextNode(this, "Get next", NextNode, Importer, false, &NextNode); - TestParsedText(this, "Final fallthrough", NextNode, "NPC", "OK"); - - } - - // Go back to the nested select - // This "else" edge should have been created automatically to fall through - TestParsedSelectEdge(this, "Nested select edge 2", SelectNode2, 1, "", Importer, &NextNode); - // Just test it gets to the fallthrough, we've already tested the continuation from there - TestParsedSelect(this, "Fallthrough select", NextNode, 2); - - } - TestParsedSelectEdge(this, "First select edge 2 (elseif)", SelectNode, 1, "{x} > 1", Importer, &NextNode); - TestParsedText(this, "Select node 2", NextNode, "NPC", "Reply when x > 1"); - TestGetParsedNextNode(this, "Get next", NextNode, Importer, false, &NextNode); - // Just test it gets to the fallthrough, we've already tested the continuation from there - TestParsedSelect(this, "Fallthrough select", NextNode, 2); - - TestParsedSelectEdge(this, "First select edge 2 (else)", SelectNode, 2, "", Importer, &NextNode); - TestParsedText(this, "Select node 3", NextNode, "NPC", "Reply when x is something else"); - TestGetParsedNextNode(this, "Get next", NextNode, Importer, false, &NextNode); - // Just test it gets to the fallthrough, we've already tested the continuation from there - TestParsedSelect(this, "Fallthrough select", NextNode, 2); - } - - return true; -} - -IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestSiblingConditionals, - "SUDSTest.TestSiblingConditionals", - EAutomationTestFlags::EditorContext | - EAutomationTestFlags::ClientContext | - EAutomationTestFlags::ProductFilter) - -bool FTestSiblingConditionals::RunTest(const FString& Parameters) -{ - FSUDSScriptImporter Importer; - FSUDSMessageLogger Logger(false); - TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(SiblingConditionalChoiceInput), SiblingConditionalChoiceInput.Len(), "SiblingConditionalChoiceInput", &Logger, true)); - - // Test the content of the parsing - auto NextNode = Importer.GetNode(0); - if (!TestNotNull("Root node should exist", NextNode)) - return false; - TestParsedText(this, "Text node", NextNode, "NPC", "Hello"); - TestGetParsedNextNode(this, "Get next", NextNode, Importer, false, &NextNode); - if (TestParsedChoice(this, "Root Choice", NextNode, 4)) - { - auto RootChoice = NextNode; - // Sibling choices all get their own nodes - // Bit wasteful to have C -> S -> C -> Text instead of just C -> T but you never know how many choices would - // be in each select level - TestParsedChoiceEdge(this, "Choice 0", RootChoice, 0, "", Importer, &NextNode); - if (TestParsedSelect(this, "Select 0", NextNode, 1)) - { - TestParsedSelectEdge(this, "Select 0", NextNode, 0, "{x} == 0", Importer, &NextNode); - TestParsedChoiceEdge(this, "Choice 0 sub", NextNode, 0, "First choice", Importer, &NextNode); - TestParsedText(this, "Text 0", NextNode, "Player", "I took the 1.1 choice"); - } - TestParsedChoiceEdge(this, "Choice 1", RootChoice, 1, "", Importer, &NextNode); - if (TestParsedSelect(this, "Select 1", NextNode, 1)) - { - TestParsedSelectEdge(this, "Select 1", NextNode, 0, "{y} == 1", Importer, &NextNode); - TestParsedChoiceEdge(this, "Choice 1 sub", NextNode, 0, "Second choice (conditional)", Importer, &NextNode); - TestParsedText(this, "Text 1", NextNode, "Player", "I took the 1.2 choice"); - } - TestParsedChoiceEdge(this, "Choice 2", RootChoice, 2, "", Importer, &NextNode); - if (TestParsedSelect(this, "Select 2", NextNode, 1)) - { - TestParsedSelectEdge(this, "Select 2", NextNode, 0, "{z} == 1", Importer, &NextNode); - TestParsedChoiceEdge(this, "Choice 2 sub", NextNode, 0, "Third choice (conditional)", Importer, &NextNode); - TestParsedText(this, "Text 1", NextNode, "Player", "I took the 1.3 choice"); - } - // Dangling choice with no condition - TestParsedChoiceEdge(this, "Choice 3", RootChoice, 3, "Second Alt Choice", Importer, &NextNode); - } - return true; -} - - -IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestSiblingConditionalsWithElse, - "SUDSTest.TestSiblingConditionalsWithElse", - EAutomationTestFlags::EditorContext | - EAutomationTestFlags::ClientContext | - EAutomationTestFlags::ProductFilter) - -bool FTestSiblingConditionalsWithElse::RunTest(const FString& Parameters) -{ - FSUDSScriptImporter Importer; - FSUDSMessageLogger Logger(false); - TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(SiblingConditionalChoiceWithElseInput), SiblingConditionalChoiceWithElseInput.Len(), "SiblingConditionalChoiceWithElseInput", &Logger, true)); - - // Test the content of the parsing - auto NextNode = Importer.GetNode(0); - if (!TestNotNull("Root node should exist", NextNode)) - return false; - TestParsedText(this, "Text node", NextNode, "NPC", "Hello"); - TestGetParsedNextNode(this, "Get next", NextNode, Importer, false, &NextNode); - if (TestParsedChoice(this, "Root Choice", NextNode, 3)) - { - auto RootChoice = NextNode; - // Sibling choices all get their own nodes - // Bit wasteful to have C -> S -> C -> Text instead of just C -> T but you never know how many choices would - // be in each select level - TestParsedChoiceEdge(this, "Choice 0", RootChoice, 0, "", Importer, &NextNode); - if (TestParsedSelect(this, "Select 0", NextNode, 3)) - { - const FSUDSParsedNode* SelectNode = NextNode; - TestParsedSelectEdge(this, "Select 0", SelectNode, 0, "{x} == 0", Importer, &NextNode); - TestParsedChoiceEdge(this, "Choice 0 sub", NextNode, 0, "First choice", Importer, &NextNode); - TestParsedText(this, "Text 0", NextNode, "Player", "I took the 1.1 choice"); - - // It's key to have the same "elseif {SomeBool}" for every sibling "if" to prove that the elseifs don't get merged - // just because their own conditions are the same. They're different because they're dependent on their own ifs not - // being true. - TestParsedSelectEdge(this, "Select 0 ELSEIF", SelectNode, 1, "{SomeBool}", Importer, &NextNode); - TestParsedChoiceEdge(this, "Choice 0 sub ELSEIF", NextNode, 0, "First alt choice elseif", Importer, &NextNode); - TestParsedText(this, "Text 0 ELSEIF", NextNode, "Player", "I took the 1.1 elseif choice"); - - TestParsedSelectEdge(this, "Select 0 ELSE", SelectNode, 2, "", Importer, &NextNode); - TestParsedChoiceEdge(this, "Choice 0 sub ELSE", NextNode, 0, "First alt choice", Importer, &NextNode); - TestParsedText(this, "Text 0 ELSE", NextNode, "Player", "I took the 1.1b choice"); - } - TestParsedChoiceEdge(this, "Choice 1", RootChoice, 1, "", Importer, &NextNode); - if (TestParsedSelect(this, "Select 1", NextNode, 3)) - { - const FSUDSParsedNode* SelectNode = NextNode; - TestParsedSelectEdge(this, "Select 1", SelectNode, 0, "{y} == 1", Importer, &NextNode); - TestParsedChoiceEdge(this, "Choice 1 sub", NextNode, 0, "Second choice (conditional)", Importer, &NextNode); - TestParsedText(this, "Text 1", NextNode, "Player", "I took the 1.2 choice"); - - TestParsedSelectEdge(this, "Select 1 ELSEIF", SelectNode, 1, "{SomeBool}", Importer, &NextNode); - TestParsedChoiceEdge(this, "Choice 1 sub ELSEIF", NextNode, 0, "Second alt choice elseif", Importer, &NextNode); - TestParsedText(this, "Text 1 ELSEIF", NextNode, "Player", "I took the 1.2 elseif choice"); - - TestParsedSelectEdge(this, "Select 1 ELSE", SelectNode, 2, "", Importer, &NextNode); - TestParsedChoiceEdge(this, "Choice 1 sub ELSE", NextNode, 0, "Second alt choice", Importer, &NextNode); - TestParsedText(this, "Text 1 ELSE", NextNode, "Player", "I took the 1.2b choice"); - } - TestParsedChoiceEdge(this, "Choice 2", RootChoice, 2, "", Importer, &NextNode); - if (TestParsedSelect(this, "Select 2", NextNode, 3)) - { - const FSUDSParsedNode* SelectNode = NextNode; - TestParsedSelectEdge(this, "Select 2", SelectNode, 0, "{z} == 1", Importer, &NextNode); - TestParsedChoiceEdge(this, "Choice 2 sub", NextNode, 0, "Third choice (conditional)", Importer, &NextNode); - TestParsedText(this, "Text 2", NextNode, "Player", "I took the 1.3 choice"); - - TestParsedSelectEdge(this, "Select 2 ELSEIF", SelectNode, 1, "{SomeBool}", Importer, &NextNode); - TestParsedChoiceEdge(this, "Choice 2 sub ELSEIF", NextNode, 0, "Third alt choice elseif", Importer, &NextNode); - TestParsedText(this, "Text 2 ELSE", NextNode, "Player", "I took the 1.3 elseif choice"); - - TestParsedSelectEdge(this, "Select 2 ELSE", SelectNode, 2, "", Importer, &NextNode); - TestParsedChoiceEdge(this, "Choice 2 sub ELSE", NextNode, 0, "Third alt choice", Importer, &NextNode); - TestParsedText(this, "Text 2 ELSE", NextNode, "Player", "I took the 1.3b choice"); - } - // Dangling choice with no condition - TestParsedChoiceEdge(this, "Choice 3", RootChoice, 3, "Second Alt Choice", Importer, &NextNode); - } - - - // Test running - auto Script = NewObject(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); - - // For the first run, do not set any state - Dlg->Start(); - - TestDialogueText(this, "First node", Dlg, "NPC", "Hello"); - if (TestEqual("Choice count", Dlg->GetNumberOfChoices(), 3)) - { - TestEqual("Choice 0", Dlg->GetChoiceText(0).ToString(), TEXT("First choice")); - TestEqual("Choice 1", Dlg->GetChoiceText(1).ToString(), TEXT("Second alt choice")); - TestEqual("Choice 2", Dlg->GetChoiceText(2).ToString(), TEXT("Third alt choice")); - } - - // restart, set some variables to alter the path - Dlg->SetVariableInt("x", 1); - Dlg->SetVariableInt("y", 1); - Dlg->SetVariableInt("z", 0); - Dlg->Restart(false); - - TestDialogueText(this, "First node", Dlg, "NPC", "Hello"); - if (TestEqual("Choice count", Dlg->GetNumberOfChoices(), 3)) - { - TestEqual("Choice 0", Dlg->GetChoiceText(0).ToString(), TEXT("First alt choice")); - TestEqual("Choice 1", Dlg->GetChoiceText(1).ToString(), TEXT("Second choice (conditional)")); - TestEqual("Choice 2", Dlg->GetChoiceText(2).ToString(), TEXT("Third alt choice")); - } - - // restart, set some variables to alter the path - Dlg->SetVariableBoolean("SomeBool", true); - Dlg->SetVariableInt("x", 1); - Dlg->SetVariableInt("y", 0); - Dlg->SetVariableInt("z", 1); - Dlg->Restart(false); - - TestDialogueText(this, "First node", Dlg, "NPC", "Hello"); - if (TestEqual("Choice count", Dlg->GetNumberOfChoices(), 3)) - { - TestEqual("Choice 0", Dlg->GetChoiceText(0).ToString(), TEXT("First alt choice elseif")); - TestEqual("Choice 1", Dlg->GetChoiceText(1).ToString(), TEXT("Second alt choice elseif")); - TestEqual("Choice 2", Dlg->GetChoiceText(2).ToString(), TEXT("Third choice (conditional)")); - } - - Script->MarkAsGarbage(); - - - return true; -} - - -IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestRunningBasicConditionals, - "SUDSTest.TestRunningBasicConditionals", - EAutomationTestFlags::EditorContext | - EAutomationTestFlags::ClientContext | - EAutomationTestFlags::ProductFilter) - - -bool FTestRunningBasicConditionals::RunTest(const FString& Parameters) -{ - FSUDSScriptImporter Importer; - FSUDSMessageLogger Logger(false); - TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(BasicConditionalInput), BasicConditionalInput.Len(), "BasicConditionalInput", &Logger, true)); - - auto Script = NewObject(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); - - // For the first run, do not set any state - Dlg->Start(); - - TestDialogueText(this, "First node", Dlg, "Player", "Hello"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text", Dlg, "NPC", "Reply when x is something else"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text", Dlg, "Player", "the end is false"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text", Dlg, "NPC", "OK"); - TestFalse("Continue", Dlg->Continue()); - TestTrue("End", Dlg->IsEnded()); - - - // restart, set some variables to alter the path - Dlg->Restart(true); - Dlg->SetVariableInt("x", 1); - Dlg->SetVariableInt("y", -1); - Dlg->SetVariableBoolean("z", true); - - TestDialogueText(this, "First node", Dlg, "Player", "Hello"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text", Dlg, "NPC", "Reply when x == 1"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text", Dlg, "Player", "the end is true"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text", Dlg, "NPC", "OK"); - TestFalse("Continue", Dlg->Continue()); - TestTrue("End", Dlg->IsEnded()); - - - // restart, set some variables to alter the path - Dlg->Restart(true); - Dlg->SetVariableInt("x", 1); - Dlg->SetVariableInt("y", 10); - Dlg->SetVariableBoolean("z", false); - - TestDialogueText(this, "First node", Dlg, "Player", "Hello"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text", Dlg, "NPC", "Reply when x == 1"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text", Dlg, "Player", "Player text when x ==1 and y > 0"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text", Dlg, "Player", "the end is false"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text", Dlg, "NPC", "OK"); - TestFalse("Continue", Dlg->Continue()); - TestTrue("End", Dlg->IsEnded()); - - Script->MarkAsGarbage(); - return true; - -} - - - -IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestConditionalChoices, - "SUDSTest.TestConditionalChoices", - EAutomationTestFlags::EditorContext | - EAutomationTestFlags::ClientContext | - EAutomationTestFlags::ProductFilter) - - -bool FTestConditionalChoices::RunTest(const FString& Parameters) -{ - FSUDSMessageLogger Logger(false); - FSUDSScriptImporter Importer; - TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ConditionalChoiceInput), ConditionalChoiceInput.Len(), "ConditionalChoiceInput", &Logger, true)); - - // Test the content of the parsing - auto NextNode = Importer.GetNode(0); - if (!TestNotNull("Root node should exist", NextNode)) - return false; - - TestParsedText(this, "First node", NextNode, "NPC", "Hello"); - TestGetParsedNextNode(this, "Get next", NextNode, Importer, false, &NextNode); - // This will have one choice edge for the unconditional choice, then an edge to the select node - if (TestParsedChoice(this, "First choice node", NextNode, 3)) - { - auto ChoiceNode = NextNode; - TestParsedChoiceEdge(this, "First choice edge 1", ChoiceNode, 0, "First choice", Importer, &NextNode); - TestParsedText(this, "First choice edge 1 text", NextNode, "Player", "I took the 1.1 choice"); - TestGetParsedNextNode(this, "Get next", NextNode, Importer, false, &NextNode); - TestParsedText(this, "Fallthrough to next question", NextNode, "NPC", "OK next question"); - - // Second edge has no text because it's going to a select for other choices - TestParsedChoiceEdge(this, "First choice edge 2", ChoiceNode, 1, "", Importer, &NextNode); - // 2 edges because only if/else - if (TestParsedSelect(this, "First choice edge 2 should link to select", NextNode, 2)) - { - auto SelectNode = NextNode; - TestParsedSelectEdge(this, "Select edge 0", SelectNode, 0, "{y} == 2", Importer, &NextNode); - // 2 choices in this if clause - if (TestParsedChoice(this, "Choice node under first if", NextNode, 2)) - { - auto Choice2 = NextNode; - TestParsedChoiceEdge(this, "y == 2 choice edge 1", Choice2, 0, "Second choice (conditional)", Importer, &NextNode); - TestParsedText(this, "y == 2 choice edge 1 text", NextNode, "Player", "I took the 1.2 choice"); - // This should fall through to text node after the choices - TestGetParsedNextNode(this, "Get next", NextNode, Importer, false, &NextNode); - TestParsedText(this, "Fallthrough to next text", NextNode, "NPC", "OK next question"); - - TestParsedChoiceEdge(this, "y == 2 choice edge 2", Choice2, 1, "Third choice (conditional)", Importer, &NextNode); - TestParsedText(this, "y == 2 choice edge 2 text", NextNode, "Player", "I took the 1.3 choice"); - // This should fall through to text node after the choices - TestGetParsedNextNode(this, "Get next", NextNode, Importer, false, &NextNode); - TestParsedText(this, "Fallthrough to next text", NextNode, "NPC", "OK next question"); - - } - TestParsedSelectEdge(this, "Select edge 1 (else)", SelectNode, 1, "", Importer, &NextNode); - // 1 choice under this else section - if (TestParsedChoice(this, "Choice node under first else", NextNode, 1)) - { - auto Choice3 = NextNode; - TestParsedChoiceEdge(this, "y == 2 choice edge 1", Choice3, 0, "Second Alt Choice", Importer, &NextNode); - TestParsedText(this, "Else edge text", NextNode, "Player", "I took the alt 1.2 choice"); - TestGetParsedNextNode(this, "Get next", NextNode, Importer, false, &NextNode); - TestParsedText(this, "Fallthrough to next question", NextNode, "NPC", "OK next question"); - } - - // Final fallthrough choice, unconditional - TestParsedChoiceEdge(this, "First choice edge 3", ChoiceNode, 2, "Common last choice", Importer, &NextNode); - TestParsedText(this, "Fallthrough to next text", NextNode, "Player", "I took the 1.4 choice"); - TestGetParsedNextNode(this, "Get next", NextNode, Importer, false, &NextNode); - TestParsedText(this, "Fallthrough to next question", NextNode, "NPC", "OK next question"); - - TestGetParsedNextNode(this, "Get next", NextNode, Importer, false, &NextNode); - // This time the first choice is already in the select, BUT we should have inserted an extra choice anyway - // Should be 2 edges because the last common choice should have attached itself - if (TestParsedChoice(this, "Inserted choice before select", NextNode, 2)) - { - auto InsertedChoice = NextNode; - TestParsedChoiceEdge(this, "Inserted choice edge 1", InsertedChoice, 0, "", Importer, &NextNode); - - // There's an if, elseif, and else edge - if (TestParsedSelect(this, "Select node under next question", NextNode, 3)) - { - auto SelectNode2 = NextNode; - TestParsedSelectEdge(this, "Select edge 0", SelectNode2, 0, "{y} == 0", Importer, &NextNode); - // Choice node under if, only 1 in this section - if (TestParsedChoice(this, "Choice node under select", NextNode, 1)) - { - TestParsedChoiceEdge(this, "First choice edge", NextNode, 0, "First conditional choice", Importer, &NextNode); - TestParsedText(this, "First choice text", NextNode, "Player", "I took the 2.1 choice"); - TestGetParsedNextNode(this, "Get next", NextNode, Importer, false, &NextNode); - TestParsedText(this, "Final fallthrough", NextNode, "NPC", "Bye"); - - } - TestParsedSelectEdge(this, "Select edge 1", SelectNode2, 1, "{y} == 1", Importer, &NextNode); - // Choice node under elseif, only 1 in this section - if (TestParsedChoice(this, "Choice node under select 2", NextNode, 2)) - { - auto Choice4 = NextNode; - TestParsedChoiceEdge(this, "Second choice edge 1", Choice4, 0, "Second conditional choice", Importer, &NextNode); - TestParsedText(this, "First choice text", NextNode, "Player", "I took the 2.2 choice"); - TestGetParsedNextNode(this, "Get next", NextNode, Importer, false, &NextNode); - TestParsedText(this, "Final fallthrough", NextNode, "NPC", "Bye"); - - // Next edge goes to a select - TestParsedChoiceEdge(this, "Second choice edge 2", Choice4, 1, "", Importer, &NextNode); - // There is only an "if" in this nested select. No implicit "else" edge should exist because it's a choice node - if (TestParsedSelect(this, "Select node under next question", NextNode, 1)) - { - TestParsedSelectEdge(this, "Nested select edge 1", NextNode, 0, "{q} == 10", Importer, &NextNode); - if (TestParsedChoice(this, "Choice node under nested select", NextNode, 1)) - { - TestParsedChoiceEdge(this, "Nested choice edge 1", NextNode, 0, "Nested conditional choice", Importer, &NextNode); - TestParsedText(this, "First choice text", NextNode, "Player", "I took the 2.2.1 choice"); - TestGetParsedNextNode(this, "Get next", NextNode, Importer, false, &NextNode); - TestParsedText(this, "Final fallthrough", NextNode, "NPC", "Bye"); - } - } - - } - TestParsedSelectEdge(this, "Select edge 2", SelectNode2, 2, "", Importer, &NextNode); - // Choice node under elseif, only 1 in this section - if (TestParsedChoice(this, "Choice node under select 3", NextNode, 1)) - { - TestParsedChoiceEdge(this, "Choice under select 3 edge", NextNode, 0, "Third conditional choice", Importer, &NextNode); - TestParsedText(this, "First choice text", NextNode, "Player", "I took the 2.3 choice"); - TestGetParsedNextNode(this, "Get next", NextNode, Importer, false, &NextNode); - TestParsedText(this, "Final fallthrough", NextNode, "NPC", "Bye"); - } - - - } - - // Finally the last catch-all choice which attached itself to the choice node we created above the select - TestParsedChoiceEdge(this, "Inserted choice edge 2", InsertedChoice, 1, "Final common choice", Importer, &NextNode); - TestParsedText(this, "First choice text", NextNode, "Player", "I took the 2.4 choice"); - TestGetParsedNextNode(this, "Get next", NextNode, Importer, false, &NextNode); - TestParsedText(this, "Final fallthrough", NextNode, "NPC", "Bye"); - - } - - } - - } - - - return true; -} - -IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestMixedChoiceAndBranch, - "SUDSTest.TestMixedChoiceAndBranch", - EAutomationTestFlags::EditorContext | - EAutomationTestFlags::ClientContext | - EAutomationTestFlags::ProductFilter) - - -bool FTestMixedChoiceAndBranch::RunTest(const FString& Parameters) -{ - FSUDSMessageLogger Logger(false); - FSUDSScriptImporter Importer; - TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(MixedChoiceAndBranchInput), MixedChoiceAndBranchInput.Len(), "MixedChoiceAndBranchInput", &Logger, true)); - - auto Script = NewObject(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); - - // For the first run, do not set any state - Dlg->Start(); - - TestDialogueText(this, "Text node", Dlg, "NPC", "Hello"); - TestEqual("Num choices", Dlg->GetNumberOfChoices(), 3); - TestEqual("Num choices", Dlg->GetChoiceText(0).ToString(), "First choice"); - TestEqual("Num choices", Dlg->GetChoiceText(1).ToString(), "Second Alt Choice"); - TestEqual("Num choices", Dlg->GetChoiceText(2).ToString(), "Common last choice"); - TestTrue("Choose", Dlg->Choose(0)); - TestDialogueText(this, "Text node", Dlg, "Player", "I took the 1.1 choice"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text node", Dlg, "NPC", "Bye"); - - // Choices are on first node so need to set before restart - Dlg->SetVariableInt("x", 10); - Dlg->Restart(false); - TestDialogueText(this, "Text node", Dlg, "NPC", "Hello"); - if (TestEqual("Num choices", Dlg->GetNumberOfChoices(), 4)) - { - 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"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text node", Dlg, "NPC", "Bye"); - } - - Dlg->SetVariableBoolean("alreadyvisited", true); - Dlg->SetVariableInt("y", -1); - Dlg->Restart(false); - TestDialogueText(this, "Text node", Dlg, "NPC", "Hello again"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text node", Dlg, "Player", "Y is less than 0"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text node", Dlg, "NPC", "How interesting"); - if (TestEqual("Num choices", Dlg->GetNumberOfChoices(), 2)) - { - TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), "You don't sound that interested"); - TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), "Well, better be off"); - TestTrue("Choose", Dlg->Choose(1)); - TestDialogueText(this, "Text node", Dlg, "NPC", "Bye"); - } - - Dlg->SetVariableBoolean("alreadyvisited", true); - Dlg->SetVariableInt("y", -1); - Dlg->SetVariableInt("x", 0); - Dlg->Restart(false); - TestDialogueText(this, "Text node", Dlg, "NPC", "Hello again"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text node", Dlg, "Player", "Y is less than 0"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text node", Dlg, "NPC", "How interesting"); - if (TestEqual("Num choices", Dlg->GetNumberOfChoices(), 3)) - { - TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), "You don't sound that interested"); - TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), "Also x is zero"); - TestEqual("Choice text", Dlg->GetChoiceText(2).ToString(), "Well, better be off"); - TestTrue("Choose", Dlg->Choose(1)); - TestDialogueText(this, "Text node", Dlg, "NPC", "Fascinating"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text node", Dlg, "Player", "This is some fallthrough text"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text node", Dlg, "NPC", "Bye"); - } - - - Dlg->SetVariableBoolean("alreadyvisited", true); - Dlg->SetVariableInt("y", 123); - Dlg->SetVariableBoolean("ponderous", true); - Dlg->Restart(false); - TestDialogueText(this, "Text node", Dlg, "NPC", "Hello again"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text node", Dlg, "Player", "Who knows what Y is anyway"); - if (TestEqual("Num choices", Dlg->GetNumberOfChoices(), 3)) - { - TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), "Who knows what anything is?"); - TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), "It's more than I can count on one hand"); - TestEqual("Choice text", Dlg->GetChoiceText(2).ToString(), "I'm done with this"); - TestTrue("Choose", Dlg->Choose(0)); - TestDialogueText(this, "Text node", Dlg, "NPC", "Get out"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text node", Dlg, "Player", "This is some fallthrough text"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text node", Dlg, "NPC", "Bye"); - - } - - Dlg->SetVariableBoolean("alreadyvisited", true); - Dlg->SetVariableInt("y", 4); - Dlg->SetVariableBoolean("ponderous", true); - Dlg->Restart(false); - TestDialogueText(this, "Text node", Dlg, "NPC", "Hello again"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text node", Dlg, "Player", "Who knows what Y is anyway"); - if (TestEqual("Num choices", Dlg->GetNumberOfChoices(), 3)) - { - TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), "Who knows what anything is?"); - TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), "It's kind of small though"); - TestEqual("Choice text", Dlg->GetChoiceText(2).ToString(), "I'm done with this"); - TestTrue("Choose", Dlg->Choose(2)); - TestDialogueText(this, "Text node", Dlg, "Player", "This is some fallthrough text"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text node", Dlg, "NPC", "Bye"); - - } - - Script->MarkAsGarbage(); - return true; -} - -const FString ChoiceAfterConditionals = R"RAWSUD( -NPC: Hello -[if {AltPath}] -Player: This is the alternate path before choice -[else] -Player: This is the main path -[endif] - * Choice 1 - NPC: Choice 1 after conditional - * Choice 2 - NPC: Choice 2 after conditional - -NPC: Bye -)RAWSUD"; - - -IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestChoiceAfterConditionals, - "SUDSTest.TestChoiceAfterConditionals", - EAutomationTestFlags::EditorContext | - EAutomationTestFlags::ClientContext | - EAutomationTestFlags::ProductFilter) - - -bool FTestChoiceAfterConditionals::RunTest(const FString& Parameters) -{ - FSUDSMessageLogger Logger(false); - FSUDSScriptImporter Importer; - TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ChoiceAfterConditionals), ChoiceAfterConditionals.Len(), "ChoiceAfterConditionals", &Logger, true)); - - auto Script = NewObject(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); - - // For the first run, do not set any state - Dlg->Start(); - - TestDialogueText(this, "Text node", Dlg, "NPC", "Hello"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text node", Dlg, "Player", "This is the main path"); - TestEqual("Number of choices", Dlg->GetNumberOfChoices(), 2); - TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), "Choice 1"); - TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), "Choice 2"); - TestTrue("Choose", Dlg->Choose(0)); - TestDialogueText(this, "Text node", Dlg, "NPC", "Choice 1 after conditional"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text node", Dlg, "NPC", "Bye"); - - Script->MarkAsGarbage(); - return true; -} - -const FString ChoiceAfterNestedConditionals = R"RAWSUD( -NPC: Hello -[if {AltPath}] - Player: This is the alternate path before choice -[else] - Player: This is the main path - [if {AltPath2}] - Player: An alt-alt path - [endif] -[endif] -:choices - * Choice 1 - NPC: Choice 1 after conditional - * Choice 2 - NPC: Choice 2 after conditional - -NPC: Bye -)RAWSUD"; - -IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestChoiceAfterNestedConditionals, - "SUDSTest.TestChoiceAfterNestedConditionals", - EAutomationTestFlags::EditorContext | - EAutomationTestFlags::ClientContext | - EAutomationTestFlags::ProductFilter) - - -bool FTestChoiceAfterNestedConditionals::RunTest(const FString& Parameters) -{ - FSUDSMessageLogger Logger(false); - FSUDSScriptImporter Importer; - TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ChoiceAfterNestedConditionals), ChoiceAfterNestedConditionals.Len(), "ChoiceAfterNestedConditionals", &Logger, true)); - - auto Script = NewObject(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); - - // For the first run, do not set any state - Dlg->Start(); - - TestDialogueText(this, "Text node", Dlg, "NPC", "Hello"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text node", Dlg, "Player", "This is the main path"); - TestEqual("Number of choices", Dlg->GetNumberOfChoices(), 2); - TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), "Choice 1"); - TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), "Choice 2"); - TestTrue("Choose", Dlg->Choose(0)); - TestDialogueText(this, "Text node", Dlg, "NPC", "Choice 1 after conditional"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text node", Dlg, "NPC", "Bye"); - - Script->MarkAsGarbage(); - return true; -} - -const FString VarsSetBetweenTextAndChoiceChoice = R"RAWSUD( -# prove that the set node is run before the choices are evaluated even though it's after the text node -NPC: Hello -[set AltPath true] -[if {AltPath}] - * Alt Choice - Player: This is the alternate path choice -[else] - * Main Choice - Player: Main path choice -[endif] - * Common Choice - Player: Common choice here -NPC: Bye -)RAWSUD"; - -IMPLEMENT_SIMPLE_AUTOMATION_TEST(FVarsSetBetweenTextAndChoiceChoice, - "SUDSTest.VarsSetBetweenTextAndChoiceChoice", - EAutomationTestFlags::EditorContext | - EAutomationTestFlags::ClientContext | - EAutomationTestFlags::ProductFilter) - - -bool FVarsSetBetweenTextAndChoiceChoice::RunTest(const FString& Parameters) -{ - FSUDSMessageLogger Logger(false); - FSUDSScriptImporter Importer; - TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(VarsSetBetweenTextAndChoiceChoice), VarsSetBetweenTextAndChoiceChoice.Len(), "VarsSetBetweenTextAndChoiceChoice", &Logger, true)); - - auto Script = NewObject(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); - - // For the first run, do not set any state - Dlg->Start(); - - TestDialogueText(this, "Text node", Dlg, "NPC", "Hello"); - TestEqual("Number of choices", Dlg->GetNumberOfChoices(), 2); - TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), "Alt Choice"); - TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), "Common Choice"); - - Script->MarkAsGarbage(); - return true; -} - - -const FString MultipleOptionalChoicesWithLinesBetweenTextAndChoice = R"RAWSUD( -NPC: Hello -NPC: Testing -# This event being here could mess up multiple sibling optional choices -[event SomeEvent] - * Choice 1? - Player: Choice 1 - [if {Opt2}] - * Optional Choice 2? - Player: Choice 2 - [endif] - [if {Opt3}] - * Optional Choice 3? - Player: Choice 3 - [endif] - * Back - Player: going back -NPC: Fallthrough -)RAWSUD"; - -IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestMultipleOptionalChoicesWithLinesBetweenTextAndChoice, - "SUDSTest.TestMultipleOptionalChoicesWithLinesBetweenTextAndChoice", - EAutomationTestFlags::EditorContext | - EAutomationTestFlags::ClientContext | - EAutomationTestFlags::ProductFilter) - - -bool FTestMultipleOptionalChoicesWithLinesBetweenTextAndChoice::RunTest(const FString& Parameters) -{ - FSUDSMessageLogger Logger(false); - FSUDSScriptImporter Importer; - TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(MultipleOptionalChoicesWithLinesBetweenTextAndChoice), MultipleOptionalChoicesWithLinesBetweenTextAndChoice.Len(), "MultipleOptionalChoicesWithLinesBetweenTextAndChoice", &Logger, true)); - - auto Script = NewObject(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); - - // For the first run, do not set any state - Dlg->Start(); - - TestDialogueText(this, "Text node", Dlg, "NPC", "Hello"); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text node", Dlg, "NPC", "Testing"); - TestEqual("Number of choices", Dlg->GetNumberOfChoices(), 2); - TestEqual("Choice text 0", Dlg->GetChoiceText(0).ToString(), "Choice 1?"); - TestEqual("Choice text 1", Dlg->GetChoiceText(1).ToString(), "Back"); - - Dlg->Restart(true); - Dlg->SetVariableBoolean("Opt2", true); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text node", Dlg, "NPC", "Testing"); - TestEqual("Number of choices", Dlg->GetNumberOfChoices(), 3); - TestEqual("Choice text 0", Dlg->GetChoiceText(0).ToString(), "Choice 1?"); - TestEqual("Choice text 1", Dlg->GetChoiceText(1).ToString(), "Optional Choice 2?"); - TestEqual("Choice text 2", Dlg->GetChoiceText(2).ToString(), "Back"); - - Dlg->Restart(true); - Dlg->SetVariableBoolean("Opt3", true); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text node", Dlg, "NPC", "Testing"); - TestEqual("Number of choices", Dlg->GetNumberOfChoices(), 3); - TestEqual("Choice text 0", Dlg->GetChoiceText(0).ToString(), "Choice 1?"); - TestEqual("Choice text 1", Dlg->GetChoiceText(1).ToString(), "Optional Choice 3?"); - TestEqual("Choice text 2", Dlg->GetChoiceText(2).ToString(), "Back"); - - Dlg->Restart(true); - Dlg->SetVariableBoolean("Opt2", true); - Dlg->SetVariableBoolean("Opt3", true); - TestTrue("Continue", Dlg->Continue()); - TestDialogueText(this, "Text node", Dlg, "NPC", "Testing"); - TestEqual("Number of choices", Dlg->GetNumberOfChoices(), 4); - TestEqual("Choice text 0", Dlg->GetChoiceText(0).ToString(), "Choice 1?"); - TestEqual("Choice text 1", Dlg->GetChoiceText(1).ToString(), "Optional Choice 2?"); - TestEqual("Choice text 2", Dlg->GetChoiceText(2).ToString(), "Optional Choice 3?"); - TestEqual("Choice text 3", Dlg->GetChoiceText(3).ToString(), "Back"); - - Script->MarkAsGarbage(); - return true; -} - - -UE_ENABLE_OPTIMIZATION \ No newline at end of file diff --git a/Plugins/SUDS/Source/SUDSTest/Private/TestEventSub.cpp b/Plugins/SUDS/Source/SUDSTest/Private/TestEventSub.cpp deleted file mode 100644 index 88eef6e5..00000000 --- a/Plugins/SUDS/Source/SUDSTest/Private/TestEventSub.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#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& Args) -{ - EventRecords.Add(FEventRecord { EventName, Args }); -} - -void UTestEventSub::OnVariableChanged(USUDSDialogue* Dlg, FName VarName, const FSUDSValue& Value, bool bFromScript) -{ - SetVarRecords.Add(FSetVarRecord { VarName, Value, bFromScript }); -} diff --git a/Plugins/SUDS/Source/SUDSTest/Private/TestEventSub.h b/Plugins/SUDS/Source/SUDSTest/Private/TestEventSub.h deleted file mode 100644 index 5f8208dc..00000000 --- a/Plugins/SUDS/Source/SUDSTest/Private/TestEventSub.h +++ /dev/null @@ -1,39 +0,0 @@ -#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 Args; - }; - struct FSetVarRecord - { - FName Name; - FSUDSValue Value; - bool bFromScript; - }; - - TArray EventRecords; - TArray SetVarRecords; - - UFUNCTION() - void OnEvent(USUDSDialogue* Dlg, FName EventName, const TArray& Args); - - UFUNCTION() - void OnVariableChanged(USUDSDialogue* Dlg, FName VarName, const FSUDSValue& Value, bool bFromScript); - - -}; diff --git a/Plugins/SUDS/Source/SUDSTest/Private/TestEvents.cpp b/Plugins/SUDS/Source/SUDSTest/Private/TestEvents.cpp deleted file mode 100644 index e7673a70..00000000 --- a/Plugins/SUDS/Source/SUDSTest/Private/TestEvents.cpp +++ /dev/null @@ -1,278 +0,0 @@ -#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(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(); - EvtSub->Init(Dlg); - - // Participant - auto Participant = NewObject(); - 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 \ No newline at end of file diff --git a/Plugins/SUDS/Source/SUDSTest/Private/TestExpressions.cpp b/Plugins/SUDS/Source/SUDSTest/Private/TestExpressions.cpp deleted file mode 100644 index 2ec718b6..00000000 --- a/Plugins/SUDS/Source/SUDSTest/Private/TestExpressions.cpp +++ /dev/null @@ -1,209 +0,0 @@ -#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 Variables; - TMap 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 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 \ No newline at end of file diff --git a/Plugins/SUDS/Source/SUDSTest/Private/TestGotoGosub.cpp b/Plugins/SUDS/Source/SUDSTest/Private/TestGotoGosub.cpp deleted file mode 100644 index 33a86361..00000000 --- a/Plugins/SUDS/Source/SUDSTest/Private/TestGotoGosub.cpp +++ /dev/null @@ -1,417 +0,0 @@ -#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(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(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(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(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(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(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 \ No newline at end of file diff --git a/Plugins/SUDS/Source/SUDSTest/Private/TestMetadata.cpp b/Plugins/SUDS/Source/SUDSTest/Private/TestMetadata.cpp deleted file mode 100644 index 2dab21e4..00000000 --- a/Plugins/SUDS/Source/SUDSTest/Private/TestMetadata.cpp +++ /dev/null @@ -1,133 +0,0 @@ -#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(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 diff --git a/Plugins/SUDS/Source/SUDSTest/Private/TestParameters.cpp b/Plugins/SUDS/Source/SUDSTest/Private/TestParameters.cpp deleted file mode 100644 index 254f6154..00000000 --- a/Plugins/SUDS/Source/SUDSTest/Private/TestParameters.cpp +++ /dev/null @@ -1,147 +0,0 @@ -#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(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(); - 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(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(); - Participant1->TestNumber = 0; // priority 0 - auto Participant2 = NewObject(); - Participant2->TestNumber = 1; // priority 100 - auto Participant3 = NewObject(); - 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 \ No newline at end of file diff --git a/Plugins/SUDS/Source/SUDSTest/Private/TestParsing.cpp b/Plugins/SUDS/Source/SUDSTest/Private/TestParsing.cpp deleted file mode 100644 index ed22b5f3..00000000 --- a/Plugins/SUDS/Source/SUDSTest/Private/TestParsing.cpp +++ /dev/null @@ -1,1189 +0,0 @@ -#include "SUDSMessageLogger.h" -#include "SUDSScript.h" -#include "Misc/AutomationTest.h" -#include "SUDSScriptImporter.h" -#include "SUDSScriptNode.h" -#include "TestUtils.h" - -UE_DISABLE_OPTIMIZATION - -const FString SimpleParsingInput = R"RAWSUD( -=== -# Nothing in header but a comment -=== - -# A comment in body - -Player: Excuse me? -NPC: Well, hello there. This is a test. - * A test? - NPC: Yes, a test. This is some indented continuation text. - Player: Oh I see, thank you. - NPC: You're welcome. - * Another option - NPC: This is another option with an embedded choice. - * How far can this go? - NPC: Theoretically forever but who knows? - * This is an extra question - NPC: That should have been added to the previous choice - * Another question? - NPC: Yep, this one too - * A third level of questions? - NPC: Yes, really! - * Wow - NPC: IKR - * Continuation with no response, just fallthrough - Player: This is a level 2 fallthrough -Player: Well, that's all for now. This should appear for all paths as a fall-through. - This, in fact, is a multi-line piece of text - Which is joined to the previous text node with the line breaks -NPC: Bye! -)RAWSUD"; - -const FString GotoParsingInput = R"RAWSUD( -:start -:alsostart -Player: This is the start - :choice - * Go to end - NPC: How rude, bye then - [goto end] - * Nested option - NPC: Some nested text with formatting - * Go to goodbye - Player: Gotta go! - [go to goodbye] - * Skip - [goto secondchoice] - * This is a mistake - NPC: Oh no - [goto this_is_an_error] -:secondchoice -NPC: Yep, this one too - * Go back to choice - NPC: Okay! - [goto choice] - * Return to the start - NPC: Gotcha - [goto start] - * Alternative start, also with no text before - [goto alsostart] -:goodbye -NPC: Bye! -)RAWSUD"; - - -const FString SetVariableParsingInput = 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"] -[set SomeFloat 12.5] -[set SomeName `AName`] -[set EmbeddedQuoteString "Hello this has some \"Embedded Quotes\""] -=== - -Player: Hello -[set SomeInt 99] -NPC: Wotcha -# 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] -Player: Well - -)RAWSUD"; - - - -const FString ConditionalParsingInput = R"RAWSUD( -=== -# Nothing in header but a comment -=== - -# A comment in body - -Player: Excuse me? -NPC: Well, hello there. This is a test. - * A test? - NPC: Yes, a test. This is some indented continuation text. - Player: Oh I see, thank you. - [if $polite == 1] - NPC: You're welcome. - [else] - NPC: Too right. - [endif] - * Another option - NPC: This is another option with an embedded choice. - * How far can this go? - NPC: Theoretically forever but who knows? - [if $extraq] - * This is an extra question - NPC: That should have been added to the previous choice - * Another question? - NPC: Yep, this one too - [endif] -Player: Well, that's all for now. This should appear for all paths as a fall-through. - This, in fact, is a multi-line piece of text - Which is joined to the previous text node with the line breaks -NPC: Bye! -)RAWSUD"; - - -IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestSimpleParsing, - "SUDSTest.TestSimpleParsing", - EAutomationTestFlags::EditorContext | - EAutomationTestFlags::ClientContext | - EAutomationTestFlags::ProductFilter) - - -bool FTestSimpleParsing::RunTest(const FString& Parameters) -{ - FSUDSMessageLogger Logger(false); - FSUDSScriptImporter Importer; - TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(SimpleParsingInput), SimpleParsingInput.Len(), "SimpleParsingInput", &Logger, true)); - - // Test the content of the parsing - auto RootNode = Importer.GetNode(0); - if (!TestNotNull("Root node should exist", RootNode)) - return false; - - TestEqual("Root node type", RootNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Root node speaker", RootNode->Identifier, "Player"); - TestEqual("Root node text", RootNode->Text, "Excuse me?"); - TestEqual("Root node path", RootNode->ChoicePath, "/"); - TestEqual("Root node edges", RootNode->Edges.Num(), 1); - - auto NextNode = Importer.GetNode(RootNode->Edges[0].TargetNodeIdx); - if (!TestNotNull("Next node should exist", NextNode)) - return false; - - TestEqual("Second node type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Second node speaker", NextNode->Identifier, "NPC"); - TestEqual("Second node text", NextNode->Text, "Well, hello there. This is a test."); - TestEqual("Second node path", RootNode->ChoicePath, "/"); - TestEqual("Second node edges", NextNode->Edges.Num(), 1); - - NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); - if (!TestNotNull("Third node should exist", NextNode)) - return false; - TestEqual("Third node type", NextNode->NodeType, ESUDSParsedNodeType::Choice); - // Choice itself is still at root, only the edges (individual choices) introduce new path levels - TestEqual("Third node path", NextNode->ChoicePath, "/"); - TestEqual("Third node edges", NextNode->Edges.Num(), 2); - - auto Choice1Node = NextNode; - const FSUDSParsedNode* FallthroughNode = nullptr; - if (NextNode->Edges.Num() >= 2) - { - TestEqual("Choice 1 node edge 0 text", Choice1Node->Edges[0].Text, "A test?"); - TestEqual("Choice 1 node edge 1 text", Choice1Node->Edges[1].Text, "Another option"); - // Follow choice 1 - NextNode = Importer.GetNode(Choice1Node->Edges[0].TargetNodeIdx); - if (TestNotNull("Next node should exist", NextNode)) - { - TestEqual("Choice 1 1st text node type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Choice 1 1st text node speaker", NextNode->Identifier, "NPC"); - TestEqual("Choice 1 1st text node text", NextNode->Text, "Yes, a test. This is some indented continuation text."); - TestEqual("Choice 1 1st text node path", NextNode->ChoicePath, "/C001/"); - TestEqual("Choice 1 1st text node edges", NextNode->Edges.Num(), 1); - NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); - if (TestNotNull("Next node should exist", NextNode)) - { - TestEqual("Choice 1 2nd text node type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Choice 1 2nd text node speaker", NextNode->Identifier, "Player"); - TestEqual("Choice 1 2nd text node text", NextNode->Text, "Oh I see, thank you."); - TestEqual("Choice 1 2nd text node path", NextNode->ChoicePath, "/C001/"); - TestEqual("Choice 1 2nd text node edges", NextNode->Edges.Num(), 1); - NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); - if (TestNotNull("Next node should exist", NextNode)) - { - TestEqual("Choice 1 3rd text node type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Choice 1 3rd text node speaker", NextNode->Identifier, "NPC"); - TestEqual("Choice 1 3rd text node text", NextNode->Text, "You're welcome."); - TestEqual("Choice 1 3rd text node path", NextNode->ChoicePath, "/C001/"); - - // Should fall through, all the way to the end and not to "level 2 fallthrough" since that's deeper level - TestEqual("Choice 1 3rd text node edges", NextNode->Edges.Num(), 1); - if (NextNode->Edges.Num() >= 1) - { - auto LinkedNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); - if (TestNotNull("Choice 1 3rd text linked node", LinkedNode)) - { - TestTrue("Choice 1 3rd text target node", LinkedNode->Text.StartsWith("Well, that's all for now")); - FallthroughNode = LinkedNode; - } - } - } - } - } - - - // Follow choice 2 - NextNode = Importer.GetNode(Choice1Node->Edges[1].TargetNodeIdx); - if (!TestNotNull("Next node should exist", NextNode)) - return false; - - TestEqual("Choice 2 1st text node type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Choice 2 1st text node speaker", NextNode->Identifier, "NPC"); - TestEqual("Choice 2 1st text node text", NextNode->Text, "This is another option with an embedded choice."); - TestEqual("Choice 2 2nd text node path", NextNode->ChoicePath, "/C002/"); - TestEqual("Choice 2 1st text node edges", NextNode->Edges.Num(), 1); - NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); - if (!TestNotNull("Next node should exist", NextNode)) - return false; - // This is nested choice node - TestEqual("Choice 2 1st text node type", NextNode->NodeType, ESUDSParsedNodeType::Choice); - TestEqual("Choice 2 nested choice edges", NextNode->Edges.Num(), 3); - - auto NestedChoiceNode = NextNode; - if (NestedChoiceNode->Edges.Num() >= 3) - { - TestEqual("Nested choice edge text 0", NestedChoiceNode->Edges[0].Text, "How far can this go?"); - - NextNode = Importer.GetNode(NestedChoiceNode->Edges[0].TargetNodeIdx); - if (TestNotNull("Next node should exist", NextNode)) - { - TestEqual("Nested Choice 1st text node type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Nested Choice 1st text node speaker", NextNode->Identifier, "NPC"); - TestEqual("Nested Choice 1st text node text", NextNode->Text, "Theoretically forever but who knows?"); - // Choice edges are assigned unique numbers in ascending order, but nested - // This helps with fallthrough - TestEqual("Nested Choice 1st text node path", NextNode->ChoicePath, "/C002/C003/"); - - if (TestEqual("Nested Choice 1st text node edges", NextNode->Edges.Num(), 1)) - { - // Should fall through - auto LinkedNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); - if (TestNotNull("Nested Choice 1st linked node", LinkedNode)) - { - TestEqual("Nested Choice 1st text target node", LinkedNode->Text, "This is a level 2 fallthrough"); - } - } - } - - TestEqual("Nested choice edge text 1", NestedChoiceNode->Edges[1].Text, "This is an extra question"); - NextNode = Importer.GetNode(NestedChoiceNode->Edges[1].TargetNodeIdx); - if (TestNotNull("Next node should exist", NextNode)) - { - TestEqual("Nested Choice 2nd text node type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Nested Choice 2nd text node speaker", NextNode->Identifier, "NPC"); - TestEqual("Nested Choice 2nd text node text", NextNode->Text, "That should have been added to the previous choice"); - TestEqual("Nested Choice 2nd text node path", NextNode->ChoicePath, "/C002/C004/"); - TestEqual("Nested Choice 2nd text node edges", NextNode->Edges.Num(), 1); - if (TestEqual("Nested Choice 2nd text node edges", NextNode->Edges.Num(), 1)) - { - // Should fall through - auto LinkedNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); - if (TestNotNull("Nested Choice 2nd linked node", LinkedNode)) - { - TestEqual("Nested Choice 2nd text target node", LinkedNode->Text, "This is a level 2 fallthrough"); - } - } - } - - TestEqual("Nested choice edge text 2", NestedChoiceNode->Edges[2].Text, "Another question?"); - NextNode = Importer.GetNode(NestedChoiceNode->Edges[2].TargetNodeIdx); - if (TestNotNull("Next node should exist", NextNode)) - { - TestEqual("Nested Choice 3rd text node type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Nested Choice 3rd text node speaker", NextNode->Identifier, "NPC"); - TestEqual("Nested Choice 3rd text node text", NextNode->Text, "Yep, this one too"); - TestEqual("Nested Choice 3rd text node path", NextNode->ChoicePath, "/C002/C005/"); - if (TestEqual("Nested Choice 3rd text node edges", NextNode->Edges.Num(), 1)) - { - // Double nested - auto DoubleChoiceNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); - if (TestNotNull("Double Nested Choice node", DoubleChoiceNode)) - { - TestEqual("Double Nested Choice node type", DoubleChoiceNode->NodeType, ESUDSParsedNodeType::Choice); - if (TestEqual("Double Nested Choice node edges", DoubleChoiceNode->Edges.Num(), 3)) - { - - TestEqual("Double Nested choice edge text 0", DoubleChoiceNode->Edges[0].Text, "A third level of questions?"); - NextNode = Importer.GetNode(DoubleChoiceNode->Edges[0].TargetNodeIdx); - if (TestNotNull("Double Nested Choice node option 0", NextNode)) - { - TestEqual("Double Nested Choice option 0 text node type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Double Nested Choice option 0 text node speaker", NextNode->Identifier, "NPC"); - TestEqual("Double Nested Choice option 0 text node text", NextNode->Text, "Yes, really!"); - // Should fall through - if (TestEqual("Double Nested Choice option 0 text edge", NextNode->Edges.Num(), 1)) - NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); - if (TestNotNull("Double Nested Choice option 0 text linked node", NextNode)) - { - TestEqual("Double Nested Choice option 0 text linked node", NextNode->Text, "This is a level 2 fallthrough"); - } - } - - TestEqual("Double Nested choice edge text 1", DoubleChoiceNode->Edges[1].Text, "Wow"); - NextNode = Importer.GetNode(DoubleChoiceNode->Edges[1].TargetNodeIdx); - if (TestNotNull("Double Nested Choice node option 1", NextNode)) - { - TestEqual("Double Nested Choice option 1 text node type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Double Nested Choice option 1 text node speaker", NextNode->Identifier, "NPC"); - TestEqual("Double Nested Choice option 1 text node text", NextNode->Text, "IKR"); - // Should fall through - if (TestEqual("Double Nested Choice option 1 text edge", NextNode->Edges.Num(), 1)) - NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); - if (TestNotNull("Double Nested Choice option 0 text linked node", NextNode)) - { - TestEqual("Double Nested Choice option 0 text linked node", NextNode->Text, "This is a level 2 fallthrough"); - } - } - - // Last Should fall through - TestEqual("Double Nested choice edge text 2", DoubleChoiceNode->Edges[2].Text, "Continuation with no response, just fallthrough"); - auto LinkedNode = Importer.GetNode(DoubleChoiceNode->Edges[2].TargetNodeIdx); - if (TestNotNull("Nested Choice 3rd linked node", LinkedNode)) - { - TestEqual("Nested Choice 3rd text target node", LinkedNode->Text, "This is a level 2 fallthrough"); - } - } - - } - - } - } - - } - - if (TestNotNull("Should have found fallthrough node", FallthroughNode)) - { - // Test the final fallthrough - TestEqual("Fallthrough node type", FallthroughNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Fallthrough node speaker", FallthroughNode->Identifier, "Player"); - TestEqual("Fallthrough node text", FallthroughNode->Text, "Well, that's all for now. This should appear for all paths as a fall-through.\nThis, in fact, is a multi-line piece of text\nWhich is joined to the previous text node with the line breaks"); - if (TestEqual("Fallthrough node edge count", FallthroughNode->Edges.Num(), 1)) - { - NextNode = Importer.GetNode(FallthroughNode->Edges[0].TargetNodeIdx); - if (TestNotNull("Fallthrough node next node not null", NextNode)) - { - TestEqual("Fallthrough node 2 type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Fallthrough node 2 speaker", NextNode->Identifier, "NPC"); - TestEqual("Fallthrough node 2 text", NextNode->Text, "Bye!"); - // Should have no further edges since is at end - TestEqual("Fallthrough node 2 edge count", NextNode->Edges.Num(), 0); - } - } - - - } - - } - - return true; -} - - -IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestGotoParsing, - "SUDSTest.TestGotoParsing", - EAutomationTestFlags::EditorContext | - EAutomationTestFlags::ClientContext | - EAutomationTestFlags::ProductFilter) - - -bool FTestGotoParsing::RunTest(const FString& Parameters) -{ - FSUDSMessageLogger Logger(false); - FSUDSScriptImporter Importer; - TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(GotoParsingInput), GotoParsingInput.Len(), "GotoParsingInput", &Logger, true)); - - // Test the content of the parsing - auto RootNode = Importer.GetNode(0); - if (!TestNotNull("Root node should exist", RootNode)) - return false; - - TestEqual("Root node type", RootNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Root node speaker", RootNode->Identifier, "Player"); - TestEqual("Root node text", RootNode->Text, "This is the start"); - TestEqual("Root node edges", RootNode->Edges.Num(), 1); - - auto NextNode = Importer.GetNode(RootNode->Edges[0].TargetNodeIdx); - if (!TestNotNull("Next node should exist", NextNode)) - return false; - - TestEqual("Choice node type", NextNode->NodeType, ESUDSParsedNodeType::Choice); - if (TestEqual("Choice node edges", NextNode->Edges.Num(), 2)) - { - auto ChoiceNode = NextNode; - TestEqual("Choice 1 text", ChoiceNode->Edges[0].Text, "Go to end"); - NextNode = Importer.GetNode(ChoiceNode->Edges[0].TargetNodeIdx); - if (TestNotNull("Next node should not be null", NextNode)) - { - TestEqual("Goto End node text type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Goto End node text speaker", NextNode->Identifier, "NPC"); - TestEqual("Goto End node text text", NextNode->Text, "How rude, bye then"); - if (TestEqual("Goto End node text edges", NextNode->Edges.Num(), 1)) - { - NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); - if (TestNotNull("Goto node should not be null", NextNode)) - { - TestEqual("Goto End node type", NextNode->NodeType, ESUDSParsedNodeType::Goto); - TestEqual("Goto End node label", NextNode->Identifier, FSUDSScriptImporter::EndGotoLabel); - } - - } - - } - TestEqual("Choice 2 text", ChoiceNode->Edges[1].Text, "Nested option"); - NextNode = Importer.GetNode(ChoiceNode->Edges[1].TargetNodeIdx); - if (TestNotNull("Next node should not be null", NextNode)) - { - TestEqual("Choice 2 node text type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Choice 2 node text speaker", NextNode->Identifier, "NPC"); - TestEqual("Choice 2 node text text", NextNode->Text, "Some nested text with formatting"); - if (TestEqual("Choice 2 node text edges", NextNode->Edges.Num(), 1)) - { - NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); - if (TestNotNull("Nested choice node should not be null", NextNode)) - { - TestEqual("Goto End node type", NextNode->NodeType, ESUDSParsedNodeType::Choice); - auto NestedChoice = NextNode; - if (TestEqual("Nested Choice node edges", NextNode->Edges.Num(), 3)) - { - TestEqual("Nested choice 0 text", NestedChoice->Edges[0].Text, "Go to goodbye"); - NextNode = Importer.GetNode(NestedChoice->Edges[0].TargetNodeIdx); - if (TestNotNull("Should not be null", NextNode)) - { - TestEqual("Nested choice 0 text type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Nested choice 0 text speaker", NextNode->Identifier, "Player"); - TestEqual("Nested choice 0 text text", NextNode->Text, "Gotta go!"); - - if (TestEqual("Nested Choice textnode edges", NextNode->Edges.Num(), 1)) - { - NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); - if (TestNotNull("Should not be null", NextNode)) - { - // This is the goto - TestEqual("Nested choice 0 goto type", NextNode->NodeType, ESUDSParsedNodeType::Goto); - TestEqual("Nested choice 0 goto label", NextNode->Identifier, "goodbye"); - const int DestIdx = Importer.GetGotoTargetNodeIndex(NextNode->Identifier); - TestNotEqual("Label should be valid", DestIdx, -1); - NextNode = Importer.GetNode(DestIdx); - if (TestNotNull("Goto dest node", NextNode)) - { - TestEqual("Goto dest text type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Goto dest text speaker", NextNode->Identifier, "NPC"); - TestEqual("Goto dest text text", NextNode->Text, "Bye!"); - } - } - } - } - TestEqual("Nested choice 1 text", NestedChoice->Edges[1].Text, "Skip"); - NextNode = Importer.GetNode(NestedChoice->Edges[1].TargetNodeIdx); - if (TestNotNull("Should not be null", NextNode)) - { - // This one goes straight to goto - TestEqual("Nested choice 1 goto type", NextNode->NodeType, ESUDSParsedNodeType::Goto); - TestEqual("Nested choice 1 goto label", NextNode->Identifier, "secondchoice"); - - const int DestIdx = Importer.GetGotoTargetNodeIndex(NextNode->Identifier); - TestNotEqual("Label should be valid", DestIdx, -1); - NextNode = Importer.GetNode(DestIdx); - if (TestNotNull("Goto dest node", NextNode)) - { - TestEqual("Goto dest text type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Goto dest text speaker", NextNode->Identifier, "NPC"); - TestEqual("Goto dest text text", NextNode->Text, "Yep, this one too"); - - } - } - TestEqual("Nested choice 2 text", NestedChoice->Edges[2].Text, "This is a mistake"); - NextNode = Importer.GetNode(NestedChoice->Edges[2].TargetNodeIdx); - if (TestNotNull("Should not be null", NextNode)) - { - TestEqual("Nested choice 2 text type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Nested choice 2 text speaker", NextNode->Identifier, "NPC"); - TestEqual("Nested choice 2 text text", NextNode->Text, "Oh no"); - - if (TestEqual("Nested Choice text node edges", NextNode->Edges.Num(), 1)) - { - NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); - if (TestNotNull("Should not be null", NextNode)) - { - // This is the goto - TestEqual("Nested choice 0 goto type", NextNode->NodeType, ESUDSParsedNodeType::Goto); - TestEqual("Nested choice 0 goto label", NextNode->Identifier, "this_is_an_error"); - TestEqual("Label should go nowhere", Importer.GetGotoTargetNodeIndex(NextNode->Identifier), -1); - } - } - - } - - } - } - - } - - } - - // Pick up the latter part (only reachable by goto) - NextNode = Importer.GetNode(Importer.GetGotoTargetNodeIndex("secondchoice")); - if (TestNotNull("End choice node should not be null", NextNode)) - { - TestEqual("End choice node text type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("End choice node text speaker", NextNode->Identifier, "NPC"); - TestEqual("End choice node text text", NextNode->Text, "Yep, this one too"); - if (TestEqual("End choice node text edges", NextNode->Edges.Num(), 1)) - { - NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); - if (TestNotNull("", NextNode)) - { - TestEqual("End choice node text type", NextNode->NodeType, ESUDSParsedNodeType::Choice); - - if (TestEqual("End choice node edges", NextNode->Edges.Num(), 3)) - { - auto EndChoiceNode = NextNode; - TestEqual("End choice 0 text", EndChoiceNode->Edges[0].Text, "Go back to choice"); - NextNode = Importer.GetNode(EndChoiceNode->Edges[0].TargetNodeIdx); - if (TestNotNull("", NextNode)) - { - TestEqual("Node text type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Node text speaker", NextNode->Identifier, "NPC"); - TestEqual("Node text text", NextNode->Text, "Okay!"); - if (TestEqual("Next node edges", NextNode->Edges.Num(), 1)) - { - NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); - if (TestNotNull("", NextNode)) - { - TestEqual("Should be goto node", NextNode->NodeType, ESUDSParsedNodeType::Goto); - TestEqual("Goto label", NextNode->Identifier, "choice"); - - // This should lead back to a choice node, ie letting the previous text node use the same choices - // without repeating the text (loop with context) - const int DestIdx = Importer.GetGotoTargetNodeIndex(NextNode->Identifier); - TestNotEqual("Label should be valid", DestIdx, -1); - NextNode = Importer.GetNode(DestIdx); - if (TestNotNull("Goto dest node", NextNode)) - { - TestEqual("Goto dest choice type", NextNode->NodeType, ESUDSParsedNodeType::Choice); - // Just make sure it's the right choice node - if (TestEqual("Goto dest choice edges", NextNode->Edges.Num(), 2)) - { - TestEqual("Goto dest choice check edge", NextNode->Edges[0].Text, "Go to end"); - } - } - - } - } - - NextNode = Importer.GetNode(EndChoiceNode->Edges[1].TargetNodeIdx); - if (TestNotNull("", NextNode)) - { - TestEqual("Node text type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Node text speaker", NextNode->Identifier, "NPC"); - TestEqual("Node text text", NextNode->Text, "Gotcha"); - if (TestEqual("Next node edges", NextNode->Edges.Num(), 1)) - { - NextNode = Importer.GetNode(NextNode->Edges[0].TargetNodeIdx); - if (TestNotNull("", NextNode)) - { - TestEqual("Should be goto node", NextNode->NodeType, ESUDSParsedNodeType::Goto); - TestEqual("Goto label", NextNode->Identifier, "start"); - - // This should lead back to a choice node, ie letting the previous text node use the same choices - // without repeating the text (loop with context) - const int DestIdx = Importer.GetGotoTargetNodeIndex(NextNode->Identifier); - TestNotEqual("Label should be valid", DestIdx, -1); - NextNode = Importer.GetNode(DestIdx); - if (TestNotNull("Goto dest node", NextNode)) - { - // Just make sure it's the right node - TestEqual("Goto dest text type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Node text speaker", NextNode->Identifier, "Player"); - TestEqual("Node text text", NextNode->Text, "This is the start"); - } - - } - } - } - NextNode = Importer.GetNode(EndChoiceNode->Edges[2].TargetNodeIdx); - if (TestNotNull("", NextNode)) - { - // Straight to goto - TestEqual("Should be goto node", NextNode->NodeType, ESUDSParsedNodeType::Goto); - TestEqual("Goto label", NextNode->Identifier, "alsostart"); - - // This should lead back to a choice node, ie letting the previous text node use the same choices - // without repeating the text (loop with context) - const int DestIdx = Importer.GetGotoTargetNodeIndex(NextNode->Identifier); - TestNotEqual("Label should be valid", DestIdx, -1); - NextNode = Importer.GetNode(DestIdx); - if (TestNotNull("Goto dest node", NextNode)) - { - // Just make sure it's the right node - TestEqual("Goto dest text type", NextNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Node text speaker", NextNode->Identifier, "Player"); - TestEqual("Node text text", NextNode->Text, "This is the start"); - } - } - } - } - } - } - - - - } - - // Test that start & autostart point to the same place - const int StartNodeIdx = Importer.GetGotoTargetNodeIndex("start"); - const auto StartNode = Importer.GetNode(StartNodeIdx); - if (TestNotNull("Second choice node should not be null", StartNode)) - { - TestEqual("Start node text type", StartNode->NodeType, ESUDSParsedNodeType::Text); - TestEqual("Start node text speaker", StartNode->Identifier, "Player"); - TestEqual("Start node text text", StartNode->Text, "This is the start"); - } - const int AlsoStartNodeIdx = Importer.GetGotoTargetNodeIndex("alsostart"); - TestEqual("start and alsostart should reference the same node", AlsoStartNodeIdx, StartNodeIdx); - - } - - return true; -} - -IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestSetVariableParsing, - "SUDSTest.TestSetVariableParsing", - EAutomationTestFlags::EditorContext | - EAutomationTestFlags::ClientContext | - EAutomationTestFlags::ProductFilter) - - -bool FTestSetVariableParsing::RunTest(const FString& Parameters) -{ - FSUDSMessageLogger Logger(false); - FSUDSScriptImporter Importer; - TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(SetVariableParsingInput), SetVariableParsingInput.Len(), "SetVariableParsingInput", &Logger, true)); - // Test the content of the parsing - - // Header nodes first - auto NextNode = Importer.GetHeaderNode(0); - TestParsedSetLiteral(this, "Header node 1", NextNode, "SpeakerName.Player", "Protagonist"); - TestGetParsedNextNode(this, "Header node 1 next", NextNode, Importer, true, &NextNode); - TestParsedSetLiteral(this, "Header node 2", NextNode, "ValetName", "Bob"); - TestGetParsedNextNode(this, "Header node 2 next", NextNode, Importer, true, &NextNode); - TestParsedSetLiteral(this, "Header node 3", NextNode, "SomeFloat", 12.5f); - TestGetParsedNextNode(this, "Header node 3 next", NextNode, Importer, true, &NextNode); - TestParsedSetLiteral(this, "Header node 4", NextNode, "SomeName", FName("AName")); - TestGetParsedNextNode(this, "Header node 4 next", NextNode, Importer, true, &NextNode); - TestParsedSetLiteral(this, "Header node 5", NextNode, "EmbeddedQuoteString", "Hello this has some \"Embedded Quotes\""); - - // Now body nodes - NextNode = Importer.GetNode(0); - TestParsedText(this, "Root node", NextNode, "Player", "Hello"); - TestGetParsedNextNode(this, "Node 1 next", NextNode, Importer, false, &NextNode); - TestParsedSetLiteral(this, "Node 2", NextNode, "SomeInt", 99); - TestGetParsedNextNode(this, "Node 2 next", NextNode, Importer, false, &NextNode); - TestParsedText(this, "Node 3 text", NextNode, "NPC", "Wotcha"); - TestGetParsedNextNode(this, "Node 3 next", NextNode, Importer, false, &NextNode); - TestParsedSetLiteral(this, "Node 4", NextNode, "SomeGender", ETextGender::Masculine); - TestGetParsedNextNode(this, "Node 4 next", NextNode, Importer, false, &NextNode); - TestParsedChoice(this, "Node 5 choice", NextNode, 2); - auto ChoiceNode = NextNode; - TestParsedChoiceEdge(this, "Choice 1 edge", ChoiceNode, 0, "Choice 1", Importer, &NextNode); - TestParsedSetLiteral(this, "Choice 1 set", NextNode, "SomeBoolean", true); - TestGetParsedNextNode(this, "Choice 1 next", NextNode, Importer, false, &NextNode); - TestParsedText(this, "Choice 1 text", NextNode, "NPC", "Truth"); - - TestParsedChoiceEdge(this, "Choice 2 edge", ChoiceNode, 1, "Choice 2", Importer, &NextNode); - TestParsedText(this, "Choice 2 text", NextNode, "NPC", "Surprise"); - TestGetParsedNextNode(this, "Choice 2 next", NextNode, Importer, false, &NextNode); - TestParsedSetLiteral(this, "Choice 2 set", NextNode, "ValetName", "Kate"); - TestGetParsedNextNode(this, "Choice 2 next 2", NextNode, Importer, false, &NextNode); - TestParsedSetLiteral(this, "Choice 2 set 2", NextNode, "SomeGender", ETextGender::Feminine); - - return true; -} - - - - -IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestConversionToRuntime, - "SUDSTest.TestConversionToRuntime", - EAutomationTestFlags::EditorContext | - EAutomationTestFlags::ClientContext | - EAutomationTestFlags::ProductFilter) - - -bool FTestConversionToRuntime::RunTest(const FString& Parameters) -{ - FSUDSMessageLogger Logger(false); - FSUDSScriptImporter Importer; - TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(GotoParsingInput), GotoParsingInput.Len(), "GotoParsingInput", &Logger, true)); - - auto Asset = NewObject(GetTransientPackage(), "Test"); - const ScopedStringTableHolder StringTableHolder; - Importer.PopulateAsset(Asset, StringTableHolder.StringTable); - - auto StartNode = Asset->GetFirstNode(); - if (!TestNotNull("Start node should be true", StartNode)) - { - return false; - } - - TestTextNode(this, "Start node", StartNode, "Player", "This is the start"); - - auto NextNode = StartNode; - if (!TestEqual("Start node edges", NextNode->GetEdgeCount(), 1)) - { - return false; - } - - auto pEdge = NextNode->GetEdge(0); - if (!TestNotNull("Start node edge", pEdge)) - { - return false; - } - - NextNode = pEdge->GetTargetNode().Get(); - if (TestChoiceNode(this, "Start node next", NextNode, 2)) - { - auto ChoiceNode = NextNode; - if (TestChoiceEdge(this, "Choice 1 text", ChoiceNode, 0, "Go to end", &NextNode)) - { - TestTextNode(this, "Goto End node text type", NextNode, "NPC", "How rude, bye then"); - // This is a goto end so should just have one edge - auto GotoEdge = NextNode->GetEdge(0); - if (TestNotNull("Goto end node", GotoEdge)) - { - TestFalse("Goto end should be null", GotoEdge->GetTargetNode().IsValid()); - } - } - - if (TestChoiceEdge(this, "Choice 2 text", ChoiceNode, 1, "Nested option", &NextNode)) - { - TestTextNode(this, "Choice 2 node text type", NextNode, "NPC", "Some nested text with formatting"); - if (TestEdge(this, "Choice 2 node text edges", NextNode, 0, &NextNode)) - { - if (TestChoiceNode(this, "Goto End node type", NextNode, 3)) - { - auto NestedChoice = NextNode; - if (TestChoiceEdge(this, "Nested choice 0 text", NestedChoice, 0, "Go to goodbye", &NextNode)) - { - TestTextNode(this, "Nested choice 0 text type", NextNode, "Player", "Gotta go!"); - if (TestEdge(this, "Nested Choice textnode edges", NextNode, 0, &NextNode)) - { - // This was the goto which should have led to goodbye - TestTextNode(this, "Check goto goodbye", NextNode, "NPC", "Bye!"); - } - } - - if (TestChoiceEdge(this, "Nested choice 1 text", NestedChoice, 1, "Skip", &NextNode)) - { - // This will go directly to secondchoice node - TestTextNode(this, "Nested choice 1 goto", NextNode, "NPC", "Yep, this one too"); - } - - if (TestChoiceEdge(this, "Nested choice 2 text", NestedChoice, 2, "This is a mistake", &NextNode)) - { - // there's a text node then a failed goto (nowhere) - TestTextNode(this, "Nested choice 2 node", NextNode, "NPC", "Oh no"); - auto GotoEdge = NextNode->GetEdge(0); - if (TestNotNull("Nested choice 2 node edge", GotoEdge)) - { - TestFalse("Nested choice 2 node goes nowhere", GotoEdge->GetTargetNode().IsValid()); - } - - } - } - } - } - - // Pick up the latter part (only reachable by goto) - NextNode = Asset->GetNodeByLabel("secondchoice"); - if (TestTextNode(this, "secondchoice node", NextNode, "NPC", "Yep, this one too")) - { - if (TestEdge(this, "secondchoice node text edges", NextNode, 0, &NextNode)) - { - if (TestChoiceNode(this, "secondchoice next choice", NextNode, 3)) - { - auto EndChoiceNode = NextNode; - if (TestChoiceEdge(this, "End choice 0 edge", EndChoiceNode, 0, "Go back to choice", &NextNode)) - { - TestTextNode(this, "Node text", NextNode, "NPC", "Okay!"); - if (TestEdge(this, "Next node edges", NextNode, 0, &NextNode)) - { - // Should go back to :choice node - TestChoiceNode(this, "Check choice goto", NextNode, 2); - TestChoiceEdge(this, "Check choice goto", NextNode, 0, "Go to end", &NextNode); - } - } - if (TestChoiceEdge(this, "End choice 1 edge", EndChoiceNode, 1, "Return to the start", &NextNode)) - { - TestTextNode(this, "Node text", NextNode, "NPC", "Gotcha"); - if (TestEdge(this, "Next node edges", NextNode, 0, &NextNode)) - { - // Should go back to :start - TestTextNode(this, "Check goto text", NextNode, "Player", "This is the start"); - } - } - if (TestChoiceEdge(this, "End choice 2 edge", EndChoiceNode, 2, "Alternative start, also with no text before", &NextNode)) - { - // Should go directly back to :alsostart, same as :start - TestTextNode(this, "Check goto text", NextNode, "Player", "This is the start"); - } - } - } - } - } - - - - - // check goto labels - auto GotoNode = Asset->GetNodeByLabel("start"); - TestTextNode(this, "Goto Start", GotoNode, "Player", "This is the start"); - GotoNode = Asset->GetNodeByLabel("alsostart"); - TestTextNode(this, "Goto alsostart", GotoNode, "Player", "This is the start"); - GotoNode = Asset->GetNodeByLabel("choice"); - TestChoiceNode(this, "Goto choice", GotoNode, 2); - GotoNode = Asset->GetNodeByLabel("secondchoice"); - TestTextNode(this, "Goto secondchoice", GotoNode, "NPC", "Yep, this one too"); - GotoNode = Asset->GetNodeByLabel("goodbye"); - TestTextNode(this, "Goto goodbye", GotoNode, "NPC", "Bye!"); - - // Test speakers - TestEqual("Num speakers", Asset->GetSpeakers().Num(), 2); - TestTrue("Speaker 1", Asset->GetSpeakers().Contains("Player")); - TestTrue("Speaker 2", Asset->GetSpeakers().Contains("NPC")); - - return true; -} - -const FString PartiallyLocalisedInput = R"RAWSUD( -Vagabond: Well met, fellow! - * Er, hi? - Vagabond: Verily, 'tis wondrous to see such a fine fellow on the road this morn! - [goto FriendlyChat] - * Jog on, mate @0001@ - Vagabond: Well, really! Good day then sir! @0002@ - [goto end] - -:FriendlyChat -Vagabond: Mayhaps we could travel together a while, and share a tale or two? @0007@ -What do you say? -)RAWSUD"; - -IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestPartiallyLocalised, - "SUDSTest.TestPartiallyLocalised", - EAutomationTestFlags::EditorContext | - EAutomationTestFlags::ClientContext | - EAutomationTestFlags::ProductFilter) - - -bool FTestPartiallyLocalised::RunTest(const FString& Parameters) -{ - FSUDSMessageLogger Logger(false); - FSUDSScriptImporter Importer; - TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(PartiallyLocalisedInput), PartiallyLocalisedInput.Len(), "PartiallyLocalisedInput", &Logger, true)); - - // Test the content of the parsing - auto NextNode = Importer.GetNode(0); - - TestParsedText(this, "Start node", NextNode, "Vagabond", "Well met, fellow!"); - // Test that we generated unique textIDs for inserted lines that are unique - // Should be after the last explicit one - TestFalse("TextID should be populated", NextNode->TextID.IsEmpty()); - TestEqual("TextID should be correct", NextNode->TextID, "@0008@"); - TestGetParsedNextNode(this, "Next", NextNode, Importer, false, &NextNode); - if (TestParsedChoice(this, "First choice", NextNode, 2)) - { - TestFalse("TextID should be populated", NextNode->Edges[0].TextID.IsEmpty()); - TestEqual("TextID should be correct", NextNode->Edges[0].TextID, "@0009@"); - TestFalse("TextID should be populated", NextNode->Edges[1].TextID.IsEmpty()); - TestEqual("TextID should be correct", NextNode->Edges[1].TextID, "@0001@"); - - TestParsedChoiceEdge(this, "First choice", NextNode, 0, "Er, hi?", Importer, &NextNode); - TestParsedText(this, "Next node", NextNode, "Vagabond", "Verily, 'tis wondrous to see such a fine fellow on the road this morn!"); - TestFalse("TextID should be populated", NextNode->TextID.IsEmpty()); - TestEqual("TextID should be correct", NextNode->TextID, "@000a@"); - - TestGetParsedNextNode(this, "Next", NextNode, Importer, false, &NextNode); - TestParsedGoto(this, "Goto", NextNode, Importer, &NextNode); - TestParsedText(this, "Next node", NextNode, "Vagabond", "Mayhaps we could travel together a while, and share a tale or two?\nWhat do you say?"); - TestEqual("TextID should be correct", NextNode->TextID, "@0007@"); - } - - NextNode = Importer.GetNode(0); - TestGetParsedNextNode(this, "Next", NextNode, Importer, false, &NextNode); - if (TestParsedChoice(this, "First choice", NextNode, 2)) - { - TestEqual("TextID should be correct", NextNode->Edges[1].TextID, "@0001@"); - TestParsedChoiceEdge(this, "First choice", NextNode, 1, "Jog on, mate", Importer, &NextNode); - TestParsedText(this, "Next node", NextNode, "Vagabond", "Well, really! Good day then sir!"); - TestEqual("TextID should be correct", NextNode->TextID, "@0002@"); - } - - return true; -} - -const FString ProblemChoiceInput = R"RAWSUD( - -NPC: Well, hello there. This is a test. -:choice - * A test? - Player: Yes! - * This is a mistake; goto goes direct to another choice - [goto choice] -NPC: Bye! -)RAWSUD"; - -IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestParseChoiceProblem, - "SUDSTest.TestParseChoiceProblem", - EAutomationTestFlags::EditorContext | - EAutomationTestFlags::ClientContext | - EAutomationTestFlags::ProductFilter) - - -bool FTestParseChoiceProblem::RunTest(const FString& Parameters) -{ - FSUDSMessageLogger Logger(false); - FSUDSScriptImporter Importer; - TestFalse("Import should fail", Importer.ImportFromBuffer(GetData(ProblemChoiceInput), ProblemChoiceInput.Len(), "ProblemChoiceInput", &Logger, true)); - - if (TestTrue("Logger should have registered errors", Logger.HasErrors())) - { - FText ErrMsg = Logger.GetErrorMessages()[0]->ToText(); - TestTrue("Error should contain 'Choices MUST show another speaker line'", ErrMsg.ToString().Contains("Choices MUST show another speaker line")); - } - return true; -} - -const FString TrailingEventInput = R"RAWSUD( - -NPC: Well, hello there. This is a test. -* Choice - [event SomeEvent 1] -)RAWSUD"; - -IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestTrailingEventProblem, - "SUDSTest.TestTrailingEventProblem", - EAutomationTestFlags::EditorContext | - EAutomationTestFlags::ClientContext | - EAutomationTestFlags::ProductFilter) - - -bool FTestTrailingEventProblem::RunTest(const FString& Parameters) -{ - FSUDSMessageLogger Logger(false); - FSUDSScriptImporter Importer; - TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(TrailingEventInput), TrailingEventInput.Len(), "TrailingEventInput", &Logger, true)); - - return true; -} - -const FString ConditionalChoicesNestedAndSiblingsInput = R"RAWSUD( - -Player: Hello - -[if {TopCondition} == 0] - * Top level choice 1 - [goto outro] -[endif] - -[if {TopCondition} == 1] - * Top level choice 2 - [goto outro] - [if {NestedCondition1} == 1] - * Nested condition choice 1 - [goto outro] - [endif] - [if {NestedCondition2} == 0] - * Nested condition choice 2 - [goto outro] - [endif] - [if {NestedCondition3} == 1] - * Nested condition choice 3 - [goto outro] - [endif] -[endif] - -:outro -Player: Bye -)RAWSUD"; - - -IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestConditionalChoicesNestedAndSiblings, - "SUDSTest.TestConditionalChoicesNestedAndSiblings", - EAutomationTestFlags::EditorContext | - EAutomationTestFlags::ClientContext | - EAutomationTestFlags::ProductFilter) - - -bool FTestConditionalChoicesNestedAndSiblings::RunTest(const FString& Parameters) -{ - FSUDSMessageLogger Logger(false); - FSUDSScriptImporter Importer; - TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ConditionalChoicesNestedAndSiblingsInput), ConditionalChoicesNestedAndSiblingsInput.Len(), "ConditionalChoicesNestedAndSiblingsInput", &Logger, true)); - - - auto Asset = NewObject(GetTransientPackage(), "Test"); - const ScopedStringTableHolder StringTableHolder; - Importer.PopulateAsset(Asset, StringTableHolder.StringTable); - - auto StartNode = Asset->GetFirstNode(); - if (!TestNotNull("Start node should be true", StartNode)) - { - return false; - } - - TestTextNode(this, "Start node", StartNode, "Player", "Hello"); - - auto NextNode = StartNode; - if (!TestEqual("Start node edges", NextNode->GetEdgeCount(), 1)) - { - return false; - } - - auto pEdge = NextNode->GetEdge(0); - if (!TestNotNull("Start node edge", pEdge)) - { - return false; - } - - /* Node structure: - T -> C1 - C1 -> S1 -> (if TopCondition == 0) C2 --> Top level choice 1 - C1 -> S2 -> (if TopCondition == 1) C3 -> Top Level choice 2 - |-> S3 -> (if NestedCondition1 == 1) -> C4 -> Nested condition choice 1 - |-> S4 -> (if NestedCondition2 == 0) -> C5 -> Nested condition choice 2 - |-> S5 -> (if NestedCondition3 == 0) -> C5 -> Nested condition choice 3 - */ - - NextNode = pEdge->GetTargetNode().Get(); - if (TestChoiceNode(this, "Start node next", NextNode, 2)) - { - auto ChoiceNode = NextNode; - // Top condition == 0 subtree - if (TestChoiceEdge(this, "Select Path 1", ChoiceNode, 0, "", &NextNode)) - { - if (TestSelectNode(this, "Select 1", NextNode, 1)) - { - if (TestSelectEdge(this, "Select 1 Edge", NextNode, 0, "{TopCondition} == 0", &NextNode)) - { - if (TestChoiceNode(this, "Choice 1 Leaf", NextNode, 1)) - { - TestChoiceEdge(this, "Choice 1 Leaf Edge", NextNode, 0, "Top level choice 1", &NextNode); - } - } - } - } - - // Top condition == 1 subtree - if (TestChoiceEdge(this, "Select Path 2", ChoiceNode, 1, "", &NextNode)) - { - if (TestSelectNode(this, "Select 2", NextNode, 1)) - { - if (TestSelectEdge(this, "Select 2 Edge", NextNode, 0, "{TopCondition} == 1", &NextNode)) - { - // This choice node holds the non-nested condition choice, plus 3 more nested selects - // Because each condition is a sibling they become separate select subtrees - if (TestChoiceNode(this, "Choice 2 Mid", NextNode, 4)) - { - auto ChoiceNode2 = NextNode; - TestChoiceEdge(this, "Choice 2 Leaf Edge 1", ChoiceNode2, 0, "Top level choice 2", &NextNode); - - if (TestChoiceEdge(this, "Choice 2 To Select Edge 1", ChoiceNode2, 1, "", &NextNode)) - { - if (TestSelectNode(this, "Select 2", NextNode, 1)) - { - if (TestSelectEdge(this, "Select 2 Edge", NextNode, 0, "{NestedCondition1} == 1", &NextNode)) - { - if (TestChoiceNode(this, "Choice 2 Leaf", NextNode, 1)) - { - TestChoiceEdge(this, "Choice 2 Leaf Edge", NextNode, 0, "Nested condition choice 1", &NextNode); - } - } - } - } - - if (TestChoiceEdge(this, "Choice 2 To Select Edge 2", ChoiceNode2, 2, "", &NextNode)) - { - if (TestSelectNode(this, "Select 3", NextNode, 1)) - { - if (TestSelectEdge(this, "Select 3 Edge", NextNode, 0, "{NestedCondition2} == 0", &NextNode)) - { - if (TestChoiceNode(this, "Choice 3 Leaf", NextNode, 1)) - { - TestChoiceEdge(this, "Choice 3 Leaf Edge", NextNode, 0, "Nested condition choice 2", &NextNode); - } - } - } - } - - if (TestChoiceEdge(this, "Choice 2 To Select Edge 3", ChoiceNode2, 3, "", &NextNode)) - { - if (TestSelectNode(this, "Select 4", NextNode, 1)) - { - if (TestSelectEdge(this, "Select 4 Edge", NextNode, 0, "{NestedCondition3} == 1", &NextNode)) - { - if (TestChoiceNode(this, "Choice 4 Leaf", NextNode, 1)) - { - TestChoiceEdge(this, "Choice 4 Leaf Edge", NextNode, 0, "Nested condition choice 3", &NextNode); - } - } - } - } - - } - } - } - } - - - } - return true; -} - -// The deliberate mistake here is that the speaker lines are not indented underneath the choices, -// which when combined with the conditionals confuses the parser so that it leaves all but the -// first choice orphaned and unreachable. Import should report this to the user -const FString ErrorsReportedForBadChoiceIndentsInput = R"RAWSUD( - -npc: hello -:chatchoices - [if {npc_hasQuests}] - * Any news? - player: Any news? - [endif] - [if {npc_canTrade}] - * Trade - player: What do you have for sale? - [endif] - [if {npc_hasLore}] - * How are you? - player: How are things? - [endif] - * Nevermind - npc: Alright. Come back anytime. -)RAWSUD"; - -IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestErrorsReportedForBadChoiceIndents, - "SUDSTest.TestErrorsReportedForBadChoiceIndents", - EAutomationTestFlags::EditorContext | - EAutomationTestFlags::ClientContext | - EAutomationTestFlags::ProductFilter) - - -bool FTestErrorsReportedForBadChoiceIndents::RunTest(const FString& Parameters) -{ - FSUDSMessageLogger Logger(false); - FSUDSScriptImporter Importer; - TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ErrorsReportedForBadChoiceIndentsInput), ErrorsReportedForBadChoiceIndentsInput.Len(), "ErrorsReportedForBadChoiceIndentsInput", &Logger, true)); - - TestTrue("Warnings should have been logged", Logger.HasWarnings()); - return true; -} - - - -UE_ENABLE_OPTIMIZATION \ No newline at end of file diff --git a/Plugins/SUDS/Source/SUDSTest/Private/TestParticipant.cpp b/Plugins/SUDS/Source/SUDSTest/Private/TestParticipant.cpp deleted file mode 100644 index 9daee109..00000000 --- a/Plugins/SUDS/Source/SUDSTest/Private/TestParticipant.cpp +++ /dev/null @@ -1,68 +0,0 @@ - -#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& 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 }); -} - diff --git a/Plugins/SUDS/Source/SUDSTest/Private/TestParticipant.h b/Plugins/SUDS/Source/SUDSTest/Private/TestParticipant.h deleted file mode 100644 index 1b414b52..00000000 --- a/Plugins/SUDS/Source/SUDSTest/Private/TestParticipant.h +++ /dev/null @@ -1,45 +0,0 @@ -#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 Args; - }; - struct FSetVarRecord - { - FName Name; - FSUDSValue Value; - bool bFromScript; - }; - - TArray EventRecords; - TArray 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& Arguments) override; - virtual void OnDialogueVariableChanged_Implementation(USUDSDialogue* Dialogue, - FName VariableName, - const FSUDSValue& Value, - bool bFromScript) override; -}; diff --git a/Plugins/SUDS/Source/SUDSTest/Private/TestRandom.cpp b/Plugins/SUDS/Source/SUDSTest/Private/TestRandom.cpp deleted file mode 100644 index 01ab85ea..00000000 --- a/Plugins/SUDS/Source/SUDSTest/Private/TestRandom.cpp +++ /dev/null @@ -1,342 +0,0 @@ -#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(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(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(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(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 \ No newline at end of file diff --git a/Plugins/SUDS/Source/SUDSTest/Private/TestRunning.cpp b/Plugins/SUDS/Source/SUDSTest/Private/TestRunning.cpp deleted file mode 100644 index 1d32b299..00000000 --- a/Plugins/SUDS/Source/SUDSTest/Private/TestRunning.cpp +++ /dev/null @@ -1,492 +0,0 @@ -#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(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(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(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(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(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 \ No newline at end of file diff --git a/Plugins/SUDS/Source/SUDSTest/Private/TestSaveState.cpp b/Plugins/SUDS/Source/SUDSTest/Private/TestSaveState.cpp deleted file mode 100644 index 30bc8f69..00000000 --- a/Plugins/SUDS/Source/SUDSTest/Private/TestSaveState.cpp +++ /dev/null @@ -1,146 +0,0 @@ -#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(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 \ No newline at end of file diff --git a/Plugins/SUDS/Source/SUDSTest/Private/TestUserMetadata.cpp b/Plugins/SUDS/Source/SUDSTest/Private/TestUserMetadata.cpp deleted file mode 100644 index 7f20aca4..00000000 --- a/Plugins/SUDS/Source/SUDSTest/Private/TestUserMetadata.cpp +++ /dev/null @@ -1,124 +0,0 @@ -#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(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 diff --git a/Plugins/SUDS/Source/SUDSTest/Private/TestUtils.h b/Plugins/SUDS/Source/SUDSTest/Private/TestUtils.h deleted file mode 100644 index cff3484c..00000000 --- a/Plugins/SUDS/Source/SUDSTest/Private/TestUtils.h +++ /dev/null @@ -1,297 +0,0 @@ -#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 -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(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(GetTransientPackage(), "TestStrings"); - } - - ~ScopedStringTableHolder() - { - // Tidy up string table - // UStringTable constructor registered this table - FStringTableRegistry::Get().UnregisterStringTable(StringTable->GetStringTableId()); - } - -}; diff --git a/Plugins/SUDS/Source/SUDSTest/SUDSTest.Build.cs b/Plugins/SUDS/Source/SUDSTest/SUDSTest.Build.cs deleted file mode 100644 index b048aaad..00000000 --- a/Plugins/SUDS/Source/SUDSTest/SUDSTest.Build.cs +++ /dev/null @@ -1,30 +0,0 @@ -// 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 - } -}