Much work on input mode switching

This commit is contained in:
2026-04-17 23:43:28 -04:00
parent 7a09da8a4e
commit 6388de9b39
12 changed files with 251 additions and 101 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,5 +1,6 @@
* UE Wingman rename functions. * UE Wingman rename functions.
* ue Wingman 'structprop' doesn't work for UWingXXXRef types, or for Widget slots. It needs to be implemented on top of getdetails.
* In the console, do not allow multi-line lua expressions unless it's something that reasonably should be multi-line, like a function definition or an if-statement. * In the console, do not allow multi-line lua expressions unless it's something that reasonably should be multi-line, like a function definition or an if-statement.
* Keyboard Event Handling * Keyboard Event Handling

View File

@@ -341,7 +341,11 @@ FString FWingProperty::GetText() const
return TEXT("None"); return TEXT("None");
} }
FString Result; FString Result;
Prop->ExportTextItem_InContainer(Result, Container, nullptr, nullptr, PPF_None); // DefaultValue == PropertyValue makes ExportText_Direct's Data==Delta
// short-circuit fire for every subfield, so default-valued fields still
// get emitted (e.g. SizeRule=Automatic on FSlateChildSize).
const void* Value = Prop->ContainerPtrToValuePtr<void>(Container);
Prop->ExportTextItem_Direct(Result, Value, /*DefaultValue=*/Value, nullptr, PPF_None);
return Result; return Result;
} }

View File

