// Portage UE 5.5 - voir LPOutdoorRoomSubsystem.h #include "LPOutdoorRoomSubsystem.h" #include "Blueprint/UserWidget.h" #include "Components/BoxComponent.h" #include "Components/CapsuleComponent.h" #include "Components/SceneComponent.h" #include "Components/StaticMeshComponent.h" #include "Engine/StaticMesh.h" #include "Engine/GameInstance.h" #include "Engine/World.h" #include "EngineUtils.h" #include "GameFramework/Actor.h" #include "GameFramework/Character.h" #include "GameFramework/InputSettings.h" #include "GameFramework/PlayerController.h" #include "GameFramework/PlayerInput.h" #include "LPKeyboardSubsystem.h" #include "PhysicsEngine/BodySetup.h" #include "UObject/UObjectIterator.h" #include "LifePlay.h" #include "UObject/UnrealType.h" namespace { // Interieurs : 0,2 a 0,4 ; exterieurs : 0,01. Le seuil laisse une marge des deux cotes. constexpr double OutdoorScaleZ = 0.05; const FName RoomPropertyName(TEXT("Room")); const FName CeilingPropertyName(TEXT("WallT")); // S-F4 : plancher des personnages (BoxComponent du Blueprint Room) et echelle Z de reference d'un interieur. const FName FloorPropertyName(TEXT("Box")); constexpr double IndoorScaleZ = 0.2; // S-F4b : decors exterieurs (fnt_ext*) dont la collision simple englobe la zone de marche ; examines toutes les 0,5 s. const TCHAR* const OutdoorDecorPrefix = TEXT("fnt_ext"); constexpr float DecorScanSeconds = 0.5f; // S-F5 : seuil d'enfoncement sous le dessus de Box, examens immobiles exiges, depart de la descente de la capsule (bas a // 30 cm au-dessus de Box : le plan de marche fnt_nav01, 9 cm au-dessus de Box, est trouve ; un banc ou un etal ne l'est pas). constexpr double SunkDepth = 15.0; constexpr int32 SunkScans = 3; constexpr double LiftProbe = 30.0; // S-F5j : examens immobiles exiges pour le joueur (0 ; 0,5 ; 1 ; 1,5 s). constexpr int32 PlayerSunkScans = 4; // S-F5k : distance minimale entre deux personnes relevees, et ecart applique le long du grand axe de la piece. constexpr double LiftSpacing = 50.0; // S-F5p : apres l'entree dans une piece exterieure, balayage a chaque tick pendant FastScanSeconds ; saut de position au-dela de // JumpDistance entre deux balayages = personne posee par le Blueprint (teleportation). constexpr float FastScanSeconds = 3.f; constexpr double JumpDistance = 50.0; // Memes menus que LPDirectControlSubsystem (MenuClasses), dont la boite de scene Scene_C. const TCHAR* const PlayerMenuClasses[] = { TEXT("Journal_C"), TEXT("MainMenu_C"), TEXT("CityMenu_C"), TEXT("StatMenu_C"), TEXT("ContactMenu_C"), TEXT("Inventory_C"), TEXT("CharGen_C"), TEXT("ModManager_C"), TEXT("BodyShapeMenu_C"), TEXT("Scene_C") }; template T* GetObjectProperty(const UObject* Container, FName Name) { const FObjectPropertyBase* Prop = Container ? CastField(Container->GetClass()->FindPropertyByName(Name)) : nullptr; return Prop ? Cast(Prop->GetObjectPropertyValue_InContainer(Container)) : nullptr; } bool ReadBoolProperty(const UObject* Container, const TCHAR* Name) { const FBoolProperty* Prop = Container ? CastField(Container->GetClass()->FindPropertyByName(Name)) : nullptr; return Prop && Prop->GetPropertyValue_InContainer(Container); } // S-F5j : le joueur pilote-t-il (une touche de MoveForward / MoveRight enfoncee) ou un menu, ou la boite de scene, est-il a l'ecran ? bool PlayerBusy(const UGameInstance* GI, UWorld* World) { const APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; const UInputSettings* Settings = UInputSettings::GetInputSettings(); if (!GI || !PC || !PC->PlayerInput || !Settings) { return true; } for (const FInputAxisKeyMapping& Map : Settings->GetAxisMappings()) { if ((Map.AxisName == FName(TEXT("MoveForward")) || Map.AxisName == FName(TEXT("MoveRight"))) && (Map.Key.IsDigital() ? PC->PlayerInput->IsPressed(Map.Key) : !FMath::IsNearlyZero(PC->PlayerInput->GetKeyValue(Map.Key)))) { return true; } } if (const ULPKeyboardSubsystem* Keys = GI->GetSubsystem()) { if (Keys->IsMenuOpen()) { return true; } } for (TObjectIterator It; It; ++It) { const UUserWidget* Widget = *It; if (!Widget || Widget->HasAnyFlags(RF_ClassDefaultObject) || Widget->GetGameInstance() != GI || !Widget->IsInViewport()) { continue; } const ESlateVisibility Visibility = Widget->GetVisibility(); if (Visibility == ESlateVisibility::Collapsed || Visibility == ESlateVisibility::Hidden) { continue; } const FString ClassName = Widget->GetClass()->GetName(); for (const TCHAR* Menu : PlayerMenuClasses) { if (ClassName == Menu) { return true; } } } return false; } // GI.PersonsUsedThisScene contient-il cet acteur (personne prise par la scene en cours) ? bool UsedInScene(const UObject* GI, const AActor* Person) { const FArrayProperty* Prop = GI ? CastField(GI->GetClass()->FindPropertyByName(TEXT("PersonsUsedThisScene"))) : nullptr; const FObjectPropertyBase* Inner = Prop ? CastField(Prop->Inner) : nullptr; if (!Inner) { return false; } FScriptArrayHelper Array(Prop, Prop->ContainerPtrToValuePtr(GI)); for (int32 Index = 0; Index < Array.Num(); ++Index) { if (Inner->GetObjectPropertyValue(Array.GetRawPtr(Index)) == Person) { return true; } } return false; } } void ULPOutdoorRoomSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &ULPOutdoorRoomSubsystem::Tick)); } void ULPOutdoorRoomSubsystem::Deinitialize() { FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); RestoreFloor(); RestoreDecor(); Super::Deinitialize(); } bool ULPOutdoorRoomSubsystem::Tick(float DeltaTime) { const AActor* Room = GetObjectProperty(GetGameInstance(), RoomPropertyName); USceneComponent* Root = IsValid(Room) ? Room->GetRootComponent() : nullptr; USceneComponent* Ceiling = IsValid(Room) ? GetObjectProperty(Room, CeilingPropertyName) : nullptr; // Plafond masque precedemment mais qui n'est plus celui de la piece courante : le rendre. if (HiddenCeiling.IsValid() && HiddenCeiling.Get() != Ceiling) { HiddenCeiling->SetHiddenInGame(false); HiddenCeiling.Reset(); } UBoxComponent* Floor = IsValid(Room) ? GetObjectProperty(Room, FloorPropertyName) : nullptr; if (ThickenedFloor.IsValid() && ThickenedFloor.Get() != Floor) { RestoreFloor(); } if (!Root) { return true; } const bool bOutdoor = Root->GetRelativeScale3D().Z < OutdoorScaleZ; if (Floor) { UpdateFloor(Floor, bOutdoor); } if (bOutdoor) { // S-F5p : nouvelle piece exterieure (echelle differente, ou retour de l'interieur) : balayage rapide, positions oubliees. const FVector Scale = Root->GetRelativeScale3D(); if (!Scale.Equals(OutdoorScale)) { OutdoorScale = Scale; FastScanTime = FastScanSeconds; LastPositions.Reset(); } const bool bFast = FastScanTime > 0.f; FastScanTime = FMath::Max(0.f, FastScanTime - DeltaTime); DecorScanDelay += DeltaTime; if (DecorScanDelay >= DecorScanSeconds) { DecorScanDelay = 0.f; IgnoreOutdoorDecor(Room); LiftSunkPersons(Room, Floor); } else if (bFast) { LiftSunkPersons(Room, Floor); } } else { OutdoorScale = FVector::ZeroVector; FastScanTime = 0.f; LastPositions.Reset(); if (IgnoredDecor.Num() > 0) { RestoreDecor(); } SunkPersons.Reset(); } if (!Ceiling) { return true; } if (bOutdoor && !Ceiling->bHiddenInGame) { Ceiling->SetHiddenInGame(true); HiddenCeiling = Ceiling; UE_LOG(LogLifePlay, Log, TEXT("Piece exterieure (echelle Z %.3f) : plafond WallT masque"), Root->GetRelativeScale3D().Z); } else if (!bOutdoor && HiddenCeiling.Get() == Ceiling) { Ceiling->SetHiddenInGame(false); HiddenCeiling.Reset(); UE_LOG(LogLifePlay, Log, TEXT("Piece interieure (echelle Z %.3f) : plafond WallT rendu visible"), Root->GetRelativeScale3D().Z); } return true; } void ULPOutdoorRoomSubsystem::UpdateFloor(UBoxComponent* Floor, const bool bOutdoor) { if (!bOutdoor) { if (ThickenedFloor.Get() == Floor) { RestoreFloor(); } return; } if (ThickenedFloor.Get() == Floor) { return; } // Box n'a pas d'echelle propre dans Room : son epaisseur monde vaut 2 x demi-hauteur x echelle Z de la racine. const double ScaleZ = Floor->GetComponentScale().Z; if (ScaleZ <= UE_KINDA_SMALL_NUMBER || !FMath::IsNearlyEqual(Floor->GetRelativeScale3D().Z, 1.0)) { return; } const FVector Extent = Floor->GetUnscaledBoxExtent(); const double TargetZ = Extent.Z * IndoorScaleZ / ScaleZ; if (TargetZ <= Extent.Z) { return; } FloorExtent = Extent; FloorLocation = Floor->GetRelativeLocation(); FloorMobility = Floor->Mobility; if (FloorMobility != EComponentMobility::Movable) { // Un composant Static refuse SetRelativeLocation en jeu. Floor->SetMobility(EComponentMobility::Movable); } // Dessus inchange : relatif Z + demi-hauteur reste egal ; seul le dessous descend. Floor->SetBoxExtent(FVector(Extent.X, Extent.Y, TargetZ), true); Floor->SetRelativeLocation(FVector(FloorLocation.X, FloorLocation.Y, FloorLocation.Z + Extent.Z - TargetZ)); ThickenedFloor = Floor; UE_LOG(LogLifePlay, Log, TEXT("Piece exterieure (echelle Z %.3f) : plancher Box epaissi a %.1f cm (demi-hauteur %.1f -> %.1f)"), ScaleZ, 2.0 * TargetZ * ScaleZ, Extent.Z, TargetZ); } void ULPOutdoorRoomSubsystem::RestoreFloor() { if (UBoxComponent* Floor = ThickenedFloor.Get()) { Floor->SetBoxExtent(FloorExtent, true); Floor->SetRelativeLocation(FloorLocation); if (Floor->Mobility != FloorMobility) { Floor->SetMobility(FloorMobility); } UE_LOG(LogLifePlay, Log, TEXT("Plancher Box rendu a sa demi-hauteur d'origine %.1f"), FloorExtent.Z); } ThickenedFloor.Reset(); } void ULPOutdoorRoomSubsystem::IgnoreOutdoorDecor(const AActor* Room) { // Depuis CHG-145, Room::addObject met chaque objet en WorldDynamic / Pawn Block ; la capsule des personnages bloque // WorldDynamic, et la collision simple d'un decor exterieur (un volume, pas le pave) les emprisonne (STG-SF4b-DIAG.md). IgnoredDecor.RemoveAll([](const TWeakObjectPtr& Comp) { return !Comp.IsValid(); }); SolidDecor.RemoveAll([](const TWeakObjectPtr& Comp) { return !Comp.IsValid(); }); TInlineComponentArray Meshes; Room->GetComponents(Meshes); int32 Added = 0; for (UStaticMeshComponent* Mesh : Meshes) { const UStaticMesh* Asset = Mesh ? Mesh->GetStaticMesh() : nullptr; if (!Asset || !Asset->GetName().StartsWith(OutdoorDecorPrefix) || IgnoredDecor.Contains(Mesh) || SolidDecor.Contains(Mesh) || Mesh->GetCollisionResponseToChannel(ECC_Pawn) != ECR_Block) { continue; } // Variante facades : collision complexe (triangles du pave et des facades), pas de volume piege : le decor reste solide. const UBodySetup* Body = Asset->GetBodySetup(); if (Body && Body->CollisionTraceFlag == CTF_UseComplexAsSimple) { SolidDecor.Add(Mesh); UE_LOG(LogLifePlay, Log, TEXT("Piece exterieure : decor %s (%s) en collision complexe, laisse en Pawn Block"), *Mesh->GetName(), *Asset->GetName()); continue; } Mesh->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore); IgnoredDecor.Add(Mesh); ++Added; } if (Added > 0) { UE_LOG(LogLifePlay, Log, TEXT("Piece exterieure : %d decor(s) fnt_ext en Pawn Ignore (%d en tout)"), Added, IgnoredDecor.Num()); } } void ULPOutdoorRoomSubsystem::RestoreDecor() { int32 Restored = 0; for (const TWeakObjectPtr& Comp : IgnoredDecor) { if (UPrimitiveComponent* Prim = Comp.Get()) { Prim->SetCollisionResponseToChannel(ECC_Pawn, ECR_Block); ++Restored; } } if (IgnoredDecor.Num() > 0) { UE_LOG(LogLifePlay, Log, TEXT("Decors fnt_ext rendus en Pawn Block : %d (sur %d, les autres detruits avec la piece)"), Restored, IgnoredDecor.Num()); } IgnoredDecor.Reset(); SolidDecor.Reset(); DecorScanDelay = 0.f; } void ULPOutdoorRoomSubsystem::LiftSunkPersons(const AActor* Room, const UBoxComponent* Floor) { const UGameInstance* GI = GetGameInstance(); UWorld* World = Room ? Room->GetWorld() : nullptr; if (!Floor || !World || ReadBoolProperty(GI, TEXT("SexOn"))) { SunkPersons.Reset(); return; } const AActor* Player = GetObjectProperty(GI, TEXT("PlayerCharacter")); // Dessus de Box en monde (Box sans rotation dans Room ; inchange par S-F4). const double FloorTop = Floor->GetComponentLocation().Z + Floor->GetScaledBoxExtent().Z; TMap, TPair> Seen; for (auto Logged = UnplacedLogged.CreateIterator(); Logged; ++Logged) { if (!Logged->IsValid()) { Logged.RemoveCurrent(); } } for (auto Known = LastPositions.CreateIterator(); Known; ++Known) { if (!Known.Key().IsValid()) { Known.RemoveCurrent(); } } for (TActorIterator It(World); It; ++It) { ACharacter* Person = *It; UCapsuleComponent* Capsule = Person ? Person->GetCapsuleComponent() : nullptr; const bool bPlayer = Person == Player; if (!Capsule || Person->IsHidden() || (!bPlayer && !Person->GetClass()->GetName().StartsWith(TEXT("Person_C"))) || Capsule->GetCollisionEnabled() == ECollisionEnabled::NoCollision || IsValid(GetObjectProperty(Person, TEXT("OccupyingFnt"))) || UsedInScene(GI, Person)) { continue; } const double HalfHeight = Capsule->GetScaledCapsuleHalfHeight(); const FVector Location = Person->GetActorLocation(); const double Bottom = Location.Z - HalfHeight; // S-F5p : personne posee depuis le balayage precedent (premiere vue dans la piece, ou saut de plus de JumpDistance). const FVector* Previous = LastPositions.Find(Person); const bool bJustPlaced = !Previous || FVector::Dist(*Previous, Location) > JumpDistance; LastPositions.Add(Person, Location); if (Bottom >= FloorTop - SunkDepth || Person->GetVelocity().Size2D() > 1.0) { if (Bottom >= FloorTop - SunkDepth) { // S-F5m : episode termine (personne a hauteur), une nouvelle ligne sera permise. UnplacedLogged.Remove(Person); } continue; } // Immobile : meme position (a 1 cm pres) sur SunkScans examens consecutifs. const TPair* Before = SunkPersons.Find(Person); const int32 Scans = (Before && FVector::Dist(Before->Key, Location) < 1.0) ? Before->Value + 1 : 1; // S-F5p : une personne qui vient d'etre posee (immobile, garde de vitesse ci-dessus) est relevee des ce premier examen. if (!bJustPlaced && Scans < (bPlayer ? PlayerSunkScans : SunkScans)) { Seen.Add(Person, TPair(Location, Scans)); continue; } // S-F5j : jamais le joueur qui pilote, ni sous un menu ou la boite de scene (examen repris a zero). if (bPlayer && PlayerBusy(GI, World)) { continue; } // S-F5k : autre personne visible a moins de LiftSpacing en 2D ? Alors essayer d'abord un point ecarte le long du grand axe // de la piece (du cote oppose a elle), puis de l'autre cote, puis sur place. const AActor* Near = nullptr; for (TActorIterator Other(World); Other; ++Other) { const ACharacter* OtherPerson = *Other; const UCapsuleComponent* OtherCapsule = OtherPerson ? OtherPerson->GetCapsuleComponent() : nullptr; if (OtherPerson && OtherPerson != Person && OtherCapsule && !OtherPerson->IsHidden() && OtherCapsule->GetCollisionEnabled() != ECollisionEnabled::NoCollision && FVector::Dist2D(OtherPerson->GetActorLocation(), Location) < LiftSpacing) { Near = OtherPerson; break; } } TArray> Candidates; if (Near) { const FVector Scale = Room->GetRootComponent() ? Room->GetRootComponent()->GetComponentScale() : FVector::OneVector; FVector Axis = (FMath::Abs(Scale.Y) >= FMath::Abs(Scale.X)) ? Room->GetActorRightVector() : Room->GetActorForwardVector(); Axis.Z = 0.0; Axis = Axis.GetSafeNormal(); const double Side = FVector::DotProduct(Location - Near->GetActorLocation(), Axis) >= 0.0 ? 1.0 : -1.0; Candidates.Add(Location + Axis * (Side * LiftSpacing)); Candidates.Add(Location - Axis * (Side * LiftSpacing)); } Candidates.Add(Location); // Descente de la capsule depuis le dessus de Box + LiftProbe : premier sol qu'elle rencontre, avec ses propres reponses. FCollisionQueryParams Query(TEXT("LPLiftSunk"), false, Person); FCollisionResponseParams Responses(Capsule->GetCollisionResponseToChannels()); FHitResult Hit; bool bPlaced = false; FVector Target = Location; // Descente en un point : vrai si la capsule trouve un sol (ou rien : dessus de Box) sans partir en recouvrement. FHitResult InPlace; auto TryPlace = [&](const FVector& Candidate) -> bool { const FVector Start(Candidate.X, Candidate.Y, FloorTop + HalfHeight + LiftProbe); const FVector End(Candidate.X, Candidate.Y, FloorTop + HalfHeight - 2.0); Hit = FHitResult(); double TargetZ = FloorTop + HalfHeight + 2.0; if (World->SweepSingleByChannel(Hit, Start, End, Capsule->GetComponentQuat(), Capsule->GetCollisionObjectType(), Capsule->GetCollisionShape(), Query, Responses)) { if (Hit.bStartPenetrating) { if (Candidate.Equals(Location)) { InPlace = Hit; } return false; } TargetZ = Hit.Location.Z + 1.0; } Target = FVector(Candidate.X, Candidate.Y, TargetZ); return true; }; for (const FVector& Candidate : Candidates) { if (TryPlace(Candidate)) { bPlaced = true; break; } } // S-F5m (STG-COMMUN8-DIAG A) : point d'arrivee colle a un decor solide (facade en collision complexe depuis COMMUN-6) : la // descente part en recouvrement. On essaie alors : S-F5n, le point donne par la normale de depenetration (partie horizontale, // profondeur + 2 cm) ; puis 50 cm le long du petit axe vers le centre de la piece (loin de la facade), 50 cm de chaque cote le // long du grand axe, et 50 cm le long du petit axe de l'autre cote. bool bMoved = false; if (!bPlaced) { TArray> Escapes; FVector Push = InPlace.Normal; Push.Z = 0.0; if (InPlace.bStartPenetrating && Push.SizeSquared() > 0.01) { Escapes.Add(Location + Push.GetSafeNormal() * (static_cast(InPlace.PenetrationDepth) + 2.0)); } const FVector Scale = Room->GetRootComponent() ? Room->GetRootComponent()->GetComponentScale() : FVector::OneVector; const bool bLongIsY = FMath::Abs(Scale.Y) >= FMath::Abs(Scale.X); FVector Long = bLongIsY ? Room->GetActorRightVector() : Room->GetActorForwardVector(); FVector Short = bLongIsY ? Room->GetActorForwardVector() : Room->GetActorRightVector(); Long.Z = 0.0; Short.Z = 0.0; Long = Long.GetSafeNormal(); Short = Short.GetSafeNormal(); const double Inward = FVector::DotProduct(Room->GetActorLocation() - Location, Short) >= 0.0 ? 1.0 : -1.0; Escapes.Add(Location + Short * (Inward * LiftSpacing)); Escapes.Add(Location + Long * LiftSpacing); Escapes.Add(Location - Long * LiftSpacing); Escapes.Add(Location - Short * (Inward * LiftSpacing)); for (const FVector& Candidate : Escapes) { if (TryPlace(Candidate)) { bPlaced = true; bMoved = true; break; } } } if (!bPlaced) { if (!UnplacedLogged.Contains(Person)) { UnplacedLogged.Add(Person); UE_LOG(LogLifePlay, Log, TEXT("Piece exterieure : %s%s enfonce de %.1f cm, aucun point libre (depart dans %s, S-F5m)"), bPlayer ? TEXT("joueur ") : TEXT(""), *Person->GetName(), FloorTop - Bottom, InPlace.GetComponent() ? *InPlace.GetComponent()->GetName() : TEXT("-")); } continue; } UnplacedLogged.Remove(Person); Person->SetActorLocation(Target, false, nullptr, ETeleportType::TeleportPhysics); ++LiftCount; UE_LOG(LogLifePlay, Log, TEXT("Piece exterieure : %s%s releve de %.1f cm (bas de capsule %.1f, dessus Box %.1f, sol %s) ; %d releve(s) en tout"), bPlayer ? TEXT("joueur ") : TEXT(""), *Person->GetName(), Target.Z - Location.Z, Bottom, FloorTop, Hit.GetComponent() ? *Hit.GetComponent()->GetName() : TEXT("Box"), LiftCount); if (bMoved) { UE_LOG(LogLifePlay, Log, TEXT("Piece exterieure : %s ecarte de %.1f cm (depart dans %s, S-F5m)"), *Person->GetName(), FVector::Dist2D(Target, Location), InPlace.GetComponent() ? *InPlace.GetComponent()->GetName() : TEXT("-")); } else if (Near) { UE_LOG(LogLifePlay, Log, TEXT("Piece exterieure : %s ecarte de %.1f cm (voisin %s a %.1f cm, S-F5k)"), *Person->GetName(), FVector::Dist2D(Target, Location), *Near->GetName(), FVector::Dist2D(Near->GetActorLocation(), Location)); } } SunkPersons = MoveTemp(Seen); }