From 45ee0c6f7d9aa1811d56b093f0392a1ae698a8f0 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Tue, 3 Mar 2026 10:21:52 +0100 Subject: [PATCH] fix crash + posture replication --- ...mNode_PS_AI_ConvAgent_FacialExpression.cpp | 7 ++- .../AnimNode_PS_AI_ConvAgent_LipSync.cpp | 9 ++- .../AnimNode_PS_AI_ConvAgent_Posture.cpp | 59 ++++++++++++------- .../PS_AI_ConvAgent_PostureComponent.cpp | 10 +++- .../PS_AI_Agent/build Lancelot.bat | 0 build.bat => Unreal/PS_AI_Agent/build.bat | 0 6 files changed, 57 insertions(+), 28 deletions(-) rename build Lancelot.bat => Unreal/PS_AI_Agent/build Lancelot.bat (100%) rename build.bat => Unreal/PS_AI_Agent/build.bat (100%) diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/AnimNode_PS_AI_ConvAgent_FacialExpression.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/AnimNode_PS_AI_ConvAgent_FacialExpression.cpp index 2d321a6..231e57f 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/AnimNode_PS_AI_ConvAgent_FacialExpression.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/AnimNode_PS_AI_ConvAgent_FacialExpression.cpp @@ -89,9 +89,12 @@ void FAnimNode_PS_AI_ConvAgent_FacialExpression::Evaluate_AnyThread(FPoseContext // covering eyes, eyebrows, cheeks, nose, and mouth mood. // The downstream Lip Sync node will override mouth-area curves // during speech, while non-mouth emotion curves pass through. - for (const auto& Pair : CachedEmotionCurves) + if (Output.Curve.IsValid()) { - Output.Curve.Set(Pair.Key, Pair.Value); + for (const auto& Pair : CachedEmotionCurves) + { + Output.Curve.Set(Pair.Key, Pair.Value); + } } } diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/AnimNode_PS_AI_ConvAgent_LipSync.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/AnimNode_PS_AI_ConvAgent_LipSync.cpp index 1950069..7883747 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/AnimNode_PS_AI_ConvAgent_LipSync.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/AnimNode_PS_AI_ConvAgent_LipSync.cpp @@ -89,11 +89,14 @@ void FAnimNode_PS_AI_ConvAgent_LipSync::Evaluate_AnyThread(FPoseContext& Output) // Skip near-zero values so that the upstream Facial Expression node's // emotion curves (eyes, brows, mouth mood) pass through during silence. // During speech, active lip sync curves override emotion's mouth curves. - for (const auto& Pair : CachedCurves) + if (Output.Curve.IsValid()) { - if (FMath::Abs(Pair.Value) > 0.01f) + for (const auto& Pair : CachedCurves) { - Output.Curve.Set(Pair.Key, Pair.Value); + if (FMath::Abs(Pair.Value) > 0.01f) + { + Output.Curve.Set(Pair.Key, Pair.Value); + } } } } diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/AnimNode_PS_AI_ConvAgent_Posture.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/AnimNode_PS_AI_ConvAgent_Posture.cpp index 00d064b..9f36c21 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/AnimNode_PS_AI_ConvAgent_Posture.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/AnimNode_PS_AI_ConvAgent_Posture.cpp @@ -379,6 +379,11 @@ void FAnimNode_PS_AI_ConvAgent_Posture::Evaluate_AnyThread(FPoseContext& Output) // Evaluate the upstream pose (pass-through) BasePose.Evaluate(Output); + // Guard: in packaged+network builds the curve container may not be + // initialized yet (skeleton not fully loaded). All Output.Curve access + // must be gated on this flag to avoid null-pointer crashes. + const bool bCurveValid = Output.Curve.IsValid(); + // ── Periodic diagnostic (runs for EVERY instance, before any early return) ─ #if !UE_BUILD_SHIPPING if (++EvalDebugFrameCounter % 300 == 1) // ~every 5 seconds at 60fps @@ -432,11 +437,14 @@ void FAnimNode_PS_AI_ConvAgent_Posture::Evaluate_AnyThread(FPoseContext& Output) #if ELEVENLABS_EYE_DIAGNOSTIC == 1 // MODE 1: CTRL curves → tests CORRECT MetaHuman CTRL naming // Real format: CTRL_expressions_eyeLook{Dir}{L/R} (NOT eyeLookUpLeft!) - static const FName ForceCTRL(TEXT("CTRL_expressions_eyeLookUpL")); - Output.Curve.Set(ForceCTRL, 1.0f); - // Zero ARKit to isolate - static const FName ZeroARKit(TEXT("eyeLookUpLeft")); - Output.Curve.Set(ZeroARKit, 0.0f); + if (bCurveValid) + { + static const FName ForceCTRL(TEXT("CTRL_expressions_eyeLookUpL")); + Output.Curve.Set(ForceCTRL, 1.0f); + // Zero ARKit to isolate + static const FName ZeroARKit(TEXT("eyeLookUpLeft")); + Output.Curve.Set(ZeroARKit, 0.0f); + } // Reset eye bone to ref pose to isolate if (LeftEyeBoneIndex.GetInt() != INDEX_NONE && LeftEyeBoneIndex.GetInt() < Output.Pose.GetNumBones()) @@ -451,11 +459,14 @@ void FAnimNode_PS_AI_ConvAgent_Posture::Evaluate_AnyThread(FPoseContext& Output) #elif ELEVENLABS_EYE_DIAGNOSTIC == 2 // MODE 2: ARKit curves → tests if mh_arkit_mapping_pose drives eyes - static const FName ForceARKit(TEXT("eyeLookUpLeft")); - Output.Curve.Set(ForceARKit, 1.0f); - // Zero CTRL to isolate - static const FName ZeroCTRL(TEXT("CTRL_expressions_eyeLookUpLeft")); - Output.Curve.Set(ZeroCTRL, 0.0f); + if (bCurveValid) + { + static const FName ForceARKit(TEXT("eyeLookUpLeft")); + Output.Curve.Set(ForceARKit, 1.0f); + // Zero CTRL to isolate + static const FName ZeroCTRL(TEXT("CTRL_expressions_eyeLookUpLeft")); + Output.Curve.Set(ZeroCTRL, 0.0f); + } // Reset eye bone to ref pose to isolate if (LeftEyeBoneIndex.GetInt() != INDEX_NONE && LeftEyeBoneIndex.GetInt() < Output.Pose.GetNumBones()) @@ -479,10 +490,13 @@ void FAnimNode_PS_AI_ConvAgent_Posture::Evaluate_AnyThread(FPoseContext& Output) (LookUp * LeftEyeRefPoseRotation).GetNormalized()); } // Zero curves to isolate - static const FName ZeroARKit(TEXT("eyeLookUpLeft")); - static const FName ZeroCTRL(TEXT("CTRL_expressions_eyeLookUpLeft")); - Output.Curve.Set(ZeroARKit, 0.0f); - Output.Curve.Set(ZeroCTRL, 0.0f); + if (bCurveValid) + { + static const FName ZeroARKit(TEXT("eyeLookUpLeft")); + static const FName ZeroCTRL(TEXT("CTRL_expressions_eyeLookUpLeft")); + Output.Curve.Set(ZeroARKit, 0.0f); + Output.Curve.Set(ZeroCTRL, 0.0f); + } if (++EyeDiagLogCounter % 300 == 1) { UE_LOG(LogPS_AI_ConvAgent_PostureAnimNode, Verbose, @@ -537,6 +551,7 @@ void FAnimNode_PS_AI_ConvAgent_Posture::Evaluate_AnyThread(FPoseContext& Output) } // (b) Blend CTRL eye curves: read animation's value, lerp with posture + if (bCurveValid) { const auto& CTRLMap = GetARKitToCTRLEyeMap(); for (const auto& Pair : CachedEyeCurves) @@ -551,15 +566,15 @@ void FAnimNode_PS_AI_ConvAgent_Posture::Evaluate_AnyThread(FPoseContext& Output) Output.Curve.Set(*CTRLName, BlendedValue); } } - } - // (c) Zero ARKit eye curves to prevent mh_arkit_mapping_pose - // from overwriting our carefully blended CTRL values. - // mh_arkit converts ARKit→CTRL additively; zeroing means - // it adds nothing for eyes, preserving our blend. - for (const auto& Pair : CachedEyeCurves) - { - Output.Curve.Set(Pair.Key, 0.0f); + // (c) Zero ARKit eye curves to prevent mh_arkit_mapping_pose + // from overwriting our carefully blended CTRL values. + // mh_arkit converts ARKit→CTRL additively; zeroing means + // it adds nothing for eyes, preserving our blend. + for (const auto& Pair : CachedEyeCurves) + { + Output.Curve.Set(Pair.Key, 0.0f); + } } } diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_PostureComponent.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_PostureComponent.cpp index 61518b1..00c96f5 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_PostureComponent.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_PostureComponent.cpp @@ -337,7 +337,15 @@ void UPS_AI_ConvAgent_PostureComponent::TickComponent( if (FMath::Abs(BodyDelta) > 0.1f) { const float BodyStep = FMath::FInterpTo(0.0f, BodyDelta, SafeDeltaTime, BodyInterpSpeed); - Owner->AddActorWorldRotation(FRotator(0.0f, BodyStep, 0.0f)); + // Only modify actor rotation on the authority (server/standalone). + // On clients, the rotation arrives via replication — calling + // AddActorWorldRotation here would fight with replicated updates, + // causing visible stuttering as the network periodically snaps + // the rotation back to the server's value. + if (Owner->HasAuthority()) + { + Owner->AddActorWorldRotation(FRotator(0.0f, BodyStep, 0.0f)); + } } } diff --git a/build Lancelot.bat b/Unreal/PS_AI_Agent/build Lancelot.bat similarity index 100% rename from build Lancelot.bat rename to Unreal/PS_AI_Agent/build Lancelot.bat diff --git a/build.bat b/Unreal/PS_AI_Agent/build.bat similarity index 100% rename from build.bat rename to Unreal/PS_AI_Agent/build.bat