// Marche et invites de proximite. Voir LPDirectControlSubsystem.h. // // Releve UI/Action : le bouton dont AmenityUID est vide appelle GI.TakeActionWithBuilding // avec le texte du bouton et un StreetMapBuilding. Le bouton d'une action chargee appelle // GI.TakeModActionWithBuilding avec la structure ActionDetail, le batiment, Multiple et // Interaction. C'est ce second appel qui porte l'identifiant. #include "LPDirectControlSubsystem.h" #include "LPAnimRedirect.h" #include "Animation/AnimInstance.h" #include "Animation/AnimationAsset.h" #include "Animation/AnimSingleNodeInstance.h" #include "Blueprint/UserWidget.h" #include "Blueprint/WidgetTree.h" #include "Camera/CameraActor.h" #include "Camera/CameraComponent.h" #include "Camera/PlayerCameraManager.h" #include "Components/Button.h" #include "Components/CapsuleComponent.h" #include "Components/RichTextBlock.h" #include "Components/SkeletalMeshComponent.h" #include "Components/StaticMeshComponent.h" #include "Components/TextBlock.h" #include "Engine/GameInstance.h" #include "Engine/GameViewportClient.h" #include "Engine/StaticMesh.h" #include "Engine/World.h" #include "EngineUtils.h" #include "Framework/Application/IInputProcessor.h" #include "Framework/Application/SlateApplication.h" #include "GameFramework/Character.h" #include "GameFramework/GameModeBase.h" #include "GameFramework/CharacterMovementComponent.h" #include "GameFramework/InputSettings.h" #include "GameFramework/PlayerController.h" #include "GameFramework/PlayerInput.h" #include "HAL/PlatformProperties.h" #include "LPKeyboardSubsystem.h" #include "LifePlay.h" #include "Misc/FileHelper.h" #include "UObject/TextProperty.h" #include "Misc/Paths.h" #include "Styling/CoreStyle.h" #include "Widgets/Input/SButton.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SBox.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SOverlay.h" #include "Widgets/Text/STextBlock.h" namespace { const FName ForwardAxis(TEXT("MoveForward")); const FName RightAxis(TEXT("MoveRight")); const FName InteractAction(TEXT("Interact")); const float Reach = 150.f; const TCHAR* MenuClasses[] = { 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") }; struct FPrompt { FString Label; FString ActionId; FString MeshPrefix; bool bPerson = false; }; struct FPromptLive { TWeakObjectPtr Owner; TSharedPtr Root; TSharedPtr Rows; TArray> Labels; double LastFire = 0.0; }; FPromptLive& PromptState() { static FPromptLive State; return State; } TArray& PersonPrompts() { static TArray Prompts; return Prompts; } TArray& MeshPrompts() { static TArray Prompts; return Prompts; } TArray& Shown() { static TArray Prompts; return Prompts; } const FProperty* FindAuthored(const UStruct* Struct, const TCHAR* Name) { if (!Struct) { return nullptr; } for (TFieldIterator It(Struct); It; ++It) { if (It->GetAuthoredName() == Name || It->GetName() == Name) { return *It; } } return nullptr; } FString DirectFieldText(const UStruct* Struct, const void* Data, const TCHAR* Name) { FString Out; if (const FProperty* Prop = FindAuthored(Struct, Name)) { Prop->ExportTextItem_Direct(Out, Prop->ContainerPtrToValuePtr(Data), nullptr, nullptr, PPF_None); } return Out.TrimQuotes(); } void SetString(UObject* Object, const TCHAR* Name, const FString& Value) { if (FStrProperty* Prop = Object ? CastField(Object->GetClass()->FindPropertyByName(Name)) : nullptr) { Prop->SetPropertyValue_InContainer(Object, Value); } } void SetObject(UObject* Object, const TCHAR* Name, UObject* Value) { if (FObjectPropertyBase* Prop = Object ? CastField(Object->GetClass()->FindPropertyByName(Name)) : nullptr) { Prop->SetObjectPropertyValue_InContainer(Object, Value); } } AActor* DirectActor(const UObject* Object, const TCHAR* Name) { const FObjectPropertyBase* Prop = Object ? CastField(Object->GetClass()->FindPropertyByName(Name)) : nullptr; return Prop ? Cast(Prop->GetObjectPropertyValue_InContainer(Object)) : nullptr; } bool KeyDown(const APlayerController* PC, const FKey& Key) { return PC && PC->IsInputKeyDown(Key); } bool ModifiersMatch(const APlayerController* PC, const FInputActionKeyMapping& Map) { const bool bCtrl = KeyDown(PC, EKeys::LeftControl) || KeyDown(PC, EKeys::RightControl); const bool bShift = KeyDown(PC, EKeys::LeftShift) || KeyDown(PC, EKeys::RightShift); const bool bAlt = KeyDown(PC, EKeys::LeftAlt) || KeyDown(PC, EKeys::RightAlt); const bool bCmd = KeyDown(PC, EKeys::LeftCommand) || KeyDown(PC, EKeys::RightCommand); return Map.bCtrl == bCtrl && Map.bShift == bShift && Map.bAlt == bAlt && Map.bCmd == bCmd; } float AxisValue(const APlayerController* PC, FName Axis) { const UInputSettings* Settings = UInputSettings::GetInputSettings(); if (!PC || !PC->PlayerInput || !Settings) { return 0.f; } float Value = 0.f; for (const FInputAxisKeyMapping& Map : Settings->GetAxisMappings()) { if (Map.AxisName == Axis) { const float Down = Map.Key.IsDigital() ? (PC->PlayerInput->IsPressed(Map.Key) ? 1.f : 0.f) : PC->PlayerInput->GetKeyValue(Map.Key); Value += Down * Map.Scale; } } return FMath::Clamp(Value, -1.f, 1.f); } bool InteractPressed(const APlayerController* PC) { const UInputSettings* Settings = UInputSettings::GetInputSettings(); if (!PC || !PC->PlayerInput || !Settings) { return false; } for (const FInputActionKeyMapping& Map : Settings->GetActionMappings()) { if (Map.ActionName == InteractAction && ModifiersMatch(PC, Map) && PC->PlayerInput->WasJustPressed(Map.Key)) { return true; } } return false; } FString ReadEnabledSignature(const UObject* GI) { const UScriptStruct* Struct = nullptr; const FArrayProperty* Prop = GI ? CastField(GI->GetClass()->FindPropertyByName(TEXT("ModEnableds"))) : nullptr; const FStructProperty* Inner = Prop ? CastField(Prop->Inner) : nullptr; Struct = Inner ? Inner->Struct : nullptr; if (!Prop || !Struct) { return FString(); } FString Signature; FScriptArrayHelper Helper(Prop, Prop->ContainerPtrToValuePtr(GI)); for (int32 Index = 0; Index < Helper.Num(); ++Index) { Signature += DirectFieldText(Struct, Helper.GetRawPtr(Index), TEXT("UniqueID")); Signature += TEXT("="); Signature += DirectFieldText(Struct, Helper.GetRawPtr(Index), TEXT("IsEnabled")); Signature += TEXT(";"); } return Signature; } bool ModuleEnabled(const UObject* GI, const FString& Module) { const FArrayProperty* Prop = GI ? CastField(GI->GetClass()->FindPropertyByName(TEXT("ModEnableds"))) : nullptr; const FStructProperty* Inner = Prop ? CastField(Prop->Inner) : nullptr; const UScriptStruct* Struct = Inner ? Inner->Struct : nullptr; if (!Prop || !Struct) { return true; } FScriptArrayHelper Helper(Prop, Prop->ContainerPtrToValuePtr(GI)); if (Helper.Num() == 0) { return true; } for (int32 Index = 0; Index < Helper.Num(); ++Index) { if (DirectFieldText(Struct, Helper.GetRawPtr(Index), TEXT("UniqueID")).Equals(Module, ESearchCase::IgnoreCase)) { return DirectFieldText(Struct, Helper.GetRawPtr(Index), TEXT("IsEnabled")).Equals(TEXT("True"), ESearchCase::IgnoreCase); } } return false; } FString ModuleOf(const FString& File, const FString& Root) { FString Rel = File; FString Base = Root; FPaths::NormalizeFilename(Rel); FPaths::NormalizeFilename(Base); if (!Base.EndsWith(TEXT("/"))) { Base += TEXT("/"); } if (Rel.StartsWith(Base, ESearchCase::IgnoreCase)) { Rel.RightChopInline(Base.Len()); } FString Module, Rest; return Rel.Split(TEXT("/"), &Module, &Rest) ? Module : FString(); } void ReadKeys(const FString& Text, TMap& Out) { TArray Lines; Text.ParseIntoArrayLines(Lines, true); for (const FString& Raw : Lines) { const FString Line = Raw.TrimStartAndEnd(); FString Key, Value; if (Line.StartsWith(TEXT("//")) || !Line.Split(TEXT(":"), &Key, &Value)) { continue; } Out.Add(Key.TrimStartAndEnd().ToUpper(), Value.TrimStartAndEnd()); } } bool WhereHasInteraction(const FString& Where) { TArray Parts; Where.ParseIntoArray(Parts, TEXT(","), true); for (const FString& Part : Parts) { if (Part.TrimStartAndEnd().Equals(TEXT("interaction"), ESearchCase::IgnoreCase)) { return true; } } return false; } void AddUnique(TArray& Dest, const FPrompt& Prompt) { for (const FPrompt& Have : Dest) { if (Have.ActionId.Equals(Prompt.ActionId, ESearchCase::IgnoreCase) && Have.MeshPrefix.Equals(Prompt.MeshPrefix, ESearchCase::IgnoreCase)) { return; } } Dest.Add(Prompt); } const void* FindActionDetail(const UObject* GI, const FString& ActionId, const UScriptStruct*& OutStruct) { OutStruct = nullptr; const FArrayProperty* Prop = GI ? CastField(GI->GetClass()->FindPropertyByName(TEXT("ActionDetail"))) : nullptr; const FStructProperty* Inner = Prop ? CastField(Prop->Inner) : nullptr; if (!Inner || !Inner->Struct) { return nullptr; } OutStruct = Inner->Struct; const TCHAR* IdNames[] = { TEXT("AmenityUID"), TEXT("ActionUID"), TEXT("UID") }; const FProperty* IdField = nullptr; for (const TCHAR* Name : IdNames) { if ((IdField = FindAuthored(OutStruct, Name)) != nullptr) { break; } } if (!IdField) { return nullptr; } FScriptArrayHelper Helper(Prop, Prop->ContainerPtrToValuePtr(GI)); for (int32 Index = 0; Index < Helper.Num(); ++Index) { FString Id; IdField->ExportTextItem_Direct(Id, IdField->ContainerPtrToValuePtr(Helper.GetRawPtr(Index)), nullptr, nullptr, PPF_None); if (Id.TrimQuotes().Equals(ActionId, ESearchCase::IgnoreCase)) { return Helper.GetRawPtr(Index); } } return nullptr; } bool ActionLoaded(const UObject* GI, const FString& ActionId) { const UScriptStruct* Struct = nullptr; if (FindActionDetail(GI, ActionId, Struct)) { return true; } const FArrayProperty* Prop = GI ? CastField(GI->GetClass()->FindPropertyByName(TEXT("ActionDetail"))) : nullptr; const FStructProperty* Inner = Prop ? CastField(Prop->Inner) : nullptr; if (!Inner) { return true; } FScriptArrayHelper Helper(Prop, Prop->ContainerPtrToValuePtr(GI)); return Helper.Num() == 0; } void LogParmsOnce(const UFunction* Function) { static TSet Logged; if (!Function || Logged.Contains(Function->GetFName())) { return; } Logged.Add(Function->GetFName()); for (TFieldIterator It(Function); It && It->HasAnyPropertyFlags(CPF_Parm); ++It) { UE_LOG(LogLifePlay, Display, TEXT("Interaction : %s.%s (%s)"), *Function->GetName(), *It->GetAuthoredName(), *It->GetClass()->GetName()); } } FProperty* FindParm(UFunction* Function, const TCHAR* Name) { for (TFieldIterator It(Function); It && It->HasAnyPropertyFlags(CPF_Parm); ++It) { if (It->GetAuthoredName() == Name || It->GetName() == Name) { return *It; } } return nullptr; } void CopyBuilding(UObject* GI, UFunction* Function, uint8* Params) { FStructProperty* Dest = CastField(FindParm(Function, TEXT("Building"))); const UObject* Street = DirectActor(GI, TEXT("StreetMapStatic")); const FStructProperty* Source = Street ? CastField(Street->GetClass()->FindPropertyByName(TEXT("CurrentPlayerBuilding"))) : nullptr; if (!Dest || !Source || Dest->Struct != Source->Struct) { return; } Dest->CopyCompleteValue(Dest->ContainerPtrToValuePtr(Params), Source->ContainerPtrToValuePtr(Street)); } void CallAction(UObject* GI, const FPrompt& Prompt, AActor* Target) { if (!GI) { return; } SetString(GI, TEXT("CurrentActionUID"), Prompt.ActionId); if (Prompt.bPerson) { SetObject(GI, TEXT("Interacting"), Target); SetObject(GI, TEXT("InteractionTarget"), Target); } UFunction* Function = GI->FindFunction(TEXT("TakeModActionWithBuilding")); const UScriptStruct* DetailStruct = nullptr; const void* Detail = FindActionDetail(GI, Prompt.ActionId, DetailStruct); const FStructProperty* ActionParm = Function ? CastField(FindParm(Function, TEXT("Action"))) : nullptr; if (!Function || !Detail || !ActionParm || ActionParm->Struct != DetailStruct) { Function = GI->FindFunction(TEXT("TakeActionWithBuilding")); Detail = nullptr; } if (!Function) { UE_LOG(LogLifePlay, Warning, TEXT("Interaction : TakeModActionWithBuilding et TakeActionWithBuilding introuvables pour %s"), *Prompt.ActionId); return; } LogParmsOnce(Function); uint8* Params = static_cast(FMemory_Alloca(Function->ParmsSize)); FMemory::Memzero(Params, Function->ParmsSize); for (TFieldIterator It(Function); It && It->HasAnyPropertyFlags(CPF_Parm); ++It) { It->InitializeValue_InContainer(Params); } if (Detail) { ActionParm->CopyCompleteValue(ActionParm->ContainerPtrToValuePtr(Params), Detail); if (FBoolProperty* Multiple = CastField(FindParm(Function, TEXT("Multiple")))) { Multiple->SetPropertyValue_InContainer(Params, false); } if (FBoolProperty* Interaction = CastField(FindParm(Function, TEXT("Interaction")))) { Interaction->SetPropertyValue_InContainer(Params, Prompt.bPerson); } } else if (FTextProperty* ActionText = CastField(FindParm(Function, TEXT("Action")))) { ActionText->SetPropertyValue_InContainer(Params, FText::FromString(Prompt.Label)); } CopyBuilding(GI, Function, Params); UE_LOG(LogLifePlay, Display, TEXT("Interaction : %s « %s » (%s) vers %s"), *Function->GetName(), *Prompt.Label, *Prompt.ActionId, *GetNameSafe(Target)); GI->ProcessEvent(Function, Params); for (TFieldIterator It(Function); It && It->HasAnyPropertyFlags(CPF_Parm); ++It) { It->DestroyValue_InContainer(Params); } } // Scene en cours : la boite de dialogue (Scene_C) est testee par MenuReason ; ici, la scene de sexe. // GI.CurrentScene n'est jamais remis a vide (STG KO 3) : il ne sert plus de temoin. bool ScenePlaying(const UObject* GI) { const FBoolProperty* Prop = GI ? CastField(GI->GetClass()->FindPropertyByName(TEXT("SexOn"))) : nullptr; return Prop && Prop->GetPropertyValue_InContainer(GI); } // Clip de marche (STG-WALK) : copie cuite /Game/Heavy/Daz//Anim, sinon le clip converti // (LPAnimRedirect::ClipPath : editeur, mods ; /Game/Quaternius n'est pas cuit). Garde en memoire ; un seul // avertissement par clip si aucun des deux ne se charge. UAnimationAsset* WalkClip(bool bMale) { static TWeakObjectPtr Cache[2]; static bool bMissing[2] = { false, false }; const int32 Slot = bMale ? 1 : 0; if (UAnimationAsset* Known = Cache[Slot].Get()) { return Known; } if (bMissing[Slot]) { return nullptr; } const TCHAR* Name = bMale ? TEXT("M_Walk_Loop") : TEXT("B_Walk_Loop"); const FString Cooked = FString::Printf(TEXT("/Game/Heavy/Daz/%s/Anim/%s.%s"), bMale ? TEXT("Michael") : TEXT("Bridget"), Name, Name); const FString Converted = LPAnimRedirect::ClipPath(Name); UAnimationAsset* Clip = LoadObject(nullptr, *Cooked, nullptr, LOAD_NoWarn | LOAD_Quiet); if (!Clip) { Clip = LoadObject(nullptr, *Converted, nullptr, LOAD_NoWarn | LOAD_Quiet); } if (!Clip) { bMissing[Slot] = true; UE_LOG(LogLifePlay, Warning, TEXT("Marche : clip %s introuvable (%s, %s), marche sans animation"), Name, *Cooked, *Converted); return nullptr; } Cache[Slot] = Clip; return Clip; } // Animation jouee en mode AnimationSingleNode. PlayAnimation ne met pas a jour AnimationData.AnimToPlay // (UE 5.8, SkeletalMeshComponent.cpp SetAnimation) : seule l'instance single-node la connait. UAnimationAsset* SingleNodeAnim(const USkeletalMeshComponent* Mesh) { const UAnimSingleNodeInstance* Single = Mesh ? Mesh->GetSingleNodeInstance() : nullptr; return Single ? Single->GetAnimationAsset() : nullptr; } // C1 (CAM-COMMUN3-DIAG § 7) : Room.CamC1On et vue = le joueur. bool FirstPersonView(const UObject* GI, const APawn* Pawn, const APlayerController* PC) { const FObjectPropertyBase* RoomProp = GI ? CastField(GI->GetClass()->FindPropertyByName(TEXT("Room"))) : nullptr; const UObject* Room = RoomProp ? RoomProp->GetObjectPropertyValue_InContainer(GI) : nullptr; const FBoolProperty* C1 = IsValid(Room) ? CastField(Room->GetClass()->FindPropertyByName(TEXT("CamC1On"))) : nullptr; return C1 && C1->GetPropertyValue_InContainer(Room) && PC && PC->GetViewTarget() == Pawn; } // Le joueur occupe un meuble (assis, action) : son lacet est celui de la place. bool OnSeat(const AActor* Player) { const FObjectPropertyBase* Fnt = Player ? CastField(Player->GetClass()->FindPropertyByName(TEXT("OccupyingFnt"))) : nullptr; return Fnt && IsValid(Fnt->GetObjectPropertyValue_InContainer(Player)); } bool CapsuleInside(const ACharacter* Player, const UPrimitiveComponent* Comp) { const UCapsuleComponent* Cap = Player ? Player->GetCapsuleComponent() : nullptr; return Cap && Comp && Comp->OverlapComponent(Cap->GetComponentLocation(), Cap->GetComponentQuat(), Cap->GetCollisionShape(-1.f)); } // KO3c : Pawn en Ignore sur chaque composant de Room que la capsule chevauche deja (place d'un meuble posee dans un ou plusieurs // volumes, CHG-145). Jamais Room.Box (plancher), jamais un composant deja en Ignore ou sans collision, jamais un autre acteur. // Chacun est rendu en Block par UpdateEscape a la sortie de son volume. int32 EscapeOverlaps(const ACharacter* Player, AActor* Room, TArray>& Escape, const TCHAR* Why) { if (!Player || !IsValid(Room)) { return 0; } const FObjectPropertyBase* FloorProp = CastField(Room->GetClass()->FindPropertyByName(TEXT("Box"))); const UObject* Floor = FloorProp ? FloorProp->GetObjectPropertyValue_InContainer(Room) : nullptr; TInlineComponentArray Components; Room->GetComponents(Components); int32 Added = 0; for (UPrimitiveComponent* Comp : Components) { if (!Comp || Comp == Floor || Comp->GetCollisionEnabled() == ECollisionEnabled::NoCollision || Comp->GetCollisionResponseToChannel(ECC_Pawn) != ECR_Block || Escape.Contains(Comp) || !CapsuleInside(Player, Comp)) { continue; } Comp->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore); Escape.Add(Comp); ++Added; UE_LOG(LogLifePlay, Display, TEXT("Marche : joueur dans le volume de %s (%s), Pawn en Ignore jusqu'a la sortie"), *Comp->GetName(), Why); } return Added; } // Premier pas du joueur assis : Person.unOccupyFnt libere la place du meuble (Room.OccupiedFnts) et vide OccupyingFnt. // Meuble d'une piece quittee (composant detruit) : unOccupyFnt ne vide pas la reference, on la vide ici. // Capsule posee dans un ou plusieurs volumes (etagere, comptoir et bureau voisin) : EscapeOverlaps. void LeaveSeat(ACharacter* Player, AActor* Room, TArray>& Escape) { FObjectPropertyBase* Fnt = Player ? CastField(Player->GetClass()->FindPropertyByName(TEXT("OccupyingFnt"))) : nullptr; UObject* Seat = Fnt ? Fnt->GetObjectPropertyValue_InContainer(Player) : nullptr; if (!Seat) { return; } if (!IsValid(Seat)) { Fnt->SetObjectPropertyValue_InContainer(Player, nullptr); UE_LOG(LogLifePlay, Display, TEXT("Marche : meuble d'une piece quittee (%s), reference videe"), *Seat->GetName()); return; } UFunction* Function = Player->FindFunction(TEXT("unOccupyFnt")); if (!Function || Function->ParmsSize != 0) { static bool bWarned = false; if (!bWarned) { bWarned = true; UE_LOG(LogLifePlay, Warning, TEXT("Marche : Person.unOccupyFnt introuvable ou signature inattendue, meuble non libere")); } return; } EscapeOverlaps(Player, Room, Escape, TEXT("meuble quitte")); UE_LOG(LogLifePlay, Display, TEXT("Marche : le joueur quitte %s (unOccupyFnt)"), *Seat->GetName()); Player->ProcessEvent(Function, nullptr); if (Fnt->GetObjectPropertyValue_InContainer(Player) == Seat) { Fnt->SetObjectPropertyValue_InContainer(Player, nullptr); UE_LOG(LogLifePlay, Warning, TEXT("Marche : OccupyingFnt toujours %s apres unOccupyFnt, reference videe"), *Seat->GetName()); } } // Rend Pawn Block a chaque composant de la liste des que la capsule n'est plus dans son volume, ou s'il n'appartient plus a la // piece courante (changement de piece), ou sans joueur (Deinitialize). Un composant detruit avec la piece est oublie. void UpdateEscape(const ACharacter* Player, const AActor* Room, TArray>& Escape) { for (int32 Index = Escape.Num() - 1; Index >= 0; --Index) { UPrimitiveComponent* Prim = Escape[Index].Get(); if (!Prim) { Escape.RemoveAt(Index); continue; } if (Player && Room && Prim->GetOwner() == Room && CapsuleInside(Player, Prim)) { continue; } Prim->SetCollisionResponseToChannel(ECC_Pawn, ECR_Block); Escape.RemoveAt(Index); UE_LOG(LogLifePlay, Display, TEXT("Marche : joueur sorti du volume de %s, Pawn rendu en Block"), *Prim->GetName()); } } // C6-b (STG-COMMUN6-DIAG, fenetre A) : apres une scene de sexe, Scene_C peut rester dans le viewport, Visible, sans rien montrer. // Le widget montre quelque chose s'il a un bouton, un texte non vide ou un widget imbrique qui en montre, visible jusqu'a la racine // de son arbre (ni Collapsed ni Hidden, opacite de rendu > 0). Arbre illisible : compte comme montre (prudence). bool ShownUpToRoot(const UWidget* Child) { for (const UWidget* Node = Child; Node; Node = Node->GetParent()) { if (!Node->IsVisible() || Node->GetRenderOpacity() <= 0.01f) { return false; } } return true; } bool ShowsContent(const UUserWidget* Widget, int32 Depth = 0) { if (!Widget || !Widget->WidgetTree || Depth > 8) { return true; } if (Widget->GetRenderOpacity() <= 0.01f) { return false; } bool bShown = false; Widget->WidgetTree->ForEachWidget([&bShown, Depth](UWidget* Child) { if (bShown || !Child) { return; } const UTextBlock* Text = Cast(Child); const URichTextBlock* Rich = Cast(Child); const UUserWidget* Nested = Cast(Child); const bool bContent = Child->IsA() || (Text && !Text->GetText().IsEmpty()) || (Rich && !Rich->GetText().IsEmpty()) || (Nested && Nested->IsVisible() && ShowsContent(Nested, Depth + 1)); bShown = bContent && ShownUpToRoot(Child); }); return bShown; } // Vrai pendant qu'une Scene_C sans contenu visible est ignoree par MenuReason (journalise par WitnessMoveKeys). bool& EmptySceneSeen() { static bool bSeen = false; return bSeen; } // Cause du blocage de la marche par un menu (M1e) : "clavier" (panneau Clavier), classe du menu affiche, ou "focus " // (saisie de texte) ; vide si aucun menu ne bloque. FString MenuReason(const UGameInstance* GI) { EmptySceneSeen() = false; if (!GI) { return FString(); } if (const ULPKeyboardSubsystem* Keys = GI->GetSubsystem()) { if (Keys->IsMenuOpen()) { return TEXT("clavier"); } } for (TObjectIterator It; It; ++It) { 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 : MenuClasses) { if (ClassName == Menu) { // C6-b : boite de scene sans rien a l'ecran, la marche n'est pas coupee ; une vraie boite (D10) la coupe toujours. if (ClassName == TEXT("Scene_C") && !ShowsContent(Widget)) { EmptySceneSeen() = true; break; } return ClassName; } } } if (!FSlateApplication::IsInitialized()) { return FString(); } TSharedPtr Focused = FSlateApplication::Get().GetKeyboardFocusedWidget(); if (Focused.IsValid() && Focused->GetTypeAsString().Contains(TEXT("Editable"))) { return TEXT("focus ") + Focused->GetTypeAsString(); } return FString(); } // M1e (STG-COMMUN6-DIAG) : touches de marche (axes MoveForward et MoveRight) vues par Slate avant tout widget, pour dire ou une // touche se perd (Slate, PlayerInput, garde de la marche). Le temoin ne consomme jamais l'evenement. bool IsMoveKey(const FKey& Key) { const UInputSettings* Settings = UInputSettings::GetInputSettings(); if (!Settings) { return false; } for (const FInputAxisKeyMapping& Map : Settings->GetAxisMappings()) { if ((Map.AxisName == ForwardAxis || Map.AxisName == RightAxis) && Map.Key == Key) { return true; } } return false; } TArray& SlateMoveKeys() { static TArray Keys; return Keys; } class FMoveKeyWitness : public IInputProcessor { public: virtual void Tick(const float, FSlateApplication&, TSharedRef) override {} virtual bool HandleKeyDownEvent(FSlateApplication&, const FKeyEvent& Event) override { if (IsMoveKey(Event.GetKey())) { SlateMoveKeys().AddUnique(Event.GetKey()); } return false; } virtual bool HandleKeyUpEvent(FSlateApplication&, const FKeyEvent& Event) override { SlateMoveKeys().Remove(Event.GetKey()); return false; } }; float FlatDistance(const FVector& A, const FVector& B) { return FVector::Dist2D(A, B); } FString MeshName(const UStaticMeshComponent* Mesh) { const UStaticMesh* Asset = Mesh ? Mesh->GetStaticMesh() : nullptr; return Asset ? Asset->GetName() : FString(); } void CollectPrompts(const UObject* GI, const FString& Mesh, bool bPerson, TArray& Out) { if (bPerson) { for (const FPrompt& Prompt : PersonPrompts()) { if (ActionLoaded(GI, Prompt.ActionId)) { Out.Add(Prompt); } } return; } int32 Best = 0; for (const FPrompt& Prompt : MeshPrompts()) { if (Mesh.StartsWith(Prompt.MeshPrefix, ESearchCase::IgnoreCase)) { Best = FMath::Max(Best, Prompt.MeshPrefix.Len()); } } for (const FPrompt& Prompt : MeshPrompts()) { if (Prompt.MeshPrefix.Len() == Best && Mesh.StartsWith(Prompt.MeshPrefix, ESearchCase::IgnoreCase) && ActionLoaded(GI, Prompt.ActionId)) { Out.Add(Prompt); } } } void PaintSelection(int32 Selected) { for (int32 Index = 0; Index < PromptState().Labels.Num(); ++Index) { if (PromptState().Labels[Index].IsValid()) { const bool bOn = Index == Selected; PromptState().Labels[Index]->SetColorAndOpacity(FSlateColor(bOn ? FLinearColor(1.f, 0.9f, 0.4f) : FLinearColor::White)); } } } } void ULPDirectControlSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &ULPDirectControlSubsystem::Tick), 0.f); if (FSlateApplication::IsInitialized()) { MoveWitness = MakeShared(); FSlateApplication::Get().RegisterInputPreProcessor(MoveWitness); } } void ULPDirectControlSubsystem::Deinitialize() { UpdateEscape(nullptr, nullptr, EscapeVolumes); HidePrompts(); RestoreAnim(); if (FollowView) { FollowView->Destroy(); FollowView = nullptr; } if (PromptState().Owner.Get() == this) { PromptState().Owner = nullptr; } if (MoveWitness.IsValid() && FSlateApplication::IsInitialized()) { FSlateApplication::Get().UnregisterInputPreProcessor(MoveWitness); } MoveWitness.Reset(); SlateMoveKeys().Reset(); FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); Super::Deinitialize(); } bool ULPDirectControlSubsystem::Tick(float DeltaTime) { MenuDelay += DeltaTime; if (MenuDelay >= 0.25f) { MenuDelay = 0.f; MenuWhy = MenuReason(GetGameInstance()); bMenuBlocks = !MenuWhy.IsEmpty(); } WitnessMoveKeys(DeltaTime); WatchSeatRelease(DeltaTime); FitPersonBounds(DeltaTime); IndexDelay += DeltaTime; if (IndexDelay >= 1.f) { IndexDelay = 0.f; RefreshIndex(); } TargetDelay += DeltaTime; if (TargetDelay >= 0.5f) { TargetDelay = 0.f; RefreshTarget(); } if (bMenuBlocks || ScenePlaying(GetGameInstance())) { // Marche coupee en plein pas (scene, menu, sexe) : rendre l'Anim BP d'origine (sans effet si rien n'est capture). RestoreAnim(); if (!Shown().IsEmpty() || bPromptOpen) { Shown().Reset(); CurrentTarget = nullptr; ShownSignature.Reset(); bPromptOpen = false; HidePrompts(); } } else { Walk(DeltaTime); PollInteract(); } return true; } // M1e (STG-COMMUN6-DIAG) : une ligne a chaque debut et fin de blocage par un menu ; tant qu'une touche de marche est tenue (vue par // Slate), une ligne par seconde au plus : la touche est-elle dans le PlayerInput, la marche est-elle coupee et pourquoi, focus, etat // d'entree du controleur et du viewport. void ULPDirectControlSubsystem::WitnessMoveKeys(float DeltaTime) { UGameInstance* GI = GetGameInstance(); if (MenuWhy != LoggedMenuWhy) { if (MenuWhy.IsEmpty()) { UE_LOG(LogLifePlay, Display, TEXT("M1e : marche rendue (fin du blocage par %s)"), *LoggedMenuWhy); } else { UE_LOG(LogLifePlay, Display, TEXT("M1e : marche coupee par le menu %s"), *MenuWhy); } LoggedMenuWhy = MenuWhy; } if (EmptySceneSeen() != bLoggedEmptyScene) { bLoggedEmptyScene = EmptySceneSeen(); UE_LOG(LogLifePlay, Display, TEXT("C6-b : %s"), bLoggedEmptyScene ? TEXT("boite Scene_C dans le viewport sans contenu visible, la marche n'est pas coupee") : TEXT("plus de boite Scene_C vide dans le viewport")); } // Fenetre du jeu quittee : le relachement des touches peut ne pas arriver, le temoin repart a vide. if (FSlateApplication::IsInitialized() && !FSlateApplication::Get().IsActive()) { SlateMoveKeys().Reset(); } MoveLogDelay = FMath::Max(0.f, MoveLogDelay - DeltaTime); if (SlateMoveKeys().IsEmpty() || MoveLogDelay > 0.f) { return; } MoveLogDelay = 1.f; UWorld* World = GetWorld(); const APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; const FKey Key = SlateMoveKeys().Last(); const bool bInput = PC && PC->PlayerInput && PC->PlayerInput->IsPressed(Key); const FString Why = !MenuWhy.IsEmpty() ? FString::Printf(TEXT("coupee (menu %s)"), *MenuWhy) : ScenePlaying(GI) ? FString(TEXT("coupee (SexOn)")) : FString(TEXT("ouverte")); const TSharedPtr Focused = FSlateApplication::IsInitialized() ? FSlateApplication::Get().GetKeyboardFocusedWidget() : nullptr; const FString FocusName = Focused.IsValid() ? Focused->GetTypeAsString() : FString(TEXT("aucun")); UGameViewportClient* Viewport = GI ? GI->GetGameViewportClient() : nullptr; // IgnoreInput() n'est pas const UE_LOG(LogLifePlay, Display, TEXT("M1e : touche %s vue par Slate ; PlayerInput=%d ; marche %s ; focus %s ; entree PC=%d move ignore=%d viewport ignore=%d ; vue %s"), *Key.ToString(), bInput ? 1 : 0, *Why, *FocusName, PC && PC->InputEnabled() ? 1 : 0, PC && PC->IsMoveInputIgnored() ? 1 : 0, Viewport && Viewport->IgnoreInput() ? 1 : 0, PC ? *GetNameSafe(PC->GetViewTarget()) : TEXT("-")); } // C7-a (STG-COMMUN7-DIAG A) : un Blueprint libere la place du joueur sans le faire marcher (debut de scene : unOccupyFnt) et le laisse // debout dans le meuble ; un PNJ peut alors recevoir la meme place. Apres 0,5 s, si la place n'a pas ete reprise, sans sexe ni marche, // et si la capsule est encore dans le meuble quitte, le joueur est pose a 80 cm : devant (lacet de la place), a droite, a gauche, puis // derriere ; premier point au sol (a 30 cm pres de sa hauteur), sans meuble ni personne a moins de 50 cm. Sinon il reste, une ligne au // journal. La place quittee en marchant (LeaveSeat) n'est pas surveillee. void ULPDirectControlSubsystem::WatchSeatRelease(float DeltaTime) { constexpr float SeatReleaseDelay = 0.5f; constexpr double StepOut = 80.0; constexpr double PersonClearance = 50.0; constexpr double MaxHeightChange = 30.0; UGameInstance* GI = GetGameInstance(); ACharacter* Player = Cast(DirectActor(GI, TEXT("PlayerCharacter"))); const FObjectPropertyBase* Fnt = Player ? CastField(Player->GetClass()->FindPropertyByName(TEXT("OccupyingFnt"))) : nullptr; UObject* Seat = Fnt ? Fnt->GetObjectPropertyValue_InContainer(Player) : nullptr; const bool bSeated = IsValid(Seat); const bool bWasSeated = LastSeat.IsValid(); UPrimitiveComponent* Released = bWasSeated && !bSeated ? Cast(LastSeat.Get()) : nullptr; LastSeat = bSeated ? Seat : nullptr; if (!Player || bSeated || ScenePlaying(GI)) { // Reassis (meme meuble ou autre), sans joueur, ou scene de sexe : rien a faire. PendingSeat.Reset(); PendingSeatTime = -1.f; return; } if (Released) { PendingSeat = Released; PendingSeatTime = 0.f; return; } if (PendingSeatTime < 0.f) { return; } PendingSeatTime += DeltaTime; if (PendingSeatTime < SeatReleaseDelay) { return; } PendingSeatTime = -1.f; UPrimitiveComponent* Furniture = PendingSeat.Get(); PendingSeat.Reset(); UWorld* World = GetWorld(); const APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; AActor* Room = DirectActor(GI, TEXT("Room")); UCapsuleComponent* Capsule = Player->GetCapsuleComponent(); if (!World || !Furniture || !Capsule || !IsValid(Room) || Furniture->GetOwner() != Room || !CapsuleInside(Player, Furniture)) { // Piece changee, meuble detruit, ou joueur deja sorti du meuble. return; } if (!FMath::IsNearlyZero(AxisValue(PC, ForwardAxis)) || !FMath::IsNearlyZero(AxisValue(PC, RightAxis)) || Player->GetVelocity().Size2D() > 1.0) { // Le joueur marche : LeaveSeat et KO3c s'en chargent. return; } const FVector Location = Player->GetActorLocation(); const double HalfHeight = Capsule->GetScaledCapsuleHalfHeight(); const FRotator Yaw(0.f, Player->GetActorRotation().Yaw, 0.f); const FVector Forward = FRotationMatrix(Yaw).GetUnitAxis(EAxis::X); const FVector Side = FRotationMatrix(Yaw).GetUnitAxis(EAxis::Y); const FVector Offsets[] = { Forward * StepOut, Side * StepOut, -Side * StepOut, -Forward * StepOut }; const TCHAR* Names[] = { TEXT("devant"), TEXT("a droite"), TEXT("a gauche"), TEXT("derriere") }; FCollisionQueryParams Query(SCENE_QUERY_STAT(LPSeatRelease), false, Player); const FCollisionResponseParams Responses(Capsule->GetCollisionResponseToChannels()); const FCollisionShape Shape = Capsule->GetCollisionShape(); const FCollisionShape Inner = Capsule->GetCollisionShape(-2.f); for (int32 Index = 0; Index < UE_ARRAY_COUNT(Offsets); ++Index) { const FVector Candidate = Location + Offsets[Index]; FHitResult Hit; const FVector Start(Candidate.X, Candidate.Y, Location.Z + MaxHeightChange); const FVector End(Candidate.X, Candidate.Y, Location.Z - HalfHeight); if (!World->SweepSingleByChannel(Hit, Start, End, Capsule->GetComponentQuat(), Capsule->GetCollisionObjectType(), Shape, Query, Responses) || Hit.bStartPenetrating || FMath::Abs(Hit.Location.Z - Location.Z) > MaxHeightChange) { continue; } const FVector Target(Candidate.X, Candidate.Y, Hit.Location.Z + 1.0); if (World->OverlapBlockingTestByChannel(Target, Capsule->GetComponentQuat(), Capsule->GetCollisionObjectType(), Inner, Query, Responses)) { continue; } bool bCrowded = false; for (TActorIterator It(World); It && !bCrowded; ++It) { const ACharacter* Other = *It; const UCapsuleComponent* OtherCapsule = Other ? Other->GetCapsuleComponent() : nullptr; bCrowded = Other && Other != Player && OtherCapsule && !Other->IsHidden() && FVector::Dist2D(Other->GetActorLocation(), Target) < PersonClearance + 2.0 * Capsule->GetScaledCapsuleRadius(); } if (bCrowded) { continue; } Player->SetActorLocation(Target, false, nullptr, ETeleportType::TeleportPhysics); UE_LOG(LogLifePlay, Display, TEXT("C7-a : place %s liberee sans marche, joueur pose %s a %.0f cm du siege"), *Furniture->GetName(), Names[Index], FVector::Dist2D(Target, Location)); return; } UE_LOG(LogLifePlay, Display, TEXT("C7-a : place %s liberee sans marche, aucun point libre a %.0f cm, joueur laisse en place"), *Furniture->GetName(), StepOut); } // B2q-a (STG-B2q-DIAG) : sans asset physique, les bornes de rendu du corps d'une Person sont celles de la pose debout autour de la racine ; // assis ou couche, le bassin en sort et le corps (vetements et cheveux compris : bUseAttachParentBound) est elimine selon l'angle. Toutes // les 0,5 s, pour chaque Person visible : facteur S par axe = (ecart bassin - centre des bornes + demi-etendue) / demi-etendue, le plus // grand des trois, au-dela d'un ecart tolere de 0,3 demi-etendue, borne a [echelle d'origine du corps, 25] (B2q-b1 : l'origine vaut 3 ; // au cafe, la racine des poses de meuble est a 3 m du bassin, d'ou un besoin de l'ordre de 23) ; applique seulement s'il change de plus de 10 %. Bornes de rendu seulement (ni collision ni trace). // T-cull : une ligne de mesure a chaque nouveau meuble occupe ; une alerte (1/s au plus) si le bassin est a l'ecran a moins de 15 m et // que le corps n'a pas ete rendu depuis 0,2 s. void ULPDirectControlSubsystem::FitPersonBounds(float DeltaTime) { constexpr float ScanInterval = 0.5f; constexpr float AlertInterval = 1.f; constexpr float MaxScale = 25.f; constexpr float ScaleChange = 0.1f; constexpr double MinExtent = 10.0; // Ecart tolere (fraction de la demi-etendue) : debout, le bassin n'est jamais exactement au centre des bornes ; S reste 1. constexpr double Tolerance = 0.3; constexpr double AlertRange = 1500.0; CullAlertDelay = FMath::Max(0.f, CullAlertDelay - DeltaTime); BoundsDelay += DeltaTime; if (BoundsDelay < ScanInterval) { return; } BoundsDelay = 0.f; UWorld* World = GetWorld(); if (!World) { return; } const APlayerController* PC = World->GetFirstPlayerController(); const FVector CameraLocation = PC && PC->PlayerCameraManager ? PC->PlayerCameraManager->GetCameraLocation() : FVector::ZeroVector; int32 ViewX = 0; int32 ViewY = 0; if (PC) { PC->GetViewportSize(ViewX, ViewY); } for (auto It = MeasuredSeats.CreateIterator(); It; ++It) { if (!It.Key().IsValid()) { It.RemoveCurrent(); } } for (auto It = OriginalBoundsScale.CreateIterator(); It; ++It) { if (!It.Key().IsValid()) { It.RemoveCurrent(); } } for (auto It = MeshZScans.CreateIterator(); It; ++It) { if (!It.Key().IsValid()) { It.RemoveCurrent(); } } // M-Z : jamais en scene de sexe ni pendant une scene (boite Scene_C comptee comme menu par MenuReason). const bool bSceneRunning = ScenePlaying(GetGameInstance()) || MenuWhy == TEXT("Scene_C"); for (TActorIterator It(World); It; ++It) { ACharacter* Person = *It; USkeletalMeshComponent* Body = Person ? Person->GetMesh() : nullptr; if (!Body || Person->IsHidden() || !Body->IsVisible() || Body->GetNumBones() == 0 || !Person->GetClass()->GetName().StartsWith(TEXT("Person_C"))) { continue; } FName PelvisBone(TEXT("hip")); if (Body->GetBoneIndex(PelvisBone) == INDEX_NONE) { PelvisBone = FName(TEXT("pelvis")); } if (Body->GetBoneIndex(PelvisBone) == INDEX_NONE) { PelvisBone = Body->GetBoneName(0); } const FVector Pelvis = Body->GetBoneLocation(PelvisBone); const float Current = FMath::Max(Body->BoundsScale, KINDA_SMALL_NUMBER); const FVector Center = Body->Bounds.Origin; const FVector Extent = Body->Bounds.BoxExtent / Current; if (Extent.GetMax() < 1.0) { // Bornes pas encore calculees (personne qui vient d'apparaitre) : au balayage suivant. continue; } const FVector Offset = (Pelvis - Center).GetAbs(); double Needed = 1.0; for (int32 Axis = 0; Axis < 3; ++Axis) { const double Half = FMath::Max(Extent[Axis], MinExtent); Needed = FMath::Max(Needed, (FMath::Max(0.0, Offset[Axis] - Tolerance * Half) + Half) / Half); } // B2q-b1 : plancher = echelle d'origine du corps (relevee avant toute retouche), jamais moins de 1 ni plus que le plafond. const float* Known = OriginalBoundsScale.Find(Body); const float Floor = Known ? *Known : OriginalBoundsScale.Add(Body, FMath::Clamp(Body->BoundsScale, 1.f, MaxScale)); const float Scale = FMath::Clamp(static_cast(Needed), Floor, MaxScale); const bool bInside = Body->Bounds.GetBox().IsInside(Pelvis); if (FMath::Abs(Scale - Current) > ScaleChange * Current) { Body->InvalidateCachedBounds(); Body->SetBoundsScale(Scale); // Vetements, cheveux, accessoires (Person::NewProxy) : ils reprennent les bornes du corps. TArray Children; Body->GetChildrenComponents(true, Children); for (USceneComponent* Child : Children) { UPrimitiveComponent* Prim = Cast(Child); if (Prim && Prim->bUseAttachParentBound) { Prim->UpdateBounds(); Prim->MarkRenderTransformDirty(); } } UE_LOG(LogLifePlay, Display, TEXT("B2q-a : %s BoundsScale %.2f -> %.2f (bassin a %.0f cm du centre des bornes, dans=%d)"), *Person->GetName(), Current, Scale, FVector::Dist(Pelvis, Center), bInside ? 1 : 0); } // T-cull, mesure : a chaque nouveau meuble occupe (assis, couche, action). const FObjectPropertyBase* Fnt = CastField(Person->GetClass()->FindPropertyByName(TEXT("OccupyingFnt"))); UObject* Seat = Fnt ? Fnt->GetObjectPropertyValue_InContainer(Person) : nullptr; if (IsValid(Seat) && MeasuredSeats.FindRef(Person).Get() != Seat) { MeasuredSeats.Add(Person, Seat); // B2q-b1 : os racine (os 0) : sa distance au bassin et au composant dit si la pose de meuble deplace la racine. const FVector Root = Body->GetBoneLocation(Body->GetBoneName(0)); UE_LOG(LogLifePlay, Display, TEXT("T-cull mesure %s meuble %s : bassin a %.0f cm du centre des bornes (dans=%d) ; bornes %.0f x %.0f x %.0f ; BoundsScale %.2f ; physAsset=%d ; racine %s a %.0f cm du bassin, a %.0f cm du composant"), *Person->GetName(), *Seat->GetName(), FVector::Dist(Pelvis, Center), bInside ? 1 : 0, 2.0 * Extent.X, 2.0 * Extent.Y, 2.0 * Extent.Z, Body->BoundsScale, Body->GetPhysicsAsset() ? 1 : 0, *Body->GetBoneName(0).ToString(), FVector::Dist(Root, Pelvis), FVector::Dist(Root, Body->GetComponentLocation())); } else if (!IsValid(Seat)) { MeasuredSeats.Remove(Person); } // M-Z (STG-COMMUN9-DIAG) : une place feminine releve le maillage (-80,4 au lieu de -90) et rien ne le redescend au lever ; la // Person debout flotte. Debout (aucun meuble), hors sexe et hors scene, capsule en collision, maillage hors de la hauteur relative // du gabarit de sa classe (CDO) depuis 2 balayages (1 s) : on remet la hauteur du gabarit (X et Y inchanges). const ACharacter* Template = Person->GetClass()->GetDefaultObject(); const USkeletalMeshComponent* TemplateBody = Template ? Template->GetMesh() : nullptr; const UCapsuleComponent* Capsule = Person->GetCapsuleComponent(); const double TemplateZ = TemplateBody ? TemplateBody->GetRelativeLocation().Z : 0.0; const FVector Relative = Body->GetRelativeLocation(); if (TemplateBody && !bSceneRunning && !IsValid(Seat) && Capsule && Capsule->GetCollisionEnabled() != ECollisionEnabled::NoCollision && FMath::Abs(Relative.Z - TemplateZ) > 1.0) { int32& Scans = MeshZScans.FindOrAdd(Person); if (++Scans >= 2) { Body->SetRelativeLocation(FVector(Relative.X, Relative.Y, TemplateZ)); MeshZScans.Remove(Person); UE_LOG(LogLifePlay, Display, TEXT("M-Z : %s maillage relatif Z %.1f -> %.1f (gabarit %s), debout sans meuble"), *Person->GetName(), Relative.Z, TemplateZ, *Person->GetClass()->GetName()); } } else { MeshZScans.Remove(Person); } // T-cull, alerte : bassin a l'ecran, corps non rendu depuis 0,2 s. FVector2D Screen; if (CullAlertDelay <= 0.f && PC && ViewX > 0 && FVector::Dist(Pelvis, CameraLocation) < AlertRange && PC->ProjectWorldLocationToScreen(Pelvis, Screen) && Screen.X >= 0.0 && Screen.Y >= 0.0 && Screen.X <= ViewX && Screen.Y <= ViewY && !Body->WasRecentlyRendered(0.2f)) { CullAlertDelay = AlertInterval; FVector2D CenterScreen; const bool bCenterOnScreen = PC->ProjectWorldLocationToScreen(Center, CenterScreen) && CenterScreen.X >= 0.0 && CenterScreen.Y >= 0.0 && CenterScreen.X <= ViewX && CenterScreen.Y <= ViewY; UE_LOG(LogLifePlay, Display, TEXT("T-cull : %s non rendue, bassin a l'ecran a %.0f cm de la camera, a %.0f cm du centre des bornes (dans=%d, centre a l'ecran=%d), BoundsScale %.2f"), *Person->GetName(), FVector::Dist(Pelvis, CameraLocation), FVector::Dist(Pelvis, Center), bInside ? 1 : 0, bCenterOnScreen ? 1 : 0, Body->BoundsScale); } } } void ULPDirectControlSubsystem::RefreshIndex() { UObject* GI = GetGameInstance(); const FString Signature = ReadEnabledSignature(GI); if (bIndexed && Signature == LastEnabled) { return; } bIndexed = true; LastEnabled = Signature; PersonPrompts().Reset(); MeshPrompts().Reset(); const FString Root = FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir() / TEXT("Modules")); TArray Actions; IFileManager::Get().FindFilesRecursive(Actions, *Root, TEXT("*.lpaction"), true, false); for (const FString& File : Actions) { const FString Module = ModuleOf(File, Root); if (!ModuleEnabled(GI, Module)) { continue; } FString Text; TMap Keys; if (!FFileHelper::LoadFileToString(Text, *File)) { continue; } ReadKeys(Text, Keys); if (!WhereHasInteraction(Keys.FindRef(TEXT("WHERE")))) { continue; } FPrompt Prompt; Prompt.Label = Keys.FindRef(TEXT("ACTION_NAME")); Prompt.ActionId = Keys.FindRef(TEXT("ACTION_UNIQUEID")); Prompt.bPerson = true; if (!Prompt.Label.IsEmpty() && !Prompt.ActionId.IsEmpty()) { AddUnique(PersonPrompts(), Prompt); } } TArray Nears; IFileManager::Get().FindFilesRecursive(Nears, *Root, TEXT("*.lpnear"), true, false); for (const FString& File : Nears) { if (!ModuleEnabled(GI, ModuleOf(File, Root))) { continue; } FString Text; if (!FFileHelper::LoadFileToString(Text, *File)) { continue; } TArray Lines; Text.ParseIntoArrayLines(Lines, true); FPrompt Current; auto Keep = [&Current]() { if (!Current.MeshPrefix.IsEmpty() && !Current.Label.IsEmpty() && !Current.ActionId.IsEmpty()) { AddUnique(MeshPrompts(), Current); } }; for (const FString& Raw : Lines) { const FString Line = Raw.TrimStartAndEnd(); FString Key, Value; if (Line.IsEmpty() || Line.StartsWith(TEXT("//")) || !Line.Split(TEXT(":"), &Key, &Value)) { continue; } Key = Key.TrimStartAndEnd().ToUpper(); Value = Value.TrimStartAndEnd(); if (Key == TEXT("TARGET")) { Keep(); Current = FPrompt(); } else if (Key == TEXT("MESH")) { Current.MeshPrefix = Value; } else if (Key == TEXT("PROMPT")) { Current.Label = Value; } else if (Key == TEXT("ACTION")) { Current.ActionId = Value; } } Keep(); } PersonPrompts().Sort([](const FPrompt& A, const FPrompt& B) { return A.Label < B.Label; }); UE_LOG(LogLifePlay, Display, TEXT("Interaction : %d invite(s) personne, %d invite(s) meuble"), PersonPrompts().Num(), MeshPrompts().Num()); } void ULPDirectControlSubsystem::RefreshTarget() { UObject* GI = GetGameInstance(); AActor* Player = DirectActor(GI, TEXT("PlayerCharacter")); AActor* Room = DirectActor(GI, TEXT("Room")); Shown().Reset(); CurrentTarget = nullptr; bCurrentIsPerson = false; if (!Player || !Room || bMenuBlocks || ScenePlaying(GI)) { ShownSignature.Reset(); HidePrompts(); return; } const FVector Origin = Player->GetActorLocation(); float Best = Reach; AActor* Person = nullptr; if (UWorld* World = GetWorld()) { for (TActorIterator It(World); It; ++It) { ACharacter* Who = *It; const UCapsuleComponent* Cap = Who ? Who->GetCapsuleComponent() : nullptr; if (!Who || Who == Player || !Cap || Cap->GetCollisionEnabled() == ECollisionEnabled::NoCollision) { continue; } const float Distance = FlatDistance(Origin, Who->GetActorLocation()); if (Distance < Best) { Best = Distance; Person = Who; } } } UStaticMeshComponent* Furniture = nullptr; float FurnitureDistance = Reach; TArray Actors; Actors.Add(Room); Room->GetAttachedActors(Actors, false, true); for (AActor* Actor : Actors) { if (!Actor) { continue; } TInlineComponentArray Meshes(Actor); for (UStaticMeshComponent* Mesh : Meshes) { const FString Name = MeshName(Mesh); if (Name.IsEmpty()) { continue; } bool bKnown = false; for (const FPrompt& Prompt : MeshPrompts()) { bKnown |= Name.StartsWith(Prompt.MeshPrefix, ESearchCase::IgnoreCase); } if (!bKnown) { continue; } const FVector Point = Mesh->Bounds.GetBox().GetClosestPointTo(Origin); const float Distance = FlatDistance(Origin, Point); if (Distance < FurnitureDistance) { FurnitureDistance = Distance; Furniture = Mesh; } } } const bool bPersonWins = Person && (!Furniture || Best <= FurnitureDistance); if (bPersonWins) { CurrentTarget = Person; bCurrentIsPerson = true; CollectPrompts(GI, FString(), true, Shown()); } else if (Furniture) { CurrentTarget = Furniture->GetOwner(); CollectPrompts(GI, MeshName(Furniture), false, Shown()); } if (Shown().IsEmpty()) { CurrentTarget = nullptr; ShownSignature.Reset(); if (bPromptOpen) { bPromptOpen = false; HidePrompts(); } return; } FString Signature = GetNameSafe(CurrentTarget.Get()); for (const FPrompt& Prompt : Shown()) { Signature += TEXT("|") + Prompt.ActionId; } if (Signature != ShownSignature) { ShownSignature = Signature; Selected = 0; if (bPromptOpen) { ShowPrompts(); } } } void ULPDirectControlSubsystem::Walk(float DeltaTime) { UObject* GI = GetGameInstance(); APawn* Pawn = Cast(DirectActor(GI, TEXT("PlayerCharacter"))); UWorld* World = GetWorld(); APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; if (!Pawn || !PC) { return; } ACharacter* Character = Cast(Pawn); AActor* Room = DirectActor(GI, TEXT("Room")); UpdateEscape(Character, Room, EscapeVolumes); if (Character) { if (UCharacterMovementComponent* Move = Character->GetCharacterMovement()) { if (!Move->bRunPhysicsWithNoController) { Move->bRunPhysicsWithNoController = true; UE_LOG(LogLifePlay, Display, TEXT("Marche : physique sans controleur, vitesse %.0f, mode %d"), Move->MaxWalkSpeed, static_cast(Move->MovementMode)); } if (Move->MaxWalkSpeed < 10.f) { Move->MaxWalkSpeed = 180.f; } if (Move->MovementMode == MOVE_None) { Move->SetMovementMode(MOVE_Walking); } // C1 : le corps (et la vue attachee a la tete) suit le lacet de controle ; hors C1, orientation vers la marche. const bool bFirstPerson = FirstPersonView(GI, Pawn, PC); if (Move->bOrientRotationToMovement == bFirstPerson) { Move->bOrientRotationToMovement = !bFirstPerson; Character->bUseControllerRotationYaw = false; } if (bFirstPerson && !OnSeat(Character)) { Character->SetActorRotation(FRotator(0.f, PC->GetControlRotation().Yaw, 0.f)); } } } const float Forward = AxisValue(PC, ForwardAxis); const float Right = AxisValue(PC, RightAxis); const bool bMoving = !FMath::IsNearlyZero(Forward) || !FMath::IsNearlyZero(Right); const AGameModeBase* Mode = World->GetAuthGameMode(); const bool bSandbox = Mode && Mode->GetClass()->GetName().Contains(TEXT("LPPairSandbox")); // Jeu empaque : marche animee (LPAnimTarget.ini n'y existe pas) ; editeur : selon LPAnimTarget.ini (IsGameTarget inchange). if (Character && !bSandbox && (LPAnimRedirect::IsGameTarget() || FPlatformProperties::RequiresCookedData())) { const float Speed = bMoving ? FMath::Max(Character->GetVelocity().Size(), 20.f) : 0.f; WalkAnim(Character, Speed); } // KO3c (2) : le joueur pousse sans avancer depuis 0,5 s ; seuls les volumes de Room que sa capsule chevauche deja sont ouverts // (un mur ou un meuble que l'on longe n'est pas chevauche et reste solide). const UCharacterMovementComponent* Moving = Character ? Character->GetCharacterMovement() : nullptr; if (bMoving && Moving && EscapeVolumes.Num() == 0 && Moving->GetCurrentAcceleration().SizeSquared() > 1.0 && Character->GetVelocity().Size2D() < 1.0) { StuckTime += DeltaTime; if (StuckTime >= 0.5f) { StuckTime = 0.f; EscapeOverlaps(Character, Room, EscapeVolumes, TEXT("bloque")); } } else { StuckTime = 0.f; } if (bMoving) { LeaveSeat(Character, Room, EscapeVolumes); // C7-a : place quittee en marchant (D6) : rien a surveiller. LastSeat.Reset(); PendingSeat.Reset(); PendingSeatTime = -1.f; const FRotator Yaw(0.f, PC->GetControlRotation().Yaw, 0.f); const FVector ForwardDir = FRotationMatrix(Yaw).GetUnitAxis(EAxis::X); const FVector RightDir = FRotationMatrix(Yaw).GetUnitAxis(EAxis::Y); Pawn->AddMovementInput(ForwardDir, Forward); Pawn->AddMovementInput(RightDir, Right); } (void)DeltaTime; (void)Character; } void ULPDirectControlSubsystem::FollowCamera(APawn* Pawn, APlayerController* PC, float DeltaTime, bool bMoving) { if (!Pawn || !PC) { return; } AActor* View = PC->GetViewTarget(); const bool bBird = View && View->GetClass()->GetName().Contains(TEXT("Birdview")); if (bMoving && bBird) { BirdView = View; if (!FollowView) { FActorSpawnParameters Params; Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; Params.ObjectFlags |= RF_Transient; FollowView = Pawn->GetWorld()->SpawnActor(Pawn->GetActorLocation(), PC->GetControlRotation(), Params); if (UCameraComponent* Camera = FollowView ? FollowView->GetCameraComponent() : nullptr) { Camera->SetFieldOfView(70.f); } UE_LOG(LogLifePlay, Display, TEXT("Marche : camera troisieme personne derriere le personnage")); } if (FollowView) { PC->SetViewTargetWithBlend(FollowView, 0.35f); } } if (!FollowView || PC->GetViewTarget() != FollowView) { return; } const FRotator Control = PC->GetControlRotation(); const FRotator Aim(FMath::Clamp(Control.Pitch, -55.f, -12.f), Control.Yaw, 0.f); const FVector LookAt = Pawn->GetActorLocation() + FVector(0.f, 0.f, 50.f); FVector Desired = LookAt - Aim.Vector() * 340.f; FHitResult Hit; FCollisionQueryParams Query(SCENE_QUERY_STAT(WalkCamera), false, Pawn); if (Pawn->GetWorld()->SweepSingleByChannel(Hit, LookAt, Desired, FQuat::Identity, ECC_Camera, FCollisionShape::MakeSphere(12.f), Query)) { Desired = Hit.Location + Hit.Normal * 12.f; } const FVector Next = FMath::VInterpTo(FollowView->GetActorLocation(), Desired, DeltaTime, 8.f); FollowView->SetActorLocationAndRotation(Next, (LookAt - Next).Rotation()); } void ULPDirectControlSubsystem::WalkAnim(ACharacter* Character, float Speed) { USkeletalMeshComponent* Mesh = Character ? Character->GetMesh() : nullptr; if (!Mesh) { return; } if (Speed < 20.f) { RestoreAnim(); return; } bool bMale = false; if (const FBoolProperty* Male = CastField(Character->GetClass()->FindPropertyByName(TEXT("IsMale")))) { bMale = Male->GetPropertyValue_InContainer(Character); } UAnimationAsset* WalkCycle = WalkClip(bMale); if (!WalkCycle) { return; } if (!bAnimCaptured) { SavedAnimMode = static_cast(Mesh->GetAnimationMode()); SavedAnimClass = Mesh->GetAnimClass(); SavedSingleAnim = SingleNodeAnim(Mesh); bAnimCaptured = true; } if (Mesh->GetAnimationMode() != EAnimationMode::AnimationSingleNode || SingleNodeAnim(Mesh) != WalkCycle) { Mesh->PlayAnimation(WalkCycle, true); UE_LOG(LogLifePlay, Display, TEXT("Marche : animation %s"), *WalkCycle->GetName()); } Mesh->SetPlayRate(FMath::Clamp(Speed / 160.f, 0.75f, 1.5f)); } void ULPDirectControlSubsystem::RestoreAnim() { if (!bAnimCaptured) { return; } bAnimCaptured = false; ACharacter* Character = Cast(DirectActor(GetGameInstance(), TEXT("PlayerCharacter"))); USkeletalMeshComponent* Mesh = Character ? Character->GetMesh() : nullptr; if (!Mesh) { return; } // Le jeu (scene, action) a deja remplace le cycle de marche : ne pas ecraser son animation (seule la vitesse est rendue). const UAnimationAsset* Playing = SingleNodeAnim(Mesh); if (Mesh->GetAnimationMode() != EAnimationMode::AnimationSingleNode || !Playing || !Playing->GetName().EndsWith(TEXT("_Walk_Loop"))) { Mesh->SetPlayRate(1.f); return; } const EAnimationMode::Type Mode = static_cast(SavedAnimMode); if (Mode == EAnimationMode::AnimationBlueprint && SavedAnimClass) { Mesh->SetAnimInstanceClass(SavedAnimClass); Mesh->SetAnimationMode(EAnimationMode::AnimationBlueprint); } else if (SavedSingleAnim) { Mesh->PlayAnimation(SavedSingleAnim, true); } else { Mesh->SetAnimationMode(Mode); } Mesh->SetPlayRate(1.f); } void ULPDirectControlSubsystem::PollInteract() { UWorld* World = GetWorld(); APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; if (!PC || !InteractPressed(PC) || Shown().IsEmpty()) { return; } if (!bPromptOpen) { bPromptOpen = true; ShowPrompts(); return; } ActivatePrompt(Selected); } void ULPDirectControlSubsystem::HighlightPrompt(int32 Index) { if (Shown().IsValidIndex(Index)) { Selected = Index; PaintSelection(Selected); } } void ULPDirectControlSubsystem::ActivatePrompt(int32 Index) { if (!Shown().IsValidIndex(Index) || FPlatformTime::Seconds() - PromptState().LastFire < 0.4) { return; } PromptState().LastFire = FPlatformTime::Seconds(); Selected = Index; PaintSelection(Selected); CallAction(GetGameInstance(), Shown()[Index], CurrentTarget.Get()); } void ULPDirectControlSubsystem::ShowPrompts() { if (!GEngine || !GEngine->GameViewport || !FSlateApplication::IsInitialized()) { return; } PromptState().Owner = this; if (!PromptState().Root.IsValid()) { TSharedPtr Rows; TSharedRef Root = SNew(SOverlay) .Visibility(EVisibility::SelfHitTestInvisible) + SOverlay::Slot().HAlign(HAlign_Center).VAlign(VAlign_Bottom).Padding(FMargin(0.f, 0.f, 0.f, 72.f)) [ SNew(SBorder) .Padding(FMargin(10.f)) .BorderImage(FCoreStyle::Get().GetBrush("ToolPanel.GroupBorder")) [ SAssignNew(Rows, SVerticalBox) ] ]; PromptState().Rows = Rows; PromptState().Root = Root; GEngine->GameViewport->AddViewportWidgetContent(Root, 50); } PromptState().Rows->ClearChildren(); PromptState().Labels.Reset(); for (int32 Index = 0; Index < Shown().Num(); ++Index) { const FString Line = FString::Printf(TEXT("E %s"), *Shown()[Index].Label); TSharedPtr Label; PromptState().Rows->AddSlot().AutoHeight().Padding(0.f, 2.f) [ SNew(SButton) .IsFocusable(false) .OnHovered_Lambda([Index]() { if (ULPDirectControlSubsystem* Sub = PromptState().Owner.Get()) { Sub->HighlightPrompt(Index); } }) .OnClicked_Lambda([Index]() { if (ULPDirectControlSubsystem* Sub = PromptState().Owner.Get()) { Sub->ActivatePrompt(Index); } return FReply::Handled(); }) [ SAssignNew(Label, STextBlock) .Text(FText::FromString(Line)) .Font(FCoreStyle::GetDefaultFontStyle("Regular", 16)) ] ]; PromptState().Labels.Add(Label); } Selected = FMath::Clamp(Selected, 0, Shown().Num() - 1); PaintSelection(Selected); } void ULPDirectControlSubsystem::HidePrompts() { if (PromptState().Root.IsValid() && GEngine && GEngine->GameViewport) { GEngine->GameViewport->RemoveViewportWidgetContent(PromptState().Root.ToSharedRef()); } PromptState().Root.Reset(); PromptState().Rows.Reset(); PromptState().Labels.Reset(); }