Initial push to repo

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

View File

@@ -0,0 +1,21 @@
#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)

View File

@@ -0,0 +1,14 @@
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleInterface.h"
DECLARE_LOG_CATEGORY_EXTERN(LogSudsTestModule, All, All)
class FSudsTestModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};

View File

@@ -0,0 +1,274 @@
#include "SUDSDialogue.h"
#include "SUDSLibrary.h"
#include "SUDSMessageLogger.h"
#include "SUDSScript.h"
#include "SUDSScriptImporter.h"
#include "TestParticipant.h"
#include "TestUtils.h"
#include "Misc/AutomationTest.h"
UE_DISABLE_OPTIMIZATION
const FString ChoicesAsSpeakerLineEnableInScriptInput = R"RAWSUD(
===
[importsetting GenerateSpeakerLinesFromChoices true]
===
Player: Hello
* Choice 1
NPC: I see
* Choice 1a
NPC: Sure
*- Not a speaker line!
Player: I had to say this separately
* Choice 2
NPC: Totally
Player: The end
)RAWSUD";
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestChoiceSpeakerLineInScript,
"SUDSTest.TestChoiceSpeakerLineInScript",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestChoiceSpeakerLineInScript::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ChoicesAsSpeakerLineEnableInScriptInput), ChoicesAsSpeakerLineEnableInScriptInput.Len(), "ChoicesAsSpeakerLineEnableInScriptInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
auto Participant = NewObject<UTestParticipant>();
Participant->TestNumber = 0;
Dlg->AddParticipant(Participant);
Dlg->Start();
TestDialogueText(this, "Line 1", Dlg, "Player", "Hello");
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1"));
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Choice 2"));
const FText ChoiceText0 = Dlg->GetChoiceText(0);
Dlg->Choose(0);
// Confirm that we got the choice as a speaker line
TestDialogueText(this, "Choice as speaker line 1", Dlg, "Player", "Choice 1");
// Confirm that text ID is the same
TestTrue("Text should be identical", ChoiceText0.IdenticalTo(Dlg->GetText(), ETextIdenticalModeFlags::DeepCompare));
TestEqual("TextIDs should match", SUDS_GET_TEXT_KEY(ChoiceText0), SUDS_GET_TEXT_KEY(Dlg->GetText()));
Dlg->Continue();
TestDialogueText(this, "Regular speaker line", Dlg, "NPC", "I see");
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1a"));
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Not a speaker line!"));
// Test choice which disables speaker line
Dlg->Choose(1);
TestDialogueText(this, "Regular speaker line", Dlg, "Player", "I had to say this separately");
Script->MarkAsGarbage();
return true;
}
const FString ChoicesAsSpeakerLineSetSpeakerIdInScriptInput = R"RAWSUD(
===
[importsetting GenerateSpeakerLinesFromChoices true]
[importsetting SpeakerIDForGeneratedLinesFromChoices `TheDude`]
===
Player: Hello
* Choice 1
NPC: You said that choice
* Choice 2
NPC: Also that one
Player: The end
)RAWSUD";
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestChoiceSpeakerSetSpeakerIdInScriptInput,
"SUDSTest.TestChoiceSpeakerSetSpeakerIdInScriptInput",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestChoiceSpeakerSetSpeakerIdInScriptInput::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ChoicesAsSpeakerLineSetSpeakerIdInScriptInput), ChoicesAsSpeakerLineSetSpeakerIdInScriptInput.Len(), "ChoicesAsSpeakerLineSetSpeakerIdInScriptInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
auto Participant = NewObject<UTestParticipant>();
Participant->TestNumber = 0;
Dlg->AddParticipant(Participant);
Dlg->Start();
TestDialogueText(this, "Line 1", Dlg, "Player", "Hello");
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1"));
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Choice 2"));
const FText ChoiceText0 = Dlg->GetChoiceText(0);
Dlg->Choose(0);
// Confirm that text ID is the same
TestTrue("Text should be identical", ChoiceText0.IdenticalTo(Dlg->GetText(), ETextIdenticalModeFlags::DeepCompare));
TestEqual("TextIDs should match", SUDS_GET_TEXT_KEY(ChoiceText0), SUDS_GET_TEXT_KEY(Dlg->GetText()));
// Confirm that we got the choice as a speaker line, with custom speaker ID
TestDialogueText(this, "Choice as speaker line 1", Dlg, "TheDude", "Choice 1");
Dlg->Continue();
TestDialogueText(this, "Regular speaker line", Dlg, "NPC", "You said that choice");
Script->MarkAsGarbage();
return true;
}
const FString ChoicesAsSpeakerLineChainChoiceInput = R"RAWSUD(
===
[importsetting GenerateSpeakerLinesFromChoices true]
===
Player: Hello
* Choice 1
* Choice 1a
NPC: Hey this worked
* Choice 1b
* Choice 2
NPC: Also that one
Player: The end
)RAWSUD";
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestChoiceSpeakerChainedChoiceInput,
"SUDSTest.TestChoiceSpeakerChainedChoiceInput",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestChoiceSpeakerChainedChoiceInput::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ChoicesAsSpeakerLineChainChoiceInput), ChoicesAsSpeakerLineChainChoiceInput.Len(), "ChoicesAsSpeakerLineChainChoiceInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
auto Participant = NewObject<UTestParticipant>();
Participant->TestNumber = 0;
Dlg->AddParticipant(Participant);
Dlg->Start();
TestDialogueText(this, "Line 1", Dlg, "Player", "Hello");
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1"));
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Choice 2"));
Dlg->Choose(0);
// Confirm that we got the choice as a speaker line, with custom speaker ID
TestDialogueText(this, "Choice as speaker line 1", Dlg, "Player", "Choice 1");
TestEqual("Choice 2nd level text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1a"));
TestEqual("Choice 2nd leveltext", Dlg->GetChoiceText(1).ToString(), TEXT("Choice 1b"));
Dlg->Choose(0);
TestDialogueText(this, "Choice as speaker line 1", Dlg, "Player", "Choice 1a");
Dlg->Continue();
TestDialogueText(this, "Regular speaker line", Dlg, "NPC", "Hey this worked");
Script->MarkAsGarbage();
return true;
}
const FString ChoicesAsSpeakerLineWithStringKeysInput = R"RAWSUD(
===
[importsetting GenerateSpeakerLinesFromChoices true]
===
Player: Hello @0012@
* Choice 1 @0016@
NPC: I see @0017@
* Choice 1a @0018@
NPC: Sure
*- Not a speaker line!
Player: I had to say this separately
* Choice 2
NPC: Totally
Player: The end
)RAWSUD";
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestChoicesAsSpeakerLineWithStringKeys,
"SUDSTest.TestChoicesAsSpeakerLineWithStringKeys",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestChoicesAsSpeakerLineWithStringKeys::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ChoicesAsSpeakerLineWithStringKeysInput), ChoicesAsSpeakerLineWithStringKeysInput.Len(), "ChoicesAsSpeakerLineWithStringKeysInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
auto Participant = NewObject<UTestParticipant>();
Participant->TestNumber = 0;
Dlg->AddParticipant(Participant);
Dlg->Start();
TestDialogueText(this, "Line 1", Dlg, "Player", "Hello");
TestEqual("String key", SUDS_GET_TEXT_KEY(Dlg->GetText()), "@0012@");
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1"));
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Choice 2"));
TestEqual("String key", SUDS_GET_TEXT_KEY(Dlg->GetChoiceText(0)), "@0016@");
FText ChoiceText0 = Dlg->GetChoiceText(0);
Dlg->Choose(0);
// Confirm that we got the choice as a speaker line
TestDialogueText(this, "Choice as speaker line 1", Dlg, "Player", "Choice 1");
// Confirm that text ID is the same
TestTrue("Text should be identical", ChoiceText0.IdenticalTo(Dlg->GetText(), ETextIdenticalModeFlags::DeepCompare));
TestEqual("TextIDs should match", SUDS_GET_TEXT_KEY(ChoiceText0), SUDS_GET_TEXT_KEY(Dlg->GetText()));
Dlg->Continue();
TestDialogueText(this, "Regular speaker line", Dlg, "NPC", "I see");
TestEqual("String key", SUDS_GET_TEXT_KEY(Dlg->GetText()), "@0017@");
TestEqual("String key", SUDS_GET_TEXT_KEY(Dlg->GetChoiceText(0)), "@0018@");
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1a"));
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Not a speaker line!"));
ChoiceText0 = Dlg->GetChoiceText(0);
Dlg->Choose(0);
TestDialogueText(this, "Duplicated speaker line", Dlg, "Player", "Choice 1a");
// Confirm that text ID is the same
TestTrue("Text should be identical", ChoiceText0.IdenticalTo(Dlg->GetText(), ETextIdenticalModeFlags::DeepCompare));
TestEqual("TextIDs should match", SUDS_GET_TEXT_KEY(ChoiceText0), SUDS_GET_TEXT_KEY(Dlg->GetText()));
Script->MarkAsGarbage();
return true;
}
UE_ENABLE_OPTIMIZATION

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
#include "TestEventSub.h"
#include "SUDSDialogue.h"
void UTestEventSub::Init(USUDSDialogue* Dlg)
{
Dlg->OnEvent.AddDynamic(this, &UTestEventSub::OnEvent);
Dlg->OnVariableChanged.AddDynamic(this, &UTestEventSub::OnVariableChanged);
}
void UTestEventSub::OnEvent(USUDSDialogue* Dlg, FName EventName, const TArray<FSUDSValue>& Args)
{
EventRecords.Add(FEventRecord { EventName, Args });
}
void UTestEventSub::OnVariableChanged(USUDSDialogue* Dlg, FName VarName, const FSUDSValue& Value, bool bFromScript)
{
SetVarRecords.Add(FSetVarRecord { VarName, Value, bFromScript });
}

View File

@@ -0,0 +1,39 @@
#pragma once
#include "CoreMinimal.h"
#include "SUDSValue.h"
#include "UObject/Object.h"
#include "TestEventSub.generated.h"
class USUDSDialogue;
UCLASS()
class SUDSTEST_API UTestEventSub : public UObject
{
GENERATED_BODY()
public:
void Init(USUDSDialogue* Dlg);
struct FEventRecord
{
FName Name;
TArray<FSUDSValue> Args;
};
struct FSetVarRecord
{
FName Name;
FSUDSValue Value;
bool bFromScript;
};
TArray<FEventRecord> EventRecords;
TArray<FSetVarRecord> SetVarRecords;
UFUNCTION()
void OnEvent(USUDSDialogue* Dlg, FName EventName, const TArray<FSUDSValue>& Args);
UFUNCTION()
void OnVariableChanged(USUDSDialogue* Dlg, FName VarName, const FSUDSValue& Value, bool bFromScript);
};

View File

