Fix conversation handoff and server-side posture for network

- 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 <noreply@anthropic.com>
This commit is contained in:
j.foucher 2026-03-02 17:44:48 +01:00
parent 215cb398fd
commit bf08bb67d9
3 changed files with 71 additions and 9 deletions

View File

@ -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<UPS_AI_ConvAgent_PostureComponent>();
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;
}
}
}

View File

@ -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)"));
}
}
}
}

View File

@ -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();
};