From bf08bb67d993e99ef9753da086f7befdd24de4be Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Mon, 2 Mar 2026 17:44:48 +0100 Subject: [PATCH] Fix conversation handoff and server-side posture for network MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Auto-end conversation on deselection: when bAutoStartConversation is true and the player walks out of range, EndConversation() is called so the NPC becomes available for other players - Server-side posture: add ApplyConversationPosture() helper called from ServerRequestConversation, ServerReleaseConversation, EndConversation (Authority path), and HandleDisconnected — fixes NPC not tracking the client on the listen server (OnRep never fires on Authority) - Guard DetachPostureTarget: only clear TargetActor if it matches our pawn, preventing the server IC from overwriting posture set by the conversation system for a remote client Co-Authored-By: Claude Opus 4.6 --- .../PS_AI_ConvAgent_ElevenLabsComponent.cpp | 38 +++++++++++++++++++ .../PS_AI_ConvAgent_InteractionComponent.cpp | 37 +++++++++++++----- .../PS_AI_ConvAgent_ElevenLabsComponent.h | 5 +++ 3 files changed, 71 insertions(+), 9 deletions(-) diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_ElevenLabsComponent.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_ElevenLabsComponent.cpp index 2154549..24b134d 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_ElevenLabsComponent.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_ElevenLabsComponent.cpp @@ -305,6 +305,7 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::EndConversation() // Reset replicated state so other players can talk to this NPC. bNetIsConversing = false; + ApplyConversationPosture(); NetConversatingPlayer = nullptr; NetConversatingPawn = nullptr; } @@ -714,6 +715,7 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::HandleDisconnected(int32 StatusCode, if (GetOwnerRole() == ROLE_Authority) { bNetIsConversing = false; + ApplyConversationPosture(); NetConversatingPlayer = nullptr; NetConversatingPawn = nullptr; } @@ -1445,6 +1447,9 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::ServerRequestConversation_Implementat NetConversatingPlayer = RequestingPlayer; NetConversatingPawn = RequestingPlayer ? RequestingPlayer->GetPawn() : nullptr; + // Update NPC posture on the server (OnRep never fires on Authority). + ApplyConversationPosture(); + StartConversation_Internal(); } @@ -1460,7 +1465,10 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::ServerReleaseConversation_Implementat WebSocketProxy = nullptr; } + // Clear posture before nullifying the pawn pointer (ApplyConversationPosture + // uses NetConversatingPawn to guard against clearing someone else's target). bNetIsConversing = false; + ApplyConversationPosture(); NetConversatingPlayer = nullptr; NetConversatingPawn = nullptr; } @@ -1694,3 +1702,33 @@ UPS_AI_ConvAgent_InteractionComponent* UPS_AI_ConvAgent_ElevenLabsComponent::Fin } return nullptr; } + +void UPS_AI_ConvAgent_ElevenLabsComponent::ApplyConversationPosture() +{ + // Same logic as OnRep_ConversationState's posture section, but callable + // from the server side where OnRep never fires (Authority). + AActor* Owner = GetOwner(); + if (!Owner) return; + + auto* Posture = Owner->FindComponentByClass(); + if (!Posture) return; + + if (bNetIsConversing && NetConversatingPawn) + { + Posture->bActive = true; + Posture->TargetActor = NetConversatingPawn; + Posture->ResetBodyTarget(); + Posture->bEnableBodyTracking = true; + } + else + { + // Only clear if the posture is still pointing at the departing player. + // Another InteractionComponent may have already set a new TargetActor. + if (!Posture->TargetActor || Posture->TargetActor == NetConversatingPawn) + { + Posture->bActive = false; + Posture->TargetActor = nullptr; + Posture->bEnableBodyTracking = false; + } + } +} diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_InteractionComponent.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_InteractionComponent.cpp index e517dab..b79c457 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_InteractionComponent.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_InteractionComponent.cpp @@ -249,18 +249,31 @@ void UPS_AI_ConvAgent_InteractionComponent::SetSelectedAgent(UPS_AI_ConvAgent_El OldAgent->GetOwner() ? *OldAgent->GetOwner()->GetName() : TEXT("(null)")); } - // ── Listening: stop ────────────────────────────────────────────── - if (bAutoManageListening) + // ── Conversation: end if auto-started ──────────────────────────── + // If we auto-started the conversation on selection, end it now so the + // NPC becomes available for other players. EndConversation() also calls + // StopListening() internally, so we skip the separate StopListening below. + if (bAutoStartConversation && (OldAgent->IsConnected() || OldAgent->bNetIsConversing)) + { + OldAgent->EndConversation(); + } + else if (bAutoManageListening) { OldAgent->StopListening(); } - // Disable body tracking on deselection. + // Disable body tracking on deselection — but only if we were the + // one who set the TargetActor. The conversation system (OnRep or + // server ApplyConversationPosture) may have set TargetActor to a + // different player; don't overwrite that. if (bAutoManagePosture) { if (UPS_AI_ConvAgent_PostureComponent* Posture = FindPostureOnAgent(OldAgent)) { - Posture->bEnableBodyTracking = false; + if (Posture->TargetActor == GetOwner()) + { + Posture->bEnableBodyTracking = false; + } } } @@ -514,12 +527,18 @@ void UPS_AI_ConvAgent_InteractionComponent::DetachPostureTarget( if (UPS_AI_ConvAgent_PostureComponent* Posture = FindPostureOnAgent(AgentPtr)) { - Posture->TargetActor = nullptr; - - if (bDebug) + // Only clear if we are the one who set the TargetActor. + // The conversation system (OnRep / ApplyConversationPosture on server) + // may have set TargetActor to a different player — don't overwrite that. + if (Posture->TargetActor == GetOwner()) { - UE_LOG(LogPS_AI_ConvAgent_Select, Log, TEXT("Posture detached: %s"), - AgentPtr->GetOwner() ? *AgentPtr->GetOwner()->GetName() : TEXT("(null)")); + Posture->TargetActor = nullptr; + + if (bDebug) + { + UE_LOG(LogPS_AI_ConvAgent_Select, Log, TEXT("Posture detached: %s"), + AgentPtr->GetOwner() ? *AgentPtr->GetOwner()->GetName() : TEXT("(null)")); + } } } } diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_ElevenLabsComponent.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_ElevenLabsComponent.h index 027b5af..c9e768a 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_ElevenLabsComponent.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_ElevenLabsComponent.h @@ -617,4 +617,9 @@ private: void StartConversation_Internal(); /** Find the InteractionComponent on the local player's pawn (for relay RPCs). */ class UPS_AI_ConvAgent_InteractionComponent* FindLocalRelayComponent() const; + + /** Update the NPC's PostureComponent from the current conversation state. + * Called on the server when bNetIsConversing / NetConversatingPawn change, + * because OnRep_ConversationState never fires on the Authority. */ + void ApplyConversationPosture(); };