Files
2025-05-18 13:04:45 +08:00

630 lines
21 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "ConversationInstance.h"
#include "ConversationRegistry.h"
#include "CommonConversationRuntimeLogging.h"
#include "ConversationContext.h"
#include "ConversationTaskNode.h"
#include "ConversationChoiceNode.h"
#include "Engine/World.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(ConversationInstance)
namespace ConversationInstanceCVars
{
static bool bShouldAbortConversationOnInvalidChoice = false;
static FAutoConsoleVariableRef CVarShouldAbortConversationOnInvalidChoice(
TEXT("Conversation.Instance.AbortConversationOnInvalidChoice"), bShouldAbortConversationOnInvalidChoice,
TEXT("About the conversation when an invalid choice is chosen."),
ECVF_Default);
}
//@TODO: CONVERSATION: Assert or otherwise guard all the Server* functions to only execute on the authority
//////////////////////////////////////////////////////////////////////
UConversationInstance::UConversationInstance()
{
}
UWorld* UConversationInstance::GetWorld() const
{
return GetTypedOuter<UWorld>();
}
#if WITH_SERVER_CODE
void UConversationInstance::ServerRemoveParticipant(const FGameplayTag& ParticipantID, const FConversationParticipants& PreservedParticipants)
{
for (auto It = Participants.List.CreateIterator(); It; ++It)
{
if (It->ParticipantID == ParticipantID)
{
if (UConversationParticipantComponent* OldParticipant = It->GetParticipantComponent())
{
if (bConversationStarted)
{
OldParticipant->ServerNotifyConversationEnded(this, PreservedParticipants);
}
}
It.RemoveCurrent();
break;
}
}
}
void UConversationInstance::ServerAssignParticipant(const FGameplayTag& ParticipantID, AActor* ParticipantActor)
{
if (!ParticipantID.IsValid() || (ParticipantActor == nullptr))
{
UE_LOG(LogCommonConversationRuntime, Error, TEXT("AConversationInstance::ServerAssignParticipant(ID=%s, Actor=%s) passed bad arguments"),
*ParticipantID.ToString(), *GetPathNameSafe(ParticipantActor));
return;
}
ServerRemoveParticipant(ParticipantID, Participants);
FConversationParticipantEntry NewEntry;
NewEntry.ParticipantID = ParticipantID;
NewEntry.Actor = ParticipantActor;
Participants.List.Add(NewEntry);
if (bConversationStarted)
{
if (UConversationParticipantComponent* ParticipantComponent = NewEntry.GetParticipantComponent())
{
ParticipantComponent->ServerNotifyConversationStarted(this, ParticipantID);
}
}
UE_LOG(LogCommonConversationRuntime, Verbose, TEXT("Conversation %s assigned participant ID=%s to Actor=%s"),
*GetName(), *ParticipantID.ToString(), *GetPathNameSafe(ParticipantActor));
}
void UConversationInstance::ServerStartConversation(const FGameplayTag& EntryPoint, const UConversationDatabase* Graph, const FString& EntryPointIdentifier)
{
UE_LOG(LogCommonConversationRuntime, Verbose, TEXT("Conversation %s starting at %s with %d participants"),
*GetName(), *EntryPoint.ToString(), Participants.List.Num());
ResetConversationProgress();
StartingEntryGameplayTag = EntryPoint;
ActiveConversationGraph = Graph;
UConversationRegistry* ConversationRegistry = UConversationRegistry::GetFromWorld(GetWorld());
TArray<FGuid> PotentialStartingPoints = ConversationRegistry->GetOutputLinkGUIDs(Graph, EntryPoint, EntryPointIdentifier);
if (PotentialStartingPoints.Num() == 0)
{
UE_LOG(LogCommonConversationRuntime, Warning, TEXT("Entry point %s did not exist or had no destination entries; conversation aborted"), *EntryPoint.ToString());
ServerAbortConversation();
return;
}
else
{
TArray<FGuid> LegalStartingPoints = DetermineBranches(PotentialStartingPoints, EConversationRequirementResult::FailedButVisible);
if (LegalStartingPoints.Num() == 0)
{
UE_LOG(LogCommonConversationRuntime, Verbose, TEXT("All branches from entry point %s are disabled, conversation aborted"), *EntryPoint.ToString());
ServerAbortConversation();
return;
}
else
{
const int32 StartingIndex = ConversationRNG.RandRange(0, LegalStartingPoints.Num() - 1);
FConversationBranchPoint StartingPoint;
StartingPoint.ClientChoice.ChoiceReference = FConversationChoiceReference(LegalStartingPoints[StartingIndex]);
StartingBranchPoint = StartingPoint;
CurrentBranchPoint = StartingPoint;
UE_LOG(LogCommonConversationRuntime, Verbose, TEXT("Chosing branch index %d to %s (of %d legal branches) from entry point %s"),
StartingIndex, *CurrentBranchPoint.ClientChoice.ChoiceReference.ToString(), LegalStartingPoints.Num(), *EntryPoint.ToString());
}
}
for (const FConversationParticipantEntry& ParticipantEntry : Participants.List)
{
if (UConversationParticipantComponent* ParticipantComponent = ParticipantEntry.GetParticipantComponent())
{
ParticipantComponent->ServerNotifyConversationStarted(this, ParticipantEntry.ParticipantID);
}
}
bConversationStarted = true;
OnAllParticipantsNotifiedOfStart.Broadcast(this);
TryStartingConversation();
}
bool UConversationInstance::AreAllParticipantsReadyToConverse() const
{
bool bEveryoneReady = true;
for (const FConversationParticipantEntry& ParticipantEntry : Participants.List)
{
if (UConversationParticipantComponent* ParticipantComponent = ParticipantEntry.GetParticipantComponent())
{
if (!ParticipantComponent->ServerIsReadyToConverse())
{
ParticipantComponent->ServerGetReadyToConverse();
bEveryoneReady = false;
}
}
}
return bEveryoneReady;
}
void UConversationInstance::TryStartingConversation()
{
// If the conversation was aborted, nevermind.
if (!bConversationStarted)
{
return;
}
if (!AreAllParticipantsReadyToConverse())
{
for (const FConversationParticipantEntry& ParticipantEntry : Participants.List)
{
if (UConversationParticipantComponent* ParticipantComponent = ParticipantEntry.GetParticipantComponent())
{
ParticipantComponent->OnParticipantReadyToConverseEvent.RemoveAll(this);
ParticipantComponent->OnParticipantReadyToConverseEvent.AddWeakLambda(this, [this](UConversationParticipantComponent* ReadyComponent) {
TryStartingConversation();
});
}
}
}
else
{
// Flush any still listening handlers.
for (const FConversationParticipantEntry& ParticipantEntry : Participants.List)
{
if (UConversationParticipantComponent* ParticipantComponent = ParticipantEntry.GetParticipantComponent())
{
ParticipantComponent->OnParticipantReadyToConverseEvent.RemoveAll(this);
}
}
ConversationRNG.Initialize(NAME_None);
OnStarted();
OnCurrentConversationNodeModified();
}
}
void UConversationInstance::ServerAdvanceConversation(const FAdvanceConversationRequest& InChoicePicked)
{
if (bConversationStarted && GetCurrentChoiceReference().IsValid())
{
UE_LOG(LogCommonConversationRuntime, Verbose, TEXT("ServerAdvanceConversation is determining destinations from %s"), *GetCurrentChoiceReference().ToString());
TArray<FConversationBranchPoint> CandidateDestinations;
const FConversationContext ServerContext = FConversationContext::CreateServerContext(this, nullptr);
const UConversationChoiceNode* ChoiceNodePicked = nullptr;
if (InChoicePicked.Choice != FConversationChoiceReference::Empty)
{
if (const FConversationBranchPoint* BranchPoint = FindBranchPointFromClientChoice(InChoicePicked.Choice))
{
UE_LOG(LogCommonConversationRuntime, Verbose, TEXT("User picked option %s, going to try that"), *BranchPoint->ClientChoice.ChoiceReference.ToString());
CandidateDestinations = { *BranchPoint };
if (const UConversationTaskNode* TaskNode = BranchPoint->ClientChoice.TryToResolveChoiceNode<UConversationTaskNode>(ServerContext))
{
for (const UConversationNode* SubNode : TaskNode->SubNodes)
{
if (const UConversationChoiceNode* ChoiceNode = Cast<UConversationChoiceNode>(SubNode))
{
ChoiceNodePicked = ChoiceNode;
ChoiceNode->NotifyChoicePickedByUser(ServerContext, BranchPoint->ClientChoice);
break;
}
}
}
}
else
{
OnInvalidBranchChoice(InChoicePicked);
return;
}
}
else
{
if (CurrentBranchPoints.Num() == 0 && ScopeStack.Num() > 0)
{
ModifyCurrentConversationNode(ScopeStack.Top());
return;
}
for (const FConversationBranchPoint& BranchPoint : CurrentBranchPoints)
{
if (BranchPoint.ClientChoice.IsChoiceAvailable())
{
CandidateDestinations.Add(BranchPoint);
}
}
}
// Double check the choices are still valid, things may have changed since the user picked the choices.
TArray<FConversationBranchPoint> ValidDestinations;
{
FConversationContext Context = FConversationContext::CreateServerContext(this, nullptr);
for (const FConversationBranchPoint& BranchPoint : CandidateDestinations)
{
if (const UConversationTaskNode* TaskNode = BranchPoint.ClientChoice.TryToResolveChoiceNode<UConversationTaskNode>(Context))
{
EConversationRequirementResult Result = TaskNode->bIgnoreRequirementsWhileAdvancingConversations ? EConversationRequirementResult::Passed : TaskNode->CheckRequirements(Context);
if (Result == EConversationRequirementResult::Passed)
{
ValidDestinations.Add(BranchPoint);
}
}
else
{
ValidDestinations.Add(BranchPoint);
}
}
}
// Allow derived conversation instances a chance to respond to a choice being picked
if (ChoiceNodePicked)
{
OnChoiceNodePickedByUser(ServerContext, ChoiceNodePicked, ValidDestinations);
}
if (ValidDestinations.Num() == 0)
{
UE_LOG(LogCommonConversationRuntime, Verbose, TEXT("No available destinations from %s, ending the conversation"), *GetCurrentChoiceReference().ToString());
ServerAbortConversation();
return;
}
else
{
const FConversationChoiceReference& PreviousNode = GetCurrentChoiceReference();
const int32 StartingIndex = ConversationRNG.RandRange(0, ValidDestinations.Num() - 1);
const FConversationBranchPoint& TargetChoice = ValidDestinations[StartingIndex];
UE_LOG(LogCommonConversationRuntime, Verbose, TEXT("Chosing destination index %d to %s (of %d legal branches) from %s"),
StartingIndex, *TargetChoice.ClientChoice.ChoiceReference.ToString(), ValidDestinations.Num(), *PreviousNode.ToString());
ModifyCurrentConversationNode(TargetChoice);
}
}
else
{
UE_LOG(LogCommonConversationRuntime, Error, TEXT("ServerAdvanceConversation called when the conversation is not active"));
}
}
void UConversationInstance::OnInvalidBranchChoice(const FAdvanceConversationRequest& InChoicePicked)
{
if (ConversationInstanceCVars::bShouldAbortConversationOnInvalidChoice)
{
UE_LOG(LogCommonConversationRuntime, Error, TEXT("User picked option %s but it's not a legal output, aborting"), *InChoicePicked.ToString());
ServerAbortConversation();
}
else
{
UE_LOG(LogCommonConversationRuntime, Warning, TEXT("User picked option %s but it's not a legal output, ignoring"), *InChoicePicked.ToString());
// Forces the client to refresh current choices in case it is not aligned with the server
FConversationContext Context = FConversationContext::CreateServerContext(this, nullptr);
const bool bForceClientRefresh = true;
for (const FConversationParticipantEntry& ConversationParticipantEntry : GetParticipantListCopy())
{
if (UConversationParticipantComponent* ParticipantComponent = ConversationParticipantEntry.GetParticipantComponent())
{
ParticipantComponent->SendClientUpdatedChoices(Context, bForceClientRefresh);
}
}
}
}
void UConversationInstance::ServerAbortConversation()
{
if (bConversationStarted)
{
UE_LOG(LogCommonConversationRuntime, Verbose, TEXT("Conversation aborted or finished"));
OnEnded();
const FConversationParticipants ParticipantsCopy = Participants;
for (const FConversationParticipantEntry& ParticipantEntry : ParticipantsCopy.List)
{
ServerRemoveParticipant(ParticipantEntry.ParticipantID, ParticipantsCopy);
}
check(Participants.List.Num() == 0);
}
ResetConversationProgress();
bConversationStarted = false;
}
void UConversationInstance::PauseConversationAndSendClientChoices(const FConversationContext& Context, const FClientConversationMessage& ClientMessage)
{
FClientConversationMessagePayload LastMessage = FClientConversationMessagePayload();
LastMessage.Message = ClientMessage;
LastMessage.Options = CurrentUserChoices;
LastMessage.CurrentNode = Context.GetCurrentNodeHandle();
LastMessage.Participants = GetParticipantsCopy();
ClientBranchPoints.Add({ CurrentBranchPoint, ScopeStack });
for (const FConversationParticipantEntry& KVP : LastMessage.Participants.List)
{
if (UConversationParticipantComponent* ParticipantComponent = KVP.GetParticipantComponent())
{
ParticipantComponent->SendClientConversationMessage(Context, LastMessage);
}
}
}
void UConversationInstance::ReturnToLastClientChoice(const FConversationContext& Context)
{
if (ClientBranchPoints.Num() > 1)
{
ClientBranchPoints.Pop();
const FCheckpoint& Checkpoint = ClientBranchPoints[ClientBranchPoints.Num() - 1];
ScopeStack = Checkpoint.ScopeStack;
ModifyCurrentConversationNode(Checkpoint.ClientBranchPoint);
}
}
void UConversationInstance::ReturnToCurrentClientChoice(const FConversationContext& Context)
{
if (ClientBranchPoints.Num() > 0)
{
const FCheckpoint Checkpoint = ClientBranchPoints[ClientBranchPoints.Num() - 1];
// Pop after get the last checkpoint since we're about to repeat and push the same one onto the stack.
ClientBranchPoints.Pop();
ScopeStack = Checkpoint.ScopeStack;
ModifyCurrentConversationNode(Checkpoint.ClientBranchPoint);
}
}
void UConversationInstance::ReturnToStart(const FConversationContext& Context)
{
FGameplayTag EntryStartPointGameplayTagCache = StartingEntryGameplayTag;
FConversationBranchPoint StartingBranchPointCache = StartingBranchPoint;
ResetConversationProgress();
StartingEntryGameplayTag = EntryStartPointGameplayTagCache;
StartingBranchPoint = StartingBranchPointCache;
ModifyCurrentConversationNode(StartingBranchPoint);
}
void UConversationInstance::ModifyCurrentConversationNode(const FConversationChoiceReference& NewChoice)
{
FConversationBranchPoint BranchPoint;
BranchPoint.ClientChoice.ChoiceReference = NewChoice;
ModifyCurrentConversationNode(BranchPoint);
}
void UConversationInstance::ModifyCurrentConversationNode(const FConversationBranchPoint& NewBranchPoint)
{
UE_LOG(LogCommonConversationRuntime, Verbose, TEXT("Modying Current Node From %s To %s"),
*NewBranchPoint.ClientChoice.ChoiceReference.ToString(), *GetCurrentChoiceReference().ToString());
CurrentBranchPoint = NewBranchPoint;
ScopeStack.Append(NewBranchPoint.ReturnScopeStack);
OnCurrentConversationNodeModified();
}
void UConversationInstance::ServerRefreshConversationChoices()
{
FConversationContext Context = FConversationContext::CreateServerContext(this, nullptr);
// Update the next choices now that we've executed the task
UpdateNextChoices(Context);
for (const FConversationParticipantEntry& KVP : GetParticipantListCopy())
{
if (UConversationParticipantComponent* ParticipantComponent = KVP.GetParticipantComponent())
{
ParticipantComponent->SendClientUpdatedChoices(Context);
}
}
}
void UConversationInstance::ServerRefreshTaskChoiceData(const FConversationNodeHandle& Handle)
{
FConversationContext Context = FConversationContext::CreateServerContext(this, nullptr);
// Technically we only need to do a gather for a single choice here from the current active subset, but gathering all for now (only choice data relevant to Handle will actually be sent)
UpdateNextChoices(Context);
for (const FConversationParticipantEntry& KVP : GetParticipantListCopy())
{
if (UConversationParticipantComponent* ParticipantComponent = KVP.GetParticipantComponent())
{
ParticipantComponent->SendClientRefreshedTaskChoiceData(Handle, Context);
}
}
}
void UConversationInstance::ServerRefreshCurrentConversationNode()
{
ProcessCurrentConversationNode();
}
#endif // #if WITH_SERVER_CODE
void UConversationInstance::ResetConversationProgress()
{
StartingEntryGameplayTag = FGameplayTag();
StartingBranchPoint = FConversationBranchPoint();
CurrentBranchPoint = FConversationBranchPoint();
CurrentBranchPoints.Reset();
ClientBranchPoints.Reset();
CurrentUserChoices.Reset();
}
void UConversationInstance::UpdateNextChoices(const FConversationContext& Context)
{
TArray<FConversationBranchPoint> AllChoices;
if (const UConversationTaskNode* TaskNode = Cast<UConversationTaskNode>(GetCurrentChoiceReference().NodeReference.TryToResolve(Context)))
{
FConversationContext ChoiceContext = Context.CreateChildContext(TaskNode);
FConversationBranchPointBuilder BranchBuilder;
TaskNode->GenerateNextChoices(BranchBuilder, ChoiceContext);
AllChoices = BranchBuilder.GetBranches();
}
SetNextChoices(AllChoices);
}
void UConversationInstance::SetNextChoices(const TArray<FConversationBranchPoint>& InAllChoices)
{
CurrentUserChoices.Reset();
CurrentBranchPoints = InAllChoices;
for (const FConversationBranchPoint& UserBranchPoint : CurrentBranchPoints)
{
if (UserBranchPoint.ClientChoice.ChoiceType != EConversationChoiceType::ServerOnly)
{
CurrentUserChoices.Add(UserBranchPoint.ClientChoice);
}
}
if (CurrentBranchPoints.Num() > 0 || ScopeStack.Num() > 0)
{
if (CurrentUserChoices.Num() == 0)
{
FClientConversationOptionEntry DefaultChoice;
DefaultChoice.ChoiceReference = FConversationChoiceReference::Empty;
DefaultChoice.ChoiceText = NSLOCTEXT("ConversationInstance", "ConversationInstance_DefaultText", "Continue");
DefaultChoice.ChoiceType = EConversationChoiceType::UserChoiceAvailable;
CurrentUserChoices.Add(DefaultChoice);
}
}
}
const FConversationBranchPoint* UConversationInstance::FindBranchPointFromClientChoice(const FConversationChoiceReference& InChoice) const
{
return CurrentBranchPoints.FindByPredicate([&InChoice](const FConversationBranchPoint& InBranchPoint) {
if (InBranchPoint.ClientChoice.ChoiceType != EConversationChoiceType::ServerOnly)
{
return InBranchPoint.ClientChoice == InChoice;
}
return false;
});
}
#if WITH_SERVER_CODE
TArray<FGuid> UConversationInstance::DetermineBranches(const TArray<FGuid>& SourceList, EConversationRequirementResult MaximumRequirementResult)
{
FConversationContext Context = FConversationContext::CreateServerContext(this, nullptr);
TArray<FGuid> EnabledPaths;
for (const FGuid& TestGUID : SourceList)
{
UConversationNode* TestNode = Context.GetConversationRegistry().GetRuntimeNodeFromGUID(TestGUID, ActiveConversationGraph.Get());
if (UConversationTaskNode* TaskNode = Cast<UConversationTaskNode>(TestNode))
{
const EConversationRequirementResult RequirementResult = TaskNode->CheckRequirements(Context);
if ((int64)RequirementResult <= (int64)MaximumRequirementResult)
{
UE_LOG(LogCommonConversationRuntime, Verbose, TEXT("\t%s is legal"), *TestGUID.ToString());
EnabledPaths.Add(TestGUID);
}
}
}
UE_LOG(LogCommonConversationRuntime, Verbose, TEXT("\t%d paths out of %d are legal"), EnabledPaths.Num(), SourceList.Num());
return EnabledPaths;
}
void UConversationInstance::OnCurrentConversationNodeModified()
{
ProcessCurrentConversationNode();
}
void UConversationInstance::ProcessCurrentConversationNode()
{
check(GetCurrentChoiceReference().IsValid());
FConversationContext AnonContext = FConversationContext::CreateServerContext(this, nullptr);
const UConversationNode* CurrentNode = GetCurrentChoiceReference().NodeReference.TryToResolve(AnonContext);
if (const UConversationTaskNode* TaskNode = Cast<UConversationTaskNode>(CurrentNode))
{
UE_LOG(LogCommonConversationRuntime, Verbose, TEXT("Executing task node %s"), *GetCurrentChoiceReference().ToString());
FConversationContext Context = AnonContext.CreateChildContext(TaskNode);
const FConversationTaskResult TaskResult = TaskNode->ExecuteTaskNodeWithSideEffects(Context);
if (ScopeStack.Num() > 0 && ScopeStack.Top() == TaskNode->GetNodeGuid())
{
// Now that we've finally executed the Subgraph / scope modifying node, we can pop it from
// the scope stack.
ScopeStack.Pop();
}
// Update the next choices now that we've executed the task
UpdateNextChoices(Context);
if (TaskResult.GetType() == EConversationTaskResultType::AbortConversation)
{
ServerAbortConversation();
}
else if (TaskResult.GetType() == EConversationTaskResultType::AdvanceConversation)
{
ServerAdvanceConversation(FAdvanceConversationRequest::Any);
}
else if (TaskResult.GetType() == EConversationTaskResultType::AdvanceConversationWithChoice)
{
//@TODO: CONVERSATION: We are only using the Choice here part of the request, but we need to complete
// support UserParameters, just so we don't have unexpected differences in the system.
ModifyCurrentConversationNode(TaskResult.GetChoice().Choice);
}
else if (TaskResult.GetType() == EConversationTaskResultType::PauseConversationAndSendClientChoices)
{
PauseConversationAndSendClientChoices(Context, TaskResult.GetMessage());
}
else if (TaskResult.GetType() == EConversationTaskResultType::ReturnToLastClientChoice)
{
ReturnToLastClientChoice(Context);
}
else if (TaskResult.GetType() == EConversationTaskResultType::ReturnToCurrentClientChoice)
{
ReturnToCurrentClientChoice(Context);
}
else if (TaskResult.GetType() == EConversationTaskResultType::ReturnToConversationStart)
{
ReturnToStart(Context);
}
else
{
ensureMsgf(false, TEXT("Invalid ResultType executing task node %s"), *GetCurrentChoiceReference().ToString());
}
}
else
{
UE_LOG(LogCommonConversationRuntime, Error, TEXT("Ended up with no task node with ID %s, aborting conversation"), *GetCurrentChoiceReference().ToString());
ServerAbortConversation();
}
}
#endif //WITH_SERVER_CODE