From 6388de9b39f36d0ca7c077f120cfb395c78e15a7 Mon Sep 17 00:00:00 2001 From: jyelon Date: Fri, 17 Apr 2026 23:43:28 -0400 Subject: [PATCH] Much work on input mode switching --- Content/Luprex/lxGameMode.uasset | 4 +- Content/Luprex/lxPlayerController.uasset | 4 +- Content/Tangibles/TAN_Character.uasset | 4 +- Content/Widgets/WB_Console.uasset | 4 +- Docs/TODO-List.md | 1 + .../Source/UEWingman/Private/WingProperty.cpp | 6 +- Plugins/UEWingman/ue-wingman-mcp.py | 35 ++-- Source/Integration/InputEvents.cpp | 66 ++++---- Source/Integration/InputEvents.h | 50 +++--- Source/Integration/LuprexGameModeBase.cpp | 1 + Source/Integration/PlayerControllerBase.cpp | 157 +++++++++++++++--- Source/Integration/PlayerControllerBase.h | 20 +++ 12 files changed, 251 insertions(+), 101 deletions(-) diff --git a/Content/Luprex/lxGameMode.uasset b/Content/Luprex/lxGameMode.uasset index 387f4c7b..0fd10e49 100644 --- a/Content/Luprex/lxGameMode.uasset +++ b/Content/Luprex/lxGameMode.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c17dd753daf96d8b3ff2cfda5a4487869d83529c29049faceff022bf66712373 -size 73701 +oid sha256:8693e25082b5b281d697a94da6fda34a29bb57b0e5a75b28f4eb88dffe4ff416 +size 53103 diff --git a/Content/Luprex/lxPlayerController.uasset b/Content/Luprex/lxPlayerController.uasset index abc06ca4..27306b64 100644 --- a/Content/Luprex/lxPlayerController.uasset +++ b/Content/Luprex/lxPlayerController.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d61a99885bd6a4572505819651c69da3f51cbe2f3c92bc679f37de7d81df8c5 -size 144474 +oid sha256:bbd69a600dca00256dc2113e7cfb8dd93f43c006b6feb4c98d84a8bc899360af +size 165639 diff --git a/Content/Tangibles/TAN_Character.uasset b/Content/Tangibles/TAN_Character.uasset index 18f3852a..a0271eba 100644 --- a/Content/Tangibles/TAN_Character.uasset +++ b/Content/Tangibles/TAN_Character.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c45edcfa1ff909cf6e0363a08cf8bb79d3c2cc84b59cfcc71fcea5f011f6c1f -size 372202 +oid sha256:1f453324daf5f08b19d32c42df3245a4d0ec376735d4f99904f6f3f1bb2d266d +size 358506 diff --git a/Content/Widgets/WB_Console.uasset b/Content/Widgets/WB_Console.uasset index bc0f8628..db2eb00b 100644 --- a/Content/Widgets/WB_Console.uasset +++ b/Content/Widgets/WB_Console.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:922737df4d08eed23042d52ace705eec39ec8db47b7a6c9e3781222b3e0dee0b -size 159257 +oid sha256:c9e0b3c67c09a9aa5c0f41ce6d616e8013dc587e21ae75dd3e5f458e8c378321 +size 223308 diff --git a/Docs/TODO-List.md b/Docs/TODO-List.md index 1c00a872..590c2e94 100644 --- a/Docs/TODO-List.md +++ b/Docs/TODO-List.md @@ -1,5 +1,6 @@ * 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. * Keyboard Event Handling diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp index aa38c698..7510252b 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp @@ -341,7 +341,11 @@ FString FWingProperty::GetText() const return TEXT("None"); } 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(Container); + Prop->ExportTextItem_Direct(Result, Value, /*DefaultValue=*/Value, nullptr, PPF_None); return Result; } diff --git a/Plugins/UEWingman/ue-wingman-mcp.py b/Plugins/UEWingman/ue-wingman-mcp.py index b36edb4a..0cc7c477 100644 --- a/Plugins/UEWingman/ue-wingman-mcp.py +++ b/Plugins/UEWingman/ue-wingman-mcp.py @@ -13,7 +13,7 @@ import socket HOST = "localhost" PORT = 9851 CONNECT_TIMEOUT = 2 -READ_TIMEOUT = 120 +READ_TIMEOUT = 30 TOOL_DESCRIPTION = ( "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) except Exception: 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."} @@ -104,6 +98,28 @@ def make_jsonrpc(msg_id, 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): """Handle one JSON-RPC message from Claude Code.""" msg_id = msg.get("id") @@ -130,10 +146,7 @@ def handle_message(msg): if isinstance(result, dict) and "error" in result: content = [{"type": "text", "text": result["error"]}] else: - try: - content = json.loads(result) - except json.JSONDecodeError: - content = [{"type": "text", "text": "Malformed response from editor."}] + content = parse_editor_response(result) return make_jsonrpc(msg_id, { "content": content, }) diff --git a/Source/Integration/InputEvents.cpp b/Source/Integration/InputEvents.cpp index 481afae0..93eb3db7 100644 --- a/Source/Integration/InputEvents.cpp +++ b/Source/Integration/InputEvents.cpp @@ -7,52 +7,44 @@ #include "InputEvents.h" #include "Common.h" -bool FlxEventRequest::operator==(const FlxEventRequest &Other) const +bool FlxInputModeRequest::operator==(const FlxInputModeRequest &Other) const { return (Widget == Other.Widget) && - (UseUIOnly == Other.UseUIOnly) && + (Focus == Other.Focus) && (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) { UE_LOG(LogLuprexIntegration, Error, TEXT("RequestEvents called with null widget.")); 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; } -void FlxEventRequests::SplitHighLow(View &High, View &Low) +void FlxInputModeRequests::SplitHighLow(View &High, View &Low) { int32 NumHigh = 0; - while ((NumHigh < Requests.Num()) && (Requests[NumHigh].UseUIOnly)) NumHigh++; + while ((NumHigh < Requests.Num()) && (Requests[NumHigh].IsHighPrio())) NumHigh++; int32 NumLow = Requests.Num() - NumHigh; High = View(Requests.GetData(), NumHigh); 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. View High, Low; SplitHighLow(High, Low); // 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 (NewRequest.UseUIOnly) + if (IsHigh) { 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. - TArray Updated; + TArray Updated; // Add all high priority requests to the updated array, new request first. - if (NewRequest.UseUIOnly) Updated.Add(NewRequest); - for (const FlxEventRequest &Req : High) + if (IsHigh) Updated.Add(NewRequest); + for (const FlxInputModeRequest &Req : High) if (Req.Widget != NewRequest.Widget) Updated.Add(Req); // Add all low priority requests to the updated array, new request first. - if (!NewRequest.UseUIOnly) Updated.Add(NewRequest); - for (const FlxEventRequest &Req : Low) + if (!IsHigh) Updated.Add(NewRequest); + for (const FlxInputModeRequest &Req : Low) if (Req.Widget != NewRequest.Widget) Updated.Add(Req); Swap(Requests, Updated); 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(); - Requests.RemoveAll([Widget](const FlxEventRequest &Entry) + Requests.RemoveAll([Widget](const FlxInputModeRequest &Entry) { return Entry.Widget == Widget; }); if (Requests.Num() < N) Dirty = true; } -void FlxEventRequests::GarbageCollect() +void FlxInputModeRequests::GarbageCollect() { int32 N = Requests.Num(); - Requests.RemoveAll([](const FlxEventRequest &Entry) + Requests.RemoveAll([](const FlxInputModeRequest &Entry) { UUserWidget *W = Entry.Widget; return W == nullptr || !IsValid(W) || W->GetParent() == nullptr; @@ -99,14 +100,3 @@ void FlxEventRequests::GarbageCollect() 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; - } -} diff --git a/Source/Integration/InputEvents.h b/Source/Integration/InputEvents.h index e7ff85a9..cc5bbe0b 100644 --- a/Source/Integration/InputEvents.h +++ b/Source/Integration/InputEvents.h @@ -11,52 +11,55 @@ #pragma once #include "CoreMinimal.h" -#include "InputCoreTypes.h" #include "Blueprint/UserWidget.h" #include "InputEvents.generated.h" //////////////////////////////////////////////////////////// // -// FlxEventRequest +// FlxInputModeRequest // // A widget's declaration of interest in input events. // // Widget: The widget that wants to receive events. -// UseUIOnly: If true, activating this request puts -// the system into UIOnly mode. +// Focus: The widget that should receive keyboard focus +// while this request is active. // ShowPointer: If true, the mouse pointer should be // visible when this widget has control. -// Hotkeys: Keys that go to this widget when the -// player is in GameOnly mode. +// BlockInput: If true, input actions should be blocked from +// reaching lower-priority consumers (e.g. the pawn). // //////////////////////////////////////////////////////////// USTRUCT(BlueprintType) -struct FlxEventRequest +struct FlxInputModeRequest { GENERATED_BODY() - FlxEventRequest() = default; - FlxEventRequest(UUserWidget *InWidget, bool InUseUIOnly, bool InShowPointer, const TArray &InHotkeys) - : Widget(InWidget), UseUIOnly(InUseUIOnly), ShowPointer(InShowPointer), Hotkeys(InHotkeys) {} + FlxInputModeRequest() = default; + FlxInputModeRequest(UUserWidget *InWidget, UWidget *InFocus, bool InShowPointer, bool InBlockInput) + : 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) UUserWidget* Widget = nullptr; UPROPERTY(BlueprintReadWrite) - bool UseUIOnly = false; + UWidget* Focus = nullptr; UPROPERTY(BlueprintReadWrite) bool ShowPointer = false; UPROPERTY(BlueprintReadWrite) - TArray Hotkeys; + bool BlockInput = false; }; USTRUCT() -struct FlxEventRequests +struct FlxInputModeRequests { GENERATED_BODY() @@ -64,27 +67,29 @@ private: UPROPERTY() // High priority requests are always before low-priority. // Otherwise, these are in order of most recent first. - TArray Requests; + TArray Requests; UPROPERTY() bool Dirty = true; public: - enum class InputMode { UIOnly, GameOnly }; - - using View = TArrayView; + using View = TArrayView; // Get the requests array. - const TArray &GetRequests() const { return Requests; } + const TArray &GetRequests() const { return Requests; } // 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. void SplitHighLow(View &High, View &Low); // 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. void Remove(UUserWidget *Widget); @@ -100,7 +105,4 @@ public: // Set the dirty flag. void SetDirty() { Dirty = true; } - - // Get the currently-requested mode. - InputMode GetRequestedMode() const; }; diff --git a/Source/Integration/LuprexGameModeBase.cpp b/Source/Integration/LuprexGameModeBase.cpp index 4910c2d7..c8dd486b 100644 --- a/Source/Integration/LuprexGameModeBase.cpp +++ b/Source/Integration/LuprexGameModeBase.cpp @@ -172,6 +172,7 @@ void ALuprexGameModeBase::OnWorldPostActorTick(UWorld* InWorld, ELevelTick InLev if (PC != nullptr) { PC->UpdateLookAt(); + PC->UpdateInputMode(); } } } diff --git a/Source/Integration/PlayerControllerBase.cpp b/Source/Integration/PlayerControllerBase.cpp index 392cf98f..3a6819fa 100644 --- a/Source/Integration/PlayerControllerBase.cpp +++ b/Source/Integration/PlayerControllerBase.cpp @@ -2,10 +2,14 @@ #include "Common.h" #include "Tangible.h" #include "TangibleManager.h" +#include "Blueprint/UserWidget.h" #include "Components/InputComponent.h" -#include "Engine/LevelScriptActor.h" -#include "Kismet/GameplayStatics.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) { @@ -55,10 +59,53 @@ FVector2D AlxPlayerControllerBase::GetLookAtPixel(const UObject *Context) 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); } @@ -121,31 +168,103 @@ void AlxPlayerControllerBase::BuildInputStack(TArray& InputSta InputStack.Push(InputComponent); } - // Sort the components first by bBlockInput, then by - // priority. We don't touch the original array, because - // we want to preserve information about which component - // was pushed last. - TArray> Pushed; - for (int32 Idx = 0; Idx < CurrentInputStack.Num(); ++Idx) + // 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()) { - UInputComponent* IC = CurrentInputStack[Idx].Get(); - if (IsValid(IC)) + TArray> Pushed; + 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 &Requests = InputModeRequests.GetRequests(); + for (int32 Idx = Requests.Num() - 1; Idx >= 0; --Idx) { - if (A.bBlockInput != B.bBlockInput) return !A.bBlockInput; - return A.Priority < B.Priority; - }); + if (UInputComponent *IC = GetWidgetInputComponent(Requests[Idx].Widget)) + { + 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(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 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 &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 SlateFocus = Top.Focus ? Top.Focus->GetCachedWidget() : nullptr; + if (SlateFocus.IsValid()) + { + SlateOperations.SetUserFocus(SlateFocus.ToSharedRef()); + } + else + { + SlateOperations.SetUserFocus(ViewportWidgetRef); + } + + GameViewportClient->SetIgnoreInput(false); } void AlxPlayerControllerBase::UpdateLookAt() diff --git a/Source/Integration/PlayerControllerBase.h b/Source/Integration/PlayerControllerBase.h index 954c2ba5..176cf854 100644 --- a/Source/Integration/PlayerControllerBase.h +++ b/Source/Integration/PlayerControllerBase.h @@ -3,6 +3,7 @@ #include "CoreMinimal.h" #include "Engine/HitResult.h" #include "GameFramework/PlayerController.h" +#include "InputEvents.h" #include "PlayerControllerBase.generated.h" UCLASS(BlueprintType, Blueprintable) @@ -36,16 +37,35 @@ public: // Called by GameMode each tick. 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. virtual void PushInputComponent(UInputComponent* InInputComponent) override; virtual bool PopInputComponent(UInputComponent* InInputComponent) override; virtual void BuildInputStack(TArray& 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. static AlxPlayerControllerBase *FromContext(const UObject *Context); UPROPERTY() FHitResult CurrentLookAt; + UPROPERTY() + FlxInputModeRequests InputModeRequests; + bool MustCallLookAtChanged = false; };