@@ -0,0 +1,278 @@
#include "SUDSDialogue.h"
#include "SUDSLibrary.h"
#include "SUDSMessageLogger.h"
#include "SUDSScript.h"
#include "SUDSScriptImporter.h"
#include "TestEventSub.h"
#include "TestParticipant.h"
#include "TestUtils.h"
UE_DISABLE_OPTIMIZATION
const FString EventParsingInput = R"RAWSUD(
Player: Ow do?
[set IntVar 2]
[set FloatVar 66.67]
[set StringVar "Ey up"]
[event SummatHappened "2 penneth", 99.99, masculine, {IntVar}, 42, false]
NPC: Alreet chook
[event Well.Blow.Me.Down {FloatVar}, true, {StringVar}]
[event Calculated {FloatVar} + 10, true or false, {StringVar}]
[event Actor.Emote `Player`, `Question`]
Player: Tara
)RAWSUD";
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestEvents,
"SUDSTest.TestEvents",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestEvents::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(EventParsingInput), EventParsingInput.Len(), "EventParsingInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
// Subscriber
auto EvtSub = NewObject<UTestEventSub>();
EvtSub->Init(Dlg);
// Participant
auto Participant = NewObject<UTestParticipant>();
Participant->TestNumber = 0;
Dlg->AddParticipant(Participant);
Dlg->Start();
TestDialogueText(this, "Line 1", Dlg, "Player", "Ow do?");
TestTrue("Continue", Dlg->Continue());
// Should have had an event
if (TestEqual("Event sub should have received", EvtSub->EventRecords.Num(), 1))
{
TestEqual("Event name", EvtSub->EventRecords[0].Name.ToString(), "SummatHappened");
if (TestEqual("Event sub arg count", EvtSub->EventRecords[0].Args.Num(), 6))
{
TestEqual("Event sub arg 0 type", EvtSub->EventRecords[0].Args[0].GetType(), ESUDSValueType::Text);
TestEqual("Event sub arg 0 value", EvtSub->EventRecords[0].Args[0].GetTextValue().ToString(), "2 penneth");
TestEqual("Event sub arg 1 type", EvtSub->EventRecords[0].Args[1].GetType(), ESUDSValueType::Float);
TestEqual("Event sub arg 1 value", EvtSub->EventRecords[0].Args[1].GetFloatValue(), 99.99f);
TestEqual("Event sub arg 2 type", EvtSub->EventRecords[0].Args[2].GetType(), ESUDSValueType::Gender);
TestEqual("Event sub arg 2 value", EvtSub->EventRecords[0].Args[2].GetGenderValue(), ETextGender::Masculine);
TestEqual("Event sub arg 3 type", EvtSub->EventRecords[0].Args[3].GetType(), ESUDSValueType::Int);
TestEqual("Event sub arg 3 value", EvtSub->EventRecords[0].Args[3].GetIntValue(), 2);
TestEqual("Event sub arg 4 type", EvtSub->EventRecords[0].Args[4].GetType(), ESUDSValueType::Int);
TestEqual("Event sub arg 4 value", EvtSub->EventRecords[0].Args[4].GetIntValue(), 42);
TestEqual("Event sub arg 5 type", EvtSub->EventRecords[0].Args[5].GetType(), ESUDSValueType::Boolean);
TestEqual("Event sub arg 5 value", EvtSub->EventRecords[0].Args[5].GetBooleanValue(), false);
}
}
if (TestEqual("Participant should have received", Participant->EventRecords.Num(), 1))
{
TestEqual("Event name", Participant->EventRecords[0].Name.ToString(), "SummatHappened");
if (TestEqual("Participant arg count", Participant->EventRecords[0].Args.Num(), 6))
{
TestEqual("Participant arg 0 type", Participant->EventRecords[0].Args[0].GetType(), ESUDSValueType::Text);
TestEqual("Participant arg 0 value", Participant->EventRecords[0].Args[0].GetTextValue().ToString(), "2 penneth");
TestEqual("Participant arg 1 type", Participant->EventRecords[0].Args[1].GetType(), ESUDSValueType::Float);
TestEqual("Participant arg 1 value", Participant->EventRecords[0].Args[1].GetFloatValue(), 99.99f);
TestEqual("Participant arg 2 type", Participant->EventRecords[0].Args[2].GetType(), ESUDSValueType::Gender);
TestEqual("Participant arg 2 value", Participant->EventRecords[0].Args[2].GetGenderValue(), ETextGender::Masculine);
TestEqual("Participant arg 3 type", Participant->EventRecords[0].Args[3].GetType(), ESUDSValueType::Int);
TestEqual("Participant arg 3 value", Participant->EventRecords[0].Args[3].GetIntValue(), 2);
TestEqual("Participant arg 4 type", Participant->EventRecords[0].Args[4].GetType(), ESUDSValueType::Int);
TestEqual("Participant arg 4 value", Participant->EventRecords[0].Args[4].GetIntValue(), 42);
TestEqual("Participant arg 5 type", Participant->EventRecords[0].Args[5].GetType(), ESUDSValueType::Boolean);
TestEqual("Participant arg 5 value", Participant->EventRecords[0].Args[5].GetBooleanValue(), false);
}
}
if (TestEqual("Event var sub should have received", EvtSub->SetVarRecords.Num(), 10))
{
// First 7 are from the participant setting variables at startup
TestEqual("Event var sub arg 0 name", EvtSub->SetVarRecords[0].Name.ToString(), "SpeakerName.Player");
TestEqual("Event var sub arg 0 type", EvtSub->SetVarRecords[0].Value.GetType(), ESUDSValueType::Text);
TestEqual("Event var sub arg 0 value", EvtSub->SetVarRecords[0].Value.GetTextValue().ToString(), "Protagonist");
TestFalse("Event var sub arg 0 fromscript", EvtSub->SetVarRecords[0].bFromScript);
TestEqual("Event var sub arg 1 name", EvtSub->SetVarRecords[1].Name.ToString(), "SpeakerName.NPC");
TestEqual("Event var sub arg 1 type", EvtSub->SetVarRecords[1].Value.GetType(), ESUDSValueType::Text);
TestEqual("Event var sub arg 1 value", EvtSub->SetVarRecords[1].Value.GetTextValue().ToString(), "An NPC");
TestFalse("Event var sub arg 1 fromscript", EvtSub->SetVarRecords[1].bFromScript);
TestEqual("Event var sub arg 2 name", EvtSub->SetVarRecords[2].Name.ToString(), "NumCats");
TestEqual("Event var sub arg 2 type", EvtSub->SetVarRecords[2].Value.GetType(), ESUDSValueType::Int);
TestEqual("Event var sub arg 2 value", EvtSub->SetVarRecords[2].Value.GetIntValue(), 3);
TestFalse("Event var sub arg 2 fromscript", EvtSub->SetVarRecords[2].bFromScript);
TestEqual("Event var sub arg 3 name", EvtSub->SetVarRecords[3].Name.ToString(), "FriendName");
TestEqual("Event var sub arg 3 type", EvtSub->SetVarRecords[3].Value.GetType(), ESUDSValueType::Text);
TestEqual("Event var sub arg 3 value", EvtSub->SetVarRecords[3].Value.GetTextValue().ToString(), "Susan");
TestFalse("Event var sub arg 3 fromscript", EvtSub->SetVarRecords[3].bFromScript);
TestEqual("Event var sub arg 4 name", EvtSub->SetVarRecords[4].Name.ToString(), "Gender");
TestEqual("Event var sub arg 4 type", EvtSub->SetVarRecords[4].Value.GetType(), ESUDSValueType::Gender);
TestEqual("Event var sub arg 4 value", EvtSub->SetVarRecords[4].Value.GetGenderValue(), ETextGender::Feminine);
TestFalse("Event var sub arg 4 fromscript", EvtSub->SetVarRecords[4].bFromScript);
TestEqual("Event var sub arg 5 name", EvtSub->SetVarRecords[5].Name.ToString(), "FloatVal");
TestEqual("Event var sub arg 5 type", EvtSub->SetVarRecords[5].Value.GetType(), ESUDSValueType::Float);
TestEqual("Event var sub arg 5 value", EvtSub->SetVarRecords[5].Value.GetFloatValue(), 12.567f);
TestFalse("Event var sub arg 5 fromscript", EvtSub->SetVarRecords[5].bFromScript);
TestEqual("Event var sub arg 6 name", EvtSub->SetVarRecords[6].Name.ToString(), "BoolVal");
TestEqual("Event var sub arg 6 type", EvtSub->SetVarRecords[6].Value.GetType(), ESUDSValueType::Boolean);
TestEqual("Event var sub arg 6 value", EvtSub->SetVarRecords[6].Value.GetBooleanValue(), true);
TestFalse("Event var sub arg 6 fromscript", EvtSub->SetVarRecords[6].bFromScript);
// Next 3 are from script
TestEqual("Event var sub arg 7 name", EvtSub->SetVarRecords[7].Name.ToString(), "IntVar");
TestEqual("Event var sub arg 7 type", EvtSub->SetVarRecords[7].Value.GetType(), ESUDSValueType::Int);
TestEqual("Event var sub arg 7 value", EvtSub->SetVarRecords[7].Value.GetIntValue(), 2);
TestTrue("Event var sub arg 7 fromscript", EvtSub->SetVarRecords[7].bFromScript);
TestEqual("Event var sub arg 8 name", EvtSub->SetVarRecords[8].Name.ToString(), "FloatVar");
TestEqual("Event var sub arg 8 type", EvtSub->SetVarRecords[8].Value.GetType(), ESUDSValueType::Float);
TestEqual("Event var sub arg 8 value", EvtSub->SetVarRecords[8].Value.GetFloatValue(), 66.67f);
TestTrue("Event var sub arg 8 fromscript", EvtSub->SetVarRecords[8].bFromScript);
TestEqual("Event var sub arg 9 name", EvtSub->SetVarRecords[9].Name.ToString(), "StringVar");
TestEqual("Event var sub arg 9 type", EvtSub->SetVarRecords[9].Value.GetType(), ESUDSValueType::Text);
TestEqual("Event var sub arg 9 value", EvtSub->SetVarRecords[9].Value.GetTextValue().ToString(), "Ey up");
TestTrue("Event var sub arg 9 fromscript", EvtSub->SetVarRecords[9].bFromScript);
}
if (TestEqual("Participant var should have received", Participant->SetVarRecords.Num(), 10))
{
// First 7 are from the participant setting variables at startup
TestEqual("Participant var arg 0 name", Participant->SetVarRecords[0].Name.ToString(), "SpeakerName.Player");
TestEqual("Participant var arg 0 type", Participant->SetVarRecords[0].Value.GetType(), ESUDSValueType::Text);
TestEqual("Participant var arg 0 value", Participant->SetVarRecords[0].Value.GetTextValue().ToString(), "Protagonist");
TestFalse("Participant var arg 0 fromscript", Participant->SetVarRecords[0].bFromScript);
TestEqual("Participant var arg 1 name", Participant->SetVarRecords[1].Name.ToString(), "SpeakerName.NPC");
TestEqual("Participant var arg 1 type", Participant->SetVarRecords[1].Value.GetType(), ESUDSValueType::Text);
TestEqual("Participant var arg 1 value", Participant->SetVarRecords[1].Value.GetTextValue().ToString(), "An NPC");
TestFalse("Participant var arg 1 fromscript", Participant->SetVarRecords[1].bFromScript);
TestEqual("Participant var arg 2 name", Participant->SetVarRecords[2].Name.ToString(), "NumCats");
TestEqual("Participant var arg 2 type", Participant->SetVarRecords[2].Value.GetType(), ESUDSValueType::Int);
TestEqual("Participant var arg 2 value", Participant->SetVarRecords[2].Value.GetIntValue(), 3);
TestFalse("Participant var arg 2 fromscript", Participant->SetVarRecords[2].bFromScript);
TestEqual("Participant var arg 3 name", Participant->SetVarRecords[3].Name.ToString(), "FriendName");
TestEqual("Participant var arg 3 type", Participant->SetVarRecords[3].Value.GetType(), ESUDSValueType::Text);
TestEqual("Participant var arg 3 value", Participant->SetVarRecords[3].Value.GetTextValue().ToString(), "Susan");
TestFalse("Participant var arg 3 fromscript", Participant->SetVarRecords[3].bFromScript);
TestEqual("Participant var arg 4 name", Participant->SetVarRecords[4].Name.ToString(), "Gender");
TestEqual("Participant var arg 4 type", Participant->SetVarRecords[4].Value.GetType(), ESUDSValueType::Gender);
TestEqual("Participant var arg 4 value", Participant->SetVarRecords[4].Value.GetGenderValue(), ETextGender::Feminine);
TestFalse("Participant var arg 4 fromscript", Participant->SetVarRecords[4].bFromScript);
TestEqual("Participant var arg 5 name", Participant->SetVarRecords[5].Name.ToString(), "FloatVal");
TestEqual("Participant var arg 5 type", Participant->SetVarRecords[5].Value.GetType(), ESUDSValueType::Float);
TestEqual("Participant var arg 5 value", Participant->SetVarRecords[5].Value.GetFloatValue(), 12.567f);
TestFalse("Participant var arg 5 fromscript", Participant->SetVarRecords[5].bFromScript);
TestEqual("Participant var arg 6 name", Participant->SetVarRecords[6].Name.ToString(), "BoolVal");
TestEqual("Participant var arg 6 type", Participant->SetVarRecords[6].Value.GetType(), ESUDSValueType::Boolean);
TestEqual("Participant var arg 6 value", Participant->SetVarRecords[6].Value.GetBooleanValue(), true);
TestFalse("Participant var arg 6 fromscript", Participant->SetVarRecords[6].bFromScript);
// Next 3 are from script
TestEqual("Participant var arg 7 name", Participant->SetVarRecords[7].Name.ToString(), "IntVar");
TestEqual("Participant var arg 7 type", Participant->SetVarRecords[7].Value.GetType(), ESUDSValueType::Int);
TestEqual("Participant var arg 7 value", Participant->SetVarRecords[7].Value.GetIntValue(), 2);
TestTrue("Participant var arg 7 fromscript", Participant->SetVarRecords[7].bFromScript);
TestEqual("Participant var arg 8 name", Participant->SetVarRecords[8].Name.ToString(), "FloatVar");
TestEqual("Participant var arg 8 type", Participant->SetVarRecords[8].Value.GetType(), ESUDSValueType::Float);
TestEqual("Participant var arg 8 value", Participant->SetVarRecords[8].Value.GetFloatValue(), 66.67f);
TestTrue("Participant var arg 8 fromscript", Participant->SetVarRecords[8].bFromScript);
TestEqual("Participant var arg 9 name", Participant->SetVarRecords[9].Name.ToString(), "StringVar");
TestEqual("Participant var arg 9 type", Participant->SetVarRecords[9].Value.GetType(), ESUDSValueType::Text);
TestEqual("Participant var arg 9 value", Participant->SetVarRecords[9].Value.GetTextValue().ToString(), "Ey up");
TestTrue("Participant var arg 9 fromscript", Participant->SetVarRecords[9].bFromScript);
}
TestDialogueText(this, "Line 2", Dlg, "NPC", "Alreet chook");
TestTrue("Continue", Dlg->Continue());
if (TestEqual("Event sub should have received", EvtSub->EventRecords.Num(), 4))
{
TestEqual("Event name", EvtSub->EventRecords[1].Name.ToString(), "Well.Blow.Me.Down");
if (TestEqual("Event sub arg count", EvtSub->EventRecords[1].Args.Num(), 3))
{
TestEqual("Event sub arg 0 type", EvtSub->EventRecords[1].Args[0].GetType(), ESUDSValueType::Float);
TestEqual("Event sub arg 0 value", EvtSub->EventRecords[1].Args[0].GetFloatValue(), 66.67f);
TestEqual("Event sub arg 1 type", EvtSub->EventRecords[1].Args[1].GetType(), ESUDSValueType::Boolean);
TestEqual("Event sub arg 1 value", EvtSub->EventRecords[1].Args[1].GetBooleanValue(), true);
TestEqual("Event sub arg 2 type", EvtSub->EventRecords[1].Args[2].GetType(), ESUDSValueType::Text);
TestEqual("Event sub arg 2 value", EvtSub->EventRecords[1].Args[2].GetTextValue().ToString(), "Ey up");
}
TestEqual("Event name", EvtSub->EventRecords[2].Name.ToString(), "Calculated");
if (TestEqual("Event sub arg count", EvtSub->EventRecords[2].Args.Num(), 3))
{
TestEqual("Event sub arg 0 type", EvtSub->EventRecords[2].Args[0].GetType(), ESUDSValueType::Float);
TestEqual("Event sub arg 0 value", EvtSub->EventRecords[2].Args[0].GetFloatValue(), 76.67f);
TestEqual("Event sub arg 2 type", EvtSub->EventRecords[2].Args[1].GetType(), ESUDSValueType::Boolean);
TestEqual("Event sub arg 2 value", EvtSub->EventRecords[2].Args[1].GetBooleanValue(), true);
TestEqual("Event sub arg 2 type", EvtSub->EventRecords[2].Args[2].GetType(), ESUDSValueType::Text);
TestEqual("Event sub arg 2 value", EvtSub->EventRecords[2].Args[2].GetTextValue().ToString(), "Ey up");
}
TestEqual("Event name", EvtSub->EventRecords[3].Name.ToString(), "Actor.Emote");
if (TestEqual("Event sub arg count", EvtSub->EventRecords[3].Args.Num(), 2))
{
TestEqual("Event sub arg 0 type", EvtSub->EventRecords[3].Args[0].GetType(), ESUDSValueType::Name);
TestEqual("Event sub arg 0 value", EvtSub->EventRecords[3].Args[0].GetNameValue(), FName("Player"));
TestEqual("Event sub arg 2 type", EvtSub->EventRecords[3].Args[1].GetType(), ESUDSValueType::Name);
TestEqual("Event sub arg 2 value", EvtSub->EventRecords[3].Args[1].GetNameValue(), FName("Question"));
}
}
if (TestEqual("Participant should have received", Participant->EventRecords.Num(), 4))
{
TestEqual("Event name", Participant->EventRecords[1].Name.ToString(), "Well.Blow.Me.Down");
if (TestEqual("Participant arg count", Participant->EventRecords[1].Args.Num(), 3))
{
TestEqual("Participant arg 0 type", Participant->EventRecords[1].Args[0].GetType(), ESUDSValueType::Float);
TestEqual("Participant arg 0 value", Participant->EventRecords[1].Args[0].GetFloatValue(), 66.67f);
TestEqual("Participant arg 1 type", Participant->EventRecords[1].Args[1].GetType(), ESUDSValueType::Boolean);
TestEqual("Participant arg 1 value", Participant->EventRecords[1].Args[1].GetBooleanValue(), true);
TestEqual("Participant arg 2 type", Participant->EventRecords[1].Args[2].GetType(), ESUDSValueType::Text);
TestEqual("Participant arg 2 value", Participant->EventRecords[1].Args[2].GetTextValue().ToString(), "Ey up");
}
TestEqual("Event name", Participant->EventRecords[2].Name.ToString(), "Calculated");
if (TestEqual("Event sub arg count", Participant->EventRecords[2].Args.Num(), 3))
{
TestEqual("Event sub arg 0 type", Participant->EventRecords[2].Args[0].GetType(), ESUDSValueType::Float);
TestEqual("Event sub arg 0 value", Participant->EventRecords[2].Args[0].GetFloatValue(), 76.67f);
TestEqual("Event sub arg 2 type", Participant->EventRecords[2].Args[1].GetType(), ESUDSValueType::Boolean);
TestEqual("Event sub arg 2 value", Participant->EventRecords[2].Args[1].GetBooleanValue(), true);
TestEqual("Event sub arg 2 type", Participant->EventRecords[2].Args[2].GetType(), ESUDSValueType::Text);
TestEqual("Event sub arg 2 value", Participant->EventRecords[2].Args[2].GetTextValue().ToString(), "Ey up");
}
TestEqual("Event name", EvtSub->EventRecords[3].Name.ToString(), "Actor.Emote");
if (TestEqual("Event sub arg count", EvtSub->EventRecords[3].Args.Num(), 2))
{
TestEqual("Event sub arg 0 type", EvtSub->EventRecords[3].Args[0].GetType(), ESUDSValueType::Name);
TestEqual("Event sub arg 0 value", EvtSub->EventRecords[3].Args[0].GetNameValue(), FName("Player"));
TestEqual("Event sub arg 2 type", EvtSub->EventRecords[3].Args[1].GetType(), ESUDSValueType::Name);
TestEqual("Event sub arg 2 value", EvtSub->EventRecords[3].Args[1].GetNameValue(), FName("Question"));
}
}
Script->MarkAsGarbage();
return true;
}
UE_ENABLE_OPTIMIZATION

