Compare commits

...

4 Commits

12 changed files with 206 additions and 201 deletions

View File

@@ -19,10 +19,10 @@ class UWing_GraphNode_SearchTypes : public UWingHandler
GENERATED_BODY() GENERATED_BODY()
public: public:
UPROPERTY(EditAnywhere, meta=(Description="Query string, can contain * wildcards")) UPROPERTY(EditAnywhere, meta=(Description="Array of query strings; each may contain * wildcards"))
FString Query; FWingJsonArray Queries;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results (default 50)")) UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results per query (default 50)"))
int32 MaxResults = 50; int32 MaxResults = 50;
UPROPERTY(EditAnywhere, meta=(Description="Target graph")) UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
@@ -40,7 +40,24 @@ public:
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>(); UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
if (!TargetGraph) return; if (!TargetGraph) return;
// Validate all entries are strings before running any searches.
TArray<FString> QueryStrings;
QueryStrings.Reserve(Queries.Array.Num());
for (const TSharedPtr<FJsonValue>& QueryVal : Queries.Array)
{
FString QueryStr;
if (!QueryVal->TryGetString(QueryStr))
{
WingOut::Stdout.Print(TEXT("ERROR: Queries must be an array of strings.\n"));
return;
}
QueryStrings.Add(QueryStr);
}
FWingGraphActions GraphActions(TargetGraph); FWingGraphActions GraphActions(TargetGraph);
for (const FString& Query : QueryStrings)
{
WingOut::Stdout.Printf(TEXT("\n=== %s ===\n\n"), *Query);
TArray<FWingGraphAction*> Results = GraphActions.Search(Query, MaxResults, false); TArray<FWingGraphAction*> Results = GraphActions.Search(Query, MaxResults, false);
for (const FWingGraphAction* Action : Results) for (const FWingGraphAction* Action : Results)
{ {
@@ -56,4 +73,5 @@ public:
WingOut::Stdout.Printf(TEXT("WARNING: Reached limit of %d results. You may specify MaxResults.\n"), MaxResults); WingOut::Stdout.Printf(TEXT("WARNING: Reached limit of %d results. You may specify MaxResults.\n"), MaxResults);
} }
} }
}
}; };

View File

@@ -0,0 +1,40 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingBasics.h"
#include "Sequence.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Sequence : public UWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Description=
"Array of subcommand JSON objects to execute in order. Each must contain 'command' and its parameters."))
FWingJsonArray Subcommands;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Execute multiple commands in one request. Each subcommand produces its own content block in the response. "
"Nested Sequence commands are not allowed."));
}
virtual void Handle() override
{
// The actual code that implements Sequence is hardwired into
// WingServer. Because of that, this handler is never actually called
// under normal conditions. The handler exists for two reasons: to
// provide documentation, and also to catch the case where somebody
// nests a sequence inside another sequence (WingServer doesn't catch
// that).
//
WingOut::Stdout.Print(TEXT("ERROR: Sequence inside a Sequence is not allowed.\n"));
}
};

View File

@@ -17,10 +17,10 @@ class UWing_Widget_SearchTypes : public UWingHandler
GENERATED_BODY() GENERATED_BODY()
public: public:
UPROPERTY(EditAnywhere, meta=(Description="Query string, can contain *")) UPROPERTY(EditAnywhere, meta=(Description="Array of query strings; each may contain *"))
FString Query; FWingJsonArray Queries;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results (default 50)")) UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results per query (default 50)"))
int32 MaxResults = 50; int32 MaxResults = 50;
virtual void Register() override virtual void Register() override
@@ -31,7 +31,24 @@ public:
} }
virtual void Handle() override virtual void Handle() override
{ {
// Validate all entries are strings before running any searches.
TArray<FString> QueryStrings;
QueryStrings.Reserve(Queries.Array.Num());
for (const TSharedPtr<FJsonValue>& QueryVal : Queries.Array)
{
FString QueryStr;
if (!QueryVal->TryGetString(QueryStr))
{
WingOut::Stdout.Print(TEXT("ERROR: Queries must be an array of strings.\n"));
return;
}
QueryStrings.Add(QueryStr);
}
WingWidgets Widgets; WingWidgets Widgets;
for (const FString& Query : QueryStrings)
{
WingOut::Stdout.Printf(TEXT("\n=== %s ===\n\n"), *Query);
TArray<WingWidgets::Type> Results = Widgets.Search(Query, MaxResults, false); TArray<WingWidgets::Type> Results = Widgets.Search(Query, MaxResults, false);
for (const WingWidgets::Type& Entry : Results) for (const WingWidgets::Type& Entry : Results)
{ {
@@ -47,4 +64,5 @@ public:
WingOut::Stdout.Printf(TEXT("WARNING: Reached limit of %d results. You may specify MaxResults.\n"), MaxResults); WingOut::Stdout.Printf(TEXT("WARNING: Reached limit of %d results. You may specify MaxResults.\n"), MaxResults);
} }
} }
}
}; };

