LifePlay-Revival/Source/LifePlay/LPDirectControlSubsystem.cpp

1772 lines
61 KiB
C++
Raw Normal View History

// 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<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);
}
}
// 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<FBoolProperty>(GI->GetClass()->FindPropertyByName(TEXT("SexOn"))) : nullptr;
return Prop && Prop->GetPropertyValue_InContainer(GI);
}
// Clip de marche (STG-WALK) : copie cuite /Game/Heavy/Daz/<Bridget|Michael>/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<UAnimationAsset> 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<UAnimationAsset>(nullptr, *Cooked, nullptr, LOAD_NoWarn | LOAD_Quiet);
if (!Clip)
{
Clip = LoadObject<UAnimationAsset>(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<FObjectPropertyBase>(GI->GetClass()->FindPropertyByName(TEXT("Room"))) : nullptr;
const UObject* Room = RoomProp ? RoomProp->GetObjectPropertyValue_InContainer(GI) : nullptr;
const FBoolProperty* C1 = IsValid(Room) ? CastField<FBoolProperty>(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<FObjectPropertyBase>(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<TWeakObjectPtr<UPrimitiveComponent>>& Escape, const TCHAR* Why)
{
if (!Player || !IsValid(Room))
{
return 0;
}
const FObjectPropertyBase* FloorProp = CastField<FObjectPropertyBase>(Room->GetClass()->FindPropertyByName(TEXT("Box")));
const UObject* Floor = FloorProp ? FloorProp->GetObjectPropertyValue_InContainer(Room) : nullptr;
TInlineComponentArray<UPrimitiveComponent*> 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<TWeakObjectPtr<UPrimitiveComponent>>& Escape)
{
FObjectPropertyBase* Fnt = Player ? CastField<FObjectPropertyBase>(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<TWeakObjectPtr<UPrimitiveComponent>>& 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<UTextBlock>(Child);
const URichTextBlock* Rich = Cast<URichTextBlock>(Child);
const UUserWidget* Nested = Cast<UUserWidget>(Child);
const bool bContent = Child->IsA<UButton>() || (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 <type>"
// (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<ULPKeyboardSubsystem>())
{
if (Keys->IsMenuOpen())
{
return TEXT("clavier");
}
}
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)
{
// 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<SWidget> 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<FKey>& SlateMoveKeys()
{
static TArray<FKey> Keys;
return Keys;
}
class FMoveKeyWitness : public IInputProcessor
{
public:
virtual void Tick(const float, FSlateApplication&, TSharedRef<ICursor>) 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<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);
if (FSlateApplication::IsInitialized())
{
MoveWitness = MakeShared<FMoveKeyWitness>();
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<SWidget> 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<ACharacter>(DirectActor(GI, TEXT("PlayerCharacter")));
const FObjectPropertyBase* Fnt = Player ? CastField<FObjectPropertyBase>(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<UPrimitiveComponent>(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<ACharacter> 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<ACharacter> 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<float>(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<USceneComponent*> Children;
Body->GetChildrenComponents(true, Children);
for (USceneComponent* Child : Children)
{
UPrimitiveComponent* Prim = Cast<UPrimitiveComponent>(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<FObjectPropertyBase>(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<ACharacter>();
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<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);
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<int32>(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<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);
}
UAnimationAsset* WalkCycle = WalkClip(bMale);
if (!WalkCycle)
{
return;
}
if (!bAnimCaptured)
{
SavedAnimMode = static_cast<uint8>(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<ACharacter>(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<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();
}