2026-10-07 13:49:08 +00:00
// 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"
2026-10-07 13:49:12 +00:00
# include "LPAnimRedirect.h"
2026-10-07 13:49:08 +00:00
# include "Animation/AnimInstance.h"
# include "Animation/AnimationAsset.h"
2026-10-19 20:57:42 +00:00
# include "Animation/AnimSingleNodeInstance.h"
2026-10-07 13:49:08 +00:00
# include "Blueprint/UserWidget.h"
2026-10-19 20:57:42 +00:00
# include "Blueprint/WidgetTree.h"
2026-10-07 13:49:08 +00:00
# include "Camera/CameraActor.h"
# include "Camera/CameraComponent.h"
2026-10-19 20:57:42 +00:00
# include "Camera/PlayerCameraManager.h"
# include "Components/Button.h"
2026-10-07 13:49:08 +00:00
# include "Components/CapsuleComponent.h"
2026-10-19 20:57:42 +00:00
# include "Components/RichTextBlock.h"
2026-10-07 13:49:08 +00:00
# include "Components/SkeletalMeshComponent.h"
# include "Components/StaticMeshComponent.h"
2026-10-19 20:57:42 +00:00
# include "Components/TextBlock.h"
2026-10-07 13:49:08 +00:00
# include "Engine/GameInstance.h"
# include "Engine/GameViewportClient.h"
# include "Engine/StaticMesh.h"
# include "Engine/World.h"
# include "EngineUtils.h"
2026-10-19 20:57:42 +00:00
# include "Framework/Application/IInputProcessor.h"
2026-10-07 13:49:08 +00:00
# include "Framework/Application/SlateApplication.h"
# include "GameFramework/Character.h"
2026-10-07 13:49:12 +00:00
# include "GameFramework/GameModeBase.h"
2026-10-07 13:49:08 +00:00
# include "GameFramework/CharacterMovementComponent.h"
# include "GameFramework/InputSettings.h"
# include "GameFramework/PlayerController.h"
# include "GameFramework/PlayerInput.h"
2026-10-19 20:57:42 +00:00
# include "HAL/PlatformProperties.h"
2026-10-07 13:49:08 +00:00
# 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 " ) ,
2026-10-19 20:57:42 +00:00
TEXT ( " BodyShapeMenu_C " ) , TEXT ( " Scene_C " )
2026-10-07 13:49:08 +00:00
} ;
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 ) ;
}
}
2026-10-19 20:57:42 +00:00
// 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.
2026-10-07 13:49:08 +00:00
bool ScenePlaying ( const UObject * GI )
{
2026-10-19 20:57:42 +00:00
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 )
2026-10-07 13:49:08 +00:00
{
return false ;
}
2026-10-19 20:57:42 +00:00
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 ;
2026-10-07 13:49:08 +00:00
}
2026-10-19 20:57:42 +00:00
// Vrai pendant qu'une Scene_C sans contenu visible est ignoree par MenuReason (journalise par WitnessMoveKeys).
bool & EmptySceneSeen ( )
2026-10-07 13:49:08 +00:00
{
2026-10-19 20:57:42 +00:00
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 ;
2026-10-07 13:49:08 +00:00
if ( ! GI )
{
2026-10-19 20:57:42 +00:00
return FString ( ) ;
2026-10-07 13:49:08 +00:00
}
if ( const ULPKeyboardSubsystem * Keys = GI - > GetSubsystem < ULPKeyboardSubsystem > ( ) )
{
if ( Keys - > IsMenuOpen ( ) )
{
2026-10-19 20:57:42 +00:00
return TEXT ( " clavier " ) ;
2026-10-07 13:49:08 +00:00
}
}
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 )
{
2026-10-19 20:57:42 +00:00
// 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 ;
2026-10-07 13:49:08 +00:00
}
}
}
if ( ! FSlateApplication : : IsInitialized ( ) )
{
2026-10-19 20:57:42 +00:00
return FString ( ) ;
2026-10-07 13:49:08 +00:00
}
TSharedPtr < SWidget > Focused = FSlateApplication : : Get ( ) . GetKeyboardFocusedWidget ( ) ;
2026-10-19 20:57:42 +00:00
if ( Focused . IsValid ( ) & & Focused - > GetTypeAsString ( ) . Contains ( TEXT ( " Editable " ) ) )
{
return TEXT ( " focus " ) + Focused - > GetTypeAsString ( ) ;
}
return FString ( ) ;
2026-10-07 13:49:08 +00:00
}
2026-10-19 20:57:42 +00:00
// 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 ;
}
} ;
2026-10-07 13:49:08 +00:00
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 ) ;
2026-10-19 20:57:42 +00:00
if ( FSlateApplication : : IsInitialized ( ) )
{
MoveWitness = MakeShared < FMoveKeyWitness > ( ) ;
FSlateApplication : : Get ( ) . RegisterInputPreProcessor ( MoveWitness ) ;
}
2026-10-07 13:49:08 +00:00
}
void ULPDirectControlSubsystem : : Deinitialize ( )
{
2026-10-19 20:57:42 +00:00
UpdateEscape ( nullptr , nullptr , EscapeVolumes ) ;
2026-10-07 13:49:08 +00:00
HidePrompts ( ) ;
RestoreAnim ( ) ;
if ( FollowView )
{
FollowView - > Destroy ( ) ;
FollowView = nullptr ;
}
if ( PromptState ( ) . Owner . Get ( ) = = this )
{
PromptState ( ) . Owner = nullptr ;
}
2026-10-19 20:57:42 +00:00
if ( MoveWitness . IsValid ( ) & & FSlateApplication : : IsInitialized ( ) )
{
FSlateApplication : : Get ( ) . UnregisterInputPreProcessor ( MoveWitness ) ;
}
MoveWitness . Reset ( ) ;
SlateMoveKeys ( ) . Reset ( ) ;
2026-10-07 13:49:08 +00:00
FTSTicker : : GetCoreTicker ( ) . RemoveTicker ( TickHandle ) ;
Super : : Deinitialize ( ) ;
}
bool ULPDirectControlSubsystem : : Tick ( float DeltaTime )
{
MenuDelay + = DeltaTime ;
if ( MenuDelay > = 0.25f )
{
MenuDelay = 0.f ;
2026-10-19 20:57:42 +00:00
MenuWhy = MenuReason ( GetGameInstance ( ) ) ;
bMenuBlocks = ! MenuWhy . IsEmpty ( ) ;
2026-10-07 13:49:08 +00:00
}
2026-10-19 20:57:42 +00:00
WitnessMoveKeys ( DeltaTime ) ;
WatchSeatRelease ( DeltaTime ) ;
FitPersonBounds ( DeltaTime ) ;
2026-10-07 13:49:08 +00:00
IndexDelay + = DeltaTime ;
if ( IndexDelay > = 1.f )
{
IndexDelay = 0.f ;
RefreshIndex ( ) ;
}
TargetDelay + = DeltaTime ;
if ( TargetDelay > = 0.5f )
{
TargetDelay = 0.f ;
RefreshTarget ( ) ;
}
if ( bMenuBlocks | | ScenePlaying ( GetGameInstance ( ) ) )
{
2026-10-19 20:57:42 +00:00
// Marche coupee en plein pas (scene, menu, sexe) : rendre l'Anim BP d'origine (sans effet si rien n'est capture).
RestoreAnim ( ) ;
2026-10-07 13:49:08 +00:00
if ( ! Shown ( ) . IsEmpty ( ) | | bPromptOpen )
{
Shown ( ) . Reset ( ) ;
CurrentTarget = nullptr ;
ShownSignature . Reset ( ) ;
bPromptOpen = false ;
HidePrompts ( ) ;
}
}
else
{
Walk ( DeltaTime ) ;
PollInteract ( ) ;
}
return true ;
}
2026-10-19 20:57:42 +00:00
// 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 ) ;
}
}
}
2026-10-07 13:49:08 +00:00
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 ) ;
2026-10-19 20:57:42 +00:00
AActor * Room = DirectActor ( GI , TEXT ( " Room " ) ) ;
UpdateEscape ( Character , Room , EscapeVolumes ) ;
2026-10-07 13:49:08 +00:00
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 ) ;
}
2026-10-19 20:57:42 +00:00
// 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 )
2026-10-07 13:49:08 +00:00
{
2026-10-19 20:57:42 +00:00
Move - > bOrientRotationToMovement = ! bFirstPerson ;
2026-10-07 13:49:08 +00:00
Character - > bUseControllerRotationYaw = false ;
}
2026-10-19 20:57:42 +00:00
if ( bFirstPerson & & ! OnSeat ( Character ) )
{
Character - > SetActorRotation ( FRotator ( 0.f , PC - > GetControlRotation ( ) . Yaw , 0.f ) ) ;
}
2026-10-07 13:49:08 +00:00
}
}
const float Forward = AxisValue ( PC , ForwardAxis ) ;
const float Right = AxisValue ( PC , RightAxis ) ;
const bool bMoving = ! FMath : : IsNearlyZero ( Forward ) | | ! FMath : : IsNearlyZero ( Right ) ;
2026-10-07 13:49:12 +00:00
const AGameModeBase * Mode = World - > GetAuthGameMode ( ) ;
const bool bSandbox = Mode & & Mode - > GetClass ( ) - > GetName ( ) . Contains ( TEXT ( " LPPairSandbox " ) ) ;
2026-10-19 20:57:42 +00:00
// Jeu empaque : marche animee (LPAnimTarget.ini n'y existe pas) ; editeur : selon LPAnimTarget.ini (IsGameTarget inchange).
if ( Character & & ! bSandbox & & ( LPAnimRedirect : : IsGameTarget ( ) | | FPlatformProperties : : RequiresCookedData ( ) ) )
2026-10-07 13:49:12 +00:00
{
const float Speed = bMoving ? FMath : : Max ( Character - > GetVelocity ( ) . Size ( ) , 20.f ) : 0.f ;
WalkAnim ( Character , Speed ) ;
}
2026-10-19 20:57:42 +00:00
// 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 ;
}
2026-10-07 13:49:08 +00:00
if ( bMoving )
{
2026-10-19 20:57:42 +00:00
LeaveSeat ( Character , Room , EscapeVolumes ) ;
// C7-a : place quittee en marchant (D6) : rien a surveiller.
LastSeat . Reset ( ) ;
PendingSeat . Reset ( ) ;
PendingSeatTime = - 1.f ;
2026-10-07 13:49:08 +00:00
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 ) ;
}
2026-10-19 20:57:42 +00:00
UAnimationAsset * WalkCycle = WalkClip ( bMale ) ;
2026-10-07 13:49:08 +00:00
if ( ! WalkCycle )
{
return ;
}
if ( ! bAnimCaptured )
{
SavedAnimMode = static_cast < uint8 > ( Mesh - > GetAnimationMode ( ) ) ;
SavedAnimClass = Mesh - > GetAnimClass ( ) ;
2026-10-19 20:57:42 +00:00
SavedSingleAnim = SingleNodeAnim ( Mesh ) ;
2026-10-07 13:49:08 +00:00
bAnimCaptured = true ;
}
2026-10-19 20:57:42 +00:00
if ( Mesh - > GetAnimationMode ( ) ! = EAnimationMode : : AnimationSingleNode | | SingleNodeAnim ( Mesh ) ! = WalkCycle )
2026-10-07 13:49:08 +00:00
{
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 ;
}
2026-10-19 20:57:42 +00:00
// 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 ;
}
2026-10-07 13:49:08 +00:00
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 ( ) ;
}