View File

@@ -0,0 +1,209 @@
#include "SUDSExpression.h"
#include "Misc/AutomationTest.h"
UE_DISABLE_OPTIMIZATION
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestExpressions,
"SUDSTest.TestExpressions",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestExpressions::RunTest(const FString& Parameters)
{
FSUDSExpression Expr;
TMap<FName, FSUDSValue> Variables;
TMap<FName, FSUDSValue> GlobalVariables;
Variables.Add("Six", 6);
TestTrue("SimpleVarParse", Expr.ParseFromString("3 + 4 * {Six} + 1", nullptr));
TestEqual("Eval", Expr.Evaluate(Variables, GlobalVariables).GetIntValue(), 28);
auto& RPN = Expr.GetQueue();
if (TestEqual("Queue len", RPN.Num(), 7))
{
// RPN should be 3 4 6 * + 1 +
TestEqual("Queue 0", RPN[0].GetOperandValue().GetIntValue(), 3);
TestEqual("Queue 1", RPN[1].GetOperandValue().GetIntValue(), 4);
TestEqual("Queue 2", RPN[2].GetOperandValue().GetVariableNameValue().ToString(), "Six");
TestEqual("Queue 3", RPN[3].GetType(), ESUDSExpressionItemType::Multiply);
TestEqual("Queue 4", RPN[4].GetType(), ESUDSExpressionItemType::Add);
TestEqual("Queue 5", RPN[5].GetOperandValue().GetIntValue(), 1);
TestEqual("Queue 6", RPN[6].GetType(), ESUDSExpressionItemType::Add);
}
if (TestEqual("Variable count", Expr.GetVariableNames().Num(), 1))
{
TestEqual("Variable name", Expr.GetVariableNames()[0].ToString(), "Six");
}
TestTrue("Arithmetic", Expr.ParseFromString("-6.7 * 2 + (21.3 - 8) * 5", nullptr));
TestEqual("Eval", Expr.Evaluate(Variables, GlobalVariables).GetFloatValue(), 53.1f);
// Modulo operator
TestTrue("ModuloIntOperator", Expr.ParseFromString("11 % 5", nullptr));
TestEqual("Eval", Expr.Evaluate(Variables, GlobalVariables).GetIntValue(), 1);
TestTrue("ModuloFloatOperator", Expr.ParseFromString("7.25 % 3.0", nullptr));
TestEqual("Eval", Expr.Evaluate(Variables, GlobalVariables).GetFloatValue(), 1.25f);
// Explicit FSUDSValue(true) needed to avoid it using the int conversion by default
Variables.Add("IsATest", FSUDSValue(true));
TestTrue("BoolSingleValueParse", Expr.ParseFromString("{IsATest}", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
if (TestEqual("Variable count", Expr.GetVariableNames().Num(), 1))
{
TestEqual("Variable name", Expr.GetVariableNames()[0].ToString(), "IsATest");
}
Variables.Add("SomethingFalse", FSUDSValue(false));
Variables.Add("SomethingTrue", FSUDSValue(true));
Variables.Add("SomethingElseFalse", FSUDSValue(false));
TestTrue("BoolCompound1", Expr.ParseFromString("!{SomethingFalse} && {SomethingTrue}", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
if (TestEqual("Variable count", Expr.GetVariableNames().Num(), 2))
{
TestEqual("Variable name", Expr.GetVariableNames()[0].ToString(), "SomethingFalse");
TestEqual("Variable name", Expr.GetVariableNames()[1].ToString(), "SomethingTrue");
}
TestTrue("BoolCompound2", Expr.ParseFromString("{SomethingFalse} || {SomethingTrue}", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("BoolCompound3", Expr.ParseFromString("{SomethingFalse} or {SomethingTrue}", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
// Test parentheses changing precedence & result
// True result for successful parsing, but false for Eval unless we parenthesise
TestTrue("BoolCompound4", Expr.ParseFromString("!{SomethingFalse} && {SomethingElseFalse} && {SomethingTrue}", nullptr));
TestFalse("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
if (TestEqual("Variable count", Expr.GetVariableNames().Num(), 3))
{
TestEqual("Variable name", Expr.GetVariableNames()[0].ToString(), "SomethingFalse");
TestEqual("Variable name", Expr.GetVariableNames()[1].ToString(), "SomethingElseFalse");
TestEqual("Variable name", Expr.GetVariableNames()[2].ToString(), "SomethingTrue");
}
TestTrue("BoolCompound5", Expr.ParseFromString("!({SomethingFalse} && {SomethingElseFalse}) && {SomethingTrue}", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("BoolCompound6", Expr.ParseFromString("not {SomethingFalse} and {SomethingElseFalse} and {SomethingTrue}", nullptr));
TestFalse("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("BoolCompound7", Expr.ParseFromString("not ({SomethingFalse} and {SomethingElseFalse}) and {SomethingTrue}", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
Variables.Add("Seven", 7);
TestTrue("Comparisons", Expr.ParseFromString("{Six} == 6", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{Six} = 6", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{Six} >= 6", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{Six} > 6", nullptr));
TestFalse("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{Six} < 6", nullptr));
TestFalse("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{Six} <= 6", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{Six} < {Seven}", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{Seven} > {Six}", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{Seven} != {Six}", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{Seven} != {Seven}", nullptr));
TestFalse("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
// Mixed float/int comparisons
Variables.Add("EightFloat", 8.1f);
TestTrue("Comparisons", Expr.ParseFromString("{EightFloat} > 8", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{EightFloat} > {Seven}", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{Six} < {EightFloat}", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
// Fuzzy float comparisons
Variables.Add("EightFloatPlusMargin", 8.1000002f);
TestTrue("Comparisons", Expr.ParseFromString("{EightFloat} == 8.1", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{EightFloat} == 8.15", nullptr));
TestFalse("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{EightFloatPlusMargin} == 8.1", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{EightFloat} == 8.1000005", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{EightFloatPlusMargin} == 8.1000005", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
// Other type comparisons
Variables.Add("SomeText", FText::FromString("Hello"));
TestTrue("Comparisons", Expr.ParseFromString("{SomeText} == \"Hello\"", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{SomeText} == \"Hi\"", nullptr));
TestFalse("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
Variables.Add("Male", ETextGender::Masculine);
Variables.Add("Female", ETextGender::Feminine);
Variables.Add("AlsoFemale", ETextGender::Feminine);
Variables.Add("Neuter", ETextGender::Neuter);
TestTrue("Comparisons", Expr.ParseFromString("{Male} == masculine", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{Male} == Masculine", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{Male} == feminine", nullptr));
TestFalse("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{Male} == Feminine", nullptr));
TestFalse("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{Female} == Feminine", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{Female} != feminine", nullptr));
TestFalse("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{Female} == {AlsoFemale}", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{Female} != {Neuter}", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{Neuter} == neuter", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("Comparisons", Expr.ParseFromString("{Neuter} == Neuter", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
// Test local / global by defining the same variable
GlobalVariables.Add("GlobalLocalTestInt", 3);
Variables.Add("GlobalLocalTestInt", 20);
TestTrue("LocalTest", Expr.ParseFromString("{GlobalLocalTestInt} == 20", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
TestTrue("GlobalTest", Expr.ParseFromString("{global.GlobalLocalTestInt} == 3", nullptr));
TestTrue("Eval", Expr.Evaluate(Variables, GlobalVariables).GetBooleanValue());
return true;
};
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestBadExpressions,
"SUDSTest.TestBadExpressions",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestBadExpressions::RunTest(const FString& Parameters)
{
FSUDSExpression Expr;
TMap<FName, FSUDSValue> Variables;
FString ParseError;
TestFalse("Missing operand", Expr.ParseFromString(" + 1", &ParseError));
TestTrue("Correct error", ParseError.Contains("Bad expression"));
TestFalse("Missing operand", Expr.ParseFromString("1 * ", &ParseError));
TestTrue("Correct error", ParseError.Contains("Bad expression"));
TestFalse("Missing parenthesis", Expr.ParseFromString("(3 + 1", &ParseError));
TestTrue("Correct error", ParseError.Contains("Mismatched parentheses"));
TestFalse("Missing parenthesis", Expr.ParseFromString("3 + 1)", &ParseError));
TestTrue("Correct error", ParseError.Contains("Mismatched parentheses"));
TestFalse("Invalid symbol", Expr.ParseFromString("something + 1", &ParseError));
TestTrue("Correct error", ParseError.Contains("Bad expression"));
return true;
}
UE_ENABLE_OPTIMIZATION

View File

@@ -0,0 +1,417 @@
#include "SUDSDialogue.h"
#include "SUDSLibrary.h"
#include "SUDSMessageLogger.h"
#include "SUDSScript.h"
#include "SUDSScriptImporter.h"
#include "TestUtils.h"
#include "Misc/AutomationTest.h"
UE_DISABLE_OPTIMIZATION
const FString GotoGosubInput = R"RAWSUD(
Player: Hello there
NPC: Hello
:choice
* Actually bye
NPC: How rude
[goto end]
* Reused
NPC: This is going to re-used dialogue
[gosub subroutine]
NPC: And now we're back
[goto goodbye]
* Gosub choice
NPC: This is going to return to the choice
[gosub subroutine]
* Choice After
NPC: It's a choice after a gosub!
[goto goodbye]
* Choice After 2
NPC: Not really much difference eh
[goto goodbye]
* Gosub choice via goto
NPC: This is going to return to the choice via goto
[gosub subroutine]
[goto choice]
:subroutine
Player: Some reused discussion
NPC: Yep, sure is
[return]
:goodbye
NPC: Bye!
)RAWSUD";
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestGotoGosub,
"SUDSTest.TestGotoGosub",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestGotoGosub::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(GotoGosubInput), GotoGosubInput.Len(), "GotoGosubInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
Dlg->Start();
TestDialogueText(this, "Start node", Dlg, "Player", "Hello there");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Next", Dlg, "NPC", "Hello");
TestEqual("Choices", Dlg->GetNumberOfChoices(), 4);
TestEqual("Choice 0 text", Dlg->GetChoiceText(0).ToString(), "Actually bye");
TestTrue("Choose 0", Dlg->Choose(0));
TestDialogueText(this, "Next", Dlg, "NPC", "How rude");
TestFalse("End", Dlg->Continue());
Dlg->Restart();
TestDialogueText(this, "Start node", Dlg, "Player", "Hello there");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Next", Dlg, "NPC", "Hello");
TestTrue("Choose 1", Dlg->Choose(1));
TestDialogueText(this, "Next", Dlg, "NPC", "This is going to re-used dialogue");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Next", Dlg, "Player", "Some reused discussion");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Next", Dlg, "NPC", "Yep, sure is");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Next", Dlg, "NPC", "And now we're back");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Next", Dlg, "NPC", "Bye!");
TestFalse("Continue", Dlg->Continue());
Dlg->Restart();
TestDialogueText(this, "Start node", Dlg, "Player", "Hello there");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Next", Dlg, "NPC", "Hello");
TestTrue("Choose 2", Dlg->Choose(2));
TestDialogueText(this, "Next", Dlg, "NPC", "This is going to return to the choice");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Next", Dlg, "Player", "Some reused discussion");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Next", Dlg, "NPC", "Yep, sure is");
// Should have a choice at the end of it this time
TestEqual("Choices", Dlg->GetNumberOfChoices(), 2);
TestEqual("Choice 0 text", Dlg->GetChoiceText(0).ToString(), "Choice After");
TestEqual("Choice 1 text", Dlg->GetChoiceText(1).ToString(), "Choice After 2");
TestTrue("Choose 1", Dlg->Choose(1));
TestDialogueText(this, "Next", Dlg, "NPC", "Not really much difference eh");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Next", Dlg, "NPC", "Bye!");
TestFalse("Continue", Dlg->Continue());
Dlg->Restart();
TestDialogueText(this, "Start node", Dlg, "Player", "Hello there");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Next", Dlg, "NPC", "Hello");
TestTrue("Choose 2", Dlg->Choose(3));
TestDialogueText(this, "Next", Dlg, "NPC", "This is going to return to the choice via goto");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Next", Dlg, "Player", "Some reused discussion");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Next", Dlg, "NPC", "Yep, sure is");
// Should have a choice at the end of it this time
TestEqual("Choices", Dlg->GetNumberOfChoices(), 4);
TestEqual("Choice 0 text", Dlg->GetChoiceText(0).ToString(), "Actually bye");
TestTrue("Choose 0", Dlg->Choose(0));
TestDialogueText(this, "Next", Dlg, "NPC", "How rude");
TestFalse("End", Dlg->Continue());
Script->MarkAsGarbage();
return true;
}
const FString NestedGosubInput = R"RAWSUD(
Player: Hello there
[gosub sub1]
NPC: Back at level 0
[goto goodbye]
:sub1
Player: This is level 1
[gosub sub2_1]
[gosub sub2_2]
Player: End of level 1
[return]
:sub2_1
Player: This is level 2, sub 1
[return]
:sub2_2
Player: This is level 2, sub 2
NPC: We have to go deeper
[gosub sub3]
Player: End of level 2, sub 2
[return]
:sub3
Player: This is level 3
[return]
:goodbye
NPC: Bye!
)RAWSUD";
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestNestedGosub,
"SUDSTest.TestNestedGosub",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestNestedGosub::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(NestedGosubInput), NestedGosubInput.Len(), "NestedGosubInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
Dlg->Start();
TestDialogueText(this, "Start node", Dlg, "Player", "Hello there");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Sub", Dlg, "Player", "This is level 1");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Sub", Dlg, "Player", "This is level 2, sub 1");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Sub", Dlg, "Player", "This is level 2, sub 2");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Sub", Dlg, "NPC", "We have to go deeper");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Sub", Dlg, "Player", "This is level 3");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Sub", Dlg, "Player", "End of level 2, sub 2");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Sub", Dlg, "Player", "End of level 1");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Sub", Dlg, "NPC", "Back at level 0");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Bye", Dlg, "NPC", "Bye!");
TestFalse("Continue", Dlg->Continue());
Script->MarkAsGarbage();
return true;
}
const FString GotoBetweenSpeakerAndChoiceInput = R"RAWSUD(
Player: Hello there
[if {SkipChoices}]
[goto end]
[endif]
* Option A
* Option B
)RAWSUD";
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestGotoBetweenSpeakerAndChoice1,
"SUDSTest.TestGotoBetweenSpeakerAndChoice1",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestGotoBetweenSpeakerAndChoice1::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(GotoBetweenSpeakerAndChoiceInput), GotoBetweenSpeakerAndChoiceInput.Len(), "GotoBetweenSpeakerAndChoiceInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
Dlg->Start();
if (TestEqual("Choice Count", Dlg->GetNumberOfChoices(), 2))
{
TestEqual("Choice 1", Dlg->GetChoiceText(0).ToString(), "Option A");
TestEqual("Choice 2", Dlg->GetChoiceText(1).ToString(), "Option B");
}
Script->MarkAsGarbage();
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestGotoBetweenSpeakerAndChoice2,
"SUDSTest.TestGotoBetweenSpeakerAndChoice2",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestGotoBetweenSpeakerAndChoice2::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(GotoBetweenSpeakerAndChoiceInput), GotoBetweenSpeakerAndChoiceInput.Len(), "GotoBetweenSpeakerAndChoiceInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
Dlg->SetVariableBoolean("SkipChoices", true);
Dlg->Start();
TestDialogueText(this, "Start node", Dlg, "Player", "Hello there");
TestTrue("Plain continue", Dlg->IsSimpleContinue());
Script->MarkAsGarbage();
return true;
}
const FString GosubBetweenSpeakerAndChoiceInput1 = R"RAWSUD(
Player: Hello there
[gosub MaybeSkipChoices]
* Option A
* Option B
[goto end]
:MaybeSkipChoices
[if {SkipChoices}]
[goto end]
[endif]
[return]
)RAWSUD";
const FString GosubBetweenSpeakerAndChoiceInput2 = R"RAWSUD(
Player: Hello there
[gosub PrintDebug]
[gosub MaybeSkipChoices]
* Option A
* Option B
[goto end]
:PrintDebug
Debug: SkipChoices is {SkipChoices}
[return]
:MaybeSkipChoices
[if {SkipChoices}]
[goto end]
[endif]
Speaker: Another sentence.
[return]
)RAWSUD";
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestGosubBetweenSpeakerAndChoice1,
"SUDSTest.TestGosubBetweenSpeakerAndChoice1",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestGosubBetweenSpeakerAndChoice1::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(GosubBetweenSpeakerAndChoiceInput1), GosubBetweenSpeakerAndChoiceInput1.Len(), "GosubBetweenSpeakerAndChoiceInput1", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
Dlg->Start();
TestDialogueText(this, "Start node", Dlg, "Player", "Hello there");
if (TestEqual("Choice Count", Dlg->GetNumberOfChoices(), 2))
{
TestEqual("Choice 1", Dlg->GetChoiceText(0).ToString(), "Option A");
TestEqual("Choice 2", Dlg->GetChoiceText(1).ToString(), "Option B");
}
// Now test the true case
Dlg->SetVariableBoolean("SkipChoices", true);
Dlg->Restart(false);
TestDialogueText(this, "Start node", Dlg, "Player", "Hello there");
TestTrue("Plain continue", Dlg->IsSimpleContinue());
Script->MarkAsGarbage();
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestGosubBetweenSpeakerAndChoice2,
"SUDSTest.TestGosubBetweenSpeakerAndChoice2",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestGosubBetweenSpeakerAndChoice2::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(GosubBetweenSpeakerAndChoiceInput2), GosubBetweenSpeakerAndChoiceInput2.Len(), "GosubBetweenSpeakerAndChoiceInput2", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
Dlg->Start();
TestDialogueText(this, "Start node", Dlg, "Player", "Hello there");
// in this case, there's another speaker line in both nested gosubs so the choices are actually associated with the
// last of those.
TestTrue("Plain continue", Dlg->Continue());
TestDialogueText(this, "Gosub 1 speaker node", Dlg, "Debug", "SkipChoices is {SkipChoices}");
TestTrue("Plain continue", Dlg->Continue());
TestDialogueText(this, "Gosub 2 speaker node", Dlg, "Speaker", "Another sentence.");
if (TestEqual("Choice Count", Dlg->GetNumberOfChoices(), 2))
{
TestEqual("Choice 1", Dlg->GetChoiceText(0).ToString(), "Option A");
TestEqual("Choice 2", Dlg->GetChoiceText(1).ToString(), "Option B");
}
// Now test the true case
Dlg->SetVariableBoolean("SkipChoices", true);
Dlg->Restart(false);
TestDialogueText(this, "Start node", Dlg, "Player", "Hello there");
TestTrue("Plain continue", Dlg->Continue());
TestDialogueText(this, "Gosub 1 speaker node", Dlg, "Debug", "SkipChoices is 1");
TestTrue("Plain continue", Dlg->IsSimpleContinue());
Script->MarkAsGarbage();
return true;
}
UE_ENABLE_OPTIMIZATION

View File

@@ -0,0 +1,133 @@
#include "SUDSDialogue.h"
#include "SUDSLibrary.h"
#include "SUDSMessageLogger.h"
#include "SUDSScript.h"
#include "SUDSScriptImporter.h"
#include "TestUtils.h"
#include "Internationalization/StringTableCore.h"
#include "Misc/AutomationTest.h"
UE_DISABLE_OPTIMIZATION
// Assign known keys to the strings so we can detect
const FString MetadataInput = R"RAWSUD(
# No metadata
Player: Hello there @001@
#= Transient metadata for next line
#= TransientData: Something here
NPC: Hello @002@
# Should have no metadata anymore
Player: Well this is nice @003@
#+ Persistent metadata
#+ PersistentData: Something longer lived
NPC: Isn't it though @004@
# Should still apply here
Player: Indeed @005@
#= Test overriding temporarily
#= PersistentData: This should override
#= ExtraTransient: This should be new only for next line
NPC: Well well @006@
Player: Metadata should have gone back now @007@
#+ Persistent metadata changed now
Player: Persistent change @008@
* Some choice @009@
NPC: How rude @010@
[goto end]
#= Temporary comment
* Another choice @011@
NPC: This should be back now @012@
#+ Persistent indented change 1
* One more choice @013@
#+ Even more indented
#+ NestedKey: This will disappear fast even though persistent
NPC: Ooops @014@
* Final choice @015@
NPC: This is going to return to the choice via goto @016@
# Nested meta should have been lost but outer meta should be back
Player: Something something @017@
)RAWSUD";
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestMetadata,
"SUDSTest.TestMetadata",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestMetadata::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(MetadataInput), MetadataInput.Len(), "MetadataInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// In this test we're only interested in the string table metadata
// Have to get the mutable string table to get at metadata
auto StrTable = StringTableHolder.StringTable->GetMutableStringTable();
TestEqual("Line 1 Comment", StrTable->GetMetaData(FTextKey("@001@"), FName("Comment")), "");
TestEqual("Line 1 Speaker", StrTable->GetMetaData(FTextKey("@001@"), FName("Speaker")), "Player");
TestEqual("Line 2 Comment", StrTable->GetMetaData(FTextKey("@002@"), FName("Comment")), "Transient metadata for next line");
TestEqual("Line 2 Speaker", StrTable->GetMetaData(FTextKey("@002@"), FName("Speaker")), "NPC");
TestEqual("Line 2 Custom", StrTable->GetMetaData(FTextKey("@002@"), FName("TransientData")), "Something here");
TestEqual("Line 3 Comment (should have reset)", StrTable->GetMetaData(FTextKey("@003@"), FName("Comment")), "");
TestEqual("Line 3 Speaker", StrTable->GetMetaData(FTextKey("@003@"), FName("Speaker")), "Player");
TestEqual("Line 3 Custom (should have reset)", StrTable->GetMetaData(FTextKey("@003@"), FName("TransientData")), "");
TestEqual("Line 4 Comment", StrTable->GetMetaData(FTextKey("@004@"), FName("Comment")), "Persistent metadata");
TestEqual("Line 4 Speaker", StrTable->GetMetaData(FTextKey("@004@"), FName("Speaker")), "NPC");
TestEqual("Line 4 Custom", StrTable->GetMetaData(FTextKey("@004@"), FName("PersistentData")), "Something longer lived");
TestEqual("Line 5 Comment", StrTable->GetMetaData(FTextKey("@005@"), FName("Comment")), "Persistent metadata");
TestEqual("Line 5 Speaker", StrTable->GetMetaData(FTextKey("@005@"), FName("Speaker")), "Player");
TestEqual("Line 5 Custom", StrTable->GetMetaData(FTextKey("@005@"), FName("PersistentData")), "Something longer lived");
TestEqual("Line 6 Comment", StrTable->GetMetaData(FTextKey("@006@"), FName("Comment")), "Test overriding temporarily");
TestEqual("Line 6 Speaker", StrTable->GetMetaData(FTextKey("@006@"), FName("Speaker")), "NPC");
TestEqual("Line 6 Custom", StrTable->GetMetaData(FTextKey("@006@"), FName("PersistentData")), "This should override");
TestEqual("Line 6 Custom 2", StrTable->GetMetaData(FTextKey("@006@"), FName("ExtraTransient")), "This should be new only for next line");
TestEqual("Line 7 Comment", StrTable->GetMetaData(FTextKey("@007@"), FName("Comment")), "Persistent metadata");
TestEqual("Line 7 Speaker", StrTable->GetMetaData(FTextKey("@007@"), FName("Speaker")), "Player");
TestEqual("Line 7 Custom", StrTable->GetMetaData(FTextKey("@007@"), FName("PersistentData")), "Something longer lived");
TestEqual("Line 8 Comment", StrTable->GetMetaData(FTextKey("@008@"), FName("Comment")), "Persistent metadata changed now");
TestEqual("Line 8 Speaker", StrTable->GetMetaData(FTextKey("@008@"), FName("Speaker")), "Player");
TestEqual("Line 8 Custom", StrTable->GetMetaData(FTextKey("@008@"), FName("PersistentData")), "Something longer lived");
TestEqual("Line 9 Comment", StrTable->GetMetaData(FTextKey("@009@"), FName("Comment")), "Persistent metadata changed now");
TestEqual("Line 9 Speaker", StrTable->GetMetaData(FTextKey("@009@"), FName("Speaker")), "Player (Choice)");
TestEqual("Line 9 Custom", StrTable->GetMetaData(FTextKey("@009@"), FName("PersistentData")), "Something longer lived");
TestEqual("Line 10 Comment", StrTable->GetMetaData(FTextKey("@010@"), FName("Comment")), "Persistent metadata changed now");
TestEqual("Line 10 Speaker", StrTable->GetMetaData(FTextKey("@010@"), FName("Speaker")), "NPC");
TestEqual("Line 10 Custom", StrTable->GetMetaData(FTextKey("@010@"), FName("PersistentData")), "Something longer lived");
TestEqual("Line 11 Comment", StrTable->GetMetaData(FTextKey("@011@"), FName("Comment")), "Temporary comment");
TestEqual("Line 11 Speaker", StrTable->GetMetaData(FTextKey("@011@"), FName("Speaker")), "Player (Choice)");
TestEqual("Line 11 Custom", StrTable->GetMetaData(FTextKey("@011@"), FName("PersistentData")), "Something longer lived");
TestEqual("Line 12 Comment", StrTable->GetMetaData(FTextKey("@012@"), FName("Comment")), "Persistent metadata changed now");
TestEqual("Line 12 Speaker", StrTable->GetMetaData(FTextKey("@012@"), FName("Speaker")), "NPC");
TestEqual("Line 12 Custom", StrTable->GetMetaData(FTextKey("@012@"), FName("PersistentData")), "Something longer lived");
TestEqual("Line 13 Comment", StrTable->GetMetaData(FTextKey("@013@"), FName("Comment")), "Persistent indented change 1");
TestEqual("Line 13 Speaker", StrTable->GetMetaData(FTextKey("@013@"), FName("Speaker")), "Player (Choice)");
TestEqual("Line 13 Custom", StrTable->GetMetaData(FTextKey("@013@"), FName("PersistentData")), "Something longer lived");
TestEqual("Line 14 Comment", StrTable->GetMetaData(FTextKey("@014@"), FName("Comment")), "Even more indented");
TestEqual("Line 14 Speaker", StrTable->GetMetaData(FTextKey("@014@"), FName("Speaker")), "NPC");
TestEqual("Line 14 Custom", StrTable->GetMetaData(FTextKey("@014@"), FName("PersistentData")), "Something longer lived");
TestEqual("Line 14 Custom 2", StrTable->GetMetaData(FTextKey("@014@"), FName("NestedKey")), "This will disappear fast even though persistent");
TestEqual("Line 15 Comment", StrTable->GetMetaData(FTextKey("@015@"), FName("Comment")), "Persistent indented change 1"); // should have gone back
TestEqual("Line 15 Speaker", StrTable->GetMetaData(FTextKey("@015@"), FName("Speaker")), "Player (Choice)");
TestEqual("Line 15 Custom", StrTable->GetMetaData(FTextKey("@015@"), FName("PersistentData")), "Something longer lived");
TestEqual("Line 15 Custom 2", StrTable->GetMetaData(FTextKey("@015@"), FName("NestedKey")), "");
TestEqual("Line 16 Comment", StrTable->GetMetaData(FTextKey("@016@"), FName("Comment")), "Persistent indented change 1"); // should have gone back
TestEqual("Line 16 Speaker", StrTable->GetMetaData(FTextKey("@016@"), FName("Speaker")), "NPC");
TestEqual("Line 16 Custom", StrTable->GetMetaData(FTextKey("@016@"), FName("PersistentData")), "Something longer lived");
TestEqual("Line 16 Custom 2", StrTable->GetMetaData(FTextKey("@016@"), FName("NestedKey")), "");
TestEqual("Line 17 Comment", StrTable->GetMetaData(FTextKey("@017@"), FName("Comment")), "Persistent metadata changed now"); // should have gone back to top level
TestEqual("Line 17 Speaker", StrTable->GetMetaData(FTextKey("@017@"), FName("Speaker")), "Player");
TestEqual("Line 17 Custom", StrTable->GetMetaData(FTextKey("@017@"), FName("PersistentData")), "Something longer lived");
Script->MarkAsGarbage();
return true;
}
UE_ENABLE_OPTIMIZATION

View File

@@ -0,0 +1,147 @@
#include "SUDSDialogue.h"
#include "SUDSLibrary.h"
#include "SUDSMessageLogger.h"
#include "SUDSScript.h"
#include "SUDSScriptImporter.h"
#include "TestParticipant.h"
#include "TestUtils.h"
#include "Internationalization/Internationalization.h"
#include "Misc/AutomationTest.h"
UE_DISABLE_OPTIMIZATION
const FString ParamsInput = R"RAWSUD(
Player: Hello, I'm {SpeakerName.Player}
NPC: Greetings, {SpeakerName.Player}, my name is {SpeakerName.NPC}
Player: My friend's name is {FriendName}, {Gender}|gender(he,she,they) {Gender}|gender(has,has,have) {NumCats} {NumCats}|plural(one=cat,other=cats)
NPC: Floating point {FloatVal} format test
Player: Boolean test {BoolVal}?
* Choose, {SpeakerName.Player}!
* Is {NumCats} {NumCats}|plural(one=cat,other=cats) too many?
NPC: No, {numcats} is fine
)RAWSUD";
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestParameters,
"SUDSTest.TestParameters",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestParameters::RunTest(const FString& Parameters)
{
// Number and plural formatting are locale-specific, so we must set it to a predefined value to produce the expected output
FInternationalization::FCultureStateSnapshot CultureStateSnapshot;
FInternationalization::Get().BackupCultureState(CultureStateSnapshot);
FInternationalization::Get().SetCurrentCulture(TEXT("en-US"));
ON_SCOPE_EXIT
{
FInternationalization::Get().RestoreCultureState(CultureStateSnapshot);
};
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ParamsInput), ParamsInput.Len(), "ParamsInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
auto Participant = NewObject<UTestParticipant>();
Participant->TestNumber = 0;
Dlg->AddParticipant(Participant);
Dlg->Start();
TestDialogueText(this, "Line 1", Dlg, "Player", "Hello, I'm Protagonist");
Dlg->Continue();
TestDialogueText(this, "Line 2", Dlg, "NPC", "Greetings, Protagonist, my name is An NPC");
Dlg->Continue();
TestDialogueText(this, "Line 3", Dlg, "Player", "My friend's name is Susan, she has 3 cats");
Dlg->Continue();
TestDialogueText(this, "Line 4", Dlg, "NPC", "Floating point 12.567 format test");
Dlg->Continue();
TestDialogueText(this, "Line 5", Dlg, "Player", "Boolean test 1?");
if (TestEqual("Number of choices", Dlg->GetNumberOfChoices(), 2))
{
TestEqual("Choice text 1", Dlg->GetChoiceText(0).ToString(), "Choose, Protagonist!");
TestEqual("Choice text 2", Dlg->GetChoiceText(1).ToString(), "Is 3 cats too many?");
}
// test case insensitivity, this next line uses {numcats} instead of {NumCats}
Dlg->Choose(1);
TestDialogueText(this, "Line 6", Dlg, "NPC", "No, 3 is fine");
Script->MarkAsGarbage();
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestParametersPriority,
"SUDSTest.TestParametersPriority",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestParametersPriority::RunTest(const FString& Parameters)
{
// Number and plural formatting are locale-specific, so we must set it to a predefined value to produce the expected output
FInternationalization::FCultureStateSnapshot CultureStateSnapshot;
FInternationalization::Get().BackupCultureState(CultureStateSnapshot);
FInternationalization::Get().SetCurrentCulture(TEXT("en-US"));
ON_SCOPE_EXIT
{
FInternationalization::Get().RestoreCultureState(CultureStateSnapshot);
};
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ParamsInput), ParamsInput.Len(), "ParamsInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
auto Participant1 = NewObject<UTestParticipant>();
Participant1->TestNumber = 0; // priority 0
auto Participant2 = NewObject<UTestParticipant>();
Participant2->TestNumber = 1; // priority 100
auto Participant3 = NewObject<UTestParticipant>();
Participant1->TestNumber = 2; // priority -200
Dlg->AddParticipant(Participant1);
Dlg->AddParticipant(Participant2);
Dlg->AddParticipant(Participant3);
Dlg->Start();
// Ordering of the participants should be from low to high, so variables from higher priority value participant should
// have overridden lower priority ones
// All of these are set by Participant 2 who is higher priority
TestDialogueText(this, "Line 1", Dlg, "Player", "Hello, I'm Hero");
Dlg->Continue();
TestDialogueText(this, "Line 2", Dlg, "NPC", "Greetings, Hero, my name is Bob The NPC");
Dlg->Continue();
TestDialogueText(this, "Line 3", Dlg, "Player", "My friend's name is Derek, he has 5 cats");
Dlg->Continue();
// These are set by Participant1 and unchanged by 2. 3 has tried to set float but should have been overridden
TestDialogueText(this, "Line 4", Dlg, "NPC", "Floating point 12.567 format test");
Dlg->Continue();
TestDialogueText(this, "Line 5", Dlg, "Player", "Boolean test 1?");
// Check that there's a variable from Participant2 which no-one else set
TestEqual("Participant3 should have set something", Dlg->GetVariableInt("SomethingUniqueTo3"), 120);
Script->MarkAsGarbage();
return true;
}
UE_ENABLE_OPTIMIZATION

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
#include "TestParticipant.h"
#include "SUDSDialogue.h"
void UTestParticipant::OnDialogueStarting_Implementation(USUDSDialogue* Dialogue, FName AtLabel)
{
switch(TestNumber)
{
default:
case 0:
Dialogue->SetVariable("SpeakerName.Player", FText::FromString("Protagonist"));
Dialogue->SetVariable("SpeakerName.NPC", FText::FromString("An NPC"));
Dialogue->SetVariable("NumCats", 3);
Dialogue->SetVariableText("FriendName", FText::FromString("Susan"));
Dialogue->SetVariable("Gender", ETextGender::Feminine);
Dialogue->SetVariableFloat("FloatVal", 12.567);
Dialogue->SetVariableBoolean("BoolVal", true);
break;
case 1:
Dialogue->SetVariable("SpeakerName.Player", FText::FromString("Hero"));
Dialogue->SetVariable("SpeakerName.NPC", FText::FromString("Bob The NPC"));
Dialogue->SetVariableText("FriendName", FText::FromString("Derek"));
Dialogue->SetVariable("Gender", ETextGender::Masculine);
Dialogue->SetVariable("NumCats", 5);
break;
case 2:
// these will all be overridden by higher priorities
Dialogue->SetVariable("SpeakerName.Player", FText::FromString("Dweeb"));
Dialogue->SetVariable("SpeakerName.NPC", FText::FromString("Imaginary Friend"));
Dialogue->SetVariable("NumCats", 10);
Dialogue->SetVariableFloat("FloatVal", 0.002);
// This one will be unique and so will still get through
Dialogue->SetVariableInt("SomethingUniqueTo3", 120);
break;
}
}
int UTestParticipant::GetDialogueParticipantPriority_Implementation() const
{
switch(TestNumber)
{
default:
case 0:
return 0;
case 1:
return 100;
case 2:
return -200;
}
}
void UTestParticipant::OnDialogueEvent_Implementation(USUDSDialogue* Dialogue,
FName EventName,
const TArray<FSUDSValue>& Arguments)
{
EventRecords.Add(FEventRecord { EventName, Arguments });
}
void UTestParticipant::OnDialogueVariableChanged_Implementation(USUDSDialogue* Dialogue,
FName VariableName,
const FSUDSValue& Value,
bool bFromScript)
{
SetVarRecords.Add(FSetVarRecord { VariableName, Value, bFromScript });
}

View File

@@ -0,0 +1,45 @@
#pragma once
#include "CoreMinimal.h"
#include "SUDSParticipant.h"
#include "SUDSValue.h"
#include "UObject/Object.h"
#include "TestParticipant.generated.h"
/**
*
*/
UCLASS()
class SUDSTEST_API UTestParticipant : public UObject, public ISUDSParticipant
{
GENERATED_BODY()
public:
int TestNumber = 0;
struct FEventRecord
{
FName Name;
TArray<FSUDSValue> Args;
};
struct FSetVarRecord
{
FName Name;
FSUDSValue Value;
bool bFromScript;
};
TArray<FEventRecord> EventRecords;
TArray<FSetVarRecord> SetVarRecords;
virtual void OnDialogueStarting_Implementation(USUDSDialogue* Dialogue, FName AtLabel) override;
virtual int GetDialogueParticipantPriority_Implementation() const override;
virtual void OnDialogueEvent_Implementation(USUDSDialogue* Dialogue,
FName EventName,
const TArray<FSUDSValue>& Arguments) override;
virtual void OnDialogueVariableChanged_Implementation(USUDSDialogue* Dialogue,
FName VariableName,
const FSUDSValue& Value,
bool bFromScript) override;
};

View File

@@ -0,0 +1,342 @@
#include "SUDSLibrary.h"
#include "SUDSMessageLogger.h"
#include "SUDSScript.h"
#include "SUDSScriptImporter.h"
#include "TestUtils.h"
UE_DISABLE_OPTIMIZATION
const FString BasicRandomInput = R"RAWSUD(
Player: Hello
:start
[random]
NPC: Reply when random == 0
[or]
NPC: Reply when random == 1
[or]
NPC: Reply when random == 2
[endrandom]
Player: OK
[goto start]
)RAWSUD";
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestRandomBasics,
"SUDSTest.TestRandomBasics",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestRandomBasics::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(BasicRandomInput), BasicRandomInput.Len(), "BasicRandomInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
// Seed random so we have consistent results
FMath::SRandInit(34);
Dlg->Start();
TestDialogueText(this, "Text node", Dlg, "Player", "Hello");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Random node", Dlg, "NPC", "Reply when random == 2");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Final node", Dlg, "Player", "OK");
// Should loop, run random again
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Random node", Dlg, "NPC", "Reply when random == 1");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Final node", Dlg, "Player", "OK");
// Should loop, run random again
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Random node", Dlg, "NPC", "Reply when random == 0");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Final node", Dlg, "Player", "OK");
// Should loop, run random again
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Random node", Dlg, "NPC", "Reply when random == 1");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Final node", Dlg, "Player", "OK");
Script->MarkAsGarbage();
return true;
}
const FString NestedRandomInput = R"RAWSUD(
Player: Hello
:start
[random]
NPC: Reply when random == 0
[or]
[random]
NPC: Reply when random == 1 && subrandom == 0
[or]
NPC: Reply when random == 1 && subrandom == 1
[endrandom]
[or]
NPC: Reply when random == 2
[endrandom]
Player: OK
[goto start]
)RAWSUD";
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestRandomNested,
"SUDSTest.TestRandomNested",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestRandomNested::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(NestedRandomInput), NestedRandomInput.Len(), "NestedRandomInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
// Seed random so we have consistent results
FMath::SRandInit(785);
Dlg->Start();
TestDialogueText(this, "Text node", Dlg, "Player", "Hello");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Random node", Dlg, "NPC", "Reply when random == 0");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Final node", Dlg, "Player", "OK");
TestTrue("Continue", Dlg->Continue());
// Restart
TestDialogueText(this, "Random node", Dlg, "NPC", "Reply when random == 1 && subrandom == 1");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Final node", Dlg, "Player", "OK");
TestTrue("Continue", Dlg->Continue());
// Restart
TestDialogueText(this, "Random node", Dlg, "NPC", "Reply when random == 1 && subrandom == 0");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Final node", Dlg, "Player", "OK");
TestTrue("Continue", Dlg->Continue());
// Restart
TestDialogueText(this, "Random node", Dlg, "NPC", "Reply when random == 2");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Final node", Dlg, "Player", "OK");
TestTrue("Continue", Dlg->Continue());
// Restart
TestDialogueText(this, "Random node", Dlg, "NPC", "Reply when random == 0");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Final node", Dlg, "Player", "OK");
TestTrue("Continue", Dlg->Continue());
Script->MarkAsGarbage();
return true;
}
const FString SiblingRandomInput = R"RAWSUD(
Player: Hello
:start
[random]
NPC: Reply when random == 0
[or]
NPC: Reply when random == 1
[or]
NPC: Reply when random == 2
[endrandom]
[random]
NPC: Second random == 0
[or]
NPC: Second random == 1
[or]
NPC: Second random == 2
[endrandom]
Player: OK
[goto start]
)RAWSUD";
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestRandomSibling,
"SUDSTest.TestRandomSibling",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestRandomSibling::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(SiblingRandomInput), SiblingRandomInput.Len(), "SiblingRandomInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
// Seed random so we have consistent results
FMath::SRandInit(2376);
Dlg->Start();
TestDialogueText(this, "Text node", Dlg, "Player", "Hello");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Text node", Dlg, "NPC", "Reply when random == 1");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Text node", Dlg, "NPC", "Second random == 0");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Text node", Dlg, "Player", "OK");
TestTrue("Continue", Dlg->Continue());
// Restart
TestDialogueText(this, "Text node", Dlg, "NPC", "Reply when random == 0");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Text node", Dlg, "NPC", "Second random == 2");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Text node", Dlg, "Player", "OK");
TestTrue("Continue", Dlg->Continue());
// Restart
TestDialogueText(this, "Text node", Dlg, "NPC", "Reply when random == 1");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Text node", Dlg, "NPC", "Second random == 2");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Text node", Dlg, "Player", "OK");
TestTrue("Continue", Dlg->Continue());
// Restart
TestDialogueText(this, "Text node", Dlg, "NPC", "Reply when random == 0");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Text node", Dlg, "NPC", "Second random == 0");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Text node", Dlg, "Player", "OK");
TestTrue("Continue", Dlg->Continue());
// Restart
TestDialogueText(this, "Text node", Dlg, "NPC", "Reply when random == 2");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Text node", Dlg, "NPC", "Second random == 2");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Text node", Dlg, "Player", "OK");
TestTrue("Continue", Dlg->Continue());
Script->MarkAsGarbage();
return true;
}
const FString MixedConditionalChoiceAndRandomInput = R"RAWSUD(
Player: Hello
* First choice
Player: I took the 1.1 choice
[if {x} > 0]
* Second choice (conditional)
[random]
Player: I took the 1.2 choice, random == 0
[or]
Player: I took the 1.2 choice, random == 1
[endif]
* Third choice (conditional)
Player: I took the 1.3 choice
[else]
* Second Alt Choice
[random]
Player: I took the alt 1.2 choice, random == 0
[or]
Player: I took the alt 1.2 choice, random == 1
[endif]
[endif]
* Common last choice
Player: I took the 1.4 choice
)RAWSUD";
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestRandomMixed,
"SUDSTest.TestRandomMixed",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestRandomMixed::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(MixedConditionalChoiceAndRandomInput), MixedConditionalChoiceAndRandomInput.Len(), "MixedRandomInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
// Seed random so we have consistent results
FMath::SRandInit(999);
Dlg->SetVariableInt("x", 5);
Dlg->Start();
TestDialogueText(this, "Start node", Dlg, "Player", "Hello");
// 2 choices from conditional, 2 common
if (TestEqual("Choice count when x=5", Dlg->GetNumberOfChoices(), 4))
{
TestTrue("Choose 1", Dlg->Choose(1));
TestDialogueText(this, "Text node", Dlg, "Player", "I took the 1.2 choice, random == 0");
}
// Restart, same again but different result if random
Dlg->Restart(false);
TestDialogueText(this, "Start node", Dlg, "Player", "Hello");
// 2 choices from conditional, 2 common
if (TestEqual("Choice count when x=5", Dlg->GetNumberOfChoices(), 4))
{
TestTrue("Choose 1", Dlg->Choose(1));
TestDialogueText(this, "Text node", Dlg, "Player", "I took the 1.2 choice, random == 1");
}
// Now change to other conditional path
Dlg->SetVariableInt("x", 0);
Dlg->Restart(false);
TestDialogueText(this, "Start node", Dlg, "Player", "Hello");
if (TestEqual("Choice count when x=0", Dlg->GetNumberOfChoices(), 3))
{
TestTrue("Choose 1", Dlg->Choose(1));
TestDialogueText(this, "Text node", Dlg, "Player", "I took the alt 1.2 choice, random == 1");
}
Dlg->Restart(false);
TestDialogueText(this, "Start node", Dlg, "Player", "Hello");
if (TestEqual("Choice count when x=0", Dlg->GetNumberOfChoices(), 3))
{
TestTrue("Choose 1", Dlg->Choose(1));
TestDialogueText(this, "Text node", Dlg, "Player", "I took the alt 1.2 choice, random == 0");
}
Script->MarkAsGarbage();
return true;
}
UE_ENABLE_OPTIMIZATION

