// 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(); } #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 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 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 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(ServerContext)) { for (const UConversationNode* SubNode : TaskNode->SubNodes) { if (const UConversationChoiceNode* ChoiceNode = Cast(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 ValidDestinations; { FConversationContext Context = FConversationContext::CreateServerContext(this, nullptr); for (const FConversationBranchPoint& BranchPoint : CandidateDestinations) { if (const UConversationTaskNode* TaskNode = BranchPoint.ClientChoice.TryToResolveChoiceNode(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 AllChoices; if (const UConversationTaskNode* TaskNode = Cast(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& 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 UConversationInstance::DetermineBranches(const TArray& SourceList, EConversationRequirementResult MaximumRequirementResult) { FConversationContext Context = FConversationContext::CreateServerContext(this, nullptr); TArray EnabledPaths; for (const FGuid& TestGUID : SourceList) { UConversationNode* TestNode = Context.GetConversationRegistry().GetRuntimeNodeFromGUID(TestGUID, ActiveConversationGraph.Get()); if (UConversationTaskNode* TaskNode = Cast(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(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