// Show a deux - voir LPShowCoupleSubsystem.h #include "LPShowCoupleSubsystem.h" #include "Animation/AnimMontage.h" #include "Animation/AnimSequence.h" #include "Animation/AnimSingleNodeInstance.h" #include "Components/CapsuleComponent.h" #include "Components/StaticMeshComponent.h" #include "Engine/StaticMesh.h" #include "EngineUtils.h" #include "Misc/PackageName.h" #include "Components/BoxComponent.h" #include "Components/CapsuleComponent.h" #include "Components/PrimitiveComponent.h" #include "Components/SkeletalMeshComponent.h" #include "DrawDebugHelpers.h" #include "Engine/GameInstance.h" #include "Engine/World.h" #include "GameFramework/Character.h" #include "GameFramework/CharacterMovementComponent.h" #include "GameFramework/PlayerController.h" #include "HAL/IConsoleManager.h" #include "LifePlay.h" #include "LPCoupleSim.h" #include "UObject/UnrealType.h" static TAutoConsoleVariable CVarShowCouple( TEXT("lp.ShowCouple"), 1, TEXT("1 : boite du couple pendant le show. 0 : ancien chemin du Blueprint."), ECVF_Default); namespace { constexpr double ShowReach = 400.0; bool IsBedMesh(const UStaticMesh* Mesh) { if (!Mesh) { return false; } static TSet Beds; static TSet Others; const FName Name = Mesh->GetFName(); if (Beds.Contains(Name)) { return true; } if (Others.Contains(Name)) { return false; } const FString MeshName = Mesh->GetName(); const bool bBed = MeshName.Contains(TEXT("bed"), ESearchCase::IgnoreCase) || MeshName.Contains(TEXT("fnt_ext011")) || MeshName.Equals(TEXT("fnt_new045")) || MeshName.Equals(TEXT("fnt_new235")); (bBed ? Beds : Others).Add(Name); return bBed; } ACharacter* AsCharacter(UObject* Object) { return Cast(Object); } AActor* ObjectActor(const UObject* Container, const TCHAR* Name) { const FObjectPropertyBase* Prop = Container ? CastField(Container->GetClass()->FindPropertyByName(Name)) : nullptr; return Prop ? Cast(Prop->GetObjectPropertyValue_InContainer(Container)) : nullptr; } ACharacter* ObjectProperty(const UObject* Container, const TCHAR* Name) { return Cast(ObjectActor(Container, Name)); } void ArrayProperty(const UObject* Container, const TCHAR* Name, TArray& Out) { const FArrayProperty* ArrayProp = Container ? CastField(Container->GetClass()->FindPropertyByName(Name)) : nullptr; const FObjectPropertyBase* Inner = ArrayProp ? CastField(ArrayProp->Inner) : nullptr; if (!Inner) { return; } FScriptArrayHelper Helper(ArrayProp, ArrayProp->ContainerPtrToValuePtr(Container)); for (int32 Index = 0; Index < Helper.Num(); ++Index) { if (ACharacter* Who = AsCharacter(Inner->GetObjectPropertyValue(Helper.GetRawPtr(Index)))) { Out.Add(Who); } } } bool CapsuleOff(const ACharacter* Who) { const UCapsuleComponent* Cap = Who ? Who->GetCapsuleComponent() : nullptr; return Cap && Cap->GetCollisionEnabled() == ECollisionEnabled::NoCollision; } void AddUniqueAnim(TArray& Out, UAnimSequence* Anim) { if (Anim) { Out.AddUnique(Anim); } } UAnimSequence* SequenceFromMontage(const UAnimMontage* Montage) { if (Montage && Montage->SlotAnimTracks.Num() > 0 && Montage->SlotAnimTracks[0].AnimTrack.AnimSegments.Num() > 0) { return Cast(Montage->SlotAnimTracks[0].AnimTrack.AnimSegments[0].GetAnimReference()); } return nullptr; } UAnimSequence* PlayingSequence(const USkeletalMeshComponent* Mesh) { if (!Mesh) { return nullptr; } if (UAnimSequence* Seq = Cast(Mesh->AnimationData.AnimToPlay)) { return Seq; } const UAnimInstance* Instance = Mesh->GetAnimInstance(); if (UAnimSequence* Seq = SequenceFromMontage(Instance ? Instance->GetCurrentActiveMontage() : nullptr)) { return Seq; } if (UAnimSingleNodeInstance* Single = Cast(const_cast(Instance))) { return Cast(Single->GetCurrentAsset()); } return nullptr; } bool IsIdleClip(const UAnimSequence* Anim) { return Anim && Anim->GetName().Contains(TEXT("idle"), ESearchCase::IgnoreCase); } UAnimSequence* PairSibling(const UAnimSequence* Anim) { if (!Anim) { return nullptr; } const FString Name = Anim->GetName(); FString Other; if (Name.EndsWith(TEXT("_F"))) { Other = Name.LeftChop(2) + TEXT("_M"); } else if (Name.EndsWith(TEXT("_M"))) { Other = Name.LeftChop(2) + TEXT("_F"); } else if (Name.EndsWith(TEXT("_0"))) { Other = Name.LeftChop(2) + TEXT("_1"); } else if (Name.EndsWith(TEXT("_1"))) { Other = Name.LeftChop(2) + TEXT("_0"); } else { return nullptr; } const FString Folder = FPaths::GetPath(Anim->GetOutermost()->GetName()); const FString Path = Folder + TEXT("/") + Other + TEXT(".") + Other; return LoadObject(nullptr, *Path); } void CompletePair(TArray& Out) { TArray Extra; for (const UAnimSequence* Anim : Out) { AddUniqueAnim(Extra, PairSibling(Anim)); } for (UAnimSequence* Anim : Extra) { Out.AddUnique(Anim); } } void KeepShowClips(TArray& Clips) { Clips.RemoveAll([](const UAnimSequence* Anim) { return IsIdleClip(Anim); }); CompletePair(Clips); } void ReadAnimProperty(const FProperty* Prop, const void* Container, TArray& Out) { if (const FArrayProperty* ArrayProp = CastField(Prop)) { const FObjectPropertyBase* Inner = CastField(ArrayProp->Inner); if (!Inner) { return; } FScriptArrayHelper Helper(ArrayProp, ArrayProp->ContainerPtrToValuePtr(Container)); for (int32 Index = 0; Index < Helper.Num(); ++Index) { UObject* Value = Inner->GetObjectPropertyValue(Helper.GetRawPtr(Index)); AddUniqueAnim(Out, Cast(Value)); AddUniqueAnim(Out, SequenceFromMontage(Cast(Value))); } } else if (const FObjectPropertyBase* ObjectProp = CastField(Prop)) { UObject* Value = ObjectProp->GetObjectPropertyValue_InContainer(Container); AddUniqueAnim(Out, Cast(Value)); AddUniqueAnim(Out, SequenceFromMontage(Cast(Value))); } } void CollectShowAnims(const UObject* GI, const ACharacter* Lead, const ACharacter* Partner, TArray& Out, FString& Label) { TArray FromShow; if (const FStructProperty* Last = GI ? CastField(GI->GetClass()->FindPropertyByName(TEXT("LastAnimation"))) : nullptr) { if (Last->Struct) { const void* Data = Last->ContainerPtrToValuePtr(GI); for (TFieldIterator It(Last->Struct); It; ++It) { ReadAnimProperty(*It, Data, FromShow); } } } KeepShowClips(FromShow); if (FromShow.Num() == 0) { auto FromActor = [&](const ACharacter* Who) { if (!Who) { return; } TInlineComponentArray Meshes; Who->GetComponents(Meshes); for (const USkeletalMeshComponent* Mesh : Meshes) { AddUniqueAnim(FromShow, PlayingSequence(Mesh)); } }; FromActor(Lead); FromActor(Partner); KeepShowClips(FromShow); } Out = MoveTemp(FromShow); for (const UAnimSequence* Anim : Out) { if (!Label.IsEmpty()) { Label += TEXT("+"); } Label += Anim->GetName(); } } } void ULPShowCoupleSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); PostTickHandle = FWorldDelegates::OnWorldPostActorTick.AddUObject(this, &ULPShowCoupleSubsystem::AfterActors); } void ULPShowCoupleSubsystem::Deinitialize() { FWorldDelegates::OnWorldPostActorTick.Remove(PostTickHandle); Release(); Super::Deinitialize(); } void ULPShowCoupleSubsystem::AfterActors(UWorld* World, ELevelTick TickType, float DeltaSeconds) { (void)TickType; (void)DeltaSeconds; if (!World || World != GetWorld() || !World->IsGameWorld()) { return; } if (CVarShowCouple.GetValueOnGameThread() == 0) { OpenUseFurniture(); LetSittersPass(World); Release(); return; } UGameInstance* GI = GetGameInstance(); TArray Found; ArrayProperty(GI, TEXT("SexPersons"), Found); if (Found.Num() < 2) { Found.Reset(); if (ACharacter* A = ObjectProperty(GI, TEXT("AP1"))) { Found.Add(A); } if (ACharacter* B = ObjectProperty(GI, TEXT("AP2"))) { Found.Add(B); } } ACharacter* Player = nullptr; if (const APlayerController* PC = World->GetFirstPlayerController()) { Player = Cast(PC->GetPawn()); } ACharacter* NextLead = nullptr; ACharacter* NextPartner = nullptr; for (ACharacter* Who : Found) { if (!IsValid(Who) || !CapsuleOff(Who)) { continue; } if (!NextLead) { NextLead = (Player && Found.Contains(Player) && CapsuleOff(Player)) ? Player : Who; } } if (NextLead) { double Best = ShowReach * ShowReach; for (ACharacter* Who : Found) { if (!IsValid(Who) || Who == NextLead || !CapsuleOff(Who)) { continue; } const double Dist = FVector::DistSquared(NextLead->GetActorLocation(), Who->GetActorLocation()); if (Dist <= Best) { Best = Dist; NextPartner = Who; } } } if (!NextLead || !NextPartner) { OpenUseFurniture(); LetSittersPass(World); Release(); return; } if (!bEngaged || Lead.Get() != NextLead || Partner.Get() != NextPartner) { Release(); Engage(NextLead, NextPartner); } Weld(); BlockRoomPawn(); TArray Anims; FString Label; CollectShowAnims(GI, NextLead, NextPartner, Anims, Label); FString Key; for (const UAnimSequence* Anim : Anims) { if (!Key.IsEmpty()) { Key += TEXT("+"); } Key += Anim->GetName(); } if (!FitToBodies() && !Key.IsEmpty() && Key != FittedPair && Key != RejectedPair) { if (FitBox(Anims, Label)) { FittedPair = Key; } else { RejectedPair = Key; } } KeepTogether(); if (UBoxComponent* Drawn = Box.Get()) { DrawDebugBox(World, Drawn->GetComponentLocation(), Drawn->GetScaledBoxExtent(), Drawn->GetComponentQuat(), FColor::Green, false, -1.f, 0, 2.f); } } void ULPShowCoupleSubsystem::Engage(ACharacter* InLead, ACharacter* InPartner) { Lead = InLead; Partner = InPartner; bEngaged = true; LastSafe = InLead->GetActorLocation(); SavedPartnerMove = LPCoupleSim::SaveAndFreeze(InPartner); UE_LOG(LogLifePlay, Warning, TEXT("Show couple : branche (%s + %s)"), *GetNameSafe(InLead), *GetNameSafe(InPartner)); } bool ULPShowCoupleSubsystem::IsUseFurniture(const UStaticMesh* Mesh) { if (!Mesh) { return false; } const FName Name = Mesh->GetFName(); if (UseFurniture.Contains(Name)) { return true; } if (SolidFurniture.Contains(Name)) { return false; } const TCHAR* Actions[] = { TEXT("sit"), TEXT("lie"), TEXT("use"), TEXT("work") }; bool bUse = false; for (const TCHAR* Action : Actions) { const FString Path = FString::Printf(TEXT("/Game/Heavy/New2/Daily/%s-f-0-%s"), *Mesh->GetName(), Action); if (FPackageName::DoesPackageExist(Path)) { bUse = true; break; } } (bUse ? UseFurniture : SolidFurniture).Add(Name); return bUse; } void ULPShowCoupleSubsystem::OpenUseFurniture() { AActor* Room = ObjectActor(GetGameInstance(), TEXT("Room")); if (!Room) { return; } TArray Actors; Actors.Add(Room); TArray Attached; Room->GetAttachedActors(Attached, true, true); Actors.Append(Attached); int32 Opened = 0; for (AActor* Actor : Actors) { TInlineComponentArray Meshes; Actor->GetComponents(Meshes); for (UStaticMeshComponent* Comp : Meshes) { if (!Comp || !IsUseFurniture(Comp->GetStaticMesh())) { continue; } if (Comp->GetCollisionResponseToChannel(ECC_Pawn) == ECR_Ignore) { continue; } Comp->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore); ++Opened; } } if (Opened > 0) { UE_LOG(LogLifePlay, Warning, TEXT("Meubles assis ou couches : %d ouverts au passage"), Opened); } } void ULPShowCoupleSubsystem::LetSittersPass(UWorld* World) { TSet Sitting; for (TActorIterator It(World); It; ++It) { ACharacter* Who = *It; UCapsuleComponent* Cap = Who ? Who->GetCapsuleComponent() : nullptr; if (!Cap || Cap->GetCollisionEnabled() == ECollisionEnabled::NoCollision) { continue; } const UAnimSequence* Anim = PlayingSequence(Who->GetMesh()); const FString Name = Anim ? Anim->GetName() : FString(); const bool bPose = Name.Contains(TEXT("-sit")) || Name.Contains(TEXT("-lie")) || Name.Contains(TEXT("-use")) || Name.Contains(TEXT("-work")) || Name.Contains(TEXT("sitting"), ESearchCase::IgnoreCase); if (!bPose) { continue; } Sitting.Add(Cap); bool bSeen = false; for (const FSavedSeat& Saved : SavedSeats) { if (Saved.Cap.Get() == Cap) { bSeen = true; break; } } if (bSeen) { continue; } SavedSeats.Add({ Cap, Cap->GetCollisionResponseToChannel(ECC_WorldDynamic) }); Cap->SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Ignore); } for (int32 Index = SavedSeats.Num() - 1; Index >= 0; --Index) { UCapsuleComponent* Cap = SavedSeats[Index].Cap.Get(); if (Cap && Sitting.Contains(Cap)) { continue; } if (Cap && Cap->GetCollisionEnabled() != ECollisionEnabled::NoCollision) { Cap->SetCollisionResponseToChannel(ECC_WorldDynamic, SavedSeats[Index].Response); } SavedSeats.RemoveAt(Index); } } void ULPShowCoupleSubsystem::BlockRoomPawn() { AActor* Room = ObjectActor(GetGameInstance(), TEXT("Room")); if (!Room) { return; } TArray Actors; TSet Seen; TArray Pending; Pending.Add(Room); for (TFieldIterator It(Room->GetClass()); It; ++It) { if (const FArrayProperty* ArrayProp = CastField(*It)) { const FObjectPropertyBase* Inner = CastField(ArrayProp->Inner); if (!Inner) { continue; } FScriptArrayHelper Helper(ArrayProp, ArrayProp->ContainerPtrToValuePtr(Room)); for (int32 Index = 0; Index < Helper.Num(); ++Index) { if (AActor* Other = Cast(Inner->GetObjectPropertyValue(Helper.GetRawPtr(Index)))) { Pending.Add(Other); } } } else if (const FObjectPropertyBase* ObjectProp = CastField(*It)) { if (AActor* Other = Cast(ObjectProp->GetObjectPropertyValue_InContainer(Room))) { Pending.Add(Other); } } } while (Pending.Num() > 0) { AActor* Actor = Pending.Pop(EAllowShrinking::No); if (!Actor || Seen.Contains(Actor) || Cast(Actor)) { continue; } bool bInRoom = Actor == Room || Actor->GetOwner() == Room; for (AActor* Cursor = Actor; Cursor && !bInRoom; Cursor = Cursor->GetAttachParentActor()) { bInRoom = Cursor == Room; } if (!bInRoom && FVector::DistSquared(Actor->GetActorLocation(), Room->GetActorLocation()) > FMath::Square(4000.0)) { continue; } Seen.Add(Actor); Actors.Add(Actor); TArray Attached; Actor->GetAttachedActors(Attached, true, true); Pending.Append(Attached); } int32 AlreadyBlock = 0; int32 Overlap = 0; int32 Flipped = 0; FString Names; for (AActor* Actor : Actors) { TInlineComponentArray Prims; Actor->GetComponents(Prims); for (UPrimitiveComponent* Prim : Prims) { if (!Prim || Prim->GetCollisionEnabled() == ECollisionEnabled::NoCollision || Cast(Prim)) { continue; } const ECollisionResponse Response = Prim->GetCollisionResponseToChannel(ECC_Pawn); if (Response == ECR_Block) { ++AlreadyBlock; continue; } if (Response == ECR_Overlap) { ++Overlap; } if (Response != ECR_Ignore) { continue; } bool bSeen = false; for (const FSavedPawn& Saved : SavedFurniture) { if (Saved.Comp.Get() == Prim) { bSeen = true; break; } } if (bSeen) { continue; } SavedFurniture.Add({ Prim, ECR_Ignore }); Prim->SetCollisionResponseToChannel(ECC_Pawn, ECR_Block); ++Flipped; if (Flipped <= 6) { if (!Names.IsEmpty()) { Names += TEXT(", "); } Names += FString::Printf(TEXT("%s/%s"), *GetNameSafe(Actor), *Prim->GetName()); } } } if (!bPawnLogged || Flipped > 0) { bPawnLogged = true; UE_LOG(LogLifePlay, Warning, TEXT("Show couple : Pawn chambre Block=%d Overlap=%d Ignore->Block=%d %s"), AlreadyBlock, Overlap, Flipped, *Names); } } void ULPShowCoupleSubsystem::Release() { if (!bEngaged && SavedFurniture.Num() == 0 && !Box.IsValid()) { return; } LPCoupleSim::Unweld(Partner.Get()); LPCoupleSim::RestoreMove(SavedPartnerMove); for (const FSavedPawn& Saved : SavedFurniture) { if (UPrimitiveComponent* Prim = Saved.Comp.Get()) { Prim->SetCollisionResponseToChannel(ECC_Pawn, Saved.Response); } } SavedFurniture.Reset(); if (UBoxComponent* Old = Box.Get()) { Old->DestroyComponent(); } Box.Reset(); FittedPair.Reset(); RejectedPair.Reset(); bPawnLogged = false; Lead.Reset(); Partner.Reset(); if (bEngaged) { UE_LOG(LogLifePlay, Warning, TEXT("Show couple : ancien chemin")); } bEngaged = false; } void ULPShowCoupleSubsystem::Weld() { ACharacter* Who = Lead.Get(); ACharacter* Other = Partner.Get(); if (!Who || !Other) { return; } LPCoupleSim::AttachPartner(Who, Other); const double Half = LPCoupleSim::CapsuleHalfOf(Who); LPCoupleSim::PlaceBodyMesh(Who, Half); LPCoupleSim::PlaceBodyMesh(Other, Half); } bool ULPShowCoupleSubsystem::FitToBodies() { ACharacter* Who = Lead.Get(); ACharacter* Other = Partner.Get(); if (!Who || !Other) { return false; } // Voie "corps vivants" : os affiches des deux meshes. Geometrie partagee avec la sandbox // (LPCoupleSim.h) ; cette voie ne recale pas le fond de boite sur la plante des pieds. const LPCoupleSim::FBoxFitParams Params; FBox Bounds(ForceInit); LPCoupleSim::AccumulateLiveBody(Who->GetMesh(), Params, Bounds); LPCoupleSim::AccumulateLiveBody(Other->GetMesh(), Params, Bounds); const LPCoupleSim::FBoxFit Fit = LPCoupleSim::FitFromLiveBounds(Bounds, Params); if (!Fit.bValid) { return false; } Box = LPCoupleSim::EnsureBox(Who, Box.Get(), TEXT("LPShowCoupleBox"), true); LPCoupleSim::ApplyBoxFit(Box.Get(), Fit); if (!LoggedExtent.Equals(Fit.Extent, 5.0)) { UE_LOG(LogLifePlay, Warning, TEXT("Show couple : boite pose centre %s etendue %s"), *Fit.Center.ToString(), *Fit.Extent.ToString()); LoggedExtent = Fit.Extent; } return true; } bool ULPShowCoupleSubsystem::FitBox(const TArray& Anims, const FString& Label) { ACharacter* Who = Lead.Get(); if (!Who || Anims.Num() == 0) { return false; } // Voie "animations" : meme mesure et meme padding que FitCoupleBox de la sandbox (LPCoupleSim.h). // Le fond de boite est recale sur la plante des pieds du porteur. LPCoupleSim::FBoxFitParams Params; Params.CapsuleHalf = LPCoupleSim::CapsuleHalfOf(Who); Params.FloorLocalZ = -Params.CapsuleHalf; const FTransform ToActor = LPCoupleSim::MeshToActor(Params.CapsuleHalf); FBox Bounds(ForceInit); for (const UAnimSequence* Anim : Anims) { LPCoupleSim::AccumulateAnimBounds(Anim, ToActor, Bounds); } const LPCoupleSim::FBoxFit Fit = LPCoupleSim::FitFromAnimBounds(Bounds, Params); if (!Fit.bValid) { UE_LOG(LogLifePlay, Error, TEXT("Show couple : boite non mesuree (%s)"), *Label); return false; } Box = LPCoupleSim::EnsureBox(Who, Box.Get(), TEXT("LPShowCoupleBox"), true); LPCoupleSim::ApplyBoxFit(Box.Get(), Fit); UE_LOG(LogLifePlay, Warning, TEXT("Show couple : boite %s centre %s etendue %s"), *Label, *Fit.Center.ToString(), *Fit.Extent.ToString()); return true; } void ULPShowCoupleSubsystem::KeepTogether() { ACharacter* Who = Lead.Get(); ACharacter* Other = Partner.Get(); UBoxComponent* Volume = Box.Get(); UWorld* World = GetWorld(); if (!Who || !Other || !Volume || !World) { return; } const FVector Now = Who->GetActorLocation(); const FVector Delta = Now - LastSafe; if (Delta.IsNearlyZero()) { return; } // Meme resolveur que la sandbox (LPCoupleSim.h), avec les reglages du jeu : balayage multiple, // 3 passes, depenetration, tous les PNJ ignores, lits franchissables, coque de la chambre ignoree. LPCoupleSim::FSweepOptions Options = LPCoupleSim::FSweepOptions::Show(); Options.IsWalkableSupport = [](const UPrimitiveComponent* Comp) { const UStaticMeshComponent* MeshComp = Cast(Comp); return MeshComp && IsBedMesh(MeshComp->GetStaticMesh()); }; Options.IsIgnoredShell = [](const AActor* Actor) { const FString Name = Actor ? Actor->GetName() : FString(); return Name == TEXT("Wall") || Name.StartsWith(TEXT("Wall_")); }; Options.OnIgnoredShell = [this](const AActor* Actor) { if (LastBlocker == TEXT("coque")) { return; } if (const UStaticMeshComponent* Shell = Actor->FindComponentByClass()) { UE_LOG(LogLifePlay, Warning, TEXT("Show couple : coque %s ignoree, maille %s, boite %s"), *Actor->GetName(), *GetNameSafe(Shell->GetStaticMesh()), *Shell->Bounds.GetBox().ToString()); } LastBlocker = TEXT("coque"); }; const LPCoupleSim::FSweepResult Move = LPCoupleSim::ResolveCoupleMove( World, Volume, Who, Other, LastSafe, Delta, Options); if (!Move.Blocker.IsEmpty() && Move.Blocker != LastBlocker) { UE_LOG(LogLifePlay, Warning, TEXT("Show couple : bloque par %s normal %s profondeur %.0f"), *Move.Blocker, *Move.BlockerNormal.ToString(), Move.BlockerDepth); LastBlocker = Move.Blocker; } if (!Move.Location.Equals(Now, LPCoupleSim::MoveEpsilon)) { Who->SetActorLocation(Move.Location, false, nullptr, ETeleportType::TeleportPhysics); } LastSafe = Who->GetActorLocation(); }