Phase Proximité (CHG-157, 166, 167). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1112 lines
32 KiB
C++
1112 lines
32 KiB
C++
// 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<ULPDirectControlSubsystem> Owner;
|
|
TSharedPtr<SWidget> Root;
|
|
TSharedPtr<SVerticalBox> Rows;
|
|
TArray<TSharedPtr<STextBlock>> Labels;
|
|
double LastFire = 0.0;
|
|
};
|
|
|
|
FPromptLive& PromptState()
|
|
{
|
|
static FPromptLive State;
|
|
return State;
|
|
}
|
|
|
|
TArray<FPrompt>& PersonPrompts()
|
|
{
|
|
static TArray<FPrompt> Prompts;
|
|
return Prompts;
|
|
}
|
|
|
|
TArray<FPrompt>& MeshPrompts()
|
|
{
|
|
static TArray<FPrompt> Prompts;
|
|
return Prompts;
|
|
}
|
|
|
|
TArray<FPrompt>& Shown()
|
|
{
|
|
static TArray<FPrompt> Prompts;
|
|
return Prompts;
|
|
}
|
|
|
|
const FProperty* FindAuthored(const UStruct* Struct, const TCHAR* Name)
|
|
{
|
|
if (!Struct)
|
|
{
|
|
return nullptr;
|
|
}
|
|
for (TFieldIterator<FProperty> 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<void>(Data), nullptr, nullptr, PPF_None);
|
|
}
|
|
return Out.TrimQuotes();
|
|
}
|
|
|
|
void SetString(UObject* Object, const TCHAR* Name, const FString& Value)
|
|
{
|
|
if (FStrProperty* Prop = Object ? CastField<FStrProperty>(Object->GetClass()->FindPropertyByName(Name)) : nullptr)
|
|
{
|
|
Prop->SetPropertyValue_InContainer(Object, Value);
|
|
}
|
|
}
|
|
|
|
void SetObject(UObject* Object, const TCHAR* Name, UObject* Value)
|
|
{
|
|
if (FObjectPropertyBase* Prop = Object ? CastField<FObjectPropertyBase>(Object->GetClass()->FindPropertyByName(Name)) : nullptr)
|
|
{
|
|
Prop->SetObjectPropertyValue_InContainer(Object, Value);
|
|
}
|
|
}
|
|
|
|
AActor* DirectActor(const UObject* Object, const TCHAR* Name)
|
|
{
|
|
const FObjectPropertyBase* Prop = Object ? CastField<FObjectPropertyBase>(Object->GetClass()->FindPropertyByName(Name)) : nullptr;
|
|
return Prop ? Cast<AActor>(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<FArrayProperty>(GI->GetClass()->FindPropertyByName(TEXT("ModEnableds"))) : nullptr;
|
|
const FStructProperty* Inner = Prop ? CastField<FStructProperty>(Prop->Inner) : nullptr;
|
|
Struct = Inner ? Inner->Struct : nullptr;
|
|
if (!Prop || !Struct)
|
|
{
|
|
return FString();
|
|
}
|
|
FString Signature;
|
|
FScriptArrayHelper Helper(Prop, Prop->ContainerPtrToValuePtr<void>(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<FArrayProperty>(GI->GetClass()->FindPropertyByName(TEXT("ModEnableds"))) : nullptr;
|
|
const FStructProperty* Inner = Prop ? CastField<FStructProperty>(Prop->Inner) : nullptr;
|
|
const UScriptStruct* Struct = Inner ? Inner->Struct : nullptr;
|
|
if (!Prop || !Struct)
|
|
{
|
|
return true;
|
|
}
|
|
FScriptArrayHelper Helper(Prop, Prop->ContainerPtrToValuePtr<void>(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<FString, FString>& Out)
|
|
{
|
|
TArray<FString> 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<FString> 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<FPrompt>& 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<FArrayProperty>(GI->GetClass()->FindPropertyByName(TEXT("ActionDetail"))) : nullptr;
|
|
const FStructProperty* Inner = Prop ? CastField<FStructProperty>(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<void>(GI));
|
|
for (int32 Index = 0; Index < Helper.Num(); ++Index)
|
|
{
|
|
FString Id;
|
|
IdField->ExportTextItem_Direct(Id, IdField->ContainerPtrToValuePtr<void>(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<FArrayProperty>(GI->GetClass()->FindPropertyByName(TEXT("ActionDetail"))) : nullptr;
|
|
const FStructProperty* Inner = Prop ? CastField<FStructProperty>(Prop->Inner) : nullptr;
|
|
if (!Inner)
|
|
{
|
|
return true;
|
|
}
|
|
FScriptArrayHelper Helper(Prop, Prop->ContainerPtrToValuePtr<void>(GI));
|
|
return Helper.Num() == 0;
|
|
}
|
|
|
|
void LogParmsOnce(const UFunction* Function)
|
|
{
|
|
static TSet<FName> Logged;
|
|
if (!Function || Logged.Contains(Function->GetFName()))
|
|
{
|
|
return;
|
|
}
|
|
Logged.Add(Function->GetFName());
|
|
for (TFieldIterator<FProperty> 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<FProperty> 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<FStructProperty>(FindParm(Function, TEXT("Building")));
|
|
const UObject* Street = DirectActor(GI, TEXT("StreetMapStatic"));
|
|
const FStructProperty* Source = Street ? CastField<FStructProperty>(Street->GetClass()->FindPropertyByName(TEXT("CurrentPlayerBuilding"))) : nullptr;
|
|
if (!Dest || !Source || Dest->Struct != Source->Struct)
|
|
{
|
|
return;
|
|
}
|
|
Dest->CopyCompleteValue(Dest->ContainerPtrToValuePtr<void>(Params), Source->ContainerPtrToValuePtr<void>(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<FStructProperty>(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<uint8*>(FMemory_Alloca(Function->ParmsSize));
|
|
FMemory::Memzero(Params, Function->ParmsSize);
|
|
for (TFieldIterator<FProperty> It(Function); It && It->HasAnyPropertyFlags(CPF_Parm); ++It)
|
|
{
|
|
It->InitializeValue_InContainer(Params);
|
|
}
|
|
if (Detail)
|
|
{
|
|
ActionParm->CopyCompleteValue(ActionParm->ContainerPtrToValuePtr<void>(Params), Detail);
|
|
if (FBoolProperty* Multiple = CastField<FBoolProperty>(FindParm(Function, TEXT("Multiple"))))
|
|
{
|
|
Multiple->SetPropertyValue_InContainer(Params, false);
|
|
}
|
|
if (FBoolProperty* Interaction = CastField<FBoolProperty>(FindParm(Function, TEXT("Interaction"))))
|
|
{
|
|
Interaction->SetPropertyValue_InContainer(Params, Prompt.bPerson);
|
|
}
|
|
}
|
|
else if (FTextProperty* ActionText = CastField<FTextProperty>(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<FProperty> It(Function); It && It->HasAnyPropertyFlags(CPF_Parm); ++It)
|
|
{
|
|
It->DestroyValue_InContainer(Params);
|
|
}
|
|
}
|
|
|
|
bool ScenePlaying(const UObject* GI)
|
|
{
|
|
const FStrProperty* Prop = GI ? CastField<FStrProperty>(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<ULPKeyboardSubsystem>())
|
|
{
|
|
if (Keys->IsMenuOpen())
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
for (TObjectIterator<UUserWidget> 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<SWidget> 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<FPrompt>& 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<FString> 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<FString, FString> 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<FString> 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<FString> 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<ACharacter> 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<AActor*> Actors;
|
|
Actors.Add(Room);
|
|
Room->GetAttachedActors(Actors, false, true);
|
|
for (AActor* Actor : Actors)
|
|
{
|
|
if (!Actor)
|
|
{
|
|
continue;
|
|
}
|
|
TInlineComponentArray<UStaticMeshComponent*> 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<APawn>(DirectActor(GI, TEXT("PlayerCharacter")));
|
|
UWorld* World = GetWorld();
|
|
APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr;
|
|
if (!Pawn || !PC)
|
|
{
|
|
return;
|
|
}
|
|
ACharacter* Character = Cast<ACharacter>(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<int32>(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<ACameraActor>(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<FBoolProperty>(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<UAnimationAsset>(nullptr, Path);
|
|
if (!WalkCycle)
|
|
{
|
|
return;
|
|
}
|
|
if (!bAnimCaptured)
|
|
{
|
|
SavedAnimMode = static_cast<uint8>(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<ACharacter>(DirectActor(GetGameInstance(), TEXT("PlayerCharacter")));
|
|
USkeletalMeshComponent* Mesh = Character ? Character->GetMesh() : nullptr;
|
|
if (!Mesh)
|
|
{
|
|
return;
|
|
}
|
|
const EAnimationMode::Type Mode = static_cast<EAnimationMode::Type>(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<SVerticalBox> Rows;
|
|
TSharedRef<SWidget> 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<STextBlock> 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();
|
|
}
|