View File

@@ -0,0 +1,492 @@
#include "SUDSDialogue.h"
#include "SUDSLibrary.h"
#include "SUDSMessageLogger.h"
#include "SUDSScript.h"
#include "SUDSScriptImporter.h"
#include "SUDSSubsystem.h"
#include "TestUtils.h"
#include "Internationalization/Internationalization.h"
#include "Misc/AutomationTest.h"
UE_DISABLE_OPTIMIZATION
const FString SimpleRunnerInput = R"RAWSUD(
:start
Player: Hello there
NPC: Salutations fellow human
:choice
* Actually no
NPC: How rude, bye then
[goto end]
* Nested option
:nestedstart
NPC: Some nesting
* Actually bye
Player: Gotta go!
[go to goodbye]
* A fallthrough choice
NPC: This should fall through to latterhalf
* A goto choice
[goto latterhalf]
* Another option
Player: What now?
NPC: This is another fallthrough
:latterhalf
Player: This is the latter half of the discussion
NPC: Yep, sure is
* Go back to choice
NPC: Okay!
[goto choice]
* Return to the start
NPC: Gotcha
[goto start]
* Continue
Player: OK I'd like to carry on now
NPC: Right you are guv, falling through
:goodbye
NPC: Bye!
)RAWSUD";
const FString SetVariableRunnerInput = R"RAWSUD(
===
# Set some vars in header
# Text var with an existing localised ID
[set SpeakerName.Player "Protagonist"] @12345@
# Text var no localised ID
[set ValetName "Bob"]
// Also test with equals signs
[set SomeFloat = 12.5]
[set SomeCalculatedInt = (3 + 4) * 2]
[set SomeCalculatedBoolean = true or false]
// Test global vars
[set global.SomeGlobalInt 3]
[set global.SomeGlobalFloat = 12.5 * 3]
===
Player: Hello
[set SomeInt 99]
# Test that we can use variables in set and that ordering works
[set SomeOtherFloat {SomeFloat} + 10]
[set SomeFloat 43.754]
# Test global values
NPC: Wotcha
Player: Values are: {global.SomeGlobalInt}, {global.SomeGlobalFloat}, {global.GlobalIntSetOutside}
# Test that inserting a set node in between text and choice doesn't break link
[set SomeGender masculine]
* Choice 1
[set SomeBoolean True]
NPC: Truth
* Choice 2
NPC: Surprise
[set ValetName "Kate"]
[set SomeGender feminine]
[set someint 101]
Player: Well
)RAWSUD";
const FString FallthroughEdgeCaseInput = R"RAWSUD(
NPC: First line
* Option 1
NPC: Fallthrough via goto
[goto secondchoice]
* Option 2
NPC: Fallthrough implicitly
* Option 3
NPC: Fallthrough implicitly again
:secondchoice
* Fallthrough 1
NPC: text 1
* Fallthrough 2
NPC: text 2
)RAWSUD";
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestSimpleRunning,
"SUDSTest.TestSimpleRunning",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestSimpleRunning::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(SimpleRunnerInput), SimpleRunnerInput.Len(), "SimpleRunnerInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
Dlg->Start();
TestDialogueText(this, "First node", Dlg, "Player", "Hello there");
TestEqual("First node choices", Dlg->GetNumberOfChoices(), 1);
TestTrue("First node choice text", Dlg->GetChoiceText(0).IsEmpty());
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Node 2", Dlg, "NPC", "Salutations fellow human");
TestEqual("Node 2 choices", Dlg->GetNumberOfChoices(), 3);
TestEqual("Node 2 choice text 0", Dlg->GetChoiceText(0).ToString(), "Actually no");
TestEqual("Node 2 choice text 1", Dlg->GetChoiceText(1).ToString(), "Nested option");
TestEqual("Node 2 choice text 2", Dlg->GetChoiceText(2).ToString(), "Another option");
TestTrue("Choice 1", Dlg->Choose(0));
TestDialogueText(this, "Choice 1 Text", Dlg, "NPC", "How rude, bye then");
// Goes straight to end
TestFalse("Choice 1 Follow On", Dlg->Continue());
TestTrue("Should be at end", Dlg->IsEnded());
// Start again
Dlg->Restart();
TestDialogueText(this, "First node", Dlg, "Player", "Hello there");
TestEqual("First node choices", Dlg->GetNumberOfChoices(), 1);
TestTrue("First node choice text", Dlg->GetChoiceText(0).IsEmpty());
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Node 2", Dlg, "NPC", "Salutations fellow human");
// Confirm we previously chose 0
TestTrue("Choice 0 taken previously", Dlg->HasChoiceIndexBeenTakenPreviously(0));
TestTrue("Choice 2", Dlg->Choose(1));
TestDialogueText(this, "Choice 2 Text", Dlg, "NPC", "Some nesting");
TestEqual("Choice 2 nested choices", Dlg->GetNumberOfChoices(), 3);
TestEqual("Choice 2 nested choice text 0", Dlg->GetChoiceText(0).ToString(), "Actually bye");
TestEqual("Choice 2 nested choice text 1", Dlg->GetChoiceText(1).ToString(), "A fallthrough choice");
TestEqual("Choice 2 nested choice text 2", Dlg->GetChoiceText(2).ToString(), "A goto choice");
TestTrue("Nested choice made", Dlg->Choose(0));
TestDialogueText(this, "Nested choice made text", Dlg, "Player", "Gotta go!");
TestTrue("Nested choice follow On", Dlg->Continue());
TestDialogueText(this, "Nested choice follow on text", Dlg, "NPC", "Bye!");
TestFalse("Nested choice follow On 2", Dlg->Continue());
TestTrue("Should be at end", Dlg->IsEnded());
// Start again, this time from nested choice, and reset all state
Dlg->Restart(true, "nestedstart");
TestDialogueText(this, "nestedchoice restart Text", Dlg, "NPC", "Some nesting");
TestTrue("Nested choice made", Dlg->Choose(1));
TestDialogueText(this, "Nested choice 2 Text", Dlg, "NPC", "This should fall through to latterhalf");
TestTrue("Nested choice 2 follow On", Dlg->Continue());
// Should have fallen through
TestDialogueText(this, "Fallthrough Text", Dlg, "Player", "This is the latter half of the discussion");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Fallthrough Text 2", Dlg, "NPC", "Yep, sure is");
TestEqual("Fallthrough choices", Dlg->GetNumberOfChoices(), 3);
TestEqual("Fallthrough choice text 0", Dlg->GetChoiceText(0).ToString(), "Go back to choice");
TestEqual("Fallthrough choice text 1", Dlg->GetChoiceText(1).ToString(), "Return to the start");
TestEqual("Fallthrough choice text 2", Dlg->GetChoiceText(2).ToString(), "Continue");
// Go back to choice
TestTrue("Fallthrough choice made", Dlg->Choose(0));
TestDialogueText(this, "Fallthrough Choice Text", Dlg, "NPC", "Okay!");
// The Goto choice should have collapsed the choices such that we can get them immediately
TestEqual("Fallthrough then goto choices", Dlg->GetNumberOfChoices(), 3);
TestEqual("Fallthrough then goto choice text 0", Dlg->GetChoiceText(0).ToString(), "Actually no");
TestEqual("Fallthrough then goto choice text 1", Dlg->GetChoiceText(1).ToString(), "Nested option");
TestEqual("Fallthrough then goto choice text 2", Dlg->GetChoiceText(2).ToString(), "Another option");
// Restart to test another path
Dlg->Restart(true, "nestedstart");
TestDialogueText(this, "nestedchoice restart Text", Dlg, "NPC", "Some nesting");
TestTrue("Nested choice made", Dlg->Choose(2));
// This should be a direct goto to latterhalf
TestDialogueText(this, "Direct goto", Dlg, "Player", "This is the latter half of the discussion");
Dlg->Restart(true);
TestTrue("Continue", Dlg->Continue());
TestTrue("Choice 3", Dlg->Choose(2));
TestDialogueText(this, "Choice 3 Text", Dlg, "Player", "What now?");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Choice 3 Text 2", Dlg, "NPC", "This is another fallthrough");
TestTrue("Continue", Dlg->Continue());
// Should have fallen through
TestDialogueText(this, "Direct goto", Dlg, "Player", "This is the latter half of the discussion");
Script->MarkAsGarbage();
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestSetVariableRunning,
"SUDSTest.TestSetVariableRunning",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestSetVariableRunning::RunTest(const FString& Parameters)
{
// Number and plural formatting are locale-specific, so we must set it to a predefined value to produce the expected output
FInternationalization::FCultureStateSnapshot CultureStateSnapshot;
FInternationalization::Get().BackupCultureState(CultureStateSnapshot);
FInternationalization::Get().SetCurrentCulture(TEXT("en-US"));
ON_SCOPE_EXIT
{
FInternationalization::Get().RestoreCultureState(CultureStateSnapshot);
};
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(SetVariableRunnerInput), SetVariableRunnerInput.Len(), "SetVariableRunnerInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// We need to use dummy global vars since subsystem doesn't exist
USUDSSubsystem::Test_DummyGlobalVariables.Empty();
USUDSSubsystem::Test_DummyGlobalVariables.Add("GlobalIntSetOutside", 245);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
Dlg->Start();
// Check headers have run & initial variables are set
TestEqual("Header: Player name", Dlg->GetVariableText("SpeakerName.Player").ToString(), "Protagonist");
TestEqual("Header: Valet name", Dlg->GetVariableText("ValetName").ToString(), "Bob");
TestEqual("Header: Some float", Dlg->GetVariableFloat("SomeFloat"), 12.5f);
// Check initial values
TestEqual("Initial: Some int", Dlg->GetVariableInt("SomeInt"), 0);
TestEqual("Initial: Some boolean", Dlg->GetVariableBoolean("SomeBoolean"), false);
TestEqual("Initial: Some gender", Dlg->GetVariableGender("SomeGender"), ETextGender::Neuter);
TestEqual("Initial: calculated int", Dlg->GetVariableInt("SomeCalculatedInt"), 14);
TestTrue("Initial: calculated bool", Dlg->GetVariableBoolean("SomeCalculatedBoolean"));
// Test globals
TestFalse("Test not setting local var", Dlg->IsVariableSet("global.SomeGlobalInt"));
TestFalse("Test not setting local var", Dlg->IsVariableSet("global.SomeGlobalFloat"));
TestTrue("Test setting global var", USUDSSubsystem::Test_DummyGlobalVariables.Contains("SomeGlobalInt"));
TestTrue("Test setting global var", USUDSSubsystem::Test_DummyGlobalVariables.Contains("SomeGlobalFloat"));
TestDialogueText(this, "Node 1", Dlg, "Player", "Hello");
TestTrue("Continue", Dlg->Continue());
// Set node should have run
TestEqual("Initial: Some int", Dlg->GetVariableInt("SomeInt"), 99);
TestDialogueText(this, "Node 2", Dlg, "NPC", "Wotcha");
// Test that setting a new variable from another variable + 10 worked
TestEqual("Some copied float", Dlg->GetVariableFloat("SomeOtherFloat"), 22.5f);
TestEqual("Original float", Dlg->GetVariableFloat("SomeFloat"), 43.754f);
// Test globals
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Global text", Dlg, "Player", "Values are: 3, 37.5, 245");
TestEqual("Choices count", Dlg->GetNumberOfChoices(), 2);
TestEqual("Choice 1 text", Dlg->GetChoiceText(0).ToString(), "Choice 1");
TestEqual("Choice 2 text", Dlg->GetChoiceText(1).ToString(), "Choice 2");
TestTrue("Choose 1", Dlg->Choose(0));
TestEqual("Gender should be set", Dlg->GetVariableGender("SomeGender"), ETextGender::Masculine);
TestEqual("Some boolean should be set", Dlg->GetVariableBoolean("SomeBoolean"), true);
TestEqual("Valet name should not have changed", Dlg->GetVariableText("ValetName").ToString(), "Bob");
TestEqual("Gender should not have changed", Dlg->GetVariableGender("SomeGender"), ETextGender::Masculine);
TestDialogueText(this, "Choice end text", Dlg, "NPC", "Truth");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Final node", Dlg, "Player", "Well");
TestFalse("Continue", Dlg->Continue());
TestTrue("At end", Dlg->IsEnded());
// Restart and DON'T reset state
Dlg->Restart(false);
// Variables should be the same
// Except for the headers, which will have run again
TestEqual("Player name should have been set again", Dlg->GetVariableText("SpeakerName.Player").ToString(), "Protagonist");
TestEqual("Valet name should have been set again", Dlg->GetVariableText("ValetName").ToString(), "Bob");
TestEqual("Some float should have been set again", Dlg->GetVariableFloat("SomeFloat"), 12.5f);
TestEqual("Int should still be set", Dlg->GetVariableInt("SomeInt"), 99);
TestEqual("Gender should still be set", Dlg->GetVariableGender("SomeGender"), ETextGender::Masculine);
TestEqual("Some boolean should still be set", Dlg->GetVariableBoolean("SomeBoolean"), true);
// Restart and DO reset state
Dlg->Restart(true);
TestEqual("Player name should have been set again", Dlg->GetVariableText("SpeakerName.Player").ToString(), "Protagonist");
TestEqual("Valet name should have been set again", Dlg->GetVariableText("ValetName").ToString(), "Bob");
TestEqual("Some float should have been set again", Dlg->GetVariableFloat("SomeFloat"), 12.5f);
TestEqual("Int should have been reset", Dlg->GetVariableInt("SomeInt"), 0);
TestEqual("Gender should have been reset", Dlg->GetVariableGender("SomeGender"), ETextGender::Neuter);
TestEqual("Some boolean should have been reset", Dlg->GetVariableBoolean("SomeBoolean"), false);
// Try the other path
TestTrue("Continue", Dlg->Continue());
TestTrue("Continue", Dlg->Continue());
TestTrue("Choose 2", Dlg->Choose(1));
TestDialogueText(this, "Choice 2 text", Dlg, "NPC", "Surprise");
TestEqual("Gender should not be changed yet", Dlg->GetVariableGender("SomeGender"), ETextGender::Masculine);
TestEqual("Valet name should not be changed yet", Dlg->GetVariableText("ValetName").ToString(), "Bob");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Final node", Dlg, "Player", "Well");
TestEqual("Gender should have changed", Dlg->GetVariableGender("SomeGender"), ETextGender::Feminine);
TestEqual("Valet name should have changed", Dlg->GetVariableText("ValetName").ToString(), "Kate");
TestEqual("Case insensitive set test", Dlg->GetVariableInt("SomeInt"), 101);
TestFalse("Continue", Dlg->Continue());
TestTrue("At end", Dlg->IsEnded());
USUDSSubsystem::Test_DummyGlobalVariables.Empty();
Script->MarkAsGarbage();
return true;
}
const FString SpeakerNamesInput = R"RAWSUD(
===
[set SpeakerName.Player "Protagonist"]
[set SpeakerName.NPC "Just Some Guy"]
===
:start
Player: Hello there
NPC: Salutations fellow human
ThirdGuy: Sup
[set SpeakerName.NPC "Actually A Villain"]
NPC: Aha!
)RAWSUD";
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestSpeakerNames,
"SUDSTest.TestSpeakerNames",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestSpeakerNames::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(SpeakerNamesInput), SpeakerNamesInput.Len(), "SpeakerNamesInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
Dlg->Start();
// Speaker ID test
TestDialogueText(this, "Initial text", Dlg, "Player", "Hello there");
TestEqual("Initial player speaker name", Dlg->GetSpeakerDisplayName().ToString(), "Protagonist");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Text 2", Dlg, "NPC", "Salutations fellow human");
TestEqual("Initial NPC speaker name", Dlg->GetSpeakerDisplayName().ToString(), "Just Some Guy");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Text 3", Dlg, "ThirdGuy", "Sup");
TestEqual("Initial ThirdGuy speaker name", Dlg->GetSpeakerDisplayName().ToString(), "ThirdGuy");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Text 4", Dlg, "NPC", "Aha!");
TestEqual("NPC speaker name should have changed", Dlg->GetSpeakerDisplayName().ToString(), "Actually A Villain");
Script->MarkAsGarbage();
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestFallthroughEdgeCase,
"SUDSTest.TestFallthroughEdgeCase",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestFallthroughEdgeCase::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(FallthroughEdgeCaseInput), FallthroughEdgeCaseInput.Len(), "FallthroughEdgeCaseInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
Dlg->Start();
TestDialogueText(this, "Initial text", Dlg, "NPC", "First line");
TestEqual("Initial num choices", Dlg->GetNumberOfChoices(), 3);
TestTrue("Choice 1", Dlg->Choose(0));
TestDialogueText(this, "Second text", Dlg, "NPC", "Fallthrough via goto");
if (TestEqual("Second num choices", Dlg->GetNumberOfChoices(), 2))
{
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), "Fallthrough 1");
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), "Fallthrough 2");
}
// Now prove that implicit fallthrough works in middle option
Dlg->Restart();
TestTrue("Choice 2", Dlg->Choose(1));
TestDialogueText(this, "Second text", Dlg, "NPC", "Fallthrough implicitly");
if (TestEqual("Second num choices", Dlg->GetNumberOfChoices(), 2))
{
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), "Fallthrough 1");
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), "Fallthrough 2");
}
// Now prove that implicit fallthrough works in last option
Dlg->Restart();
TestTrue("Choice 2", Dlg->Choose(2));
TestDialogueText(this, "Second text", Dlg, "NPC", "Fallthrough implicitly again");
if (TestEqual("Second num choices", Dlg->GetNumberOfChoices(), 2))
{
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), "Fallthrough 1");
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), "Fallthrough 2");
}
Script->MarkAsGarbage();
return true;
}
const FString LinesBetweenTextAndChoiceInput = R"RAWSUD(
NPC: Hello
NPC: Here's some choices
[event SomeEvent]
* Option 1
NPC: This is option 1
* Option 2
NPC: This is option 2
)RAWSUD";
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestLinesBetweenTextAndChoice,
"SUDSTest.TestLinesBetweenTextAndChoiceInput",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestLinesBetweenTextAndChoice::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(LinesBetweenTextAndChoiceInput), LinesBetweenTextAndChoiceInput.Len(), "LinesBetweenTextAndChoiceInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
Dlg->Start();
TestDialogueText(this, "Initial text", Dlg, "NPC", "Hello");
TestTrue("Continue", Dlg->Continue());
TestDialogueText(this, "Initial text", Dlg, "NPC", "Here's some choices");
TestEqual("Initial num choices", Dlg->GetNumberOfChoices(), 2);
return true;
}
UE_ENABLE_OPTIMIZATION

