From 1c345a33ebe18427530f09dfa69d05e2264933ad Mon Sep 17 00:00:00 2001 From: "adnan.mujkic" Date: Wed, 22 Jul 2026 14:03:18 +0200 Subject: [PATCH] Remove suds --- .../Editor/Slate/Icons/SUDSScript_16x.png | 3 - .../Editor/Slate/Icons/SUDSScript_64x.png | 3 - .../PygmentsLexer/.vscode/settings.json | 6 - Plugins/SUDS/Extras/PygmentsLexer/setup.py | 12 - .../PygmentsLexer/sudslexer/__init__.py | 0 .../Extras/PygmentsLexer/sudslexer/lexer.py | 71 - Plugins/SUDS/License.txt | 18 - Plugins/SUDS/Resources/Icon128.png | 3 - Plugins/SUDS/Resources/logo.afdesign | Bin 76713 -> 0 bytes Plugins/SUDS/SUDS.uplugin | 33 - Plugins/SUDS/Source/SUDS/Private/SUDS.cpp | 21 - .../SUDS/Source/SUDS/Private/SUDSCommon.cpp | 4 - .../SUDS/Source/SUDS/Private/SUDSDialogue.cpp | 1379 -------- .../Source/SUDS/Private/SUDSExpression.cpp | 465 --- .../SUDS/Source/SUDS/Private/SUDSInternal.h | 41 - .../SUDS/Source/SUDS/Private/SUDSLibrary.cpp | 154 - .../Source/SUDS/Private/SUDSParticipant.cpp | 5 - .../SUDS/Source/SUDS/Private/SUDSScript.cpp | 276 -- .../Source/SUDS/Private/SUDSScriptEdge.cpp | 79 - .../Source/SUDS/Private/SUDSScriptNode.cpp | 38 - .../SUDS/Private/SUDSScriptNodeEvent.cpp | 12 - .../Source/SUDS/Private/SUDSScriptNodeSet.cpp | 11 - .../SUDS/Private/SUDSScriptNodeText.cpp | 62 - .../Source/SUDS/Private/SUDSSubsystem.cpp | 199 -- .../SUDS/Source/SUDS/Private/SUDSValue.cpp | 100 - Plugins/SUDS/Source/SUDS/Public/SUDS.h | 15 - Plugins/SUDS/Source/SUDS/Public/SUDSCommon.h | 23 - .../SUDS/Source/SUDS/Public/SUDSDialogue.h | 694 ----- .../SUDS/Source/SUDS/Public/SUDSExpression.h | 234 -- Plugins/SUDS/Source/SUDS/Public/SUDSLibrary.h | 145 - .../SUDS/Source/SUDS/Public/SUDSParticipant.h | 129 - Plugins/SUDS/Source/SUDS/Public/SUDSScript.h | 114 - .../SUDS/Source/SUDS/Public/SUDSScriptEdge.h | 88 - .../SUDS/Source/SUDS/Public/SUDSScriptNode.h | 81 - .../Source/SUDS/Public/SUDSScriptNodeEvent.h | 33 - .../Source/SUDS/Public/SUDSScriptNodeGosub.h | 49 - .../Source/SUDS/Public/SUDSScriptNodeSet.h | 33 - .../Source/SUDS/Public/SUDSScriptNodeText.h | 72 - .../SUDS/Source/SUDS/Public/SUDSSubsystem.h | 292 -- Plugins/SUDS/Source/SUDS/Public/SUDSValue.h | 416 --- Plugins/SUDS/Source/SUDS/SUDS.Build.cs | 53 - .../Source/SUDSEditor/Private/SUDSEditor.cpp | 65 - .../Private/SUDSEditorScriptTools.cpp | 280 -- .../SUDSEditor/Private/SUDSEditorSettings.cpp | 48 - .../SUDSEditor/Private/SUDSEditorToolkit.cpp | 1590 ---------- .../SUDSEditor/Private/SUDSEditorToolkit.h | 343 -- .../Private/SUDSEditorVoiceOverTools.cpp | 381 --- .../SUDSEditor/Private/SUDSMessageLogger.cpp | 93 - .../SUDSEditor/Private/SUDSScriptActions.cpp | 134 - .../SUDSEditor/Private/SUDSScriptFactory.cpp | 262 -- .../SUDSEditor/Private/SUDSScriptImporter.cpp | 2765 ----------------- .../Private/SUDSScriptReimportFactory.cpp | 149 - .../Source/SUDSEditor/Public/SUDSEditor.h | 24 - .../SUDSEditor/Public/SUDSEditorScriptTools.h | 22 - .../SUDSEditor/Public/SUDSEditorSettings.h | 73 - .../Public/SUDSEditorVoiceOverTools.h | 34 - .../SUDSEditor/Public/SUDSMessageLogger.h | 60 - .../SUDSEditor/Public/SUDSScriptActions.h | 37 - .../SUDSEditor/Public/SUDSScriptFactory.h | 37 - .../SUDSEditor/Public/SUDSScriptImporter.h | 520 ---- .../Public/SUDSScriptReimportFactory.h | 27 - .../Source/SUDSEditor/SUDSEditor.Build.cs | 62 - .../SUDSTest/Private/SudsTestModule.cpp | 21 - .../Source/SUDSTest/Private/SudsTestModule.h | 14 - .../Private/TestChoiceSpeakerLines.cpp | 274 -- .../SUDSTest/Private/TestConditionals.cpp | 1045 ------- .../Source/SUDSTest/Private/TestEventSub.cpp | 20 - .../Source/SUDSTest/Private/TestEventSub.h | 39 - .../Source/SUDSTest/Private/TestEvents.cpp | 278 -- .../SUDSTest/Private/TestExpressions.cpp | 209 -- .../Source/SUDSTest/Private/TestGotoGosub.cpp | 417 --- .../Source/SUDSTest/Private/TestMetadata.cpp | 133 - .../SUDSTest/Private/TestParameters.cpp | 147 - .../Source/SUDSTest/Private/TestParsing.cpp | 1189 ------- .../SUDSTest/Private/TestParticipant.cpp | 68 - .../Source/SUDSTest/Private/TestParticipant.h | 45 - .../Source/SUDSTest/Private/TestRandom.cpp | 342 -- .../Source/SUDSTest/Private/TestRunning.cpp | 492 --- .../Source/SUDSTest/Private/TestSaveState.cpp | 146 - .../SUDSTest/Private/TestUserMetadata.cpp | 124 - .../SUDS/Source/SUDSTest/Private/TestUtils.h | 297 -- .../SUDS/Source/SUDSTest/SUDSTest.Build.cs | 30 - 82 files changed, 17802 deletions(-) delete mode 100644 Plugins/SUDS/Content/Editor/Slate/Icons/SUDSScript_16x.png delete mode 100644 Plugins/SUDS/Content/Editor/Slate/Icons/SUDSScript_64x.png delete mode 100644 Plugins/SUDS/Extras/PygmentsLexer/.vscode/settings.json delete mode 100644 Plugins/SUDS/Extras/PygmentsLexer/setup.py delete mode 100644 Plugins/SUDS/Extras/PygmentsLexer/sudslexer/__init__.py delete mode 100644 Plugins/SUDS/Extras/PygmentsLexer/sudslexer/lexer.py delete mode 100644 Plugins/SUDS/License.txt delete mode 100644 Plugins/SUDS/Resources/Icon128.png delete mode 100644 Plugins/SUDS/Resources/logo.afdesign delete mode 100644 Plugins/SUDS/SUDS.uplugin delete mode 100644 Plugins/SUDS/Source/SUDS/Private/SUDS.cpp delete mode 100644 Plugins/SUDS/Source/SUDS/Private/SUDSCommon.cpp delete mode 100644 Plugins/SUDS/Source/SUDS/Private/SUDSDialogue.cpp delete mode 100644 Plugins/SUDS/Source/SUDS/Private/SUDSExpression.cpp delete mode 100644 Plugins/SUDS/Source/SUDS/Private/SUDSInternal.h delete mode 100644 Plugins/SUDS/Source/SUDS/Private/SUDSLibrary.cpp delete mode 100644 Plugins/SUDS/Source/SUDS/Private/SUDSParticipant.cpp delete mode 100644 Plugins/SUDS/Source/SUDS/Private/SUDSScript.cpp delete mode 100644 Plugins/SUDS/Source/SUDS/Private/SUDSScriptEdge.cpp delete mode 100644 Plugins/SUDS/Source/SUDS/Private/SUDSScriptNode.cpp delete mode 100644 Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeEvent.cpp delete mode 100644 Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeSet.cpp delete mode 100644 Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeText.cpp delete mode 100644 Plugins/SUDS/Source/SUDS/Private/SUDSSubsystem.cpp delete mode 100644 Plugins/SUDS/Source/SUDS/Private/SUDSValue.cpp delete mode 100644 Plugins/SUDS/Source/SUDS/Public/SUDS.h delete mode 100644 Plugins/SUDS/Source/SUDS/Public/SUDSCommon.h delete mode 100644 Plugins/SUDS/Source/SUDS/Public/SUDSDialogue.h delete mode 100644 Plugins/SUDS/Source/SUDS/Public/SUDSExpression.h delete mode 100644 Plugins/SUDS/Source/SUDS/Public/SUDSLibrary.h delete mode 100644 Plugins/SUDS/Source/SUDS/Public/SUDSParticipant.h delete mode 100644 Plugins/SUDS/Source/SUDS/Public/SUDSScript.h delete mode 100644 Plugins/SUDS/Source/SUDS/Public/SUDSScriptEdge.h delete mode 100644 Plugins/SUDS/Source/SUDS/Public/SUDSScriptNode.h delete mode 100644 Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeEvent.h delete mode 100644 Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeGosub.h delete mode 100644 Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeSet.h delete mode 100644 Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeText.h delete mode 100644 Plugins/SUDS/Source/SUDS/Public/SUDSSubsystem.h delete mode 100644 Plugins/SUDS/Source/SUDS/Public/SUDSValue.h delete mode 100644 Plugins/SUDS/Source/SUDS/SUDS.Build.cs delete mode 100644 Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditor.cpp delete mode 100644 Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorScriptTools.cpp delete mode 100644 Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorSettings.cpp delete mode 100644 Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.cpp delete mode 100644 Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.h delete mode 100644 Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorVoiceOverTools.cpp delete mode 100644 Plugins/SUDS/Source/SUDSEditor/Private/SUDSMessageLogger.cpp delete mode 100644 Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptActions.cpp delete mode 100644 Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptFactory.cpp delete mode 100644 Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptImporter.cpp delete mode 100644 Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptReimportFactory.cpp delete mode 100644 Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditor.h delete mode 100644 Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditorScriptTools.h delete mode 100644 Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditorSettings.h delete mode 100644 Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditorVoiceOverTools.h delete mode 100644 Plugins/SUDS/Source/SUDSEditor/Public/SUDSMessageLogger.h delete mode 100644 Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptActions.h delete mode 100644 Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptFactory.h delete mode 100644 Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptImporter.h delete mode 100644 Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptReimportFactory.h delete mode 100644 Plugins/SUDS/Source/SUDSEditor/SUDSEditor.Build.cs delete mode 100644 Plugins/SUDS/Source/SUDSTest/Private/SudsTestModule.cpp delete mode 100644 Plugins/SUDS/Source/SUDSTest/Private/SudsTestModule.h delete mode 100644 Plugins/SUDS/Source/SUDSTest/Private/TestChoiceSpeakerLines.cpp delete mode 100644 Plugins/SUDS/Source/SUDSTest/Private/TestConditionals.cpp delete mode 100644 Plugins/SUDS/Source/SUDSTest/Private/TestEventSub.cpp delete mode 100644 Plugins/SUDS/Source/SUDSTest/Private/TestEventSub.h delete mode 100644 Plugins/SUDS/Source/SUDSTest/Private/TestEvents.cpp delete mode 100644 Plugins/SUDS/Source/SUDSTest/Private/TestExpressions.cpp delete mode 100644 Plugins/SUDS/Source/SUDSTest/Private/TestGotoGosub.cpp delete mode 100644 Plugins/SUDS/Source/SUDSTest/Private/TestMetadata.cpp delete mode 100644 Plugins/SUDS/Source/SUDSTest/Private/TestParameters.cpp delete mode 100644 Plugins/SUDS/Source/SUDSTest/Private/TestParsing.cpp delete mode 100644 Plugins/SUDS/Source/SUDSTest/Private/TestParticipant.cpp delete mode 100644 Plugins/SUDS/Source/SUDSTest/Private/TestParticipant.h delete mode 100644 Plugins/SUDS/Source/SUDSTest/Private/TestRandom.cpp delete mode 100644 Plugins/SUDS/Source/SUDSTest/Private/TestRunning.cpp delete mode 100644 Plugins/SUDS/Source/SUDSTest/Private/TestSaveState.cpp delete mode 100644 Plugins/SUDS/Source/SUDSTest/Private/TestUserMetadata.cpp delete mode 100644 Plugins/SUDS/Source/SUDSTest/Private/TestUtils.h delete mode 100644 Plugins/SUDS/Source/SUDSTest/SUDSTest.Build.cs 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 f50f54b4b35309d40ed0b294e5e8fb5bbef1b623..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 76713 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

+P8M54uIuP_@J44&2`t`$64|cI^SO^s-B=d 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 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 - } -}