From 5e901bef3161c435decb579608c5a08abd9015f1 Mon Sep 17 00:00:00 2001 From: edujiro5972 <336072401+edujiro5972@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:49:08 +0200 Subject: [PATCH] =?UTF-8?q?Proximit=C3=A9=20:=20LPDirectControlSubsystem?= =?UTF-8?q?=20(marche=20ZQSD/WASD,=20touche=20E),=20DefaultInput,=20index?= =?UTF-8?q?=20furniture.lpnear?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase Proximité (CHG-157, 166, 167). Co-Authored-By: Claude Opus 5.5 --- Config/DefaultInput.ini | 12 +- Source/LifePlay/LPDirectControlSubsystem.cpp | 1112 ++++++++++++++++++ Source/LifePlay/LPDirectControlSubsystem.h | 77 ++ 3 files changed, 1196 insertions(+), 5 deletions(-) create mode 100644 Source/LifePlay/LPDirectControlSubsystem.cpp create mode 100644 Source/LifePlay/LPDirectControlSubsystem.h diff --git a/Config/DefaultInput.ini b/Config/DefaultInput.ini index e7b60c7..af0e3c3 100644 --- a/Config/DefaultInput.ini +++ b/Config/DefaultInput.ini @@ -123,8 +123,8 @@ DoubleClickTime=0.200000 +ActionMappings=(ActionName="SpeedUp",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=Equals) +ActionMappings=(ActionName="SpeedDown",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=Hyphen) +ActionMappings=(ActionName="Recommended",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=R) -+ActionMappings=(ActionName="Journal",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=Q) +ActionMappings=(ActionName="Journal",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=J) ++ActionMappings=(ActionName="Interact",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=E) +ActionMappings=(ActionName="QuickSave",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=F5) +ActionMappings=(ActionName="Quickload",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=F9) +ActionMappings=(ActionName="Favorite1",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=One) @@ -152,10 +152,12 @@ DoubleClickTime=0.200000 +AxisMappings=(AxisName="DownUp",Scale=1.000000,Key=Up) +AxisMappings=(AxisName="DownUp",Scale=-1.000000,Key=Down) +AxisMappings=(AxisName="ScrollDownUp",Scale=1.000000,Key=MouseWheelAxis) -+AxisMappings=(AxisName="LeftRight",Scale=1.000000,Key=D) -+AxisMappings=(AxisName="LeftRight",Scale=-1.000000,Key=A) -+AxisMappings=(AxisName="DownUp",Scale=1.000000,Key=W) -+AxisMappings=(AxisName="DownUp",Scale=-1.000000,Key=S) ++AxisMappings=(AxisName="MoveForward",Scale=1.000000,Key=Z) ++AxisMappings=(AxisName="MoveForward",Scale=1.000000,Key=W) ++AxisMappings=(AxisName="MoveForward",Scale=-1.000000,Key=S) ++AxisMappings=(AxisName="MoveRight",Scale=-1.000000,Key=Q) ++AxisMappings=(AxisName="MoveRight",Scale=-1.000000,Key=A) ++AxisMappings=(AxisName="MoveRight",Scale=1.000000,Key=D) +AxisMappings=(AxisName="CursorX",Scale=1.000000,Key=MouseX) +AxisMappings=(AxisName="CursorY",Scale=1.000000,Key=MouseY) +AxisMappings=(AxisName="PenisRotation",Scale=1.000000,Key=PageUp) diff --git a/Source/LifePlay/LPDirectControlSubsystem.cpp b/Source/LifePlay/LPDirectControlSubsystem.cpp new file mode 100644 index 0000000..fe88c6c --- /dev/null +++ b/Source/LifePlay/LPDirectControlSubsystem.cpp @@ -0,0 +1,1112 @@ +// 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 "Animation/AnimInstance.h" +#include "Animation/AnimationAsset.h" +#include "Blueprint/UserWidget.h" +#include "Camera/CameraActor.h" +#include "Camera/CameraComponent.h" +#include "Components/CapsuleComponent.h" +#include "Components/SkeletalMeshComponent.h" +#include "Components/StaticMeshComponent.h" +#include "Engine/GameInstance.h" +#include "Engine/GameViewportClient.h" +#include "Engine/StaticMesh.h" +#include "Engine/World.h" +#include "EngineUtils.h" +#include "Framework/Application/SlateApplication.h" +#include "GameFramework/Character.h" +#include "GameFramework/CharacterMovementComponent.h" +#include "GameFramework/InputSettings.h" +#include "GameFramework/PlayerController.h" +#include "GameFramework/PlayerInput.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") + }; + + 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); + } + } + + bool ScenePlaying(const UObject* GI) + { + const FStrProperty* Prop = GI ? CastField(GI->GetClass()->FindPropertyByName(TEXT("CurrentScene"))) : nullptr; + if (!Prop) + { + return false; + } + const FString Scene = Prop->GetPropertyValue_InContainer(GI).TrimStartAndEnd(); + return !Scene.IsEmpty() && !Scene.Equals(TEXT("None"), ESearchCase::IgnoreCase); + } + + bool MenuOnScreen(const UGameInstance* GI) + { + if (!GI) + { + return false; + } + if (const ULPKeyboardSubsystem* Keys = GI->GetSubsystem()) + { + if (Keys->IsMenuOpen()) + { + return true; + } + } + 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) + { + return true; + } + } + } + if (!FSlateApplication::IsInitialized()) + { + return false; + } + TSharedPtr Focused = FSlateApplication::Get().GetKeyboardFocusedWidget(); + return Focused.IsValid() && Focused->GetTypeAsString().Contains(TEXT("Editable")); + } + + 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); +} + +void ULPDirectControlSubsystem::Deinitialize() +{ + HidePrompts(); + RestoreAnim(); + if (FollowView) + { + FollowView->Destroy(); + FollowView = nullptr; + } + if (PromptState().Owner.Get() == this) + { + PromptState().Owner = nullptr; + } + FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); + Super::Deinitialize(); +} + +bool ULPDirectControlSubsystem::Tick(float DeltaTime) +{ + MenuDelay += DeltaTime; + if (MenuDelay >= 0.25f) + { + MenuDelay = 0.f; + bMenuBlocks = MenuOnScreen(GetGameInstance()); + } + IndexDelay += DeltaTime; + if (IndexDelay >= 1.f) + { + IndexDelay = 0.f; + RefreshIndex(); + } + TargetDelay += DeltaTime; + if (TargetDelay >= 0.5f) + { + TargetDelay = 0.f; + RefreshTarget(); + } + if (bMenuBlocks || ScenePlaying(GetGameInstance())) + { + if (!Shown().IsEmpty() || bPromptOpen) + { + Shown().Reset(); + CurrentTarget = nullptr; + ShownSignature.Reset(); + bPromptOpen = false; + HidePrompts(); + } + } + else + { + Walk(DeltaTime); + PollInteract(); + } + return true; +} + +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); + 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); + } + if (!Move->bOrientRotationToMovement) + { + Move->bOrientRotationToMovement = true; + Character->bUseControllerRotationYaw = false; + } + } + } + const float Forward = AxisValue(PC, ForwardAxis); + const float Right = AxisValue(PC, RightAxis); + const bool bMoving = !FMath::IsNearlyZero(Forward) || !FMath::IsNearlyZero(Right); + if (bMoving) + { + 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); + } + const TCHAR* Path = bMale + ? TEXT("/Game/Heavy/New2/Anims2/walk_M.walk_M") + : TEXT("/Game/Heavy/New2/Anims2/walk_F.walk_F"); + UAnimationAsset* WalkCycle = LoadObject(nullptr, Path); + if (!WalkCycle) + { + return; + } + if (!bAnimCaptured) + { + SavedAnimMode = static_cast(Mesh->GetAnimationMode()); + SavedAnimClass = Mesh->GetAnimClass(); + SavedSingleAnim = Mesh->AnimationData.AnimToPlay; + bAnimCaptured = true; + } + if (Mesh->GetAnimationMode() != EAnimationMode::AnimationSingleNode || Mesh->AnimationData.AnimToPlay != 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; + } + 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(); +} diff --git a/Source/LifePlay/LPDirectControlSubsystem.h b/Source/LifePlay/LPDirectControlSubsystem.h new file mode 100644 index 0000000..959946b --- /dev/null +++ b/Source/LifePlay/LPDirectControlSubsystem.h @@ -0,0 +1,77 @@ +// Marche du personnage et invites de proximite. +// +// LeftRight et DownUp restent la vue de dessus (BPs/PC : LeftRightBirdView, DownUpBirdview). +// La marche lit deux axes neufs, MoveForward et MoveRight. Elle ne tourne pas quand un menu +// a le clavier (reglages, journal, panneau Clavier, saisie de texte). +// +// Les invites viennent des textes des modules : WHERE contenant interaction, et les fichiers +// .lpnear (MESH, PROMPT, ACTION). La liste est tenue ici, sur la personne ou le maillage le +// plus proche, et elle reste cachee. E l'affiche ; un second E, ou un clic, lance l'action. +// Le personnage n'est pas possede : la marche active bRunPhysicsWithNoController. +// Le cycle walk_F et la camera suiveuse ne sont plus appliques ici : ils cassaient +// le corps et la vue. L'essai est la sandbox interaction (LifePlay_interaction_sandbox.bat). +// Aucun Blueprint n'est modifie. + +#pragma once + +#include "CoreMinimal.h" +#include "Animation/AnimationAsset.h" +#include "Camera/CameraActor.h" +#include "Containers/Ticker.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "LPDirectControlSubsystem.generated.h" + +class AActor; + +UCLASS() +class LIFEPLAY_API ULPDirectControlSubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + + void ActivatePrompt(int32 Index); + void HighlightPrompt(int32 Index); + +private: + bool Tick(float DeltaTime); + void RefreshIndex(); + void RefreshTarget(); + void Walk(float DeltaTime); + void FollowCamera(APawn* Pawn, APlayerController* PC, float DeltaTime, bool bMoving); + void WalkAnim(ACharacter* Character, float Speed); + void RestoreAnim(); + void PollInteract(); + void ShowPrompts(); + void HidePrompts(); + + FTSTicker::FDelegateHandle TickHandle; + float MenuDelay = 0.f; + float IndexDelay = 0.f; + float TargetDelay = 0.f; + bool bMenuBlocks = false; + FString LastEnabled; + bool bIndexed = false; + FString ShownSignature; + int32 Selected = 0; + bool bPromptOpen = false; + TWeakObjectPtr CurrentTarget; + bool bCurrentIsPerson = false; + + UPROPERTY() + TObjectPtr FollowView; + + UPROPERTY() + TObjectPtr BirdView; + + UPROPERTY() + TObjectPtr SavedAnimClass; + + UPROPERTY() + TObjectPtr SavedSingleAnim; + + uint8 SavedAnimMode = 0; + bool bAnimCaptured = false; +};