View File

@@ -0,0 +1,146 @@
#include "SUDSDialogue.h"
#include "SUDSLibrary.h"
#include "SUDSMessageLogger.h"
#include "SUDSScript.h"
#include "SUDSScriptImporter.h"
#include "TestUtils.h"
#include "Misc/AutomationTest.h"
UE_DISABLE_OPTIMIZATION
const FString SaveStateInput = R"RAWSUD(
===
# To confirm that when resuming we don't run headers again
[set y = -2.4]
===
[if {alreadyvisited}]
[goto secondvisit]
[endif]
NPC: Hello
* First choice
Player: I took the 1.1 choice
[if {x} > 0]
* Second choice (conditional)
Player: I took the 1.2 choice
* Third choice (conditional)
Player: I took the 1.3 choice
[else]
* Second Alt Choice
Player: I took the alt 1.2 choice
[endif]
* Common last choice
Player: I took the 1.4 choice
[goto goodbye]
:secondvisit
NPC: Hello again you!
[if {y} < 0]
Player: Y is less than 0
NPC: How interesting
* You don't sound that interested
NPC: Well I was trying to be nice
[if {x} == 0]
* Also x is zero
NPC: Fascinating
[endif]
* Well, better be off
[goto goodbye]
[elseif {y} == 0]
Player: Y is zero
NPC: So is my interest level
Player: Rude
[else]
Player: Who knows what Y is anyway
[if {ponderous}]
* Who knows what anything is?
NPC: Get out
[endif]
[if {y} > 4.99]
* It's more than I can count on one hand
NPC: Well, that helps
[else]
* It's kind of small though
NPC: Much like your brain
[endif]
* I'm done with this
[endif]
Player: This is some fallthrough text
:goodbye
NPC: Bye
)RAWSUD";
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestSaveState,
"SUDSTest.TestSaveState",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestSaveState::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(SaveStateInput), SaveStateInput.Len(), "SaveStateInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
// Set x before start
// No point settings y because that gets changed by headers
Dlg->SetVariableInt("x", 5);
Dlg->Start();
TestDialogueText(this, "Text Node", Dlg, "NPC", "Hello");
if (!TestEqual("Num choices", Dlg->GetNumberOfChoices(), 4))
return true;
TestEqual("ChoiceText", Dlg->GetChoiceText(0).ToString(), "First choice");
TestEqual("ChoiceText", Dlg->GetChoiceText(1).ToString(), "Second choice (conditional)");
TestEqual("ChoiceText", Dlg->GetChoiceText(2).ToString(), "Third choice (conditional)");
TestEqual("ChoiceText", Dlg->GetChoiceText(3).ToString(), "Common last choice");
TestTrue("Choose", Dlg->Choose(2));
TestDialogueText(this, "Text node", Dlg, "Player", "I took the 1.3 choice");
// Set value of y to test it's retained, and not reset by running headers
Dlg->SetVariableFloat("y", 23.5f);
// Save it here
auto SaveState = Dlg->GetSavedState();
// Re-construct the dialogue & restore
auto Dlg2 = USUDSLibrary::CreateDialogue(Script, Script);
Dlg2->RestoreSavedState(SaveState);
// We should be back at the same point
TestDialogueText(this, "Text node", Dlg2, "Player", "I took the 1.3 choice");
// Check vars
TestEqual("x value", Dlg2->GetVariableInt("x"), 5);
TestEqual("y value", Dlg2->GetVariableFloat("y"), 23.5f);
TestTrue("Continue", Dlg2->Continue());
TestDialogueText(this, "Text node", Dlg2, "NPC", "Bye");
// Restart to check choices were remembered
Dlg2->Restart();
TestDialogueText(this, "Text Node", Dlg2, "NPC", "Hello");
TestFalse("Choice not taken", Dlg2->HasChoiceIndexBeenTakenPreviously(0));
TestFalse("Choice not taken", Dlg2->HasChoiceIndexBeenTakenPreviously(1));
TestTrue("Choice not taken", Dlg2->HasChoiceIndexBeenTakenPreviously(2));
TestFalse("Choice not taken", Dlg2->HasChoiceIndexBeenTakenPreviously(3));
Script->MarkAsGarbage();
return true;
}
UE_ENABLE_OPTIMIZATION