@@ -13,7 +13,7 @@ import socket
HOST = "localhost" HOST = "localhost"
PORT = 9851 PORT = 9851
CONNECT_TIMEOUT = 2 CONNECT_TIMEOUT = 2
READ_TIMEOUT = 120 READ_TIMEOUT = 30
TOOL_DESCRIPTION = ( TOOL_DESCRIPTION = (
"Send a command to the Unreal Editor's UE Wingman plugin. " "Send a command to the Unreal Editor's UE Wingman plugin. "
@@ -91,12 +91,6 @@ def forward_to_editor(arguments):
return send_and_receive(arguments) return send_and_receive(arguments)
except Exception: except Exception:
disconnect() disconnect()
# Retry once in case the connection was stale
if connect():
try:
return send_and_receive(arguments)
except Exception:
disconnect()
return {"error": "Lost connection to Unreal Editor."} return {"error": "Lost connection to Unreal Editor."}
@@ -104,6 +98,28 @@ def make_jsonrpc(msg_id, result):
return {"jsonrpc": "2.0", "id": msg_id, "result": result} return {"jsonrpc": "2.0", "id": msg_id, "result": result}
def parse_editor_response(result):
"""Parse and validate a raw editor response into an MCP content list.
MCP expects `content` to be a list of objects, each with at least a
string "type" field (e.g. {"type": "text", "text": "..."}). Anything
else is replaced with a single error item so the client sees a clear
message instead of a schema violation.
"""
try:
parsed = json.loads(result)
except json.JSONDecodeError:
return [{"type": "text", "text": "Malformed response from editor: invalid JSON."}]
if not isinstance(parsed, list):
return [{"type": "text", "text": "Malformed response from editor: expected a list."}]
for item in parsed:
if not isinstance(item, dict):
return [{"type": "text", "text": "Malformed response from editor: list item is not an object."}]
if not isinstance(item.get("type"), str):
return [{"type": "text", "text": "Malformed response from editor: item missing string 'type' field."}]
return parsed
def handle_message(msg): def handle_message(msg):
"""Handle one JSON-RPC message from Claude Code.""" """Handle one JSON-RPC message from Claude Code."""
msg_id = msg.get("id") msg_id = msg.get("id")
@@ -130,10 +146,7 @@ def handle_message(msg):
if isinstance(result, dict) and "error" in result: if isinstance(result, dict) and "error" in result:
content = [{"type": "text", "text": result["error"]}] content = [{"type": "text", "text": result["error"]}]
else: else:
try: content = parse_editor_response(result)
content = json.loads(result)
except json.JSONDecodeError:
content = [{"type": "text", "text": "Malformed response from editor."}]
return make_jsonrpc(msg_id, { return make_jsonrpc(msg_id, {
"content": content, "content": content,
}) })

View File

@@ -7,52 +7,44 @@
#include "InputEvents.h" #include "InputEvents.h"
#include "Common.h" #include "Common.h"
bool FlxEventRequest::operator==(const FlxEventRequest &Other) const bool FlxInputModeRequest::operator==(const FlxInputModeRequest &Other) const
{ {
return (Widget == Other.Widget) && return (Widget == Other.Widget) &&
(UseUIOnly == Other.UseUIOnly) && (Focus == Other.Focus) &&
(ShowPointer == Other.ShowPointer) && (ShowPointer == Other.ShowPointer) &&
(Hotkeys == Other.Hotkeys); (BlockInput == Other.BlockInput);
} }
bool FlxEventRequests::SanityCheck(const FlxEventRequest &Request) bool FlxInputModeRequests::SanityCheck(const FlxInputModeRequest &Request)
{ {
if (Request.Widget == nullptr) if (Request.Widget == nullptr)
{ {
UE_LOG(LogLuprexIntegration, Error, TEXT("RequestEvents called with null widget.")); UE_LOG(LogLuprexIntegration, Error, TEXT("RequestEvents called with null widget."));
return false; return false;
} }
if (Request.ShowPointer && !Request.UseUIOnly)
{
UE_LOG(LogLuprexIntegration, Error, TEXT("RequestEvents: ShowPointer requires UseUIOnly."));
return false;
}
if (Request.UseUIOnly && !Request.Hotkeys.IsEmpty())
{
UE_LOG(LogLuprexIntegration, Error, TEXT("RequestEvents: Widget asked for all events, and also, specific events"));
return false;
}
return true; return true;
} }
void FlxEventRequests::SplitHighLow(View &High, View &Low) void FlxInputModeRequests::SplitHighLow(View &High, View &Low)
{ {
int32 NumHigh = 0; int32 NumHigh = 0;
while ((NumHigh < Requests.Num()) && (Requests[NumHigh].UseUIOnly)) NumHigh++; while ((NumHigh < Requests.Num()) && (Requests[NumHigh].IsHighPrio())) NumHigh++;
int32 NumLow = Requests.Num() - NumHigh; int32 NumLow = Requests.Num() - NumHigh;
High = View(Requests.GetData(), NumHigh); High = View(Requests.GetData(), NumHigh);
Low = View(Requests.GetData() + NumHigh, NumLow); Low = View(Requests.GetData() + NumHigh, NumLow);
} }
void FlxEventRequests::Request(const FlxEventRequest &NewRequest) void FlxInputModeRequests::Request(const FlxInputModeRequest &NewRequest)
{ {
bool IsHigh = NewRequest.IsHighPrio();
// Divide the array into a high-priority slice and a low-priority slice. // Divide the array into a high-priority slice and a low-priority slice.
View High, Low; View High, Low;
SplitHighLow(High, Low); SplitHighLow(High, Low);
// This is a simple test to see if anything is going to change. // This is a simple test to see if anything is going to change.
// If not, we return early and avoid setting the dirty bit. // If not, we return early and avoid setting the dirty bit.
if (NewRequest.UseUIOnly) if (IsHigh)
{ {
if ((High.Num() > 0) && (High[0] == NewRequest)) return; if ((High.Num() > 0) && (High[0] == NewRequest)) return;
} }
@@ -62,36 +54,45 @@ void FlxEventRequests::Request(const FlxEventRequest &NewRequest)
} }
// We're going to build a new version of the requests array. // We're going to build a new version of the requests array.
TArray<FlxEventRequest> Updated; TArray<FlxInputModeRequest> Updated;
// Add all high priority requests to the updated array, new request first. // Add all high priority requests to the updated array, new request first.
if (NewRequest.UseUIOnly) Updated.Add(NewRequest); if (IsHigh) Updated.Add(NewRequest);
for (const FlxEventRequest &Req : High) for (const FlxInputModeRequest &Req : High)
if (Req.Widget != NewRequest.Widget) Updated.Add(Req); if (Req.Widget != NewRequest.Widget) Updated.Add(Req);
// Add all low priority requests to the updated array, new request first. // Add all low priority requests to the updated array, new request first.
if (!NewRequest.UseUIOnly) Updated.Add(NewRequest); if (!IsHigh) Updated.Add(NewRequest);
for (const FlxEventRequest &Req : Low) for (const FlxInputModeRequest &Req : Low)
if (Req.Widget != NewRequest.Widget) Updated.Add(Req); if (Req.Widget != NewRequest.Widget) Updated.Add(Req);
Swap(Requests, Updated); Swap(Requests, Updated);
Dirty = true; Dirty = true;
} }
void FlxEventRequests::Remove(UUserWidget *Widget) void FlxInputModeRequests::EnsureWidget(UUserWidget *Widget)
{
for (const FlxInputModeRequest &Req : Requests)
{
if (Req.Widget == Widget) return;
}
Request(FlxInputModeRequest(Widget, nullptr, false, false));
}
void FlxInputModeRequests::Remove(UUserWidget *Widget)
{ {
int32 N = Requests.Num(); int32 N = Requests.Num();
Requests.RemoveAll([Widget](const FlxEventRequest &Entry) Requests.RemoveAll([Widget](const FlxInputModeRequest &Entry)
{ {
return Entry.Widget == Widget; return Entry.Widget == Widget;
}); });
if (Requests.Num() < N) Dirty = true; if (Requests.Num() < N) Dirty = true;
} }
void FlxEventRequests::GarbageCollect() void FlxInputModeRequests::GarbageCollect()
{ {
int32 N = Requests.Num(); int32 N = Requests.Num();
Requests.RemoveAll([](const FlxEventRequest &Entry) Requests.RemoveAll([](const FlxInputModeRequest &Entry)
{ {
UUserWidget *W = Entry.Widget; UUserWidget *W = Entry.Widget;
return W == nullptr || !IsValid(W) || W->GetParent() == nullptr; return W == nullptr || !IsValid(W) || W->GetParent() == nullptr;
@@ -99,14 +100,3 @@ void FlxEventRequests::GarbageCollect()
if (Requests.Num() < N) Dirty = true; if (Requests.Num() < N) Dirty = true;
} }
FlxEventRequests::InputMode FlxEventRequests::GetRequestedMode() const
{
if ((Requests.Num() > 0) && (Requests[0].UseUIOnly))
{
return InputMode::UIOnly;
}
else
{
return InputMode::GameOnly;
}
}

View File

@@ -11,52 +11,55 @@
#pragma once #pragma once
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "InputCoreTypes.h"
#include "Blueprint/UserWidget.h" #include "Blueprint/UserWidget.h"
#include "InputEvents.generated.h" #include "InputEvents.generated.h"
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
// //
// FlxEventRequest // FlxInputModeRequest
// //
// A widget's declaration of interest in input events. // A widget's declaration of interest in input events.
// //
// Widget: The widget that wants to receive events. // Widget: The widget that wants to receive events.
// UseUIOnly: If true, activating this request puts // Focus: The widget that should receive keyboard focus
// the system into UIOnly mode. // while this request is active.
// ShowPointer: If true, the mouse pointer should be // ShowPointer: If true, the mouse pointer should be
// visible when this widget has control. // visible when this widget has control.
// Hotkeys: Keys that go to this widget when the // BlockInput: If true, input actions should be blocked from
// player is in GameOnly mode. // reaching lower-priority consumers (e.g. the pawn).
// //
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
USTRUCT(BlueprintType) USTRUCT(BlueprintType)
struct FlxEventRequest struct FlxInputModeRequest
{ {
GENERATED_BODY() GENERATED_BODY()
FlxEventRequest() = default; FlxInputModeRequest() = default;
FlxEventRequest(UUserWidget *InWidget, bool InUseUIOnly, bool InShowPointer, const TArray<FKey> &InHotkeys) FlxInputModeRequest(UUserWidget *InWidget, UWidget *InFocus, bool InShowPointer, bool InBlockInput)
: Widget(InWidget), UseUIOnly(InUseUIOnly), ShowPointer(InShowPointer), Hotkeys(InHotkeys) {} : Widget(InWidget), Focus(InFocus), ShowPointer(InShowPointer), BlockInput(InBlockInput) {}
bool operator == (const FlxEventRequest &Other) const; bool operator == (const FlxInputModeRequest &Other) const;
// True if this request wants any high-priority resource:
// keyboard focus, the mouse pointer, or input blocking.
bool IsHighPrio() const { return Focus != nullptr || ShowPointer || BlockInput; }
UPROPERTY(BlueprintReadWrite) UPROPERTY(BlueprintReadWrite)
UUserWidget* Widget = nullptr; UUserWidget* Widget = nullptr;
UPROPERTY(BlueprintReadWrite) UPROPERTY(BlueprintReadWrite)
bool UseUIOnly = false; UWidget* Focus = nullptr;
UPROPERTY(BlueprintReadWrite) UPROPERTY(BlueprintReadWrite)
bool ShowPointer = false; bool ShowPointer = false;
UPROPERTY(BlueprintReadWrite) UPROPERTY(BlueprintReadWrite)
TArray<FKey> Hotkeys; bool BlockInput = false;
}; };
USTRUCT() USTRUCT()
struct FlxEventRequests struct FlxInputModeRequests
{ {
GENERATED_BODY() GENERATED_BODY()
@@ -64,27 +67,29 @@ private:
UPROPERTY() UPROPERTY()
// High priority requests are always before low-priority. // High priority requests are always before low-priority.
// Otherwise, these are in order of most recent first. // Otherwise, these are in order of most recent first.
TArray<FlxEventRequest> Requests; TArray<FlxInputModeRequest> Requests;
UPROPERTY() UPROPERTY()
bool Dirty = true; bool Dirty = true;
public: public:
enum class InputMode { UIOnly, GameOnly }; using View = TArrayView<FlxInputModeRequest>;
using View = TArrayView<FlxEventRequest>;
// Get the requests array. // Get the requests array.
const TArray<FlxEventRequest> &GetRequests() const { return Requests; } const TArray<FlxInputModeRequest> &GetRequests() const { return Requests; }
// Sanity check a request to see if it is reasonable. // Sanity check a request to see if it is reasonable.
static bool SanityCheck(const FlxEventRequest &Request); static bool SanityCheck(const FlxInputModeRequest &Request);
// Divide Requests into a high-priority slice and a low-priority slice. // Divide Requests into a high-priority slice and a low-priority slice.
void SplitHighLow(View &High, View &Low); void SplitHighLow(View &High, View &Low);
// Apply a request. Replaces any previous request by the same widget. // Apply a request. Replaces any previous request by the same widget.
void Request(const FlxEventRequest &NewRequest); void Request(const FlxInputModeRequest &NewRequest);
// Ensure the specified widget has a request in the array.
// If it isn't already present, register a low-priority request for it.
void EnsureWidget(UUserWidget *Widget);
// Remove all requests by the specified widget. // Remove all requests by the specified widget.
void Remove(UUserWidget *Widget); void Remove(UUserWidget *Widget);
@@ -100,7 +105,4 @@ public:
// Set the dirty flag. // Set the dirty flag.
void SetDirty() { Dirty = true; } void SetDirty() { Dirty = true; }
// Get the currently-requested mode.
InputMode GetRequestedMode() const;
}; };

View File

@@ -172,6 +172,7 @@ void ALuprexGameModeBase::OnWorldPostActorTick(UWorld* InWorld, ELevelTick InLev
if (PC != nullptr) if (PC != nullptr)
{ {
PC->UpdateLookAt(); PC->UpdateLookAt();
PC->UpdateInputMode();
} }
} }
} }

View File

@@ -2,10 +2,14 @@
#include "Common.h" #include "Common.h"
#include "Tangible.h" #include "Tangible.h"
#include "TangibleManager.h" #include "TangibleManager.h"
#include "Blueprint/UserWidget.h"
#include "Components/InputComponent.h" #include "Components/InputComponent.h"
#include "Engine/LevelScriptActor.h"
#include "Kismet/GameplayStatics.h"
#include "Engine/GameInstance.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) AlxPlayerControllerBase *AlxPlayerControllerBase::FromContext(const UObject *Context)
{ {
@@ -55,10 +59,53 @@ FVector2D AlxPlayerControllerBase::GetLookAtPixel(const UObject *Context)
return ScreenPosition; 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<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));
}
void AlxPlayerControllerBase::PushInputComponent(UInputComponent* InInputComponent) void AlxPlayerControllerBase::PushInputComponent(UInputComponent* InInputComponent)
{ {
if (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<UUserWidget>(InInputComponent->GetOuter()))
{
InputModeRequests.EnsureWidget(Widget);
return;
}
CurrentInputStack.RemoveSingle(InInputComponent); CurrentInputStack.RemoveSingle(InInputComponent);
CurrentInputStack.Add(InInputComponent); CurrentInputStack.Add(InInputComponent);
} }
@@ -121,31 +168,103 @@ void AlxPlayerControllerBase::BuildInputStack(TArray<UInputComponent*>& InputSta
InputStack.Push(InputComponent); InputStack.Push(InputComponent);
} }
// Sort the components first by bBlockInput, then by // The current input stack is unlikely to have anything,
// priority. We don't touch the original array, because // given that we've moved widgets to their own mechanism.
// we want to preserve information about which component // But, if there's anything here, sort it and push it.
// was pushed last. if (!CurrentInputStack.IsEmpty())
TArray<UInputComponent*, TInlineAllocator<20>> Pushed;
for (int32 Idx = 0; Idx < CurrentInputStack.Num(); ++Idx)
{ {
UInputComponent* IC = CurrentInputStack[Idx].Get(); TArray<UInputComponent*, TInlineAllocator<20>> Pushed;
if (IsValid(IC)) for (int32 Idx = 0; Idx < CurrentInputStack.Num(); ++Idx)
{ {
Pushed.Add(IC); UInputComponent* IC = CurrentInputStack[Idx].Get();
if (IsValid(IC)) Pushed.Add(IC);
else CurrentInputStack.RemoveAt(Idx--);
} }
else
Pushed.StableSort([](const UInputComponent& A, const UInputComponent& B)
{ {
CurrentInputStack.RemoveAt(Idx--); if (A.bBlockInput != B.bBlockInput) return !A.bBlockInput;
} return A.Priority < B.Priority;
});
InputStack.Append(Pushed);
} }
Pushed.StableSort([](const UInputComponent& A, const UInputComponent& B) // 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)
{ {
if (A.bBlockInput != B.bBlockInput) return !A.bBlockInput; if (UInputComponent *IC = GetWidgetInputComponent(Requests[Idx].Widget))
return A.Priority < B.Priority; {
}); IC->bBlockInput = Requests[Idx].BlockInput;
InputStack.Push(IC);
}
}
}
InputStack.Append(Pushed); 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();
// The first entry in InputModeRequests (highest priority) 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 TArray<FlxInputModeRequest> &Requests = InputModeRequests.GetRequests();
const FlxInputModeRequest &Top = Requests.IsEmpty() ? EmptyRequest : Requests[0];
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<SWidget> SlateFocus = Top.Focus ? Top.Focus->GetCachedWidget() : nullptr;
if (SlateFocus.IsValid())
{
SlateOperations.SetUserFocus(SlateFocus.ToSharedRef());
}
else
{
SlateOperations.SetUserFocus(ViewportWidgetRef);
}
GameViewportClient->SetIgnoreInput(false);
} }
void AlxPlayerControllerBase::UpdateLookAt() void AlxPlayerControllerBase::UpdateLookAt()

