1380 lines
36 KiB
C++
1380 lines
36 KiB
C++
// Copyright Steve Streeting 2022
|
|
// Released under the MIT license https://opensource.org/license/MIT/
|
|
#include "SUDSDialogue.h"
|
|
|
|
#include "SUDSInternal.h"
|
|
#include "SUDSLibrary.h"
|
|
#include "SUDSParticipant.h"
|
|
#include "SUDSScript.h"
|
|
#include "SUDSScriptNode.h"
|
|
#include "SUDSScriptNodeEvent.h"
|
|
#include "SUDSScriptNodeGosub.h"
|
|
#include "SUDSScriptNodeSet.h"
|
|
#include "SUDSScriptNodeText.h"
|
|
#include "SUDSSubsystem.h"
|
|
#include "Kismet/GameplayStatics.h"
|
|
#include "Sound/DialogueSoundWaveProxy.h"
|
|
#include "Sound/DialogueWave.h"
|
|
|
|
DEFINE_LOG_CATEGORY(LogSUDSDialogue);
|
|
|
|
const FText USUDSDialogue::DummyText = FText::FromString("INVALID");
|
|
const FString USUDSDialogue::DummyString = "INVALID";
|
|
|
|
|
|
|
|
FArchive& operator<<(FArchive& Ar, FSUDSDialogueState& Value)
|
|
{
|
|
Ar << Value.TextNodeID;
|
|
Ar << Value.Variables;
|
|
Ar << Value.ChoicesTaken;
|
|
Ar << Value.ReturnStack;
|
|
|
|
return Ar;
|
|
}
|
|
|
|
void operator<<(FStructuredArchive::FSlot Slot, FSUDSDialogueState& Value)
|
|
{
|
|
FStructuredArchive::FRecord Record = Slot.EnterRecord();
|
|
Record
|
|
<< SA_VALUE(TEXT("TextNodeID"), Value.TextNodeID)
|
|
<< SA_VALUE(TEXT("Variables"), Value.Variables)
|
|
<< SA_VALUE(TEXT("ChoicesTaken"), Value.ChoicesTaken)
|
|
<< SA_VALUE(TEXT("ReturnStack"), Value.ReturnStack);
|
|
|
|
}
|
|
|
|
USUDSDialogue::USUDSDialogue(): BaseScript(nullptr),
|
|
CurrentSpeakerNode(nullptr),
|
|
CurrentRootChoiceNode(nullptr),
|
|
bParamNamesExtracted(false),
|
|
CurrentSourceLineNo(0)
|
|
{
|
|
}
|
|
|
|
void USUDSDialogue::Initialise(const USUDSScript* Script)
|
|
{
|
|
BaseScript = Script;
|
|
CurrentSpeakerNode = nullptr;
|
|
|
|
InitVariables();
|
|
|
|
CurrentSpeakerNode = nullptr;
|
|
|
|
}
|
|
|
|
void USUDSDialogue::InitVariables()
|
|
{
|
|
VariableState.Empty();
|
|
// Run header nodes immediately (only set nodes)
|
|
RunUntilNextSpeakerNodeOrEnd(BaseScript->GetHeaderNode(), false);
|
|
}
|
|
|
|
void USUDSDialogue::Start(FName Label)
|
|
{
|
|
// Only start if not already on a speaker node
|
|
// This makes the restore sequence easier, you don't have to test IsEnded
|
|
if (!IsValid(CurrentSpeakerNode))
|
|
{
|
|
// Note that we don't reset state by default here. This is to allow long-term memory on dialogue, such as
|
|
// knowing whether you've met a character before etc.
|
|
// We also don't re-run headers here since they will have been run on Initialise()
|
|
// This is to allow callers to set variables before Start() that override headers
|
|
Restart(false, Label, false);
|
|
}
|
|
}
|
|
|
|
void USUDSDialogue::SetParticipants(const TArray<UObject*>& InParticipants)
|
|
{
|
|
// Protect against null participants
|
|
// Since our stable sort dereferences
|
|
Participants.Empty();
|
|
for (auto P : InParticipants)
|
|
{
|
|
if (IsValid(P))
|
|
{
|
|
Participants.AddUnique(P);
|
|
}
|
|
}
|
|
SortParticipants();
|
|
}
|
|
|
|
void USUDSDialogue::AddParticipant(UObject* Participant)
|
|
{
|
|
if (IsValid(Participant))
|
|
{
|
|
Participants.AddUnique(Participant);
|
|
SortParticipants();
|
|
}
|
|
}
|
|
|
|
void USUDSDialogue::SortParticipants()
|
|
{
|
|
if (!Participants.IsEmpty())
|
|
{
|
|
// We order by ascending priority so that higher priority values are later in the list
|
|
// Which means they're called last and get to override values set by earlier ones
|
|
// We'll do a stable sort so that otherwise order is maintained
|
|
Participants.StableSort([](const UObject& A, const UObject& B)
|
|
{
|
|
if (A.Implements<USUDSParticipant>() && B.Implements<USUDSParticipant>())
|
|
{
|
|
return ISUDSParticipant::Execute_GetDialogueParticipantPriority(&A) <
|
|
ISUDSParticipant::Execute_GetDialogueParticipantPriority(&B);
|
|
}
|
|
// Be deterministic
|
|
return &A < &B;
|
|
});
|
|
}
|
|
}
|
|
|
|
void USUDSDialogue::RunUntilNextSpeakerNodeOrEnd(USUDSScriptNode* NextNode, bool bRaiseAtEnd)
|
|
{
|
|
// We run through nodes which don't require a speaker line prompt
|
|
// E.g. set nodes, select nodes which are all automatically resolved
|
|
// Starting with this node
|
|
while (NextNode && !IsChoiceOrTextNode(NextNode->GetNodeType()))
|
|
{
|
|
NextNode = RunNode(NextNode);
|
|
}
|
|
|
|
if (NextNode)
|
|
{
|
|
if (NextNode->GetNodeType() == ESUDSScriptNodeType::Text)
|
|
{
|
|
SetCurrentSpeakerNode(Cast<USUDSScriptNodeText>(NextNode), false);
|
|
}
|
|
else
|
|
{
|
|
// This can happen if for example user creates a choice node as the first thing
|
|
UE_LOG(LogSUDSDialogue,
|
|
Error,
|
|
TEXT("Error in %s line %d: Tried to run to next speaker node but encountered unexpected node of type %s"),
|
|
*BaseScript->GetName(),
|
|
NextNode->GetSourceLineNo(),
|
|
*(StaticEnum<ESUDSScriptNodeType>()->GetValueAsString(NextNode->GetNodeType()))
|
|
);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
End(!bRaiseAtEnd);
|
|
}
|
|
|
|
}
|
|
|
|
USUDSScriptNode* USUDSDialogue::RunNode(USUDSScriptNode* Node)
|
|
{
|
|
CurrentSourceLineNo = Node->GetSourceLineNo();
|
|
switch (Node->GetNodeType())
|
|
{
|
|
case ESUDSScriptNodeType::Select:
|
|
return RunSelectNode(Node);
|
|
case ESUDSScriptNodeType::SetVariable:
|
|
return RunSetVariableNode(Node);
|
|
case ESUDSScriptNodeType::Event:
|
|
return RunEventNode(Node);
|
|
case ESUDSScriptNodeType::Gosub:
|
|
return RunGosubNode(Node);
|
|
case ESUDSScriptNodeType::Return:
|
|
return RunReturnNode(Node);
|
|
default: ;
|
|
}
|
|
|
|
UE_LOG(LogSUDSDialogue,
|
|
Error,
|
|
TEXT("Error in %s line %d: Attempted to run non-runnable node type %s"),
|
|
*BaseScript->GetName(),
|
|
Node->GetSourceLineNo(),
|
|
*(StaticEnum<ESUDSScriptNodeType>()->GetValueAsString(Node->GetNodeType()))
|
|
)
|
|
return nullptr;
|
|
}
|
|
|
|
USUDSScriptNode* USUDSDialogue::RunSelectNode(USUDSScriptNode* Node)
|
|
{
|
|
// Define internal random selection variable (used in random selects)
|
|
if (Node->IsRandomSelect())
|
|
{
|
|
// Random picker
|
|
// Could try to NOT pick the same ones we already picked, but this would require some additional state, similar
|
|
// to "ChoicesTaken" state but for random text nodes already chosen. For now, keep it simple
|
|
|
|
const int OptCount = Node->GetEdgeCount();
|
|
// Use SRand() so can be seeded if required
|
|
const int RandChoice = FMath::Min(OptCount-1, FMath::TruncToInt(FMath::SRand() * (float)OptCount));
|
|
|
|
SetVariableInt(FSUDSConstants::RandomItemSelectIndexVarName, RandChoice);
|
|
}
|
|
|
|
for (auto& Edge : Node->GetEdges())
|
|
{
|
|
if (Edge.GetCondition().IsValid())
|
|
{
|
|
// use the first satisfied edge
|
|
RaiseExpressionVariablesRequested(Edge.GetCondition(), Edge.GetSourceLineNo());
|
|
const bool bSuccess = Edge.GetCondition().EvaluateBoolean(VariableState, GetGlobalVariables(), BaseScript->GetName());
|
|
#if WITH_EDITOR
|
|
{
|
|
FString ExprStr = Edge.GetCondition().GetSourceString();
|
|
if (ExprStr.IsEmpty())
|
|
{
|
|
// Lack of condition is an else / final random option
|
|
ExprStr = "else";
|
|
}
|
|
InternalOnSelectEval.ExecuteIfBound(this, ExprStr, bSuccess, Edge.GetSourceLineNo());
|
|
}
|
|
#endif
|
|
|
|
if (bSuccess)
|
|
{
|
|
return Edge.GetTargetNode().Get();
|
|
}
|
|
}
|
|
}
|
|
// NOTE: if no valid path, go to end
|
|
// We've already created fall-through else nodes if possible
|
|
return nullptr;
|
|
}
|
|
|
|
USUDSScriptNode* USUDSDialogue::RunEventNode(USUDSScriptNode* Node)
|
|
{
|
|
if (USUDSScriptNodeEvent* EvtNode = Cast<USUDSScriptNodeEvent>(Node))
|
|
{
|
|
// Build a resolved args list, because we need to evaluate expressions
|
|
TArray<FSUDSValue> ArgsResolved;
|
|
|
|
for (auto& Expr : EvtNode->GetArgs())
|
|
{
|
|
RaiseExpressionVariablesRequested(Expr, EvtNode->GetSourceLineNo());
|
|
ArgsResolved.Add(Expr.Evaluate(VariableState, GetGlobalVariables()));
|
|
}
|
|
|
|
for (const auto& P : Participants)
|
|
{
|
|
if (P->GetClass()->ImplementsInterface(USUDSParticipant::StaticClass()))
|
|
{
|
|
ISUDSParticipant::Execute_OnDialogueEvent(P, this, EvtNode->GetEventName(), ArgsResolved);
|
|
}
|
|
}
|
|
OnEvent.Broadcast(this, EvtNode->GetEventName(), ArgsResolved);
|
|
#if WITH_EDITOR
|
|
InternalOnEvent.ExecuteIfBound(this, EvtNode->GetEventName(), ArgsResolved, EvtNode->GetSourceLineNo());
|
|
#endif
|
|
}
|
|
return GetNextNode(Node);
|
|
}
|
|
|
|
USUDSScriptNode* USUDSDialogue::RunGosubNode(USUDSScriptNode* Node)
|
|
{
|
|
if (USUDSScriptNodeGosub* GosubNode = Cast<USUDSScriptNodeGosub>(Node))
|
|
{
|
|
if (auto TargetNode = BaseScript->GetNodeByLabel(GosubNode->GetLabelName()))
|
|
{
|
|
// Push this gosub node to the return stack, then jump
|
|
GosubReturnStack.Push(GosubNode);
|
|
return TargetNode;
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogSUDSDialogue,
|
|
Error,
|
|
TEXT("Error in %s: Cannot gosub to label '%s', was not found"),
|
|
*BaseScript->GetName(),
|
|
*GosubNode->GetLabelName().ToString());
|
|
|
|
}
|
|
}
|
|
return GetNextNode(Node);
|
|
}
|
|
|
|
USUDSScriptNode* USUDSDialogue::RunReturnNode(USUDSScriptNode* Node)
|
|
{
|
|
if (GosubReturnStack.Num() > 0)
|
|
{
|
|
// We return to the next node after the gosub, which temporarily redirected
|
|
const auto GoSubNode = GosubReturnStack.Pop();
|
|
return GetNextNode(GoSubNode);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogSUDSDialogue,
|
|
Error,
|
|
TEXT("Attempted to return at %s:%d but there was no previous gosub to return to"),
|
|
*BaseScript->GetName(),
|
|
Node->GetSourceLineNo());
|
|
return nullptr;
|
|
|
|
}
|
|
}
|
|
|
|
USUDSScriptNode* USUDSDialogue::RunSetVariableNode(USUDSScriptNode* Node)
|
|
{
|
|
if (USUDSScriptNodeSet* SetNode = Cast<USUDSScriptNodeSet>(Node))
|
|
{
|
|
if (SetNode->GetExpression().IsValid())
|
|
{
|
|
RaiseExpressionVariablesRequested(SetNode->GetExpression(), SetNode->GetSourceLineNo());
|
|
FSUDSValue Value = SetNode->GetExpression().Evaluate(VariableState, GetGlobalVariables());
|
|
FName Identifier;
|
|
if (USUDSLibrary::IsDialogueVariableGlobal(SetNode->GetIdentifier(), Identifier))
|
|
{
|
|
InternalSetGlobalVariable(this->GetWorld(), Identifier, Value, true, BaseScript->GetName(), SetNode->GetSourceLineNo());
|
|
}
|
|
else
|
|
{
|
|
SetVariableImpl(SetNode->GetIdentifier(), Value, true, SetNode->GetSourceLineNo());
|
|
}
|
|
#if WITH_EDITOR
|
|
// We do this here so that we have access to the expression
|
|
InternalOnSetVar.ExecuteIfBound(this,
|
|
SetNode->GetIdentifier(),
|
|
Value,
|
|
SetNode->GetExpression().IsLiteral()
|
|
? ""
|
|
: SetNode->GetExpression().GetSourceString(),
|
|
SetNode->GetSourceLineNo());
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// Always one edge
|
|
return GetNextNode(Node);
|
|
|
|
}
|
|
|
|
void USUDSDialogue::RaiseVariableChange(const FName& VarName, const FSUDSValue& Value, bool bFromScript, int LineNo)
|
|
{
|
|
for (const auto& P : Participants)
|
|
{
|
|
if (P->GetClass()->ImplementsInterface(USUDSParticipant::StaticClass()))
|
|
{
|
|
ISUDSParticipant::Execute_OnDialogueVariableChanged(P, this, VarName, Value, bFromScript);
|
|
}
|
|
}
|
|
OnVariableChanged.Broadcast(this, VarName, Value, bFromScript);
|
|
#if WITH_EDITOR
|
|
if (!bFromScript)
|
|
{
|
|
// Script setting is raised in SetNode so we have access to expressions
|
|
InternalOnSetVarByCode.ExecuteIfBound(this, VarName, Value);
|
|
}
|
|
#endif
|
|
|
|
}
|
|
|
|
void USUDSDialogue::RaiseVariableRequested(const FName& VarName, int LineNo)
|
|
{
|
|
// Because variables set by participants should "win", raise event first
|
|
OnVariableRequested.Broadcast(this, VarName);
|
|
for (const auto& P : Participants)
|
|
{
|
|
if (P->GetClass()->ImplementsInterface(USUDSParticipant::StaticClass()))
|
|
{
|
|
ISUDSParticipant::Execute_OnDialogueVariableRequested(P, this, VarName);
|
|
}
|
|
}
|
|
}
|
|
|
|
void USUDSDialogue::RaiseExpressionVariablesRequested(const FSUDSExpression& Expression, int LineNo)
|
|
{
|
|
for (auto& Var : Expression.GetVariableNames())
|
|
{
|
|
RaiseVariableRequested(Var, LineNo);
|
|
}
|
|
}
|
|
|
|
const TMap<FName, FSUDSValue>& USUDSDialogue::GetGlobalVariables() const
|
|
{
|
|
return InternalGetGlobalVariables(this->GetWorld());
|
|
}
|
|
|
|
void USUDSDialogue::SetCurrentSpeakerNode(USUDSScriptNodeText* Node, bool bQuietly)
|
|
{
|
|
CurrentSpeakerNode = Node;
|
|
|
|
CurrentSpeakerDisplayName = FText::GetEmpty();
|
|
bParamNamesExtracted = false;
|
|
if (Node)
|
|
{
|
|
CurrentSourceLineNo = Node->GetSourceLineNo();
|
|
}
|
|
else
|
|
{
|
|
CurrentSourceLineNo = 0;
|
|
}
|
|
UpdateChoices();
|
|
|
|
if (!bQuietly)
|
|
{
|
|
if (CurrentSpeakerNode)
|
|
RaiseNewSpeakerLine();
|
|
else
|
|
RaiseFinished();
|
|
}
|
|
|
|
}
|
|
|
|
FText USUDSDialogue::ResolveParameterisedText(const TArray<FName> Params, const FTextFormat& TextFormat, int LineNo)
|
|
{
|
|
for (const auto& P : Params)
|
|
{
|
|
RaiseVariableRequested(P, LineNo);
|
|
}
|
|
// Need to make a temp arg list for compatibility
|
|
// Also lets us just set the ones we need to
|
|
FFormatNamedArguments Args;
|
|
GetTextFormatArgs(Params, Args);
|
|
return FText::Format(TextFormat, Args);
|
|
|
|
}
|
|
|
|
void USUDSDialogue::GetTextFormatArgs(const TArray<FName>& ArgNames, FFormatNamedArguments& OutArgs) const
|
|
{
|
|
for (auto& Name : ArgNames)
|
|
{
|
|
FName GlobalName;
|
|
if (USUDSLibrary::IsDialogueVariableGlobal(Name, GlobalName))
|
|
{
|
|
auto& Globals = InternalGetGlobalVariables(this->GetWorld());
|
|
if (const FSUDSValue* Value = Globals.Find(GlobalName))
|
|
{
|
|
// Add to format args using name with prefix
|
|
OutArgs.Add(Name.ToString(), Value->ToFormatArg());
|
|
}
|
|
}
|
|
else if (const FSUDSValue* Value = VariableState.Find(Name))
|
|
{
|
|
// Use the operator conversion
|
|
OutArgs.Add(Name.ToString(), Value->ToFormatArg());
|
|
}
|
|
}
|
|
}
|
|
|
|
FText USUDSDialogue::GetText()
|
|
{
|
|
if (CurrentSpeakerNode)
|
|
{
|
|
if (CurrentSpeakerNode->HasParameters())
|
|
{
|
|
return ResolveParameterisedText(CurrentSpeakerNode->GetParameterNames(),
|
|
CurrentSpeakerNode->GetTextFormat(),
|
|
CurrentSpeakerNode->GetSourceLineNo());
|
|
}
|
|
else
|
|
{
|
|
return CurrentSpeakerNode->GetText();
|
|
}
|
|
}
|
|
return DummyText;
|
|
}
|
|
|
|
UDialogueWave* USUDSDialogue::GetWave() const
|
|
{
|
|
if (CurrentSpeakerNode)
|
|
{
|
|
return CurrentSpeakerNode->GetWave();
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
bool USUDSDialogue::IsCurrentLineVoiced() const
|
|
{
|
|
if (CurrentSpeakerNode)
|
|
{
|
|
return IsValid(CurrentSpeakerNode->GetWave());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
const FString& USUDSDialogue::GetSpeakerID() const
|
|
{
|
|
if (CurrentSpeakerNode)
|
|
return CurrentSpeakerNode->GetSpeakerID();
|
|
|
|
return DummyString;
|
|
}
|
|
|
|
FText USUDSDialogue::GetSpeakerDisplayName() const
|
|
{
|
|
if (CurrentSpeakerDisplayName.IsEmpty())
|
|
{
|
|
// Derive speaker display name
|
|
// Is just a special variable "SpeakerName.SpeakerID"
|
|
// or just the SpeakerID if none specified
|
|
static const FString SpeakerIDPrefix = "SpeakerName.";
|
|
FName Key(SpeakerIDPrefix + GetSpeakerID());
|
|
if (auto Arg = VariableState.Find(Key))
|
|
{
|
|
if (Arg->GetType() == ESUDSValueType::Text)
|
|
{
|
|
CurrentSpeakerDisplayName = Arg->GetTextValue();
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogSUDSDialogue,
|
|
Error,
|
|
TEXT("Error in %s: %s was set to a value that was not text, cannot use"),
|
|
*BaseScript->GetName(),
|
|
*Key.ToString());
|
|
}
|
|
}
|
|
if (CurrentSpeakerDisplayName.IsEmpty())
|
|
{
|
|
// If no display name was specified, use the (non-localised) speaker ID
|
|
CurrentSpeakerDisplayName = FText::FromString(GetSpeakerID());
|
|
}
|
|
}
|
|
return CurrentSpeakerDisplayName;
|
|
}
|
|
|
|
UDialogueVoice* USUDSDialogue::GetSpeakerVoice() const
|
|
{
|
|
if (CurrentSpeakerNode)
|
|
{
|
|
return GetVoice(CurrentSpeakerNode->GetSpeakerID());
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
UDialogueVoice* USUDSDialogue::GetVoice(FString Name) const
|
|
{
|
|
return BaseScript->GetSpeakerVoice(Name);
|
|
}
|
|
|
|
UDialogueVoice* USUDSDialogue::GetTargetVoice() const
|
|
{
|
|
if (CurrentSpeakerNode)
|
|
{
|
|
// Assume that target is the first party that's NOT speaking
|
|
for (auto& Name : BaseScript->GetSpeakers())
|
|
{
|
|
if (Name != CurrentSpeakerNode->GetSpeakerID())
|
|
{
|
|
return BaseScript->GetSpeakerVoice(Name);
|
|
}
|
|
}
|
|
}
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
USoundBase* USUDSDialogue::GetSoundForCurrentLine(bool bAllowAnyTarget) const
|
|
{
|
|
// UDialogueWave's contexts have both speakers and targets, but the GetWaveFromContext method is too restrictive
|
|
// Instead we'll search the contexts ourselves and be more fuzzy
|
|
if (auto Wave = GetWave())
|
|
{
|
|
auto SpeakerVoice = GetSpeakerVoice();
|
|
auto TargetVoice = GetTargetVoice();
|
|
for (auto& Ctx : Wave->ContextMappings)
|
|
{
|
|
// Match specific target voice first, and unspecified targets
|
|
if (Ctx.Context.Speaker == SpeakerVoice)
|
|
{
|
|
if (Ctx.Context.Targets.Contains(TargetVoice))
|
|
{
|
|
// Need to use the proxy according to DialogueWave
|
|
return Ctx.Proxy;
|
|
}
|
|
}
|
|
}
|
|
// If we got here, match more leniently
|
|
if (bAllowAnyTarget)
|
|
{
|
|
for (auto& Ctx : Wave->ContextMappings)
|
|
{
|
|
// Match specific target voice first, and unspecified targets
|
|
if (Ctx.Context.Speaker == SpeakerVoice)
|
|
{
|
|
// Need to use the proxy according to DialogueWave
|
|
return Ctx.Proxy;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
USoundConcurrency* USUDSDialogue::GetVoiceSoundConcurrency() const
|
|
{
|
|
return GetSUDSSubsystem(this->GetWorld())->GetVoicedLineConcurrency();
|
|
}
|
|
|
|
void USUDSDialogue::PlayVoicedLine2D(float VolumeMultiplier, float PitchMultiplier, bool bLooselyMatchTarget)
|
|
{
|
|
if (auto Sound = GetSoundForCurrentLine(bLooselyMatchTarget))
|
|
{
|
|
UGameplayStatics::PlaySound2D(this, Sound, VolumeMultiplier, PitchMultiplier, 0, GetVoiceSoundConcurrency());
|
|
}
|
|
}
|
|
|
|
void USUDSDialogue::PlayVoicedLineAtLocation(FVector Location,
|
|
FRotator Rotation,
|
|
float VolumeMultiplier,
|
|
float PitchMultiplier,
|
|
USoundAttenuation* AttenuationSettings, bool bLooselyMatchTarget)
|
|
{
|
|
if (auto Sound = GetSoundForCurrentLine(bLooselyMatchTarget))
|
|
{
|
|
UGameplayStatics::PlaySoundAtLocation(this,
|
|
Sound,
|
|
Location,
|
|
Rotation,
|
|
VolumeMultiplier,
|
|
PitchMultiplier,
|
|
0,
|
|
AttenuationSettings,
|
|
GetVoiceSoundConcurrency());
|
|
}
|
|
}
|
|
|
|
UAudioComponent* USUDSDialogue::SpawnVoicedLine2D(float VolumeMultiplier, float PitchMultiplier, bool bLooselyMatchTarget)
|
|
{
|
|
if (auto Sound = GetSoundForCurrentLine(bLooselyMatchTarget))
|
|
{
|
|
return UGameplayStatics::SpawnSound2D(this,
|
|
Sound,
|
|
VolumeMultiplier,
|
|
PitchMultiplier,
|
|
0,
|
|
GetVoiceSoundConcurrency());
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
UAudioComponent* USUDSDialogue::SpawnVoicedLineAtLocation(FVector Location,
|
|
FRotator Rotation,
|
|
float VolumeMultiplier,
|
|
float PitchMultiplier,
|
|
USoundAttenuation* AttenuationSettings,
|
|
bool bLooselyMatchTarget)
|
|
{
|
|
if (auto Sound = GetSoundForCurrentLine(bLooselyMatchTarget))
|
|
{
|
|
return UGameplayStatics::SpawnSoundAtLocation(this,
|
|
Sound,
|
|
Location,
|
|
Rotation,
|
|
VolumeMultiplier,
|
|
PitchMultiplier,
|
|
0,
|
|
AttenuationSettings,
|
|
GetVoiceSoundConcurrency());
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
USoundBase* USUDSDialogue::GetVoicedLineSound(bool bLooselyMatchTarget)
|
|
{
|
|
return GetSoundForCurrentLine(bLooselyMatchTarget);
|
|
}
|
|
|
|
USUDSScriptNode* USUDSDialogue::GetNextNode(USUDSScriptNode* Node)
|
|
{
|
|
// In the case of select or random, we need to evaluate to get the next node
|
|
if (Node->GetNodeType() == ESUDSScriptNodeType::Select)
|
|
{
|
|
return RunSelectNode(Node);
|
|
}
|
|
else
|
|
{
|
|
return BaseScript->GetNextNode(Node);
|
|
}
|
|
}
|
|
|
|
bool USUDSDialogue::IsChoiceOrTextNode(ESUDSScriptNodeType Type)
|
|
{
|
|
return Type == ESUDSScriptNodeType::Text || Type == ESUDSScriptNodeType::Choice;
|
|
}
|
|
|
|
const USUDSScriptNode* USUDSDialogue::WalkToNextChoiceNode(USUDSScriptNode* FromNode, bool bExecute)
|
|
{
|
|
if (FromNode && FromNode->GetEdgeCount() == 1)
|
|
{
|
|
const auto NextNode = GetNextNode(FromNode);
|
|
TArray<TObjectPtr<USUDSScriptNodeGosub>> TempGosubStack;
|
|
if (!bExecute)
|
|
{
|
|
// Make a copy of the gosub stack so we can safely explore gosubs
|
|
TempGosubStack.Append(GosubReturnStack);
|
|
}
|
|
|
|
const auto ResultNode = RecurseWalkToNextChoiceOrTextNode(NextNode, bExecute, bExecute ? GosubReturnStack : TempGosubStack);
|
|
if (ResultNode && ResultNode->GetNodeType() == ESUDSScriptNodeType::Choice)
|
|
{
|
|
return ResultNode;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
USUDSScriptNode* USUDSDialogue::RecurseWalkToNextChoiceOrTextNode(USUDSScriptNode* Node, bool bExecute, TArray<TObjectPtr<USUDSScriptNodeGosub>>& LocalGosubStack)
|
|
{
|
|
auto NextNode = Node;
|
|
while (NextNode && !IsChoiceOrTextNode(NextNode->GetNodeType()))
|
|
{
|
|
// Special case gosub/return in non-execute mode, since only RunNode will explore them
|
|
if (!bExecute)
|
|
{
|
|
if (NextNode->GetNodeType() == ESUDSScriptNodeType::Gosub)
|
|
{
|
|
// We need to special case Gosubs, since to find the choice we have to go into them and potentially out again
|
|
if (USUDSScriptNodeGosub* GosubNode = Cast<USUDSScriptNodeGosub>(NextNode))
|
|
{
|
|
if (auto SubNode = BaseScript->GetNodeByLabel(GosubNode->GetLabelName()))
|
|
{
|
|
LocalGosubStack.Add(GosubNode);
|
|
NextNode = RecurseWalkToNextChoiceOrTextNode(SubNode, bExecute, LocalGosubStack);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
}
|
|
else if (NextNode->GetNodeType() == ESUDSScriptNodeType::Return)
|
|
{
|
|
if (LocalGosubStack.Num() > 0)
|
|
{
|
|
// We try to find the next choice node after the gosub, which temporarily redirected
|
|
const auto GoSubNode = LocalGosubStack.Pop();
|
|
NextNode = RecurseWalkToNextChoiceOrTextNode(GetNextNode(GoSubNode), bExecute, LocalGosubStack);
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
return nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bExecute)
|
|
{
|
|
NextNode = RunNode(NextNode);
|
|
}
|
|
else
|
|
{
|
|
NextNode = GetNextNode(NextNode);
|
|
}
|
|
}
|
|
|
|
return NextNode;
|
|
}
|
|
|
|
const USUDSScriptNode* USUDSDialogue::RunUntilNextChoiceNode(USUDSScriptNode* FromNode)
|
|
{
|
|
return WalkToNextChoiceNode(FromNode, true);
|
|
}
|
|
const USUDSScriptNode* USUDSDialogue::FindNextChoiceNode(USUDSScriptNode* FromNode)
|
|
{
|
|
return WalkToNextChoiceNode(FromNode, false);
|
|
}
|
|
|
|
const TArray<FSUDSScriptEdge>& USUDSDialogue::GetChoices() const
|
|
{
|
|
return CurrentChoices;
|
|
}
|
|
|
|
void USUDSDialogue::RecurseAppendChoices(const USUDSScriptNode* Node, TArray<FSUDSScriptEdge>& OutChoices)
|
|
{
|
|
if (!Node)
|
|
return;
|
|
|
|
// We only cascade into choices or selects
|
|
if(Node->GetNodeType() != ESUDSScriptNodeType::Choice &&
|
|
Node->GetNodeType() != ESUDSScriptNodeType::Select)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (auto& Edge : Node->GetEdges())
|
|
{
|
|
switch (Edge.GetType())
|
|
{
|
|
case ESUDSEdgeType::Decision:
|
|
OutChoices.Add(Edge);
|
|
break;
|
|
case ESUDSEdgeType::Condition:
|
|
// Conditional edges are under selects
|
|
if (Edge.GetCondition().IsValid())
|
|
{
|
|
RaiseExpressionVariablesRequested(Edge.GetCondition(), Edge.GetSourceLineNo());
|
|
if (Edge.GetCondition().EvaluateBoolean(VariableState, GetGlobalVariables(), BaseScript->GetName()))
|
|
{
|
|
RecurseAppendChoices(Edge.GetTargetNode().Get(), OutChoices);
|
|
// When we choose a path on a select, we don't check the other paths, we can only go down one
|
|
return;
|
|
}
|
|
}
|
|
break;
|
|
case ESUDSEdgeType::Chained:
|
|
RecurseAppendChoices(Edge.GetTargetNode().Get(), OutChoices);
|
|
break;
|
|
default:
|
|
case ESUDSEdgeType::Continue:
|
|
UE_LOG(LogSUDSDialogue, Fatal, TEXT("Should not have encountered invalid edge in RecurseAppendChoices"))
|
|
break;
|
|
};
|
|
|
|
}
|
|
}
|
|
|
|
void USUDSDialogue::UpdateChoices()
|
|
{
|
|
CurrentChoices.Reset();
|
|
CurrentRootChoiceNode = nullptr;
|
|
if (CurrentSpeakerNode)
|
|
{
|
|
// If we've either found choices through static checking (on one or other select paths), we look for them now
|
|
// We also check if we're inside a gosub, since the call site changes whether there may be choices or not
|
|
if (CurrentSpeakerNode->MayHaveChoices() ||
|
|
GosubReturnStack.Num() > 0)
|
|
{
|
|
// We MIGHT have a choice; conditionals can result in HasChoices() being true but the current state not actually
|
|
// taking us to a choice path
|
|
CurrentRootChoiceNode = FindNextChoiceNode(CurrentSpeakerNode);
|
|
if (CurrentRootChoiceNode)
|
|
{
|
|
// Run any e.g. set nodes between text and choice
|
|
// These can be set nodes directly under the text and before the first choice, which get run for all choices
|
|
RunUntilNextChoiceNode(CurrentSpeakerNode);
|
|
|
|
// Once we've found & run up to the root choice, there can be potentially a tree of mixed choice/select nodes
|
|
// for supporting conditional choices
|
|
RecurseAppendChoices(CurrentRootChoiceNode, CurrentChoices);
|
|
}
|
|
}
|
|
|
|
if (CurrentChoices.Num() == 0)
|
|
{
|
|
if (auto Edge = CurrentSpeakerNode->GetEdge(0))
|
|
{
|
|
// Simple no-choice progression
|
|
// May occur if HasChoices was true but in current state no choice was found
|
|
CurrentChoices.Add(*Edge);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
int USUDSDialogue::GetNumberOfChoices() const
|
|
{
|
|
return CurrentChoices.Num();
|
|
}
|
|
|
|
bool USUDSDialogue::IsSimpleContinue() const
|
|
{
|
|
return CurrentChoices.Num() == 1 && CurrentChoices[0].GetText().IsEmpty();
|
|
}
|
|
|
|
FText USUDSDialogue::GetChoiceText(int Index)
|
|
{
|
|
|
|
if (CurrentChoices.IsValidIndex(Index))
|
|
{
|
|
auto& Choice = CurrentChoices[Index];
|
|
if (Choice.HasParameters())
|
|
{
|
|
return ResolveParameterisedText(Choice.GetParameterNames(), Choice.GetTextFormat(), Choice.GetSourceLineNo());
|
|
}
|
|
else
|
|
{
|
|
return Choice.GetText();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogSUDSDialogue, Error, TEXT("Invalid choice index %d on node %s"), Index, *GetText().ToString());
|
|
}
|
|
|
|
return DummyText;
|
|
}
|
|
|
|
bool USUDSDialogue::HasChoiceIndexBeenTakenPreviously(int Index)
|
|
{
|
|
if (CurrentChoices.IsValidIndex(Index))
|
|
{
|
|
return HasChoiceBeenTakenPreviously(CurrentChoices[Index]);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool USUDSDialogue::HasChoiceBeenTakenPreviously(const FSUDSScriptEdge& Choice)
|
|
{
|
|
return ChoicesTaken.Contains(Choice.GetTextID());
|
|
}
|
|
|
|
bool USUDSDialogue::Continue()
|
|
{
|
|
if (GetNumberOfChoices() == 1)
|
|
{
|
|
return Choose(0);
|
|
}
|
|
return !IsEnded();
|
|
}
|
|
|
|
bool USUDSDialogue::Choose(int Index)
|
|
{
|
|
if (CurrentChoices.IsValidIndex(Index))
|
|
{
|
|
// ONLY run to choice node if there is one!
|
|
// This method is called for Continue() too, which has no choice node
|
|
if (CurrentNodeHasChoices())
|
|
{
|
|
const auto& Choice = CurrentChoices[Index];
|
|
ChoicesTaken.Add(Choice.GetTextID());
|
|
|
|
RaiseChoiceMade(Index, Choice.GetSourceLineNo());
|
|
RaiseProceeding();
|
|
}
|
|
else
|
|
{
|
|
RaiseProceeding();
|
|
}
|
|
// Then choose path
|
|
RunUntilNextSpeakerNodeOrEnd(CurrentChoices[Index].GetTargetNode().Get(), true);
|
|
return !IsEnded();
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogSUDSDialogue, Error, TEXT("Invalid choice index %d on node %s"), Index, *GetText().ToString());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool USUDSDialogue::CurrentNodeHasChoices() const
|
|
{
|
|
return CurrentRootChoiceNode != nullptr;
|
|
}
|
|
|
|
bool USUDSDialogue::IsEnded() const
|
|
{
|
|
return CurrentSpeakerNode == nullptr;
|
|
}
|
|
|
|
bool USUDSDialogue::IsFinalLine() const
|
|
{
|
|
return CurrentSpeakerNode && CurrentChoices.Num() == 1 && CurrentChoices[0].GetTargetNode() == nullptr;
|
|
}
|
|
|
|
void USUDSDialogue::End(bool bQuietly)
|
|
{
|
|
SetCurrentSpeakerNode(nullptr, bQuietly);
|
|
}
|
|
|
|
int USUDSDialogue::GetCurrentSourceLine() const
|
|
{
|
|
return CurrentSourceLineNo;
|
|
}
|
|
|
|
void USUDSDialogue::ResetState(bool bResetVariables, bool bResetPosition, bool bResetVisited)
|
|
{
|
|
if (bResetVariables)
|
|
InitVariables();
|
|
if (bResetPosition)
|
|
SetCurrentSpeakerNode(nullptr, true);
|
|
if (bResetVisited)
|
|
ChoicesTaken.Reset();
|
|
}
|
|
|
|
FSUDSDialogueState USUDSDialogue::GetSavedState() const
|
|
{
|
|
const FString CurrentNodeId = CurrentSpeakerNode
|
|
? SUDS_GET_TEXT_KEY(CurrentSpeakerNode->GetText())
|
|
: FString();
|
|
|
|
TArray<FString> ExportReturnStack;
|
|
for (auto Node : GosubReturnStack)
|
|
{
|
|
if (auto GN = Cast<USUDSScriptNodeGosub>(Node))
|
|
{
|
|
ExportReturnStack.Add(GN->GetGosubID());
|
|
}
|
|
|
|
}
|
|
return FSUDSDialogueState(CurrentNodeId, VariableState, ChoicesTaken, ExportReturnStack);
|
|
|
|
}
|
|
|
|
void USUDSDialogue::RestoreSavedState(const FSUDSDialogueState& State)
|
|
{
|
|
// Don't just empty variables
|
|
// Re-run init to ensure header state is initialised then merge; important for it script is altered since state saved
|
|
InitVariables();
|
|
VariableState.Append(State.GetVariables());
|
|
ChoicesTaken.Empty();
|
|
ChoicesTaken.Append(State.GetChoicesTaken());
|
|
GosubReturnStack.Empty();
|
|
for (auto ID : State.GetReturnStack())
|
|
{
|
|
USUDSScriptNodeGosub* Node = BaseScript->GetNodeByGosubID(ID);
|
|
if (!Node)
|
|
{
|
|
UE_LOG(LogSUDSDialogue, Error, TEXT("Restore: Can't find Gosub with ID %s, returns referencing it will go to end"), *ID);
|
|
}
|
|
// Add anyway, will just go to end
|
|
GosubReturnStack.Add(Node);
|
|
}
|
|
|
|
// If not found this will be null
|
|
if (!State.GetTextNodeID().IsEmpty())
|
|
{
|
|
USUDSScriptNodeText* Node = BaseScript->GetNodeByTextID(State.GetTextNodeID());
|
|
SetCurrentSpeakerNode(Node, true);
|
|
}
|
|
else
|
|
{
|
|
SetCurrentSpeakerNode(nullptr, true);
|
|
}
|
|
}
|
|
|
|
void USUDSDialogue::Restart(bool bResetState, FName StartLabel, bool bReRunHeader)
|
|
{
|
|
if (bResetState)
|
|
{
|
|
ResetState();
|
|
}
|
|
// Always reset return stack
|
|
GosubReturnStack.Empty();
|
|
CurrentSourceLineNo = 0;
|
|
RaiseStarting(StartLabel);
|
|
|
|
if (!bResetState && bReRunHeader)
|
|
{
|
|
// Run header nodes but don't re-init
|
|
RunUntilNextSpeakerNodeOrEnd(BaseScript->GetHeaderNode(), false);
|
|
}
|
|
|
|
if (StartLabel != NAME_None)
|
|
{
|
|
// Check that StartLabel leads to a text node
|
|
// Labels can lead to choices or select nodes for looping, but there has to be a text node to start with.
|
|
auto StartNode = BaseScript->GetNodeByLabel(StartLabel);
|
|
if (!StartNode)
|
|
{
|
|
UE_LOG(LogSUDSDialogue, Error, TEXT("No start label called %s in dialogue %s"), *StartLabel.ToString(), *BaseScript->GetName());
|
|
StartNode = BaseScript->GetFirstNode();
|
|
}
|
|
else if (StartNode->GetNodeType() == ESUDSScriptNodeType::Choice)
|
|
{
|
|
UE_LOG(LogSUDSDialogue,
|
|
Error,
|
|
TEXT("Label %s in dialogue %s cannot be used as a start point, points to a choice."),
|
|
*StartLabel.ToString(),
|
|
*BaseScript->GetName());
|
|
StartNode = BaseScript->GetFirstNode();
|
|
}
|
|
RunUntilNextSpeakerNodeOrEnd(StartNode, true);
|
|
}
|
|
else
|
|
{
|
|
RunUntilNextSpeakerNodeOrEnd(BaseScript->GetFirstNode(), true);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
TSet<FName> USUDSDialogue::GetParametersInUse()
|
|
{
|
|
// Build on demand, may not be needed
|
|
if (!bParamNamesExtracted)
|
|
{
|
|
CurrentRequestedParamNames.Reset();
|
|
if (CurrentSpeakerNode && CurrentSpeakerNode->HasParameters())
|
|
{
|
|
CurrentRequestedParamNames.Append(CurrentSpeakerNode->GetParameterNames());
|
|
}
|
|
for (auto& Choice : CurrentChoices)
|
|
{
|
|
if (Choice.HasParameters())
|
|
{
|
|
CurrentRequestedParamNames.Append(Choice.GetParameterNames());
|
|
}
|
|
}
|
|
bParamNamesExtracted = true;
|
|
}
|
|
|
|
return CurrentRequestedParamNames;
|
|
|
|
}
|
|
|
|
void USUDSDialogue::RaiseStarting(FName StartLabel)
|
|
{
|
|
for (const auto& P : Participants)
|
|
{
|
|
if (P->GetClass()->ImplementsInterface(USUDSParticipant::StaticClass()))
|
|
{
|
|
ISUDSParticipant::Execute_OnDialogueStarting(P, this, StartLabel);
|
|
}
|
|
}
|
|
OnStarting.Broadcast(this, StartLabel);
|
|
#if WITH_EDITOR
|
|
InternalOnStarting.ExecuteIfBound(this, StartLabel);
|
|
#endif
|
|
}
|
|
|
|
void USUDSDialogue::RaiseFinished()
|
|
{
|
|
for (const auto& P : Participants)
|
|
{
|
|
if (P->GetClass()->ImplementsInterface(USUDSParticipant::StaticClass()))
|
|
{
|
|
ISUDSParticipant::Execute_OnDialogueFinished(P, this);
|
|
}
|
|
}
|
|
OnFinished.Broadcast(this);
|
|
#if WITH_EDITOR
|
|
InternalOnFinished.ExecuteIfBound(this);
|
|
#endif
|
|
|
|
}
|
|
|
|
void USUDSDialogue::RaiseNewSpeakerLine()
|
|
{
|
|
for (const auto& P : Participants)
|
|
{
|
|
if (P->GetClass()->ImplementsInterface(USUDSParticipant::StaticClass()))
|
|
{
|
|
ISUDSParticipant::Execute_OnDialogueSpeakerLine(P, this);
|
|
}
|
|
}
|
|
|
|
// Event listeners get it after
|
|
OnSpeakerLine.Broadcast(this);
|
|
#if WITH_EDITOR
|
|
InternalOnSpeakerLine.ExecuteIfBound(this, GetCurrentSourceLine());
|
|
#endif
|
|
}
|
|
|
|
void USUDSDialogue::RaiseChoiceMade(int Index, int LineNo)
|
|
{
|
|
for (const auto& P : Participants)
|
|
{
|
|
if (P->GetClass()->ImplementsInterface(USUDSParticipant::StaticClass()))
|
|
{
|
|
ISUDSParticipant::Execute_OnDialogueChoiceMade(P, this, Index);
|
|
}
|
|
}
|
|
// Event listeners get it after
|
|
OnChoice.Broadcast(this, Index);
|
|
#if WITH_EDITOR
|
|
InternalOnChoice.ExecuteIfBound(this, Index, LineNo);
|
|
#endif
|
|
}
|
|
|
|
void USUDSDialogue::RaiseProceeding()
|
|
{
|
|
for (const auto& P : Participants)
|
|
{
|
|
if (P->GetClass()->ImplementsInterface(USUDSParticipant::StaticClass()))
|
|
{
|
|
ISUDSParticipant::Execute_OnDialogueProceeding(P, this);
|
|
}
|
|
}
|
|
// Event listeners get it after
|
|
OnProceeding.Broadcast(this);
|
|
#if WITH_EDITOR
|
|
InternalOnProceeding.ExecuteIfBound(this);
|
|
#endif
|
|
}
|
|
|
|
FText USUDSDialogue::GetVariableText(FName Name) const
|
|
{
|
|
if (const auto Arg = VariableState.Find(Name))
|
|
{
|
|
if (Arg->GetType() == ESUDSValueType::Text)
|
|
{
|
|
return Arg->GetTextValue();
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogSUDSDialogue, Error, TEXT("Requested variable %s of type text but was not a compatible type"), *Name.ToString());
|
|
}
|
|
}
|
|
return FText();
|
|
}
|
|
|
|
void USUDSDialogue::SetVariableInt(FName Name, int32 Value)
|
|
{
|
|
SetVariable(Name, Value);
|
|
}
|
|
|
|
int USUDSDialogue::GetVariableInt(FName Name) const
|
|
{
|
|
if (const auto Arg = VariableState.Find(Name))
|
|
{
|
|
switch (Arg->GetType())
|
|
{
|
|
case ESUDSValueType::Int:
|
|
return Arg->GetIntValue();
|
|
case ESUDSValueType::Float:
|
|
UE_LOG(LogSUDSDialogue, Warning, TEXT("Casting variable %s to int, data loss may occur"), *Name.ToString());
|
|
return Arg->GetFloatValue();
|
|
default:
|
|
case ESUDSValueType::Gender:
|
|
case ESUDSValueType::Text:
|
|
UE_LOG(LogSUDSDialogue, Error, TEXT("Variable %s is not a compatible integer type"), *Name.ToString());
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void USUDSDialogue::SetVariableFloat(FName Name, float Value)
|
|
{
|
|
SetVariable(Name, Value);
|
|
}
|
|
|
|
float USUDSDialogue::GetVariableFloat(FName Name) const
|
|
{
|
|
if (const auto Arg = VariableState.Find(Name))
|
|
{
|
|
switch (Arg->GetType())
|
|
{
|
|
case ESUDSValueType::Int:
|
|
return Arg->GetIntValue();
|
|
case ESUDSValueType::Float:
|
|
return Arg->GetFloatValue();
|
|
default:
|
|
case ESUDSValueType::Gender:
|
|
case ESUDSValueType::Text:
|
|
UE_LOG(LogSUDSDialogue, Error, TEXT("Variable %s is not a compatible float type"), *Name.ToString());
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void USUDSDialogue::SetVariableGender(FName Name, ETextGender Value)
|
|
{
|
|
SetVariable(Name, Value);
|
|
}
|
|
|
|
ETextGender USUDSDialogue::GetVariableGender(FName Name) const
|
|
{
|
|
if (const auto Arg = VariableState.Find(Name))
|
|
{
|
|
switch (Arg->GetType())
|
|
{
|
|
case ESUDSValueType::Gender:
|
|
return Arg->GetGenderValue();
|
|
default:
|
|
case ESUDSValueType::Int:
|
|
case ESUDSValueType::Float:
|
|
case ESUDSValueType::Text:
|
|
UE_LOG(LogSUDSDialogue, Error, TEXT("Variable %s is not a compatible gender type"), *Name.ToString());
|
|
}
|
|
}
|
|
return ETextGender::Neuter;
|
|
}
|
|
|
|
void USUDSDialogue::SetVariableBoolean(FName Name, bool Value)
|
|
{
|
|
// Use explicit FSUDSValue constructor to avoid default int conversion
|
|
SetVariable(Name, FSUDSValue(Value));
|
|
}
|
|
|
|
bool USUDSDialogue::GetVariableBoolean(FName Name) const
|
|
{
|
|
if (const auto Arg = VariableState.Find(Name))
|
|
{
|
|
switch (Arg->GetType())
|
|
{
|
|
case ESUDSValueType::Boolean:
|
|
return Arg->GetBooleanValue();
|
|
case ESUDSValueType::Int:
|
|
return Arg->GetIntValue() != 0;
|
|
default:
|
|
case ESUDSValueType::Float:
|
|
case ESUDSValueType::Gender:
|
|
case ESUDSValueType::Text:
|
|
UE_LOG(LogSUDSDialogue, Error, TEXT("Variable %s is not a compatible boolean type"), *Name.ToString());
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void USUDSDialogue::SetVariableName(FName Name, FName Value)
|
|
{
|
|
SetVariable(Name, FSUDSValue(Value, false));
|
|
}
|
|
|
|
FName USUDSDialogue::GetVariableName(FName Name) const
|
|
{
|
|
if (const auto Arg = VariableState.Find(Name))
|
|
{
|
|
if (Arg->GetType() == ESUDSValueType::Name)
|
|
{
|
|
return Arg->GetNameValue();
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogSUDSDialogue, Error, TEXT("Requested variable %s of type text but was not a compatible type"), *Name.ToString());
|
|
}
|
|
}
|
|
return NAME_None;
|
|
}
|
|
|
|
void USUDSDialogue::UnSetVariable(FName Name)
|
|
{
|
|
VariableState.Remove(Name);
|
|
}
|
|
|
|
FSUDSValue USUDSDialogue::GetSpeakerLineUserMetadata(FName Key) const
|
|
{
|
|
if (CurrentSpeakerNode)
|
|
{
|
|
if (auto pExpr = CurrentSpeakerNode->GetUserMetadata().Find(Key))
|
|
{
|
|
return pExpr->Evaluate(VariableState, GetGlobalVariables());
|
|
}
|
|
}
|
|
return FSUDSValue();
|
|
}
|
|
|
|
TMap<FName, FSUDSValue> USUDSDialogue::GetAllSpeakerLineUserMetadata() const
|
|
{
|
|
TMap<FName, FSUDSValue> Ret;
|
|
if (CurrentSpeakerNode)
|
|
{
|
|
const TMap<FName, FSUDSValue>& GlobalVariables = GetGlobalVariables();
|
|
const auto& InMeta = CurrentSpeakerNode->GetUserMetadata();
|
|
for (const auto& Pair : InMeta)
|
|
{
|
|
Ret.Add(Pair.Key, Pair.Value.Evaluate(VariableState, GlobalVariables));
|
|
}
|
|
}
|
|
return Ret;
|
|
}
|
|
|
|
FSUDSValue USUDSDialogue::GetChoiceUserMetadata(int Index, FName Key) const
|
|
{
|
|
if (CurrentChoices.IsValidIndex(Index))
|
|
{
|
|
if (auto pExpr = CurrentChoices[Index].GetUserMetadata().Find(Key))
|
|
{
|
|
return pExpr->Evaluate(VariableState, GetGlobalVariables());
|
|
}
|
|
}
|
|
return FSUDSValue();
|
|
}
|
|
|
|
TMap<FName, FSUDSValue> USUDSDialogue::GetAllChoiceUserMetadata(int Index) const
|
|
{
|
|
TMap<FName, FSUDSValue> Ret;
|
|
if (CurrentChoices.IsValidIndex(Index))
|
|
{
|
|
const TMap<FName, FSUDSValue>& GlobalVariables = GetGlobalVariables();
|
|
const auto& InMeta = CurrentChoices[Index].GetUserMetadata();
|
|
for (const auto& Pair : InMeta)
|
|
{
|
|
Ret.Add(Pair.Key, Pair.Value.Evaluate(VariableState, GlobalVariables));
|
|
}
|
|
}
|
|
return Ret;
|
|
}
|