View File

@@ -0,0 +1,124 @@
#include "SUDSDialogue.h"
#include "SUDSLibrary.h"
#include "SUDSMessageLogger.h"
#include "SUDSScript.h"
#include "SUDSScriptImporter.h"
#include "TestUtils.h"
#include "Misc/AutomationTest.h"
UE_DISABLE_OPTIMIZATION
// Assign known keys to the strings so we can detect
const FString UserMetadataInput = R"RAWSUD(
# No metadata
Player: Good day sir
#% IsGreeting = true
NPC: Salutations to you too
#% IntValue = 1
#% Politeness 3.142
Player: What are we doing today?
# Test a variable-derived, expression metadata
[set IntVariable 2]
#% IntValue = {IntVariable} + 3
#% LookMumNoEquals `OK_Dear`
NPC: Looks like we're doing some testing. How about a choice?
#% RequirePoshness = 10
* Choice 1
Player: Capital, old chap
#% RequirePoshness = 15
#% RequireTopHat = true
* Choice 2
Player: Top hole, dear boy
* Choice 3
Player: OK then
)RAWSUD";
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestUserMetadata,
"SUDSTest.TestUserMetadata",
EAutomationTestFlags::EditorContext |
EAutomationTestFlags::ClientContext |
EAutomationTestFlags::ProductFilter)
bool FTestUserMetadata::RunTest(const FString& Parameters)
{
FSUDSMessageLogger Logger(false);
FSUDSScriptImporter Importer;
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(UserMetadataInput), UserMetadataInput.Len(), "UserMetadataInput", &Logger, true));
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
const ScopedStringTableHolder StringTableHolder;
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
Dlg->Start();
TestDialogueText(this, "First node", Dlg, "Player", "Good day sir");
TestEqual("Check metadata empty", Dlg->GetAllSpeakerLineUserMetadata().Num(), 0);
Dlg->Continue();
TestDialogueText(this, "Next", Dlg, "NPC", "Salutations to you too");
FSUDSValue Actual = Dlg->GetSpeakerLineUserMetadata(FName("IsGreeting"));
if (TestTrue("Boolean test", Actual.GetType() == ESUDSValueType::Boolean))
{
TestEqual("Boolean test", Actual.GetBooleanValue(), true);
}
Dlg->Continue();
TestDialogueText(this, "Next", Dlg, "Player", "What are we doing today?");
Actual = Dlg->GetSpeakerLineUserMetadata(FName("IntValue"));
if (TestTrue("Int test", Actual.GetType() == ESUDSValueType::Int))
{
TestEqual("Int test", Actual.GetIntValue(), 1);
}
Actual = Dlg->GetSpeakerLineUserMetadata(FName("Politeness"));
if (TestTrue("Float test", Actual.GetType() == ESUDSValueType::Float))
{
TestEqual("Float test", Actual.GetFloatValue(), 3.142f);
}
Dlg->Continue();
TestDialogueText(this, "Next", Dlg, "NPC", "Looks like we're doing some testing. How about a choice?");
Actual = Dlg->GetSpeakerLineUserMetadata(FName("IntValue"));
if (TestTrue("Expression test", Actual.GetType() == ESUDSValueType::Int))
{
TestEqual("Expression test", Actual.GetIntValue(), 5);
}
Actual = Dlg->GetSpeakerLineUserMetadata(FName("LookMumNoEquals"));
if (TestTrue("Name & no equals test", Actual.GetType() == ESUDSValueType::Name))
{
TestEqual("Name & no equals test", Actual.GetNameValue(), FName("OK_Dear"));
}
if (TestEqual("Choice count", Dlg->GetNumberOfChoices(), 3))
{
Actual = Dlg->GetChoiceUserMetadata(0, FName("RequirePoshness"));
if (TestTrue("Choice 0 test", Actual.GetType() == ESUDSValueType::Int))
{
TestEqual("Choice 0 test", Actual.GetIntValue(), 10);
}
Actual = Dlg->GetChoiceUserMetadata(1, FName("RequirePoshness"));
if (TestTrue("Choice 1 test A", Actual.GetType() == ESUDSValueType::Int))
{
TestEqual("Choice 1 test A", Actual.GetIntValue(), 15);
}
Actual = Dlg->GetChoiceUserMetadata(1, FName("RequireTopHat"));
if (TestTrue("Choice 1 test B", Actual.GetType() == ESUDSValueType::Boolean))
{
TestEqual("Choice 1 test B", Actual.GetBooleanValue(), true);
}
auto Choice1All = Dlg->GetAllChoiceUserMetadata(1);
TestEqual("Choice 1 test count", Choice1All.Num(), 2);
TestEqual("Choice 2 test", Dlg->GetAllChoiceUserMetadata(2).Num(), 0);
}
Script->MarkAsGarbage();
return true;
}
UE_ENABLE_OPTIMIZATION