View File

@@ -170,6 +170,70 @@ TStatId UWingServer::GetStatId() const
// ============================================================ // ============================================================
FString UWingServer::HandleRequest(const FString& Line) FString UWingServer::HandleRequest(const FString& Line)
{
// Parse the request as JSON before doing anything else.
TSharedPtr<FJsonValue> Value;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Line);
if (!FJsonSerializer::Deserialize(Reader, Value))
return PackageResponses({TEXT("Invalid Json")});
const TSharedPtr<FJsonObject>* RequestPtr = nullptr;
if (!Value->TryGetObject(RequestPtr))
return PackageResponses({TEXT("Json must be an object")});
TSharedPtr<FJsonObject> Request = *RequestPtr;
FString Command;
Request->TryGetStringField(TEXT("command"), Command);
if (Command == TEXT("Sequence"))
{
const TArray<TSharedPtr<FJsonValue>>* Subcommands = nullptr;
if (!Request->TryGetArrayField(TEXT("subcommands"), Subcommands))
return PackageResponses({TEXT("Sequence requires a 'subcommands' array.")});
TArray<FString> Responses;
Responses.Reserve(Subcommands->Num());
for (const TSharedPtr<FJsonValue>& Sub : *Subcommands)
{
const TSharedPtr<FJsonObject>* SubObjPtr = nullptr;
if (!Sub->TryGetObject(SubObjPtr))
Responses.Add(TEXT("Subcommand must be a JSON object."));
else
Responses.Add(HandleJsonRequest(*SubObjPtr));
}
return PackageResponses(Responses);
}
return PackageResponses({HandleJsonRequest(Request)});
}
FString UWingServer::PackageResponses(const TArray<FString>& Responses)
{
TArray<TSharedPtr<FJsonValue>> Blocks;
Blocks.Reserve(Responses.Num());
for (const FString& Response : Responses)
{
// Unreal's JSON writer terminates string serialization at the first
// embedded null byte rather than escaping it, which would silently
// truncate output. Sanitize null bytes to spaces.
FString Sanitized = Response;
for (int32 i = 0; i < Sanitized.Len(); ++i)
{
if (Sanitized[i] == TEXT('\0')) Sanitized[i] = TEXT(' ');
}
TSharedPtr<FJsonObject> Block = MakeShared<FJsonObject>();
Block->SetStringField(TEXT("type"), TEXT("text"));
Block->SetStringField(TEXT("text"), Sanitized);
Blocks.Add(MakeShared<FJsonValueObject>(Block));
}
FString OutJson;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutJson);
FJsonSerializer::Serialize(Blocks, Writer);
return OutJson;
}
FString UWingServer::HandleJsonRequest(TSharedPtr<FJsonObject> Request)
{ {
LogCapture.CapturedErrors.Empty(); LogCapture.CapturedErrors.Empty();
LogCapture.bEnabled = true; LogCapture.bEnabled = true;
@@ -178,7 +242,7 @@ FString UWingServer::HandleRequest(const FString& Line)
bSuggestHandlerHelp = false; bSuggestHandlerHelp = false;
LastHandler = nullptr; LastHandler = nullptr;
TryCallHandler(Line); TryCallHandler(Request);
Notifier.SendNotifications(); Notifier.SendNotifications();
LogCapture.bEnabled = false; LogCapture.bEnabled = false;
@@ -202,25 +266,11 @@ FString UWingServer::HandleRequest(const FString& Line)
} }
FString Result = WingOut::StdoutBuffer.ToString(); FString Result = WingOut::StdoutBuffer.ToString();
WingOut::StdoutBuffer.Reset(); WingOut::StdoutBuffer.Reset();
for (int32 i = 0; i < Result.Len(); ++i)
{
if (Result[i] == TEXT('\0')) Result[i] = TEXT(' ');
}
return Result; return Result;
} }
void UWingServer::TryCallHandler(const FString &Line) void UWingServer::TryCallHandler(TSharedPtr<FJsonObject> Request)
{ {
// Turn the request string into a JSON tree.
TSharedPtr<FJsonObject> Request;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Line);
FJsonSerializer::Deserialize(Reader, Request);
if (!Request.IsValid())
{
WingOut::Stdout.Printf(TEXT("Request is not valid JSON"));
return;
}
// Extract the command from the request. // Extract the command from the request.
FString Command; FString Command;
if (!Request->TryGetStringField(TEXT("command"), Command)) if (!Request->TryGetStringField(TEXT("command"), Command))

View File

@@ -11,6 +11,7 @@
#include "WingServer.generated.h" #include "WingServer.generated.h"
class FSocket; class FSocket;
class FJsonObject;
/** /**
* UWingServer — editor subsystem that listens on a TCP socket and dispatches * UWingServer — editor subsystem that listens on a TCP socket and dispatches
@@ -61,6 +62,9 @@ public:
static void AddHandler(UObject* Obj, const FString& Name, UObject* Config, EWingHandlerKind Kind, UClass* FactoryClass, const FString& Documentation); static void AddHandler(UObject* Obj, const FString& Name, UObject* Config, EWingHandlerKind Kind, UClass* FactoryClass, const FString& Documentation);
static const TArray<FWingHandlerConfig>& AllHandlers() { return GWingServer->WingHandlerRegistry; } static const TArray<FWingHandlerConfig>& AllHandlers() { return GWingServer->WingHandlerRegistry; }
/** Package a list of response texts into a single serialized JSON content-block array. */
static FString PackageResponses(const TArray<FString>& Responses);
private: private:
static UWingServer* GWingServer; static UWingServer* GWingServer;
@@ -78,7 +82,8 @@ private:
// Handle a complete JSON line and return the response JSON // Handle a complete JSON line and return the response JSON
FString HandleRequest(const FString& Line); FString HandleRequest(const FString& Line);
void TryCallHandler(const FString &Line); FString HandleJsonRequest(TSharedPtr<FJsonObject> Request);
void TryCallHandler(TSharedPtr<FJsonObject> Request);
// ----- TCP server ----- // ----- TCP server -----
FSocket* ListenSocket = nullptr; FSocket* ListenSocket = nullptr;

View File

@@ -128,11 +128,14 @@ def handle_message(msg):
arguments = params.get("arguments", {}) arguments = params.get("arguments", {})
result = forward_to_editor(arguments) result = forward_to_editor(arguments)
if isinstance(result, dict) and "error" in result: if isinstance(result, dict) and "error" in result:
text = result["error"] content = [{"type": "text", "text": result["error"]}]
else: else:
text = result try:
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": [{"type": "text", "text": text}], "content": content,
}) })
return { return {

View File

@@ -57,9 +57,21 @@ def main():
try: try:
parsed = json.loads(result) parsed = json.loads(result)
print(json.dumps(parsed, indent=2))
except json.JSONDecodeError: except json.JSONDecodeError:
print(result) print("Error: response is not valid JSON.")
sys.exit(1)
if not isinstance(parsed, list):
print("Error: response is not a list of content blocks.")
sys.exit(1)
for block in parsed:
if not (isinstance(block, dict)
and block.get("type") == "text"
and isinstance(block.get("text"), str)):
print("Error: response contains a non-text block.")
sys.exit(1)
print("\n---\n".join(block["text"] for block in parsed))
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

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

View File

@@ -28,7 +28,7 @@ void UlxUserWidget::BackupInputComponent()
} }
} }
void UlxUserWidget::DisableEventBinding(const UInputAction* InputAction) void UlxUserWidget::DisableInputAction(const UInputAction* InputAction)
{ {
UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(InputComponent); UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(InputComponent);
if (!EIC) return; if (!EIC) return;
@@ -40,9 +40,9 @@ void UlxUserWidget::DisableEventBinding(const UInputAction* InputAction)
}); });
} }
void UlxUserWidget::RestoreInputBinding(const UInputAction* InputAction) void UlxUserWidget::RestoreInputAction(const UInputAction* InputAction)
{ {
DisableEventBinding(InputAction); DisableInputAction(InputAction);
UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(InputComponent); UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(InputComponent);
if (!EIC) return; if (!EIC) return;
@@ -59,7 +59,7 @@ void UlxUserWidget::RestoreInputBinding(const UInputAction* InputAction)
void UlxUserWidget::RedirectInputAction(const UInputAction* From, const UInputAction* To) void UlxUserWidget::RedirectInputAction(const UInputAction* From, const UInputAction* To)
{ {
DisableEventBinding(From); DisableInputAction(From);
UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(InputComponent); UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(InputComponent);
if (!EIC) return; if (!EIC) return;

View File

@@ -22,20 +22,18 @@ public:
// from the component and reinstated without losing their delegates. // from the component and reinstated without losing their delegates.
void BackupInputComponent(); void BackupInputComponent();
// Remove every live event binding whose action is InputAction. // Removes all handlers for 'InputAction'. That includes temporarily
// No-op if there are none, or if InputComponent isn't enhanced. // deactivating event graph nodes that handle 'InputAction'.
UFUNCTION(BlueprintCallable, Category="Luprex|Widget Enhanced Input") UFUNCTION(BlueprintCallable, Category="Luprex|Widget Enhanced Input")
void DisableEventBinding(const UInputAction* InputAction); void DisableInputAction(const UInputAction* InputAction);
// Replace any live bindings for InputAction with fresh clones of every // Reactivates any graph nodes that handle 'InputAction', and
// saved binding for that action. Leaves the backup array intact so this // removes any other handlers for 'InputAction'.
// can be called repeatedly.
UFUNCTION(BlueprintCallable, Category="Luprex|Widget Enhanced Input") UFUNCTION(BlueprintCallable, Category="Luprex|Widget Enhanced Input")
void RestoreInputBinding(const UInputAction* InputAction); void RestoreInputAction(const UInputAction* InputAction);
// Install live bindings on From that, when fired, dispatch through a // Any event graph nodes that handle 'to' are made to also
// clone of each saved binding for To. Clears any pre-existing live // handle 'From' events. Any other handlers of 'From' are removed.
// bindings on From first. Backup array is untouched.
UFUNCTION(BlueprintCallable, Category="Luprex|Widget Enhanced Input") UFUNCTION(BlueprintCallable, Category="Luprex|Widget Enhanced Input")
void RedirectInputAction(const UInputAction* From, const UInputAction* To); void RedirectInputAction(const UInputAction* From, const UInputAction* To);

View File

@@ -4,24 +4,6 @@
#include "TangibleManager.h" #include "TangibleManager.h"
#include "Kismet/GameplayStatics.h" #include "Kismet/GameplayStatics.h"
#include "Engine/GameInstance.h" #include "Engine/GameInstance.h"
#include "Engine/GameViewportClient.h"
#include "Framework/Application/SlateApplication.h"
#include "Widgets/SViewport.h"
#include "Slate/SObjectWidget.h"
FString AlxPlayerControllerBase::GetUserWidgetName(SWidget *W)
{
while (W)
{
if (W->GetType() == FName("SObjectWidget"))
{
UUserWidget *UW = static_cast<SObjectWidget*>(W)->GetWidgetObject();
if (UW) return UW->GetClass()->GetName();
}
W = W->GetParentWidget().Get();
}
return TEXT("Unknown Widget");
}
AlxPlayerControllerBase *AlxPlayerControllerBase::FromContext(const UObject *Context) AlxPlayerControllerBase *AlxPlayerControllerBase::FromContext(const UObject *Context)
{ {
@@ -71,96 +53,6 @@ FVector2D AlxPlayerControllerBase::GetLookAtPixel(const UObject *Context)
return ScreenPosition; return ScreenPosition;
} }
void AlxPlayerControllerBase::BeginPlay()
{
Super::BeginPlay();
HotkeyInputComponent = NewObject<UInputComponent>(this);
HotkeyInputComponent->bBlockInput = false;
PushInputComponent(HotkeyInputComponent);
}
void AlxPlayerControllerBase::UpdateEventDispatch()
{
EventRequests.GarbageCollect();
// If we're in GameOnly mode, check that focus is still on the viewport.
if (CurrentInputMode == InputMode::GameOnly)
{
UGameViewportClient *GVC = GetWorld() ? GetWorld()->GetGameViewport() : nullptr;
if (GVC)
{
TSharedPtr<SViewport> ViewportWidget = GVC->GetGameViewportWidget();
if (ViewportWidget.IsValid())
{
TSharedPtr<SWidget> Focused = FSlateApplication::Get().GetKeyboardFocusedWidget();
if (Focused.Get() != ViewportWidget.Get())
{
UE_LOG(LogLuprexIntegration, Error, TEXT("In GameOnly mode, keyboard focus must stay on viewport, but was stolen by: %s. Restoring."), *GetUserWidgetName(Focused.Get()));
EventRequests.SetDirty();
}
if (!ViewportWidget->HasMouseCapture())
{
UE_LOG(LogLuprexIntegration, Error, TEXT("In GameOnly mode, viewport must have mouse capture, but lost it. Restoring."));
EventRequests.SetDirty();
}
}
}
}
if (!EventRequests.IsDirty()) return;
EventRequests.ClearDirty();
CurrentInputMode = EventRequests.GetRequestedMode();
const TArray<FlxEventRequest> &Requests = EventRequests.GetRequests();
if (CurrentInputMode == InputMode::UIOnly)
{
SetInputMode(FInputModeUIOnly().SetWidgetToFocus(Requests[0].Widget->GetCachedWidget()));
}
else
{
SetInputMode(FInputModeGameOnly());
HotkeyInputComponent->KeyBindings.Empty();
TSet<FKey> BoundKeys;
for (const FlxEventRequest &Req : Requests)
{
for (const FKey &Key : Req.Hotkeys)
{
if (!BoundKeys.Contains(Key))
{
BoundKeys.Add(Key);
HotkeyInputComponent->BindKey(Key, IE_Pressed, this, &AlxPlayerControllerBase::ForwardKeyEvent);
}
}
}
}
}
void AlxPlayerControllerBase::ForwardKeyEvent(FKey Key)
{
// TODO: implement
}
void AlxPlayerControllerBase::RequestEvents(const FlxEventRequest &Request)
{
if (!FlxEventRequests::SanityCheck(Request)) return;
AlxPlayerControllerBase *PC = FromContext(Request.Widget);
PC->EventRequests.Request(Request);
}
void AlxPlayerControllerBase::UnRequestEvents(UUserWidget *Widget)
{
if (Widget == nullptr)
{
UE_LOG(LogLuprexIntegration, Error, TEXT("UnRequestEvents called with null widget."));
return;
}
AlxPlayerControllerBase *PC = FromContext(Widget);
PC->EventRequests.Remove(Widget);
}
void AlxPlayerControllerBase::UpdateLookAt() void AlxPlayerControllerBase::UpdateLookAt()
{ {
UlxTangibleManager *TM = GetGameInstance()->GetSubsystem<UlxTangibleManager>(); UlxTangibleManager *TM = GetGameInstance()->GetSubsystem<UlxTangibleManager>();

View File

@@ -3,7 +3,6 @@
#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)
@@ -12,8 +11,6 @@ class INTEGRATION_API AlxPlayerControllerBase : public APlayerController
GENERATED_BODY() GENERATED_BODY()
public: public:
using InputMode = FlxEventRequests::InputMode;
UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context"), Category = "Luprex|Look-At Detection") UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context"), Category = "Luprex|Look-At Detection")
static void SetLookAt(const UObject *Context, const FHitResult &HitResult); static void SetLookAt(const UObject *Context, const FHitResult &HitResult);
@@ -29,12 +26,6 @@ public:
UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context"), Category = "Luprex|Look-At Detection") UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context"), Category = "Luprex|Look-At Detection")
static void SetLookAtChanged(const UObject *Context); static void SetLookAtChanged(const UObject *Context);
UFUNCTION(BlueprintCallable, Category = "Luprex|Input Events")
static void RequestEvents(const FlxEventRequest &Request);
UFUNCTION(BlueprintCallable, Category = "Luprex|Input Events")
static void UnRequestEvents(UUserWidget *Widget);
// Blueprint events // Blueprint events
UFUNCTION(BlueprintImplementableEvent, Category = "Luprex|Look-At Detection") UFUNCTION(BlueprintImplementableEvent, Category = "Luprex|Look-At Detection")
void CalculateLookAt(); void CalculateLookAt();
@@ -42,35 +33,14 @@ public:
UFUNCTION(BlueprintImplementableEvent, Category = "Luprex|Look-At Detection") UFUNCTION(BlueprintImplementableEvent, Category = "Luprex|Look-At Detection")
void LookAtChanged(); void LookAtChanged();
virtual void BeginPlay() override;
// Called by GameMode each tick. // Called by GameMode each tick.
void UpdateLookAt(); void UpdateLookAt();
// Rebuild input component and switch input mode.
void UpdateEventDispatch();
// Handler for GameOnly mode hotkey presses.
void ForwardKeyEvent(FKey Key);
// Walk up from a Slate widget to find the nearest UMG widget class name.
static FString GetUserWidgetName(SWidget *W);
// 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()
FlxEventRequests EventRequests;
// Input component for GameOnly mode: catches hotkeys only.
UPROPERTY()
UInputComponent *HotkeyInputComponent = nullptr;
// Current input mode.
InputMode CurrentInputMode = InputMode::GameOnly;
bool MustCallLookAtChanged = false; bool MustCallLookAtChanged = false;
}; };