2026-03-03 17:44:04 -05:00
|
|
|
#include "PlayerControllerBase.h"
|
|
|
|
|
#include "Common.h"
|
|
|
|
|
#include "Tangible.h"
|
|
|
|
|
#include "TangibleManager.h"
|
2026-04-17 23:43:28 -04:00
|
|
|
#include "Blueprint/UserWidget.h"
|
2026-04-17 17:56:10 -04:00
|
|
|
#include "Components/InputComponent.h"
|
2026-04-17 23:43:28 -04:00
|
|
|
#include "Engine/GameInstance.h"
|
|
|
|
|
#include "Engine/GameViewportClient.h"
|
2026-04-17 17:56:10 -04:00
|
|
|
#include "Engine/LevelScriptActor.h"
|
2026-04-17 23:43:28 -04:00
|
|
|
#include "Engine/LocalPlayer.h"
|
2026-03-03 17:44:04 -05:00
|
|
|
#include "Kismet/GameplayStatics.h"
|
2026-04-17 23:43:28 -04:00
|
|
|
#include "Widgets/SViewport.h"
|
2026-03-03 17:44:04 -05:00
|
|
|
|
|
|
|
|
AlxPlayerControllerBase *AlxPlayerControllerBase::FromContext(const UObject *Context)
|
|
|
|
|
{
|
|
|
|
|
APlayerController *PC = Context->GetWorld()->GetFirstPlayerController();
|
|
|
|
|
AlxPlayerControllerBase *Result = Cast<AlxPlayerControllerBase>(PC);
|
|
|
|
|
if (Result == nullptr)
|
|
|
|
|
{
|
|
|
|
|
UE_LOG(LogLuprexIntegration, Fatal, TEXT("Not currently using a Luprex Player Controller."));
|
|
|
|
|
}
|
|
|
|
|
return Result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const FHitResult &AlxPlayerControllerBase::GetLookAt(const UObject *Context)
|
|
|
|
|
{
|
|
|
|
|
return FromContext(Context)->CurrentLookAt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const AActor *AlxPlayerControllerBase::GetLookAtActor(const UObject *Context)
|
|
|
|
|
{
|
|
|
|
|
return FromContext(Context)->CurrentLookAt.GetActor();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AlxPlayerControllerBase::SetLookAt(const UObject *Context, const FHitResult &HitResult)
|
|
|
|
|
{
|
|
|
|
|
AlxPlayerControllerBase *PC = FromContext(Context);
|
|
|
|
|
if (PC->CurrentLookAt.HitObjectHandle != HitResult.HitObjectHandle)
|
|
|
|
|
{
|
|
|
|
|
PC->MustCallLookAtChanged = true;
|
|
|
|
|
}
|
|
|
|
|
PC->CurrentLookAt = HitResult;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AlxPlayerControllerBase::SetLookAtChanged(const UObject *Context)
|
|
|
|
|
{
|
|
|
|
|
AlxPlayerControllerBase *PC = FromContext(Context);
|
|
|
|
|
PC->MustCallLookAtChanged = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FVector2D AlxPlayerControllerBase::GetLookAtPixel(const UObject *Context)
|
|
|
|
|
{
|
|
|
|
|
AlxPlayerControllerBase *PC = FromContext(Context);
|
|
|
|
|
FVector2D ScreenPosition;
|
|
|
|
|
if (!UGameplayStatics::ProjectWorldToScreen(PC, PC->CurrentLookAt.Location, ScreenPosition, false))
|
|
|
|
|
{
|
|
|
|
|
return FVector2D();
|
|
|
|
|
}
|
|
|
|
|
return ScreenPosition;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 23:43:28 -04:00
|
|
|
UInputComponent* AlxPlayerControllerBase::GetWidgetInputComponent(UUserWidget *Widget)
|
|
|
|
|
{
|
|
|
|
|
if (!IsValid(Widget)) return nullptr;
|
|
|
|
|
|
|
|
|
|
// Cache the FProperty on first call. FProperties are owned by
|
|
|
|
|
// the native UUserWidget UClass, which lives for the process
|
|
|
|
|
// lifetime, so the pointer stays valid without us needing to
|
|
|
|
|
// root anything against GC. Static local init is thread-safe.
|
|
|
|
|
static FObjectProperty *InputComponentProp = FindFProperty<FObjectProperty>(
|
|
|
|
|
UUserWidget::StaticClass(), TEXT("InputComponent"));
|
|
|
|
|
check(InputComponentProp);
|
|
|
|
|
|
|
|
|
|
UObject *Value = InputComponentProp->GetObjectPropertyValue_InContainer(Widget);
|
|
|
|
|
return Cast<UInputComponent>(Value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AlxPlayerControllerBase::WidgetRequestInputMode(UUserWidget *Widget, bool ShowPointer, bool BlockInput, UWidget *Focus)
|
|
|
|
|
{
|
|
|
|
|
if (!IsValid(Widget))
|
|
|
|
|
{
|
|
|
|
|
UE_LOG(LogLuprexIntegration, Error, TEXT("WidgetRequestInputMode called with an invalid widget."));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
APlayerController *OwningPC = Widget->GetOwningPlayer();
|
|
|
|
|
AlxPlayerControllerBase *PC = Cast<AlxPlayerControllerBase>(OwningPC);
|
|
|
|
|
if (PC == nullptr)
|
|
|
|
|
{
|
|
|
|
|
UE_LOG(LogLuprexIntegration, Error,
|
|
|
|
|
TEXT("WidgetRequestInputMode: widget '%s' owning player is not an AlxPlayerControllerBase (got %s)."),
|
|
|
|
|
*Widget->GetName(), *GetNameSafe(OwningPC));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
PC->InputModeRequests.Request(FlxInputModeRequest(Widget, Focus, ShowPointer, BlockInput));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 17:56:10 -04:00
|
|
|
void AlxPlayerControllerBase::PushInputComponent(UInputComponent* InInputComponent)
|
|
|
|
|
{
|
|
|
|
|
if (InInputComponent)
|
|
|
|
|
{
|
2026-04-17 23:43:28 -04:00
|
|
|
// Widgets don't go on the current input stack. Instead, they're
|
|
|
|
|
// tracked in InputModeRequests, where the PlayerController can arbitrate
|
|
|
|
|
// focus, pointer visibility, and input blocking across them.
|
|
|
|
|
if (UUserWidget *Widget = Cast<UUserWidget>(InInputComponent->GetOuter()))
|
|
|
|
|
{
|
|
|
|
|
InputModeRequests.EnsureWidget(Widget);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-04-17 17:56:10 -04:00
|
|
|
CurrentInputStack.RemoveSingle(InInputComponent);
|
|
|
|
|
CurrentInputStack.Add(InInputComponent);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool AlxPlayerControllerBase::PopInputComponent(UInputComponent* InInputComponent)
|
|
|
|
|
{
|
|
|
|
|
if (InInputComponent)
|
|
|
|
|
{
|
|
|
|
|
if (CurrentInputStack.RemoveSingle(InInputComponent) > 0)
|
|
|
|
|
{
|
|
|
|
|
InInputComponent->ClearBindingValues();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AlxPlayerControllerBase::BuildInputStack(TArray<UInputComponent*>& InputStack)
|
|
|
|
|
{
|
|
|
|
|
// Controlled pawn gets last dibs on the input stack
|
|
|
|
|
APawn* ControlledPawn = GetPawnOrSpectator();
|
|
|
|
|
if (ControlledPawn)
|
|
|
|
|
{
|
|
|
|
|
if (ControlledPawn->InputEnabled())
|
|
|
|
|
{
|
|
|
|
|
// Get the explicit input component that is created upon Pawn possession. This one gets last dibs.
|
|
|
|
|
if (ControlledPawn->InputComponent)
|
|
|
|
|
{
|
|
|
|
|
InputStack.Push(ControlledPawn->InputComponent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// See if there is another InputComponent that was added to the Pawn's components array (possibly by script).
|
|
|
|
|
for (UActorComponent* ActorComponent : ControlledPawn->GetComponents())
|
|
|
|
|
{
|
|
|
|
|
UInputComponent* PawnInputComponent = Cast<UInputComponent>(ActorComponent);
|
|
|
|
|
if (PawnInputComponent && PawnInputComponent != ControlledPawn->InputComponent)
|
|
|
|
|
{
|
|
|
|
|
InputStack.Push(PawnInputComponent);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LevelScriptActors are put on the stack next
|
|
|
|
|
for (ULevel* Level : GetWorld()->GetLevels())
|
|
|
|
|
{
|
|
|
|
|
ALevelScriptActor* ScriptActor = Level->GetLevelScriptActor();
|
|
|
|
|
if (ScriptActor)
|
|
|
|
|
{
|
|
|
|
|
if (ScriptActor->InputEnabled() && ScriptActor->InputComponent)
|
|
|
|
|
{
|
|
|
|
|
InputStack.Push(ScriptActor->InputComponent);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (InputEnabled())
|
|
|
|
|
{
|
|
|
|
|
InputStack.Push(InputComponent);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 23:43:28 -04:00
|
|
|
// The current input stack is unlikely to have anything,
|
|
|
|
|
// given that we've moved widgets to their own mechanism.
|
|
|
|
|
// But, if there's anything here, sort it and push it.
|
|
|
|
|
if (!CurrentInputStack.IsEmpty())
|
2026-04-17 17:56:10 -04:00
|
|
|
{
|
2026-04-17 23:43:28 -04:00
|
|
|
TArray<UInputComponent*, TInlineAllocator<20>> Pushed;
|
|
|
|
|
for (int32 Idx = 0; Idx < CurrentInputStack.Num(); ++Idx)
|
2026-04-17 17:56:10 -04:00
|
|
|
{
|
2026-04-17 23:43:28 -04:00
|
|
|
UInputComponent* IC = CurrentInputStack[Idx].Get();
|
|
|
|
|
if (IsValid(IC)) Pushed.Add(IC);
|
|
|
|
|
else CurrentInputStack.RemoveAt(Idx--);
|
2026-04-17 17:56:10 -04:00
|
|
|
}
|
2026-04-17 23:43:28 -04:00
|
|
|
|
|
|
|
|
Pushed.StableSort([](const UInputComponent& A, const UInputComponent& B)
|
2026-04-17 17:56:10 -04:00
|
|
|
{
|
2026-04-17 23:43:28 -04:00
|
|
|
if (A.bBlockInput != B.bBlockInput) return !A.bBlockInput;
|
|
|
|
|
return A.Priority < B.Priority;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
InputStack.Append(Pushed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Push the widget input components from InputModeRequests. Requests
|
|
|
|
|
// are ordered high-priority first; the input stack is processed
|
|
|
|
|
// from top (last index) down, so we push back-to-front: low
|
|
|
|
|
// priority first, high priority last (ending up on top).
|
|
|
|
|
const TArray<FlxInputModeRequest> &Requests = InputModeRequests.GetRequests();
|
|
|
|
|
for (int32 Idx = Requests.Num() - 1; Idx >= 0; --Idx)
|
|
|
|
|
{
|
2026-04-18 01:11:21 -04:00
|
|
|
UUserWidget *Widget = Requests[Idx].Widget;
|
|
|
|
|
if (!Widget->IsInViewport()) continue;
|
|
|
|
|
if (UInputComponent *IC = GetWidgetInputComponent(Widget))
|
2026-04-17 23:43:28 -04:00
|
|
|
{
|
|
|
|
|
IC->bBlockInput = Requests[Idx].BlockInput;
|
|
|
|
|
InputStack.Push(IC);
|
2026-04-17 17:56:10 -04:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-17 23:43:28 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AlxPlayerControllerBase::UpdateInputMode()
|
|
|
|
|
{
|
|
|
|
|
InputModeRequests.GarbageCollect();
|
|
|
|
|
|
|
|
|
|
// Get all the various objects we need to be able to manipulate
|
|
|
|
|
// the input mode.
|
|
|
|
|
UGameViewportClient *GameViewportClient = GetWorld()->GetGameViewport();
|
|
|
|
|
ULocalPlayer *LocalPlayer = Cast<ULocalPlayer>(Player);
|
|
|
|
|
if (GameViewportClient == nullptr || LocalPlayer == nullptr) return;
|
|
|
|
|
TSharedPtr<SViewport> ViewportWidget = GameViewportClient->GetGameViewportWidget();
|
|
|
|
|
if (!ViewportWidget.IsValid()) return;
|
|
|
|
|
TSharedRef<SViewport> ViewportWidgetRef = ViewportWidget.ToSharedRef();
|
|
|
|
|
FReply &SlateOperations = LocalPlayer->GetSlateOperations();
|
|
|
|
|
|
2026-04-18 01:11:21 -04:00
|
|
|
// The first active entry in InputModeRequests dictates the
|
2026-04-17 23:43:28 -04:00
|
|
|
// pointer / capture / focus state. If there are no requests at all,
|
|
|
|
|
// fall back to a static default-constructed request.
|
|
|
|
|
static const FlxInputModeRequest EmptyRequest;
|
2026-04-18 01:11:21 -04:00
|
|
|
const FlxInputModeRequest *Top = &EmptyRequest;
|
|
|
|
|
for (const FlxInputModeRequest &Req : InputModeRequests.GetRequests())
|
|
|
|
|
{
|
|
|
|
|
if (Req.Widget->IsInViewport()) { Top = &Req; break; }
|
|
|
|
|
}
|
2026-04-17 23:43:28 -04:00
|
|
|
|
2026-04-18 01:11:21 -04:00
|
|
|
SetShowMouseCursor(Top->ShowPointer);
|
2026-04-17 17:56:10 -04:00
|
|
|
|
2026-04-18 01:11:21 -04:00
|
|
|
if (Top->ShowPointer)
|
2026-04-17 17:56:10 -04:00
|
|
|
{
|
2026-04-17 23:43:28 -04:00
|
|
|
// Pointer-visible mode: full macroexpansion of SetInputModeGameAndUI
|
|
|
|
|
// — the BP wrapper, APlayerController::SetInputMode, and
|
|
|
|
|
// FInputModeGameAndUI::ApplyInputMode all inlined. Defaults:
|
|
|
|
|
// MouseLockMode=DoNotLock, WidgetToFocus=null, bHideCursorDuringCapture=true.
|
|
|
|
|
SlateOperations.ReleaseMouseLock();
|
|
|
|
|
SlateOperations.ReleaseMouseCapture();
|
|
|
|
|
GameViewportClient->SetMouseLockMode(EMouseLockMode::DoNotLock);
|
|
|
|
|
GameViewportClient->SetHideCursorDuringCapture(false);
|
|
|
|
|
GameViewportClient->SetMouseCaptureMode(EMouseCaptureMode::CaptureDuringMouseDown);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Pointer-invisible mode: full macroexpansion of SetInputModeGameOnly
|
|
|
|
|
// — the BP wrapper, APlayerController::SetInputMode, and
|
|
|
|
|
// FInputModeGameOnly::ApplyInputMode all inlined.
|
|
|
|
|
SlateOperations.UseHighPrecisionMouseMovement(ViewportWidgetRef);
|
|
|
|
|
SlateOperations.LockMouseToWidget(ViewportWidgetRef);
|
|
|
|
|
GameViewportClient->SetMouseLockMode(EMouseLockMode::LockOnCapture);
|
|
|
|
|
GameViewportClient->SetMouseCaptureMode(EMouseCaptureMode::CapturePermanently);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UWidget holds its live SWidget in a cached TSharedPtr; we need a
|
|
|
|
|
// TSharedRef for SetUserFocus. Fall back to the viewport if the
|
|
|
|
|
// target has never been constructed (cached widget is null).
|
2026-04-18 01:11:21 -04:00
|
|
|
TSharedPtr<SWidget> SlateFocus = Top->Focus ? Top->Focus->GetCachedWidget() : nullptr;
|
2026-04-17 23:43:28 -04:00
|
|
|
if (SlateFocus.IsValid())
|
|
|
|
|
{
|
|
|
|
|
SlateOperations.SetUserFocus(SlateFocus.ToSharedRef());
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
SlateOperations.SetUserFocus(ViewportWidgetRef);
|
|
|
|
|
}
|
2026-04-17 17:56:10 -04:00
|
|
|
|
2026-04-17 23:43:28 -04:00
|
|
|
GameViewportClient->SetIgnoreInput(false);
|
2026-04-17 17:56:10 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-03 17:44:04 -05:00
|
|
|
void AlxPlayerControllerBase::UpdateLookAt()
|
|
|
|
|
{
|
|
|
|
|
UlxTangibleManager *TM = GetGameInstance()->GetSubsystem<UlxTangibleManager>();
|
|
|
|
|
if (TM == nullptr) return;
|
|
|
|
|
UlxTangible *Possessed = TM->GetPossessedTangible();
|
|
|
|
|
if (Possessed == nullptr) return;
|
|
|
|
|
APawn *Pawn = GetPawn();
|
|
|
|
|
if (Pawn == nullptr) return;
|
2026-03-12 19:12:37 -04:00
|
|
|
if (Possessed->GetActor() != Cast<AActor>(Pawn)) return;
|
2026-03-03 17:44:04 -05:00
|
|
|
if (PlayerCameraManager == nullptr) return;
|
|
|
|
|
|
|
|
|
|
CalculateLookAt();
|
|
|
|
|
|
|
|
|
|
if (MustCallLookAtChanged)
|
|
|
|
|
{
|
|
|
|
|
MustCallLookAtChanged = false;
|
|
|
|
|
LookAtChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|