View File

@@ -0,0 +1,297 @@
#pragma once
#include "SUDSDialogue.h"
#include "SUDSScriptNode.h"
#include "SUDSScriptNodeText.h"
#include "Internationalization/StringTable.h"
#include "Internationalization/StringTableRegistry.h"
#include "Misc/AutomationTest.h"
#include "UObject/Package.h"
FORCEINLINE void TestDialogueText(FAutomationTestBase* T, const FString& NameForTest, USUDSDialogue* D, const FString& SpeakerID, const FString& Text)
{
T->TestEqual(NameForTest, D->GetSpeakerID(), SpeakerID);
T->TestEqual(NameForTest, D->GetText().ToString(), Text);
}
FORCEINLINE bool TestParsedText(FAutomationTestBase* T, const FString& NameForTest, const FSUDSParsedNode* Node, const FString& Speaker, const FString& Text)
{
if (T->TestNotNull(NameForTest, Node))
{
T->TestEqual(NameForTest, Node->NodeType, ESUDSParsedNodeType::Text);
T->TestEqual(NameForTest, Node->Identifier, Speaker);
T->TestEqual(NameForTest, Node->Text, Text);
return true;
}
return false;
}
FORCEINLINE bool TestArgValue(FAutomationTestBase* T, const FString& NameForTest, const FSUDSValue& Actual, int Expected)
{
if (T->TestEqual(NameForTest, Actual.GetType(), ESUDSValueType::Int))
{
return T->TestEqual(NameForTest, Actual.GetIntValue(), Expected);
}
return false;
}
FORCEINLINE bool TestArgValue(FAutomationTestBase* T, const FString& NameForTest, const FSUDSValue& Actual, float Expected)
{
if (T->TestEqual(NameForTest, Actual.GetType(), ESUDSValueType::Float))
{
return T->TestEqual(NameForTest, Actual.GetFloatValue(), Expected);
}
return false;
}
FORCEINLINE bool TestArgValue(FAutomationTestBase* T, const FString& NameForTest, const FSUDSValue& Actual, ETextGender Expected)
{
if (T->TestEqual(NameForTest, Actual.GetType(), ESUDSValueType::Gender))
{
return T->TestEqual(NameForTest, Actual.GetGenderValue(), Expected);
}
return false;
}
FORCEINLINE bool TestArgValue(FAutomationTestBase* T, const FString& NameForTest, const FSUDSValue& Actual, const FString& Expected)
{
if (T->TestEqual(NameForTest, Actual.GetType(), ESUDSValueType::Text))
{
return T->TestEqual(NameForTest, Actual.GetTextValue().ToString(), Expected);
}
return false;
}
template <typename V>
FORCEINLINE bool TestParsedSetLiteral(FAutomationTestBase* T, const FString& NameForTest, const FSUDSParsedNode* Node, const FString& VarName, V Literal)
{
if (T->TestNotNull(NameForTest, Node))
{
T->TestEqual(NameForTest, Node->NodeType, ESUDSParsedNodeType::SetVariable);
T->TestEqual(NameForTest, Node->Identifier, VarName);
if (T->TestTrue(NameForTest, Node->Expression.IsLiteral()))
{
TestArgValue(T, NameForTest,Node->Expression.GetLiteralValue(), Literal);
}
return true;
}
return false;
}
// Explicit bool version of the above since otherwise it gets converted to int & fails
FORCEINLINE bool TestParsedSetLiteral(FAutomationTestBase* T, const FString& NameForTest, const FSUDSParsedNode* Node, const FString& VarName, bool Literal)
{
if (T->TestNotNull(NameForTest, Node))
{
T->TestEqual(NameForTest, Node->NodeType, ESUDSParsedNodeType::SetVariable);
T->TestEqual(NameForTest, Node->Identifier, VarName);
if (T->TestTrue(NameForTest, Node->Expression.IsLiteral()))
{
T->TestEqual(NameForTest,Node->Expression.GetBooleanLiteralValue(), Literal);
}
return true;
}
return false;
}
FORCEINLINE bool TestParsedSetLiteral(FAutomationTestBase* T, const FString& NameForTest, const FSUDSParsedNode* Node, const FString& VarName, const FName& Name)
{
if (T->TestNotNull(NameForTest, Node))
{
T->TestEqual(NameForTest, Node->NodeType, ESUDSParsedNodeType::SetVariable);
T->TestEqual(NameForTest, Node->Identifier, VarName);
if (T->TestTrue(NameForTest, Node->Expression.IsLiteral()))
{
T->TestEqual(NameForTest,Node->Expression.GetNameLiteralValue(), Name);
}
return true;
}
return false;
}
FORCEINLINE bool TestGetParsedNextNode(FAutomationTestBase* T, const FString& NameForTest, const FSUDSParsedNode* Node, FSUDSScriptImporter& Importer, bool bIsHeader, const FSUDSParsedNode** OutNextNode)
{
if (T->TestNotNull(NameForTest, Node))
{
if (T->TestEqual(NameForTest, Node->Edges.Num(), 1))
{
const int NextNodeIdx = Node->Edges[0].TargetNodeIdx;
*OutNextNode = bIsHeader ? Importer.GetHeaderNode(NextNodeIdx) : Importer.GetNode(NextNodeIdx);
return true;
}
}
*OutNextNode = nullptr;
return false;
}
FORCEINLINE bool TestParsedChoice(FAutomationTestBase* T, const FString& NameForTest, const FSUDSParsedNode* Node, int ExpectedNumChoices)
{
if (T->TestNotNull(NameForTest, Node))
{
T->TestEqual(NameForTest, Node->NodeType, ESUDSParsedNodeType::Choice);
T->TestEqual(NameForTest, Node->Edges.Num(), ExpectedNumChoices);
return true;
}
return false;
}
FORCEINLINE bool TestParsedSelect(FAutomationTestBase* T, const FString& NameForTest, const FSUDSParsedNode* Node, int ExpectedNumEdges)
{
if (T->TestNotNull(NameForTest, Node))
{
T->TestEqual(NameForTest, Node->NodeType, ESUDSParsedNodeType::Select);
T->TestEqual(NameForTest, Node->Edges.Num(), ExpectedNumEdges);
return true;
}
return false;
}
FORCEINLINE bool TestParsedChoiceEdge(FAutomationTestBase* T, const FString& NameForTest, const FSUDSParsedNode* Node, int EdgeIndex, const FString& Text, FSUDSScriptImporter& Importer, const FSUDSParsedNode** OutNode)
{
*OutNode = nullptr;
if (Node && Node->Edges.Num() > EdgeIndex)
{
auto& Edge = Node->Edges[EdgeIndex];
T->TestEqual(NameForTest, Edge.Text, Text);
const int Idx = Edge.TargetNodeIdx;
*OutNode = Importer.GetNode(Idx);
return true;
}
return false;
}
FORCEINLINE bool TestParsedSelectEdge(FAutomationTestBase* T, const FString& NameForTest, const FSUDSParsedNode* Node, int EdgeIndex, const FString& ConditionStr, FSUDSScriptImporter& Importer, const FSUDSParsedNode** OutNode)
{
*OutNode = nullptr;
if (Node && Node->Edges.Num() > EdgeIndex)
{
auto& Edge = Node->Edges[EdgeIndex];
T->TestEqual(NameForTest, Edge.ConditionExpression.GetSourceString(), ConditionStr);
const int Idx = Edge.TargetNodeIdx;
*OutNode = Importer.GetNode(Idx);
return true;
}
return false;
}
FORCEINLINE bool TestParsedGoto(FAutomationTestBase* T, const FString& NameForTest, const FSUDSParsedNode* Node, FSUDSScriptImporter& Importer, const FSUDSParsedNode** OutNode)
{
if (T->TestNotNull(NameForTest, Node))
{
T->TestEqual(NameForTest, Node->NodeType, ESUDSParsedNodeType::Goto);
const int Target = Importer.GetGotoTargetNodeIndex(Node->Identifier);
*OutNode = Importer.GetNode(Target);
return true;
}
return false;
}
FORCEINLINE bool TestTextNode(FAutomationTestBase* T, const FString& NameForTest, const USUDSScriptNode* Node, const FString& Speaker, const FString& Text)
{
if (T->TestNotNull(NameForTest, Node))
{
T->TestEqual(NameForTest, Node->GetNodeType(), ESUDSScriptNodeType::Text);
if (auto TextNode = Cast<USUDSScriptNodeText>(Node))
{
T->TestEqual(NameForTest, TextNode->GetSpeakerID(), Speaker);
T->TestEqual(NameForTest, TextNode->GetText().ToString(), Text);
}
return true;
}
return false;
}
FORCEINLINE bool TestEdge(FAutomationTestBase* T, const FString& NameForTest, USUDSScriptNode* Node, int EdgeIndex, USUDSScriptNode** OutNode)
{
*OutNode = nullptr;
if (Node && Node->GetEdgeCount() > EdgeIndex)
{
if (auto Edge = Node->GetEdge(EdgeIndex))
{
*OutNode = Edge->GetTargetNode().Get();
return T->TestNotNull(NameForTest, *OutNode);
}
}
return false;
}
FORCEINLINE bool TestChoiceNode(FAutomationTestBase* T, const FString& NameForTest, const USUDSScriptNode* Node, int NumChoices)
{
if (T->TestNotNull(NameForTest, Node))
{
T->TestEqual(NameForTest, Node->GetNodeType(), ESUDSScriptNodeType::Choice);
return T->TestEqual(NameForTest, Node->GetEdgeCount(), NumChoices);
}
return false;
}
FORCEINLINE bool TestSelectNode(FAutomationTestBase* T, const FString& NameForTest, const USUDSScriptNode* Node, int NumEdges)
{
if (T->TestNotNull(NameForTest, Node))
{
T->TestEqual(NameForTest, Node->GetNodeType(), ESUDSScriptNodeType::Select);
return T->TestEqual(NameForTest, Node->GetEdgeCount(), NumEdges);
}
return false;
}
FORCEINLINE bool TestChoiceEdge(FAutomationTestBase* T, const FString& NameForTest, USUDSScriptNode* Node, int EdgeIndex, const FString& Text, USUDSScriptNode** OutNode)
{
*OutNode = nullptr;
if (Node && Node->GetEdgeCount() > EdgeIndex)
{
if (auto Edge = Node->GetEdge(EdgeIndex))
{
T->TestEqual(NameForTest, Edge->GetText().ToString(), Text);
*OutNode = Edge->GetTargetNode().Get();
return T->TestNotNull(NameForTest, *OutNode);
}
}
return false;
}
FORCEINLINE bool TestSelectEdge(FAutomationTestBase* T, const FString& NameForTest, USUDSScriptNode* Node, int EdgeIndex, const FString& ConditionStr, USUDSScriptNode** OutNode)
{
*OutNode = nullptr;
if (Node && Node->GetEdgeCount() > EdgeIndex)
{
if (auto Edge = Node->GetEdge(EdgeIndex))
{
T->TestEqual(NameForTest, Edge->GetCondition().GetSourceString(), ConditionStr);
*OutNode = Edge->GetTargetNode().Get();
return T->TestNotNull(NameForTest, *OutNode);
}
}
return false;
}
// Helper to provide a string table just in scope
struct ScopedStringTableHolder
{
public:
UStringTable* StringTable;
ScopedStringTableHolder()
{
StringTable = NewObject<UStringTable>(GetTransientPackage(), "TestStrings");
}
~ScopedStringTableHolder()
{
// Tidy up string table
// UStringTable constructor registered this table
FStringTableRegistry::Get().UnregisterStringTable(StringTable->GetStringTableId());
}
};

View File

@@ -0,0 +1,30 @@
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class SUDSTest : ModuleRules
{
public SUDSTest(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PrivateDependencyModuleNames.AddRange(
new string[]
{
"Core",
"CoreUObject",
"Engine",
"SUDS",
"SUDSEditor"
}
);
// Uncomment if you are using Slate UI
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
// Uncomment if you are using online features
// PrivateDependencyModuleNames.Add("OnlineSubsystem");
// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
}
}