View File

@@ -3,6 +3,7 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "Engine/HitResult.h" #include "Engine/HitResult.h"
#include "GameFramework/PlayerController.h" #include "GameFramework/PlayerController.h"
#include "InputEvents.h"
#include "PlayerControllerBase.generated.h" #include "PlayerControllerBase.generated.h"
UCLASS(BlueprintType, Blueprintable) UCLASS(BlueprintType, Blueprintable)
@@ -36,16 +37,35 @@ public:
// Called by GameMode each tick. // Called by GameMode each tick.
void UpdateLookAt(); void UpdateLookAt();
// Called by GameMode each tick. GCs dead requests and will
// eventually reconcile focus, pointer, and capture state.
void UpdateInputMode();
// Input stack overrides: unsorted, append-on-push. // Input stack overrides: unsorted, append-on-push.
virtual void PushInputComponent(UInputComponent* InInputComponent) override; virtual void PushInputComponent(UInputComponent* InInputComponent) override;
virtual bool PopInputComponent(UInputComponent* InInputComponent) override; virtual bool PopInputComponent(UInputComponent* InInputComponent) override;
virtual void BuildInputStack(TArray<UInputComponent*>& InputStack) override; virtual void BuildInputStack(TArray<UInputComponent*>& InputStack) override;
// Read UUserWidget::InputComponent via reflection. The field is
// protected and has no public accessor; this reaches through the
// FProperty so we always see the current value without caching it.
static class UInputComponent* GetWidgetInputComponent(class UUserWidget *Widget);
// Blueprint-facing entry point. Looks like a method on UUserWidget
// (thanks to DefaultToSelf + HideSelfPin): the widget self-binds,
// we find its owning PlayerController, and register the request.
UFUNCTION(BlueprintCallable, Category = "Luprex|Input Mode",
meta = (DefaultToSelf = "Widget", HideSelfPin = "true"))
static void WidgetRequestInputMode(class UUserWidget *Widget, bool ShowPointer, bool BlockInput, class UWidget *Focus);
// Get the player controller, cast to AlxPlayerControllerBase. // Get the player controller, cast to AlxPlayerControllerBase.
static AlxPlayerControllerBase *FromContext(const UObject *Context); static AlxPlayerControllerBase *FromContext(const UObject *Context);
UPROPERTY() UPROPERTY()
FHitResult CurrentLookAt; FHitResult CurrentLookAt;
UPROPERTY()
FlxInputModeRequests InputModeRequests;
bool MustCallLookAtChanged = false; bool MustCallLookAtChanged = false;
}; };