#include "PlayerControllerBase.h" #include "Common.h" #include "Tangible.h" #include "TangibleManager.h" #include "Blueprint/UserWidget.h" #include "Components/InputComponent.h" #include "Engine/GameInstance.h" #include "Engine/GameViewportClient.h" #include "Engine/LevelScriptActor.h" #include "Engine/LocalPlayer.h" #include "Kismet/GameplayStatics.h" #include "Widgets/SViewport.h" AlxPlayerControllerBase *AlxPlayerControllerBase::FromContext(const UObject *Context) { APlayerController *PC = Context->GetWorld()->GetFirstPlayerController(); AlxPlayerControllerBase *Result = Cast(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; } 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( UUserWidget::StaticClass(), TEXT("InputComponent")); check(InputComponentProp); UObject *Value = InputComponentProp->GetObjectPropertyValue_InContainer(Widget); return Cast(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(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)); } void AlxPlayerControllerBase::PushInputComponent(UInputComponent* InInputComponent) { if (InInputComponent) { // 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(InInputComponent->GetOuter())) { InputModeRequests.EnsureWidget(Widget); return; } 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& 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(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); } // 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()) { TArray> Pushed; for (int32 Idx = 0; Idx < CurrentInputStack.Num(); ++Idx) { UInputComponent* IC = CurrentInputStack[Idx].Get(); if (IsValid(IC)) Pushed.Add(IC); else CurrentInputStack.RemoveAt(Idx--); } Pushed.StableSort([](const UInputComponent& A, const UInputComponent& B) { 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 &Requests = InputModeRequests.GetRequests(); for (int32 Idx = Requests.Num() - 1; Idx >= 0; --Idx) { UUserWidget *Widget = Requests[Idx].Widget; if (!Widget->IsInViewport()) continue; if (UInputComponent *IC = GetWidgetInputComponent(Widget)) { IC->bBlockInput = Requests[Idx].BlockInput; InputStack.Push(IC); } } } 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(Player); if (GameViewportClient == nullptr || LocalPlayer == nullptr) return; TSharedPtr ViewportWidget = GameViewportClient->GetGameViewportWidget(); if (!ViewportWidget.IsValid()) return; TSharedRef ViewportWidgetRef = ViewportWidget.ToSharedRef(); FReply &SlateOperations = LocalPlayer->GetSlateOperations(); // The first active entry in InputModeRequests dictates the // pointer / capture / focus state. If there are no requests at all, // fall back to a static default-constructed request. static const FlxInputModeRequest EmptyRequest; const FlxInputModeRequest *Top = &EmptyRequest; for (const FlxInputModeRequest &Req : InputModeRequests.GetRequests()) { if (Req.Widget->IsInViewport()) { Top = &Req; break; } } SetShowMouseCursor(Top->ShowPointer); if (Top->ShowPointer) { // 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). TSharedPtr SlateFocus = Top->Focus ? Top->Focus->GetCachedWidget() : nullptr; if (SlateFocus.IsValid()) { SlateOperations.SetUserFocus(SlateFocus.ToSharedRef()); } else { SlateOperations.SetUserFocus(ViewportWidgetRef); } GameViewportClient->SetIgnoreInput(false); } void AlxPlayerControllerBase::UpdateLookAt() { UlxTangibleManager *TM = GetGameInstance()->GetSubsystem(); if (TM == nullptr) return; UlxTangible *Possessed = TM->GetPossessedTangible(); if (Possessed == nullptr) return; APawn *Pawn = GetPawn(); if (Pawn == nullptr) return; if (Possessed->GetActor() != Cast(Pawn)) return; if (PlayerCameraManager == nullptr) return; CalculateLookAt(); if (MustCallLookAtChanged) { MustCallLookAtChanged = false; LookAtChanged(); } }