diff --git a/Content/Luprex/lxPrompts.uasset b/Content/Luprex/lxPrompts.uasset new file mode 100644 index 00000000..ba8adf90 --- /dev/null +++ b/Content/Luprex/lxPrompts.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9eaccb9a872be766dae1e65e58c13eeaf9ba9ff3022450f4dd096ff1cdf9a872 +size 59768 diff --git a/Content/Widgets/WB_Hotkey_Image.uasset b/Content/Widgets/WB_Hotkey_Image.uasset index 2fdba1bb..8f666b7b 100644 --- a/Content/Widgets/WB_Hotkey_Image.uasset +++ b/Content/Widgets/WB_Hotkey_Image.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef2a95d588d11125cddc2fafd5ab79914d7345a2e5996fcc2c95b89d1461e459 -size 93861 +oid sha256:9eb7d9a6a4c4ce3800e850970235feff856cf6b6b7c57c4cd52f8150b79a37df +size 93798 diff --git a/Content/Widgets/XUserWidget.uasset b/Content/Widgets/XUserWidget.uasset new file mode 100644 index 00000000..9a2f4879 --- /dev/null +++ b/Content/Widgets/XUserWidget.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbaec08329c774ce3440bf05b965a06d9353be5eea6f703dec05a61350b01b1f +size 24857 diff --git a/Docs/Blueprint-MCP-Registration.md b/Docs/Blueprint-MCP-Registration.md deleted file mode 100644 index 5d6fe442..00000000 --- a/Docs/Blueprint-MCP-Registration.md +++ /dev/null @@ -1,33 +0,0 @@ - -# Ideas for BlueprintMCP handler registration - -UCLASS() -class USpawnNodeHandler : public UMCPHandler -{ - UPROPERTY() - FString BlueprintName; - - UPROPERTY(); - FString GraphName; - - UPROPERTY(); - FString ActionName; - - UPROPERTY(meta = "optional"); - int PosX; - - UPROPERTY(meta = "optional"); - int PosY; - - UPROPERTY() - ElxLuaValueType ValueType; - - // Dummy field. Nothing is actually extracted by the argument parser, - // but this tells the argument parser that the parameter "subtree" is - // supposed to be there. - UPROPERTY() - void Subtree; - - virtual void Handle(Json, Result) override; -}; - diff --git a/Docs/Blueprint-Text-Export.md b/Docs/Blueprint-Text-Export.md deleted file mode 100644 index 897acdf0..00000000 --- a/Docs/Blueprint-Text-Export.md +++ /dev/null @@ -1,49 +0,0 @@ -# Blueprint Text Export - -Blueprints are stored as binary `.uasset` files that Claude Code cannot read directly. To work around this, a text exporter automatically converts blueprint graphs to readable text files whenever a blueprint is saved in the Unreal editor. - -## How It Works - -The game module (`FlxIntegrationModuleImpl` in `Source/Integration/Integration.cpp`) hooks into `UPackage::PackageSavedWithContextEvent`. When a blueprint is saved, it iterates each `UEdGraph` in the blueprint and runs `FlxBlueprintExporter` on it. The output is written to `Saved/BlueprintExports//.txt`. - -The exporter class (`Source/Integration/BlueprintExporter.h/.cpp`) processes one graph at a time. The constructor runs all passes and the result is available via `GetOutput()`. - -## Output Format - -The graph file is written to `Saved/BlueprintExports//.txt`. A details file with node-name-to-GUID mappings is written to `Saved/BlueprintExports//DETAILS/.txt`. - -Every line in the graph file starts with a keyword, making it easy to parse. The format is: - -``` -node Event_Tick - return Output_Delegate, Delta_Seconds - goto Set_Tick_Delta_Seconds - -node Set_Tick_Delta_Seconds - input Real Tick_Delta_Seconds = Event_Tick.Delta_Seconds - return Output_Get - goto CallFunctionByName -``` - -### Line Keywords - -- `node Name` — starts a new node block. -- `input Type Name = Source` — an input data pin. Source is a `Node.Pin` reference, a literal value, ``, or ``. -- `return Pin1, Pin2` — output data pins. -- `goto Target` — exec flow (single output). -- `goto-if PinName Target` — exec flow (multiple outputs, e.g. branch true/false). -- `// comment text` — comment node. - -### Special Handling - -- String defaults are shown in quotes. -- Variable get nodes are inlined (the variable name appears directly at the point of use rather than as a separate node). -- Knot (reroute) nodes are followed through transparently. - -## Node Ordering - -Nodes are sorted by a traversal algorithm: find starter nodes (exec output but no exec input), sort them by Y position, then traverse each. The traversal visits a node's inputs first (so data sources appear before their consumers), then emits the node itself, then follows exec outputs. This produces a readable top-down flow. Any unvisited nodes are appended at the end. - -## Node Naming - -Names are derived from `ENodeTitleType::ListView` titles, sanitized to alphanumeric plus underscores. Duplicates get `_2`, `_3`, etc. diff --git a/Source/Integration/Common.h b/Source/Integration/Common.h index 8312c68f..214ce8c1 100644 --- a/Source/Integration/Common.h +++ b/Source/Integration/Common.h @@ -125,6 +125,21 @@ enum class ElxLuaSyntaxCheck : uint8 { InvalidLua, }; +//////////////////////////////////////////////////////////// +// +// ElxInputMode +// +// The three input modes recognized by the game. +// +//////////////////////////////////////////////////////////// + +UENUM(BlueprintType) +enum class ElxInputMode : uint8 { + KeyboardMouse, + XboxGamepad, + PlayStationGamepad, +}; + //////////////////////////////////////////////////////////// // // Log Categories diff --git a/Source/Integration/PlayerControllerBase.cpp b/Source/Integration/PlayerControllerBase.cpp index cf2d65ea..3d0f3214 100644 --- a/Source/Integration/PlayerControllerBase.cpp +++ b/Source/Integration/PlayerControllerBase.cpp @@ -1,5 +1,7 @@ #include "PlayerControllerBase.h" #include "Common.h" +#include "GameFramework/InputDeviceSubsystem.h" +#include "GameFramework/InputSettings.h" #include "RootCanvas.h" #include "Tangible.h" #include "TangibleManager.h" @@ -246,6 +248,25 @@ void AlxPlayerControllerBase::BuildInputStack(TArray& InputSta } } +static ElxInputMode DetectInputMode(const ULocalPlayer *LocalPlayer) +{ + UInputDeviceSubsystem *IDS = GEngine->GetEngineSubsystem(); + if (!IDS) return ElxInputMode::KeyboardMouse; + + FHardwareDeviceIdentifier Device = IDS->GetMostRecentlyUsedHardwareDevice(LocalPlayer->GetPlatformUserId()); + if (Device.PrimaryDeviceType != EHardwareDevicePrimaryType::Gamepad) + { + return ElxInputMode::KeyboardMouse; + } + + FString DeviceName = Device.HardwareDeviceIdentifier.ToString(); + if (DeviceName.Contains(TEXT("PS4")) || DeviceName.Contains(TEXT("PS5")) || DeviceName.Contains(TEXT("PlayStation"))) + { + return ElxInputMode::PlayStationGamepad; + } + return ElxInputMode::XboxGamepad; +} + void AlxPlayerControllerBase::UpdateInputMode() { // Get all the various objects we need to be able to manipulate @@ -302,6 +323,13 @@ void AlxPlayerControllerBase::UpdateInputMode() GameViewportClient->SetIgnoreInput(false); + ElxInputMode NewInputMode = DetectInputMode(LocalPlayer); + if (NewInputMode != CurrentInputMode) + { + CurrentInputMode = NewInputMode; + OnInputModeChanged.Broadcast(CurrentInputMode); + } + // We always put keyboard focus on whatever user widget is in // front. If the front widget doesn't want keyboard focus, // then we put keyboard focus on the viewport. @@ -318,6 +346,11 @@ void AlxPlayerControllerBase::UpdateInputMode() } } +ElxInputMode AlxPlayerControllerBase::GetInputMode(const UObject *Context) +{ + return FromContext(Context)->CurrentInputMode; +} + void AlxPlayerControllerBase::UpdateLookAt() { UlxTangibleManager *TM = GetGameInstance()->GetSubsystem(); diff --git a/Source/Integration/PlayerControllerBase.h b/Source/Integration/PlayerControllerBase.h index 9b769e05..0b91b745 100644 --- a/Source/Integration/PlayerControllerBase.h +++ b/Source/Integration/PlayerControllerBase.h @@ -1,11 +1,14 @@ #pragma once #include "CoreMinimal.h" +#include "Common.h" #include "Engine/HitResult.h" #include "GameFramework/PlayerController.h" #include "UObject/ObjectKey.h" #include "PlayerControllerBase.generated.h" +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FlxInputModeChanged, ElxInputMode, NewMode); + class UlxRootCanvasPanel; class UWidget; @@ -40,6 +43,12 @@ public: // Called by GameMode each tick. void UpdateLookAt(); + UFUNCTION(BlueprintPure, meta = (WorldContext = "Context"), Category = "Luprex|Input Mode") + static ElxInputMode GetInputMode(const UObject *Context); + + UPROPERTY(BlueprintAssignable, Category = "Luprex|Input Mode") + FlxInputModeChanged OnInputModeChanged; + // Called by GameMode each tick. GCs dead requests and will // eventually reconcile focus, pointer, and capture state. void UpdateInputMode(); @@ -88,4 +97,7 @@ public: UlxRootCanvasPanel *RootCanvas = nullptr; bool MustCallLookAtChanged = false; + + UPROPERTY() + ElxInputMode CurrentInputMode = ElxInputMode::KeyboardMouse; }; diff --git a/Source/Integration/PromptWidget.cpp b/Source/Integration/PromptWidget.cpp new file mode 100644 index 00000000..64df2669 --- /dev/null +++ b/Source/Integration/PromptWidget.cpp @@ -0,0 +1,189 @@ +#include "PromptWidget.h" +#include "PlayerControllerBase.h" +#include "Widgets/SOverlay.h" +#include "Widgets/Images/SImage.h" +#include "Widgets/Text/STextBlock.h" +#include "Widgets/Layout/SScaleBox.h" + +// +// Atlas contents: +// +// Row 1: Keyboard Button no Glyph, Left Mouse, Middle Mouse, Right Mouse +// Row 2: Left Trigger, Right Trigger, Left Shoulder, Right Shoulder +// Row 3: XBFace Down, XBFace Left, XBFace Up, XBFace Right +// Row 4: PSFace Down, PSFace Left, PSFace Up, PSFace Right +// Row 5: Arrow Down, Arrow Left, Arrow Up, Arrow Right +// Row 6: DPad Down, DPad Left, DPad Up, DPad Right +// Row 7: Space Bar, unused, unused, unused +// Row 8: unused, unused, unused, unused. + +void UlxPromptWidget::CalcAppearance(int32& OutIcon, FString& OutGlyph) const +{ + UWorld* World = GetWorld(); + APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; + AlxPlayerControllerBase* LPC = Cast(PC); + + ElxInputMode Mode = LPC ? LPC->CurrentInputMode : ElxInputMode::KeyboardMouse; + if (Mode == ElxInputMode::KeyboardMouse) + { + CalcKeyboardAppearance(OutIcon, OutGlyph); + } + else + { + CalcGamepadAppearance(Mode, OutIcon, OutGlyph); + } +} + +void UlxPromptWidget::CalcKeyboardAppearance(int32& OutIcon, FString& OutGlyph) const +{ + // Fixed-icon keys: no glyph needed. + if (KeyboardKey == EKeys::LeftMouseButton) { OutIcon = 1; OutGlyph = TEXT(""); return; } + if (KeyboardKey == EKeys::MiddleMouseButton) { OutIcon = 2; OutGlyph = TEXT(""); return; } + if (KeyboardKey == EKeys::RightMouseButton) { OutIcon = 3; OutGlyph = TEXT(""); return; } + if (KeyboardKey == EKeys::SpaceBar) { OutIcon = 24; OutGlyph = TEXT(""); return; } + if (KeyboardKey == EKeys::Down) { OutIcon = 16; OutGlyph = TEXT(""); return; } + if (KeyboardKey == EKeys::Left) { OutIcon = 17; OutGlyph = TEXT(""); return; } + if (KeyboardKey == EKeys::Up) { OutIcon = 18; OutGlyph = TEXT(""); return; } + if (KeyboardKey == EKeys::Right) { OutIcon = 19; OutGlyph = TEXT(""); return; } + + // Generic keyboard key: blank icon with the key name as glyph. + OutIcon = 0; + OutGlyph = KeyboardKey.GetDisplayName().ToString(); +} + +void UlxPromptWidget::CalcGamepadAppearance(ElxInputMode Mode, int32& OutIcon, FString& OutGlyph) const +{ + OutGlyph = TEXT(""); + + // Triggers and shoulders. + if (GamepadKey == EKeys::Gamepad_LeftTrigger) { OutIcon = 4; return; } + if (GamepadKey == EKeys::Gamepad_RightTrigger) { OutIcon = 5; return; } + if (GamepadKey == EKeys::Gamepad_LeftShoulder) { OutIcon = 6; return; } + if (GamepadKey == EKeys::Gamepad_RightShoulder) { OutIcon = 7; return; } + + // Face buttons — Xbox vs PlayStation. + bool bPS = (Mode == ElxInputMode::PlayStationGamepad); + if (GamepadKey == EKeys::Gamepad_FaceButton_Bottom) { OutIcon = bPS ? 12 : 8; return; } + if (GamepadKey == EKeys::Gamepad_FaceButton_Left) { OutIcon = bPS ? 13 : 9; return; } + if (GamepadKey == EKeys::Gamepad_FaceButton_Top) { OutIcon = bPS ? 14 : 10; return; } + if (GamepadKey == EKeys::Gamepad_FaceButton_Right) { OutIcon = bPS ? 15 : 11; return; } + + // DPad. + if (GamepadKey == EKeys::Gamepad_DPad_Down) { OutIcon = 20; return; } + if (GamepadKey == EKeys::Gamepad_DPad_Left) { OutIcon = 21; return; } + if (GamepadKey == EKeys::Gamepad_DPad_Up) { OutIcon = 22; return; } + if (GamepadKey == EKeys::Gamepad_DPad_Right) { OutIcon = 23; return; } + + // Fallback: blank key with name. + OutIcon = 0; + OutGlyph = GamepadKey.GetDisplayName().ToString(); +} + +FBox2f UlxPromptWidget::GetIconUVs(int32 IconIndex) +{ + const float ColWidth = 1.0f / 4.0f; + const float RowHeight = 1.0f / 8.0f; + float U = (IconIndex % 4) * ColWidth; + float V = (IconIndex / 4) * RowHeight; + return FBox2f(FVector2f(U, V), FVector2f(U + ColWidth, V + RowHeight)); +} + +FMargin UlxPromptWidget::GetScaledMargins() const +{ + return FMargin( + GlyphMargins.Left * Size.X, + GlyphMargins.Top * Size.Y, + GlyphMargins.Right * Size.X, + GlyphMargins.Bottom * Size.Y); +} + +void UlxPromptWidget::SynchronizeProperties() +{ + Super::SynchronizeProperties(); + if (!MyImage.IsValid()) return; + + int32 Icon = 0; + FString Glyph; + CalcAppearance(Icon, Glyph); + + MyBrush.SetUVRegion(GetIconUVs(Icon)); + MyImage->InvalidateImage(); + MyImage->SetDesiredSizeOverride(Size); + MyGlyphSlot->SetPadding(GetScaledMargins()); + MyGlyph->SetColorAndOpacity(GlyphColor); + + if (!Glyph.IsEmpty()) + { + MyGlyph->SetText(FText::FromString(Glyph)); + MyGlyph->SetVisibility(EVisibility::HitTestInvisible); + } + else + { + MyGlyph->SetVisibility(EVisibility::Collapsed); + } +} + +void UlxPromptWidget::SetSize(FVector2D InSize) +{ + Size = InSize; + SynchronizeProperties(); +} + +void UlxPromptWidget::SetGlyphMargins(FMargin InMargins) +{ + GlyphMargins = InMargins; + SynchronizeProperties(); +} + +void UlxPromptWidget::SetGlyphColor(FLinearColor InColor) +{ + GlyphColor = InColor; + SynchronizeProperties(); +} + +void UlxPromptWidget::SetKeys(FKey InGamepadKey, FKey InKeyboardKey) +{ + GamepadKey = InGamepadKey; + KeyboardKey = InKeyboardKey; + SynchronizeProperties(); +} + +TSharedRef UlxPromptWidget::RebuildWidget() +{ + int32 Icon = 0; + FString Glyph; + CalcAppearance(Icon, Glyph); + + MyBrush.SetResourceObject(ButtonAtlas); + MyBrush.ImageSize = Size; + MyBrush.SetUVRegion(GetIconUVs(Icon)); + + SAssignNew(MyImage, SImage).Image(&MyBrush); + + EVisibility GlyphVisibility = Glyph.IsEmpty() ? EVisibility::Collapsed : EVisibility::HitTestInvisible; + SAssignNew(MyGlyph, STextBlock) + .Text(FText::FromString(Glyph)) + .ColorAndOpacity(GlyphColor) + .Visibility(GlyphVisibility); + SAssignNew(MyScaleBox, SScaleBox) + .Stretch(EStretch::ScaleToFit) + [ MyGlyph.ToSharedRef() ]; + + MyOverlay = SNew(SOverlay) + + SOverlay::Slot() [ MyImage.ToSharedRef() ]; + + MyOverlay->AddSlot().Padding(GetScaledMargins()).HAlign(HAlign_Fill).VAlign(VAlign_Fill).Expose(MyGlyphSlot) + [ MyScaleBox.ToSharedRef() ]; + + return MyOverlay.ToSharedRef(); +} + +void UlxPromptWidget::ReleaseSlateResources(bool bReleaseChildren) +{ + Super::ReleaseSlateResources(bReleaseChildren); + MyOverlay.Reset(); + MyImage.Reset(); + MyScaleBox.Reset(); + MyGlyph.Reset(); + MyGlyphSlot = nullptr; +} diff --git a/Source/Integration/PromptWidget.h b/Source/Integration/PromptWidget.h new file mode 100644 index 00000000..e2875b79 --- /dev/null +++ b/Source/Integration/PromptWidget.h @@ -0,0 +1,67 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Common.h" +#include "Components/Widget.h" +#include "InputCoreTypes.h" +#include "Widgets/Layout/SScaleBox.h" +#include "PromptWidget.generated.h" + + +UCLASS(BlueprintType, Blueprintable) +class INTEGRATION_API UlxPromptWidget : public UWidget +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, Category="Prompt") + TObjectPtr ButtonAtlas; + + UFUNCTION(BlueprintCallable, Category="Prompt") + void SetKeys(FKey InGamepadKey, FKey InKeyboardKey); + + UPROPERTY(EditAnywhere, Setter, Category="Prompt") + FVector2D Size = FVector2D(64, 64); + + UPROPERTY(EditAnywhere, Setter, Category="Prompt") + FMargin GlyphMargins = FMargin(0.0f, 0.1f, 0.0f, 0.1f); + + UPROPERTY(EditAnywhere, Setter, Category="Prompt") + FLinearColor GlyphColor = FLinearColor::White; + + + UFUNCTION(BlueprintCallable, Category="Prompt") + void SetGlyphMargins(FMargin InMargins); + + UFUNCTION(BlueprintCallable, Category="Prompt") + void SetGlyphColor(FLinearColor InColor); + + + UFUNCTION(BlueprintCallable, Category="Prompt") + void SetSize(FVector2D InSize); + +protected: + virtual TSharedRef RebuildWidget() override; + virtual void SynchronizeProperties() override; + virtual void ReleaseSlateResources(bool bReleaseChildren) override; + + UPROPERTY(EditAnywhere, Category="Prompt") + FKey GamepadKey = EKeys::Gamepad_FaceButton_Left; + + UPROPERTY(EditAnywhere, Category="Prompt") + FKey KeyboardKey = EKeys::Z; + +private: + FSlateBrush MyBrush; + TSharedPtr MyOverlay; + TSharedPtr MyImage; + TSharedPtr MyScaleBox; + TSharedPtr MyGlyph; + SOverlay::FOverlaySlot* MyGlyphSlot = nullptr; + + FBox2f GetIconUVs(int32 IconIndex); + FMargin GetScaledMargins() const; + void CalcAppearance(int32& OutIcon, FString& OutGlyph) const; + void CalcKeyboardAppearance(int32& OutIcon, FString& OutGlyph) const; + void CalcGamepadAppearance(ElxInputMode Mode, int32& OutIcon, FString& OutGlyph) const; +};