Compare commits
13 Commits
94e6385f14
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d737879ed6 | |||
| 46051526e6 | |||
| 8dab0d16b7 | |||
| 933c1ac6c3 | |||
| 521d4726ad | |||
| 2bfa3024f1 | |||
| f7983b1f02 | |||
| 36ec4a3b9b | |||
| 1c2be1b4d8 | |||
| 0b23c82e73 | |||
| b1defd821b | |||
| e17f5417f2 | |||
| c0848c2670 |
@@ -25,9 +25,16 @@
|
||||
- `Docs/` — Documentation.
|
||||
- `Config/` — Unreal config files
|
||||
- `EnginePatches/` — Custom engine modifications
|
||||
- `Plugins/UEWingman/` - A plugin that gives you control over the unreal editor. Drive it from bash via `python3 Plugins/UEWingman/ue-wingman.py <Command> key=value ...` (values starting with `[` or `{` are parsed as JSON).
|
||||
- `Plugins/UEWingman/` - A plugin that gives you control over the unreal editor.
|
||||
- `../integration.UE/` - the unreal engine source tree
|
||||
|
||||
## Using ue-wingman
|
||||
|
||||
- Drive it from bash using: ue-wingman <Command> <Arg1> <Arg2> ...
|
||||
- ue-wingman Documentation_Manual
|
||||
- ue-wingman Documentation_Commands
|
||||
- ue-wingman Documentation_Command <specific_command>
|
||||
|
||||
## Coding Conventions
|
||||
|
||||
- Prefer early returns and `continue` to reduce nesting (never-nester style).
|
||||
|
||||
BIN
Content/Luprex/InputActions/IA_Menu.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_Menu.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/lxGameMode.uasset
LFS
BIN
Content/Luprex/lxGameMode.uasset
LFS
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Content/Testing/WB_Test.uasset
LFS
BIN
Content/Testing/WB_Test.uasset
LFS
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Content/Widgets/WB_Menu.uasset
LFS
BIN
Content/Widgets/WB_Menu.uasset
LFS
Binary file not shown.
Binary file not shown.
BIN
Content/Widgets/basic-border.uasset
LFS
Normal file
BIN
Content/Widgets/basic-border.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Widgets/teardrop.uasset
LFS
Normal file
BIN
Content/Widgets/teardrop.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Widgets/white-dot.uasset
LFS
BIN
Content/Widgets/white-dot.uasset
LFS
Binary file not shown.
Binary file not shown.
BIN
Content/testing/bp_test.uasset
LFS
BIN
Content/testing/bp_test.uasset
LFS
Binary file not shown.
@@ -1,20 +1,12 @@
|
||||
|
||||
* 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
|
||||
|
||||
* Menus
|
||||
* Add a slash-command to reload lua source code.
|
||||
|
||||
* Skeletal Mesh Tangible
|
||||
|
||||
* Implement Interactive Temporary Variables
|
||||
|
||||
* A better text console
|
||||
|
||||
* Get rid of 3x3 Gridpanel stuff
|
||||
|
||||
* Object-Oriented Lua Support
|
||||
|
||||
|
||||
|
||||
@@ -12,41 +12,21 @@ class UWing_Documentation_Manual : public UWingHandler
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, meta=(Description="section of the manual"))
|
||||
FString Section;
|
||||
|
||||
virtual void Register() override
|
||||
{
|
||||
TStringBuilder<128> Docs;
|
||||
Docs.Append(TEXT("Print a section of the manual. Valid sections: "));
|
||||
WingManual::PrintSectionNames(nullptr, WingManual::GetSections(), Docs);
|
||||
UWingServer::AddHandler(this, Docs.ToString());
|
||||
UWingServer::AddHandler(this, TEXT("Print the entire manual."));
|
||||
}
|
||||
virtual void Handle() override
|
||||
{
|
||||
TSet<FName> Sections = WingManual::GetSections();
|
||||
if (Section.IsEmpty())
|
||||
{
|
||||
UWingManualSections::FetcherPaths();
|
||||
UWingManualSections::ExpressingTypes();
|
||||
UWingManualSections::VariableDeclarations();
|
||||
UWingManualSections::EscapeSequencesInFNames();
|
||||
UWingManualSections::MaterialEditing();
|
||||
UWingManualSections::ImportantCommands();
|
||||
}
|
||||
else
|
||||
{
|
||||
FName SectionName(*Section);
|
||||
if (WingManual::PrintSection(SectionName))
|
||||
{
|
||||
WingOut::Stdout.Printf(TEXT("\n"));
|
||||
WingManual::PrintSectionNames(TEXT("Other manual sections:"), Sections, WingOut::Stdout);
|
||||
}
|
||||
else
|
||||
{
|
||||
WingOut::Stdout.Printf(TEXT("Unknown manual section '%s'\n"), *Section);
|
||||
WingManual::PrintSectionNames(TEXT("Valid manual sections:"), Sections, WingOut::Stdout);
|
||||
}
|
||||
}
|
||||
UWingManualSections::FetcherPaths();
|
||||
UWingManualSections::ExpressingTypes();
|
||||
UWingManualSections::VariableDeclarations();
|
||||
UWingManualSections::EscapeSequencesInFNames();
|
||||
UWingManualSections::MaterialEditing();
|
||||
UWingManualSections::NodeContextMenus();
|
||||
UWingManualSections::VariableGettersAndSetters();
|
||||
UWingManualSections::BestPerformance();
|
||||
UWingManualSections::ImportantCommands();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "WingBasics.h"
|
||||
#include "WingManual.h"
|
||||
#include "WingServer.h"
|
||||
#include "Documentation_Section.generated.h"
|
||||
|
||||
UCLASS()
|
||||
class UWing_Documentation_Section : public UWingHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, meta=(Description="Manual section"))
|
||||
FString Section;
|
||||
|
||||
virtual void Register() override
|
||||
{
|
||||
TStringBuilder<128> Docs;
|
||||
Docs.Append(TEXT("Print a section of the manual. Valid sections: "));
|
||||
WingManual::PrintSectionNames(nullptr, WingManual::GetSections(), Docs);
|
||||
UWingServer::AddHandler(this, Docs.ToString());
|
||||
}
|
||||
virtual void Handle() override
|
||||
{
|
||||
FName SectionName(*Section);
|
||||
if (WingManual::PrintSection(SectionName))
|
||||
{
|
||||
WingOut::Stdout.Printf(TEXT("\n"));
|
||||
WingManual::PrintSectionNames(TEXT("Other manual sections:"), WingManual::GetSections(), WingOut::Stdout);
|
||||
}
|
||||
else
|
||||
{
|
||||
WingOut::Stdout.Printf(TEXT("Unknown manual section '%s'\n"), *Section);
|
||||
WingManual::PrintSectionNames(TEXT("Valid manual sections:"), WingManual::GetSections(), WingOut::Stdout);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -38,7 +38,7 @@ public:
|
||||
MaterialObj->ForceRecompileForRendering();
|
||||
|
||||
// Wait for compilation to finish, then check for errors
|
||||
FMaterialResource* Resource = MaterialObj->GetMaterialResource(GetFeatureLevelShaderPlatform(GMaxRHIFeatureLevel));
|
||||
FMaterialResource* Resource = MaterialObj->GetMaterialResource(GMaxRHIFeatureLevel);
|
||||
TArray<FString> Errors;
|
||||
if (Resource)
|
||||
{
|
||||
|
||||
@@ -119,7 +119,7 @@ public:
|
||||
|
||||
// Register a variable GUID for the new widget. UMG's compiler
|
||||
// ensures every widget in the tree is present in this map.
|
||||
BP->OnVariableAdded(NewWidget->GetFName());
|
||||
// BP->OnVariableAdded(NewWidget->GetFName());
|
||||
|
||||
WingOut::Stdout.Printf(TEXT("Created widget '%s' of type '%s'\n"), *Name, *Type);
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ UEdGraphNode* FWingGraphAction::Execute(const FVector2D &Location) const
|
||||
}
|
||||
else
|
||||
{
|
||||
return Action->PerformAction(Graph, nullptr, UE::Slate::CastToVector2f(Location), /*bSelectNewNode=*/false);
|
||||
return Action->PerformAction(Graph, nullptr, Location, /*bSelectNewNode=*/false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -221,10 +221,11 @@ void UWingManualSections::ImportantCommands()
|
||||
WingOut::Stdout.Print(TEXT(
|
||||
"\n IMPORTANT COMMANDS:"
|
||||
"\n"
|
||||
"\n Documentation_Manual: print manual sections"
|
||||
"\n Documentation_Commands: A concise list of all ue-wingman commands"
|
||||
"\n Documentation_Command: Detailed documentation for a single ue-wingman command"
|
||||
"\n Documentation_CreateAssets: Additional commands that create new assets"
|
||||
"\n Documentation_Manual: print the entire manual"
|
||||
"\n Documentation_Section: print a single section of the manual"
|
||||
"\n Documentation_Commands: print concise list of all ue-wingman commands"
|
||||
"\n Documentation_Command: detailed documentation for a single ue-wingman command"
|
||||
"\n Documentation_CreateAssets: list of commands that create new assets"
|
||||
"\n Blueprint_Dump: a summary of any blueprint"
|
||||
"\n Graph_Dump: a fairly detailed listing of any Graph"
|
||||
"\n Details_Dump: Dump the details panel for a given object"
|
||||
|
||||
@@ -223,15 +223,21 @@ FString UWingServer::PostCallHandler()
|
||||
return Result;
|
||||
}
|
||||
|
||||
void UWingServer::TryCallHandler(const TArray<FString>& Argv)
|
||||
void UWingServer::TryCallHandler(TArrayView<const FString> Argv)
|
||||
{
|
||||
if (Argv.Num() < 1)
|
||||
FString Command = "Documentation_Manual";
|
||||
if (Argv.Num() > 0)
|
||||
{
|
||||
WingOut::Stdout.Print(TEXT("Missing command\n"));
|
||||
return;
|
||||
Command = Argv[0];
|
||||
Argv = Argv.RightChop(1);
|
||||
}
|
||||
|
||||
FString Command = Argv[0];
|
||||
if ((Command.Equals(TEXT("--help"))) ||
|
||||
(Command.Equals(TEXT("-help"))) ||
|
||||
(Command.Equals(TEXT("help"))))
|
||||
{
|
||||
Command = "Documentation_Manual";
|
||||
}
|
||||
|
||||
// Find the handler for the specified command.
|
||||
FWingHandlerConfig* Found = FindHandler(Command);
|
||||
@@ -250,7 +256,7 @@ void UWingServer::TryCallHandler(const TArray<FString>& Argv)
|
||||
|
||||
// Populate the handler object with argv parameters.
|
||||
TArray<FWingProperty> Props = FWingProperty::GetVisible(Handler, true);
|
||||
if (!FWingProperty::PopulateFromArgv(Props, MakeArrayView(Argv).RightChop(1), WingOut::Stdout))
|
||||
if (!FWingProperty::PopulateFromArgv(Props, Argv, WingOut::Stdout))
|
||||
{
|
||||
UWingServer::SuggestHandlerHelp();
|
||||
return;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Materials/MaterialParameters.h"
|
||||
#include "MaterialTypes.h"
|
||||
#include "WingBasics.h"
|
||||
#include "WingParameterEditor.generated.h"
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ private:
|
||||
TArray<uint8> HandleRequest(const TArray<uint8>& RequestBytes);
|
||||
void PreCallHandler();
|
||||
FString PostCallHandler();
|
||||
void TryCallHandler(const TArray<FString>& Argv);
|
||||
void TryCallHandler(TArrayView<const FString> Argv);
|
||||
|
||||
// ----- TCP server -----
|
||||
FSocket* ListenSocket = nullptr;
|
||||
|
||||
@@ -317,6 +317,12 @@ FFormatArgumentData UlxFormatDataLibrary::FormatArgumentDataEnum(uint8 Value, co
|
||||
return Result;
|
||||
}
|
||||
|
||||
FText UlxFormatDataLibrary::FormatMessageInternal(const FString &InPattern, TArray<FFormatArgumentData> InArgs)
|
||||
{
|
||||
FText InPatternText(FText::FromString(InPattern));
|
||||
return FTextFormatter::Format(MoveTemp(InPatternText), MoveTemp(InArgs), false, false);
|
||||
}
|
||||
|
||||
void UlxFormatDataLibrary::FormatLogMessageInternal(UObject *Context, ElxFormatLogVerbosity Verbosity, const FString &InPattern, TArray<FFormatArgumentData> InArgs)
|
||||
{
|
||||
// For throttled verbosity levels, suppress repeated messages with the
|
||||
@@ -338,8 +344,7 @@ void UlxFormatDataLibrary::FormatLogMessageInternal(UObject *Context, ElxFormatL
|
||||
|
||||
// Generate the formatted string.
|
||||
//
|
||||
FText InPatternText(FText::FromString(InPattern));
|
||||
FText Message = FTextFormatter::Format(MoveTemp(InPatternText), MoveTemp(InArgs), false, false);
|
||||
FText Message = FormatMessageInternal(InPattern, MoveTemp(InArgs));
|
||||
FString MessageString = Message.ToString();
|
||||
|
||||
// Get the blueprint name.
|
||||
|
||||
@@ -105,9 +105,16 @@ public:
|
||||
//
|
||||
static UFunction* GetConverterForPinType(const UEdGraphSchema_K2 *Schema, const FEdGraphPinType& PinType, bool AllowWild);
|
||||
|
||||
// Format a message using FTextFormatter::Format.
|
||||
// Meant to be used internally by the Format Message K2Node,
|
||||
// which needs an impure wrapper around formatting.
|
||||
//
|
||||
UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly = "true"))
|
||||
static FText FormatMessageInternal(const FString &InPattern, TArray<FFormatArgumentData> InArgs);
|
||||
|
||||
// Format a message using FTextFormatter::Format, and send
|
||||
// it to UE_LOG. The Context object's name is used as the
|
||||
// log category. Meant to be used internally by the Format
|
||||
// log category. Meant to be used internally by the Format
|
||||
// Log Message K2Node.
|
||||
//
|
||||
UFUNCTION(BlueprintCallable, meta=(WorldContext = "Context", BlueprintInternalUseOnly = "true"))
|
||||
|
||||
@@ -250,7 +250,7 @@ void UK2Node_FormatMessage::ExpandNode(class FKismetCompilerContext& CompilerCon
|
||||
}
|
||||
else
|
||||
{
|
||||
FormatFunction = UKismetTextLibrary::StaticClass()->FindFunctionByName(GET_MEMBER_NAME_CHECKED(UKismetTextLibrary, Format));
|
||||
FormatFunction = UlxFormatDataLibrary::StaticClass()->FindFunctionByName(GET_MEMBER_NAME_CHECKED(UlxFormatDataLibrary, FormatMessageInternal));
|
||||
}
|
||||
|
||||
// This is the node that does all the Format work and outputs the message.
|
||||
|
||||
105
Source/Integration/InputDeviceTracker.cpp
Normal file
105
Source/Integration/InputDeviceTracker.cpp
Normal file
@@ -0,0 +1,105 @@
|
||||
|
||||
#include "InputDeviceTracker.h"
|
||||
#include "Framework/Application/SlateApplication.h"
|
||||
#include "GenericPlatform/GenericApplicationMessageHandler.h"
|
||||
#include "InputCoreTypes.h"
|
||||
|
||||
// Last observed device classification. Read with no
|
||||
// synchronization; updates happen on the game thread
|
||||
// from Slate's input pipeline, and stale reads on
|
||||
// other threads are acceptable for this use case.
|
||||
//
|
||||
static ElxControllerType GLastControllerType = ElxControllerType::KeyboardMouse;
|
||||
|
||||
// Keywords identifying a PlayStation-family gamepad.
|
||||
// Matched case-insensitively against the InputDeviceName
|
||||
// and HardwareDeviceIdentifier fields of the current
|
||||
// FInputDeviceScope.
|
||||
//
|
||||
static const TCHAR* const PlaystationKeywords[] = {
|
||||
TEXT("Playstation"),
|
||||
TEXT("PS3"),
|
||||
TEXT("PS4"),
|
||||
TEXT("PS5"),
|
||||
TEXT("PS6"),
|
||||
TEXT("PS7"),
|
||||
TEXT("Dualsense"),
|
||||
TEXT("Dualshock"),
|
||||
};
|
||||
|
||||
namespace
|
||||
{
|
||||
bool ContainsAnyPlaystationKeyword(const FString& Haystack)
|
||||
{
|
||||
for (const TCHAR* Keyword : PlaystationKeywords)
|
||||
{
|
||||
if (Haystack.Contains(Keyword, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Classifies the active gamepad by scanning the current
|
||||
// FInputDeviceScope. Defaults to Xbox; switches to
|
||||
// PlayStation only on a keyword match.
|
||||
//
|
||||
ElxControllerType ClassifyGamepadFromScope()
|
||||
{
|
||||
const FInputDeviceScope* Scope = FInputDeviceScope::GetCurrent();
|
||||
if (Scope != nullptr)
|
||||
{
|
||||
if (ContainsAnyPlaystationKeyword(Scope->InputDeviceName.ToString()) ||
|
||||
ContainsAnyPlaystationKeyword(Scope->HardwareDeviceIdentifier))
|
||||
{
|
||||
return ElxControllerType::PlayStationGamepad;
|
||||
}
|
||||
}
|
||||
return ElxControllerType::XboxGamepad;
|
||||
}
|
||||
}
|
||||
|
||||
bool FInputDeviceTrackerProcessor::HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& KeyEvent)
|
||||
{
|
||||
if (KeyEvent.GetKey().IsGamepadKey())
|
||||
{
|
||||
GLastControllerType = ClassifyGamepadFromScope();
|
||||
}
|
||||
else
|
||||
{
|
||||
GLastControllerType = ElxControllerType::KeyboardMouse;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FInputDeviceTrackerProcessor::HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent)
|
||||
{
|
||||
GLastControllerType = ElxControllerType::KeyboardMouse;
|
||||
return false;
|
||||
}
|
||||
|
||||
void UlxInputDeviceTracker::Initialize(FSubsystemCollectionBase& Collection)
|
||||
{
|
||||
Super::Initialize(Collection);
|
||||
if (FSlateApplication::IsInitialized())
|
||||
{
|
||||
Processor = MakeShared<FInputDeviceTrackerProcessor>();
|
||||
FSlateApplication::Get().RegisterInputPreProcessor(Processor);
|
||||
}
|
||||
}
|
||||
|
||||
void UlxInputDeviceTracker::Deinitialize()
|
||||
{
|
||||
if (Processor.IsValid() && FSlateApplication::IsInitialized())
|
||||
{
|
||||
FSlateApplication::Get().UnregisterInputPreProcessor(Processor);
|
||||
}
|
||||
Processor.Reset();
|
||||
Super::Deinitialize();
|
||||
}
|
||||
|
||||
ElxControllerType UlxInputDeviceTracker::GetLastControllerType()
|
||||
{
|
||||
return GLastControllerType;
|
||||
}
|
||||
64
Source/Integration/InputDeviceTracker.h
Normal file
64
Source/Integration/InputDeviceTracker.h
Normal file
@@ -0,0 +1,64 @@
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// InputDeviceTracker.h
|
||||
//
|
||||
// Tracks the most recently used input device, classifying
|
||||
// it as keyboard/mouse, Xbox gamepad, or PlayStation
|
||||
// gamepad. The subsystem registers a Slate input
|
||||
// preprocessor that watches button-down events; analog
|
||||
// and mouse-move events are ignored. Read the current
|
||||
// classification via the static accessor.
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Subsystems/GameInstanceSubsystem.h"
|
||||
#include "Framework/Application/IInputProcessor.h"
|
||||
#include "Common.h"
|
||||
#include "InputDeviceTracker.generated.h"
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// FInputDeviceTrackerProcessor
|
||||
//
|
||||
// Slate input preprocessor. Updates the device-class
|
||||
// static on each button-down event. Never consumes
|
||||
// events (always returns false).
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
class FInputDeviceTrackerProcessor : public IInputProcessor
|
||||
{
|
||||
public:
|
||||
virtual void Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor) override {}
|
||||
virtual bool HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& KeyEvent) override;
|
||||
virtual bool HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override;
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// UlxInputDeviceTracker
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
UCLASS(MinimalAPI)
|
||||
class UlxInputDeviceTracker : public UGameInstanceSubsystem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||
virtual void Deinitialize() override;
|
||||
|
||||
// Returns the classification of the most recently used
|
||||
// input device. Defaults to KeyboardMouse until the
|
||||
// first gamepad button event is observed.
|
||||
//
|
||||
UFUNCTION(BlueprintCallable, Category="Luprex|Input")
|
||||
static ElxControllerType GetLastControllerType();
|
||||
|
||||
private:
|
||||
TSharedPtr<FInputDeviceTrackerProcessor> Processor;
|
||||
};
|
||||
@@ -31,6 +31,7 @@ public class Integration : ModuleRules
|
||||
"UMG",
|
||||
"UMGEditor",
|
||||
"EditorSubsystem",
|
||||
"ApplicationCore",
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.Add("Slate");
|
||||
|
||||
@@ -73,7 +73,6 @@ void ALuprexGameModeBase::UpdateConsoleOutput() {
|
||||
}
|
||||
}
|
||||
|
||||
#pragma optimize("", off)
|
||||
void ALuprexGameModeBase::UpdateTangibles() {
|
||||
double radius = 1000.0; // Hardwired for now.
|
||||
using TanArray = UlxTangibleManager::TanArray;
|
||||
@@ -132,8 +131,9 @@ void ALuprexGameModeBase::UpdatePossessedTangible() {
|
||||
|
||||
void ALuprexGameModeBase::UpdateLuaSourceCode() {
|
||||
FlxLockedWrapper lockedwrap;
|
||||
if (lockedwrap->get_rescan_lua_source(lockedwrap.Get()))
|
||||
if (lockedwrap->get_rescan_lua_source(lockedwrap.Get()) || ReloadSource)
|
||||
{
|
||||
ReloadSource = false;
|
||||
drvutil::ostringstream srcpak;
|
||||
FString LuprexRoot = FPaths::Combine(FPaths::ProjectDir(), TEXT("luprex"));
|
||||
std::string srcpakerr = drvutil::package_lua_source(TCHAR_TO_UTF8(*LuprexRoot), &srcpak);
|
||||
@@ -259,4 +259,9 @@ ALuprexGameModeBase *ALuprexGameModeBase::FromContext(const UObject *context) {
|
||||
return result;
|
||||
}
|
||||
|
||||
void ALuprexGameModeBase::TriggerReloadSource(const UObject *WorldContextObject) {
|
||||
ALuprexGameModeBase *GameMode = FromContext(WorldContextObject);
|
||||
GameMode->ReloadSource = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -62,6 +62,11 @@ public:
|
||||
|
||||
// Get the current Luprex Game Mode Base, given a Context object.
|
||||
static ALuprexGameModeBase *FromContext(const UObject *Context);
|
||||
|
||||
// Set the ReloadSource flag on the current Luprex game mode, causing
|
||||
// the Lua source to be reloaded on the next tick.
|
||||
UFUNCTION(BlueprintCallable, Category = "Luprex|Miscellaneous", meta = (WorldContext = "WorldContextObject"))
|
||||
static void TriggerReloadSource(const UObject *WorldContextObject);
|
||||
|
||||
// The sensitivity level at which a log message triggers a debugger breakpoint.
|
||||
UPROPERTY(EditAnywhere, Category="Debugging Tools")
|
||||
@@ -76,6 +81,9 @@ public:
|
||||
// This is always true unless you use the debugger to set it to false.
|
||||
bool TickEnabled = true;
|
||||
|
||||
// True to trigger a source reload.
|
||||
bool ReloadSource = false;
|
||||
|
||||
// Current Player ID
|
||||
int64 PlayerId = 0;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "PromptWidget.h"
|
||||
#include "InputDeviceTracker.h"
|
||||
#include "UtilityLibrary.h"
|
||||
#include "PlayerControllerBase.h"
|
||||
#include "InputAction.h"
|
||||
@@ -194,7 +195,7 @@ void UlxPromptWidget::SetKeysFromBindings(const UInputMappingContext* InputMappi
|
||||
|
||||
bool UlxPromptWidget::OnTick(float DeltaTime)
|
||||
{
|
||||
ElxControllerType Type = UlxUtilityLibrary::DetectControllerType(GetOwningLocalPlayer());
|
||||
ElxControllerType Type = UlxInputDeviceTracker::GetLastControllerType();
|
||||
if (ControllerType != Type)
|
||||
{
|
||||
ControllerType = Type;
|
||||
@@ -207,7 +208,7 @@ TSharedRef<SWidget> UlxPromptWidget::RebuildWidget()
|
||||
{
|
||||
if (!IsDesignTime())
|
||||
{
|
||||
ControllerType = UlxUtilityLibrary::DetectControllerType(GetOwningLocalPlayer());
|
||||
ControllerType = UlxInputDeviceTracker::GetLastControllerType();
|
||||
TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &UlxPromptWidget::OnTick));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +1,73 @@
|
||||
#include "RadialMenu.h"
|
||||
#include "Rendering/DrawElements.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "Fonts/FontMeasure.h"
|
||||
#include "Framework/Application/SlateApplication.h"
|
||||
#include "Styling/SlateBrush.h"
|
||||
#include "Widgets/SLeafWidget.h"
|
||||
|
||||
|
||||
void URadialMenuLayout::Configure(int32 NItems, float ItemHeight, float InnerRadius, float MinSpoke, float Spread)
|
||||
TArray<FRadialMenuItem> FRadialMenuItem::Calculate(const FRadialMenuConfig &Config)
|
||||
{
|
||||
NumRight = (NItems / 2);
|
||||
NumLeft = NItems - NumRight;
|
||||
Items.SetNum(NItems);
|
||||
TArray<FRadialMenuItem> Items;
|
||||
const int32 NumItems = Config.MenuItems.Num();
|
||||
if (NumItems <= 0) return Items;
|
||||
|
||||
int32 NumRight = (NumItems / 2);
|
||||
int32 NumLeft = NumItems - NumRight;
|
||||
Items.SetNum(NumItems);
|
||||
|
||||
// Measure each non-empty label first; the spoke layout's item height
|
||||
// is derived from the tallest label.
|
||||
const TSharedRef<FSlateFontMeasure> FontMeasure = FSlateApplication::Get().GetRenderer()->GetFontMeasureService();
|
||||
double MaxTextHeight = 5.0;
|
||||
for (int32 I = 0; I < NumItems; I++)
|
||||
{
|
||||
if (Config.MenuItems[I].IsEmpty()) continue;
|
||||
Items[I].TextSize = FontMeasure->Measure(Config.MenuItems[I], Config.Font);
|
||||
MaxTextHeight = FMath::Max(MaxTextHeight, Items[I].TextSize.Y);
|
||||
}
|
||||
const float ItemHeight = static_cast<float>(MaxTextHeight * 1.2);
|
||||
|
||||
View LeftItems(Items.GetData(), NumLeft);
|
||||
View RightItems(Items.GetData() + NumLeft, NumRight);
|
||||
|
||||
CalculateSpokes(LeftItems, ItemHeight, InnerRadius, MinSpoke);
|
||||
CalculateSpokes(RightItems, ItemHeight, InnerRadius, MinSpoke);
|
||||
CalculateSpokes(LeftItems, NumItems, ItemHeight, Config.InnerRadius, Config.MinSpoke);
|
||||
CalculateSpokes(RightItems, NumItems, ItemHeight, Config.InnerRadius, Config.MinSpoke);
|
||||
double LeftWidth = WidestSpoke(LeftItems);
|
||||
double RightWidth = WidestSpoke(RightItems);
|
||||
double HalfWidth = FMath::Max(LeftWidth, RightWidth) + Spread;
|
||||
double HalfWidth = FMath::Max(LeftWidth, RightWidth) + Config.Spread;
|
||||
CalculateSpread(LeftItems, HalfWidth - LeftWidth);
|
||||
CalculateSpread(RightItems, HalfWidth - RightWidth);
|
||||
|
||||
FlipHorizontal(LeftItems);
|
||||
return Items;
|
||||
}
|
||||
|
||||
FVector2D URadialMenuLayout::SpokeVector(int32 I, int32 NSide, int32 NTotal)
|
||||
TArray<FVector2D> FRadialMenuItem::CalculateDirections(int32 NumItems)
|
||||
{
|
||||
TArray<FVector2D> Result;
|
||||
if (NumItems <= 0) return Result;
|
||||
|
||||
const int32 NumRight = (NumItems / 2);
|
||||
const int32 NumLeft = NumItems - NumRight;
|
||||
Result.SetNum(NumItems);
|
||||
|
||||
// Left side: SpokeVector with X flipped (matches FlipHorizontal in Calculate).
|
||||
for (int32 I = 0; I < NumLeft; I++)
|
||||
{
|
||||
FVector2D V = SpokeVector(I, NumLeft, NumItems);
|
||||
V.X = -V.X;
|
||||
Result[I] = V;
|
||||
}
|
||||
for (int32 I = 0; I < NumRight; I++)
|
||||
{
|
||||
Result[NumLeft + I] = SpokeVector(I, NumRight, NumItems);
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
FVector2D FRadialMenuItem::SpokeVector(int32 I, int32 NSide, int32 NTotal)
|
||||
{
|
||||
double SpokeAngle = 1.0 / NTotal;
|
||||
double OffsetAngle = 0.5 * (0.5 - ((NSide - 1) * SpokeAngle));
|
||||
@@ -34,7 +76,7 @@ FVector2D URadialMenuLayout::SpokeVector(int32 I, int32 NSide, int32 NTotal)
|
||||
return FVector2D(FMath::Sin(Radians), -FMath::Cos(Radians));
|
||||
}
|
||||
|
||||
void URadialMenuLayout::FlipHorizontal(View V)
|
||||
void FRadialMenuItem::FlipHorizontal(View V)
|
||||
{
|
||||
for (FRadialMenuItem& Item : V)
|
||||
{
|
||||
@@ -45,7 +87,7 @@ void URadialMenuLayout::FlipHorizontal(View V)
|
||||
}
|
||||
}
|
||||
|
||||
double URadialMenuLayout::WidestSpoke(View V)
|
||||
double FRadialMenuItem::WidestSpoke(View V)
|
||||
{
|
||||
double Result = 0.0;
|
||||
for (const FRadialMenuItem &Item : V)
|
||||
@@ -55,7 +97,7 @@ double URadialMenuLayout::WidestSpoke(View V)
|
||||
return Result;
|
||||
}
|
||||
|
||||
void URadialMenuLayout::CalculateSpread(View V, double Offset)
|
||||
void FRadialMenuItem::CalculateSpread(View V, double Offset)
|
||||
{
|
||||
for (FRadialMenuItem &Item : V)
|
||||
{
|
||||
@@ -63,7 +105,7 @@ void URadialMenuLayout::CalculateSpread(View V, double Offset)
|
||||
}
|
||||
}
|
||||
|
||||
void URadialMenuLayout::CalculateSpokes(View V, float ItemHeight, float InnerRadius, float MinSpoke)
|
||||
void FRadialMenuItem::CalculateSpokes(View V, int32 TotalItems, float ItemHeight, float InnerRadius, float MinSpoke)
|
||||
{
|
||||
if (V.Num() == 0) return;
|
||||
|
||||
@@ -72,7 +114,7 @@ void URadialMenuLayout::CalculateSpokes(View V, float ItemHeight, float InnerRad
|
||||
for (int32 I = 0; I < V.Num(); I++)
|
||||
{
|
||||
V[I].RightSide = true;
|
||||
V[I].Point1 = SpokeVector(I, V.Num(), Items.Num()) * InnerRadius;
|
||||
V[I].Point1 = SpokeVector(I, V.Num(), TotalItems) * InnerRadius;
|
||||
}
|
||||
|
||||
// Calculate point2 for all spokes.
|
||||
@@ -86,7 +128,7 @@ void URadialMenuLayout::CalculateSpokes(View V, float ItemHeight, float InnerRad
|
||||
}
|
||||
for (int32 I = Mid; I < V.Num(); I++)
|
||||
{
|
||||
FVector2D UnitVec = SpokeVector(I, V.Num(), Items.Num());
|
||||
FVector2D UnitVec = SpokeVector(I, V.Num(), TotalItems);
|
||||
double Y = (UnitVec.Y * (InnerRadius + MinSpoke));
|
||||
if (Y < NextLineMin) Y = NextLineMin;
|
||||
NextLineMin = Y + ItemHeight;
|
||||
@@ -114,48 +156,31 @@ public:
|
||||
SLATE_BEGIN_ARGS(SRadialMenu) {}
|
||||
SLATE_END_ARGS()
|
||||
|
||||
void Construct(const FArguments& InArgs, URadialMenuLayout* InLayout)
|
||||
void Construct(const FArguments& InArgs, const FRadialMenuConfig& InConfig)
|
||||
{
|
||||
Layout = InLayout;
|
||||
Config = InConfig;
|
||||
}
|
||||
|
||||
void Refresh()
|
||||
void SetConfig(const FRadialMenuConfig& NewConfig)
|
||||
{
|
||||
Config = NewConfig;
|
||||
Items = FRadialMenuItem::Calculate(Config);
|
||||
Invalidate(EInvalidateWidgetReason::Layout);
|
||||
}
|
||||
|
||||
void SetLineThickness(float NewLineThickness)
|
||||
void SetPointer(FVector2D NewPointer)
|
||||
{
|
||||
LineThickness = NewLineThickness;
|
||||
Invalidate(EInvalidateWidgetReason::Paint);
|
||||
}
|
||||
|
||||
void SetLineColor(FLinearColor NewLineColor)
|
||||
{
|
||||
LineColor = NewLineColor;
|
||||
Invalidate(EInvalidateWidgetReason::Paint);
|
||||
}
|
||||
|
||||
void SetDotTexture(UTexture2D* NewDotTexture)
|
||||
{
|
||||
DotTexture = NewDotTexture;
|
||||
DotBrush.SetResourceObject(NewDotTexture);
|
||||
Invalidate(EInvalidateWidgetReason::Paint);
|
||||
}
|
||||
|
||||
void SetDotRadius(float NewDotRadius)
|
||||
{
|
||||
DotRadius = NewDotRadius;
|
||||
if (PointerVector == NewPointer) return;
|
||||
PointerVector = NewPointer;
|
||||
Invalidate(EInvalidateWidgetReason::Paint);
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual FVector2D ComputeDesiredSize(float) const override
|
||||
{
|
||||
if (!Layout.IsValid()) return FVector2D::ZeroVector;
|
||||
FVector2D Min(0.0, 0.0);
|
||||
FVector2D Max(0.0, 0.0);
|
||||
for (const FRadialMenuItem &Item : Layout->GetItems())
|
||||
for (const FRadialMenuItem &Item : Items)
|
||||
{
|
||||
for (const FVector2D &P : { Item.Point1, Item.Point2, Item.Point3 })
|
||||
{
|
||||
@@ -164,6 +189,14 @@ protected:
|
||||
Max.X = FMath::Max(Max.X, P.X);
|
||||
Max.Y = FMath::Max(Max.Y, P.Y);
|
||||
}
|
||||
if (Item.TextSize.X <= 0) continue;
|
||||
const double Gap = Config.Spread * 0.5;
|
||||
const double TextLeft = Item.RightSide ? (Item.Point3.X + Gap) : (Item.Point3.X - Gap - Item.TextSize.X);
|
||||
const double TextRight = Item.RightSide ? (Item.Point3.X + Gap + Item.TextSize.X) : (Item.Point3.X - Gap);
|
||||
Min.X = FMath::Min(Min.X, TextLeft);
|
||||
Max.X = FMath::Max(Max.X, TextRight);
|
||||
Min.Y = FMath::Min(Min.Y, Item.Point3.Y - Item.TextSize.Y * 0.5);
|
||||
Max.Y = FMath::Max(Max.Y, Item.Point3.Y + Item.TextSize.Y * 0.5);
|
||||
}
|
||||
// Symmetric around the origin so the menu draws centered.
|
||||
double HalfW = FMath::Max(FMath::Abs(Min.X), FMath::Abs(Max.X));
|
||||
@@ -175,120 +208,121 @@ protected:
|
||||
const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements,
|
||||
int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override
|
||||
{
|
||||
if (!Layout.IsValid()) return LayerId;
|
||||
|
||||
const FVector2D Center = AllottedGeometry.GetLocalSize() * 0.5;
|
||||
const FPaintGeometry PaintGeom = AllottedGeometry.ToPaintGeometry();
|
||||
|
||||
for (const FRadialMenuItem &Item : Layout->GetItems())
|
||||
for (int32 I = 0; I < Items.Num(); I++)
|
||||
{
|
||||
const FRadialMenuItem& Item = Items[I];
|
||||
const bool bSelected = (I == Config.SelectedItem);
|
||||
const FLinearColor& Color = bSelected ? Config.SelectedColor : Config.ItemColor;
|
||||
const float Thickness = bSelected ? Config.SelectedThickness : Config.LineThickness;
|
||||
TArray<FVector2D> Points;
|
||||
Points.Add(Center + Item.Point1);
|
||||
Points.Add(Center + Item.Point2);
|
||||
Points.Add(Center + Item.Point3);
|
||||
FSlateDrawElement::MakeLines(OutDrawElements, LayerId, PaintGeom, Points, ESlateDrawEffect::None, LineColor, true, LineThickness);
|
||||
FSlateDrawElement::MakeLines(OutDrawElements, LayerId, PaintGeom, Points, ESlateDrawEffect::None, Color, true, Thickness);
|
||||
}
|
||||
|
||||
if (DotRadius <= 0.0f) return LayerId;
|
||||
if (!DotTexture.IsValid()) return LayerId;
|
||||
|
||||
const FVector2D DotSize(DotRadius * 2.0f);
|
||||
for (const FRadialMenuItem &Item : Layout->GetItems())
|
||||
// Draw item labels, vertically centered on Point3. Right-side items
|
||||
// are left-aligned to Point3; left-side items are right-aligned.
|
||||
for (int32 I = 0; I < Items.Num(); I++)
|
||||
{
|
||||
for (const FVector2D& Point : { Item.Point1, Item.Point3 })
|
||||
const FRadialMenuItem& Item = Items[I];
|
||||
if (Item.TextSize.X <= 0) continue;
|
||||
const bool bSelected = (I == Config.SelectedItem);
|
||||
const FLinearColor& Color = bSelected ? Config.SelectedColor : Config.ItemColor;
|
||||
const double Gap = Config.Spread * 0.5;
|
||||
FVector2D TextPos;
|
||||
TextPos.X = Item.RightSide ? (Item.Point3.X + Gap) : (Item.Point3.X - Gap - Item.TextSize.X);
|
||||
TextPos.Y = Item.Point3.Y - Item.TextSize.Y * 0.5;
|
||||
FSlateDrawElement::MakeText(
|
||||
OutDrawElements,
|
||||
LayerId + 1,
|
||||
AllottedGeometry.ToPaintGeometry(Item.TextSize, FSlateLayoutTransform(Center + TextPos)),
|
||||
Config.MenuItems[I],
|
||||
Config.Font,
|
||||
ESlateDrawEffect::None,
|
||||
Color);
|
||||
}
|
||||
|
||||
if (Config.DotRadius > 0.0f && Config.DotTexture != nullptr)
|
||||
{
|
||||
FSlateBrush DotBrush;
|
||||
DotBrush.SetResourceObject(Config.DotTexture);
|
||||
const FVector2D DotSize(Config.DotRadius * 2.0f);
|
||||
for (int32 I = 0; I < Items.Num(); I++)
|
||||
{
|
||||
const FVector2D LocalPoint = Center + Point;
|
||||
FSlateDrawElement::MakeBox(
|
||||
OutDrawElements,
|
||||
LayerId,
|
||||
AllottedGeometry.ToPaintGeometry(DotSize, FSlateLayoutTransform(LocalPoint - FVector2D(DotRadius))),
|
||||
&DotBrush,
|
||||
ESlateDrawEffect::None,
|
||||
LineColor);
|
||||
const FRadialMenuItem& Item = Items[I];
|
||||
const bool bSelected = (I == Config.SelectedItem);
|
||||
const FLinearColor& Color = bSelected ? Config.SelectedColor : Config.ItemColor;
|
||||
for (const FVector2D& Point : { Item.Point1, Item.Point3 })
|
||||
{
|
||||
const FVector2D LocalPoint = Center + Point;
|
||||
FSlateDrawElement::MakeBox(
|
||||
OutDrawElements,
|
||||
LayerId + 1,
|
||||
AllottedGeometry.ToPaintGeometry(DotSize, FSlateLayoutTransform(LocalPoint - FVector2D(Config.DotRadius))),
|
||||
&DotBrush,
|
||||
ESlateDrawEffect::None,
|
||||
Color);
|
||||
}
|
||||
}
|
||||
}
|
||||
return LayerId;
|
||||
|
||||
// Teardrop: visualizes PointerVector inside InnerRadius. The texture's
|
||||
// tip naturally points up (0,-1); rotate it to align with PointerVector.
|
||||
// When PointerVector is (0,0), the teardrop stays upright at the center.
|
||||
if (Config.TeardropRadius > 0.0f && Config.Teardrop != nullptr)
|
||||
{
|
||||
FSlateBrush TeardropBrush;
|
||||
TeardropBrush.SetResourceObject(Config.Teardrop);
|
||||
const FVector2D TeardropSize(Config.TeardropRadius * 2.0f);
|
||||
const FVector2D TeardropCenter = Center + PointerVector * Config.InnerRadius;
|
||||
const float Angle = PointerVector.IsZero()
|
||||
? 0.0f
|
||||
: static_cast<float>(FMath::Atan2(PointerVector.X, -PointerVector.Y));
|
||||
FSlateDrawElement::MakeRotatedBox(
|
||||
OutDrawElements,
|
||||
LayerId + 1,
|
||||
AllottedGeometry.ToPaintGeometry(TeardropSize, FSlateLayoutTransform(TeardropCenter - FVector2D(Config.TeardropRadius))),
|
||||
&TeardropBrush,
|
||||
ESlateDrawEffect::None,
|
||||
Angle);
|
||||
}
|
||||
return LayerId + 1;
|
||||
}
|
||||
|
||||
private:
|
||||
TWeakObjectPtr<URadialMenuLayout> Layout;
|
||||
float LineThickness = 1.0f;
|
||||
FLinearColor LineColor = FLinearColor::White;
|
||||
TWeakObjectPtr<UTexture2D> DotTexture;
|
||||
FSlateBrush DotBrush;
|
||||
float DotRadius = 0.0f;
|
||||
FRadialMenuConfig Config;
|
||||
|
||||
TArray<FRadialMenuItem> Items;
|
||||
|
||||
FVector2D PointerVector = {0,0};
|
||||
};
|
||||
|
||||
|
||||
void URadialMenuWidget::SetNumItems(int32 NewNumItems)
|
||||
void URadialMenuWidget::SetMenuItems(const TArray<FString>& NewMenuItems)
|
||||
{
|
||||
if (NumItems == NewNumItems) return;
|
||||
if (Config.MenuItems == NewMenuItems) return;
|
||||
|
||||
NumItems = NewNumItems;
|
||||
Config.MenuItems = NewMenuItems;
|
||||
Config.SelectedItem = -1;
|
||||
PointerVector = FVector2D();
|
||||
SynchronizeProperties();
|
||||
}
|
||||
|
||||
void URadialMenuWidget::SetItemHeight(float NewItemHeight)
|
||||
FString URadialMenuWidget::GetSelectedMenuItem() const
|
||||
{
|
||||
if (ItemHeight == NewItemHeight) return;
|
||||
|
||||
ItemHeight = NewItemHeight;
|
||||
SynchronizeProperties();
|
||||
if (!Config.MenuItems.IsValidIndex(Config.SelectedItem)) return FString();
|
||||
return Config.MenuItems[Config.SelectedItem];
|
||||
}
|
||||
|
||||
void URadialMenuWidget::SetInnerRadius(float NewInnerRadius)
|
||||
void URadialMenuWidget::SetSelectedItem(int32 NewSelectedItem)
|
||||
{
|
||||
if (InnerRadius == NewInnerRadius) return;
|
||||
if (Config.SelectedItem == NewSelectedItem) return;
|
||||
|
||||
InnerRadius = NewInnerRadius;
|
||||
SynchronizeProperties();
|
||||
}
|
||||
|
||||
void URadialMenuWidget::SetMinSpoke(float NewMinSpoke)
|
||||
{
|
||||
if (MinSpoke == NewMinSpoke) return;
|
||||
|
||||
MinSpoke = NewMinSpoke;
|
||||
SynchronizeProperties();
|
||||
}
|
||||
|
||||
void URadialMenuWidget::SetSpread(float NewSpread)
|
||||
{
|
||||
if (Spread == NewSpread) return;
|
||||
|
||||
Spread = NewSpread;
|
||||
SynchronizeProperties();
|
||||
}
|
||||
|
||||
void URadialMenuWidget::SetLineThickness(float NewLineThickness)
|
||||
{
|
||||
if (LineThickness == NewLineThickness) return;
|
||||
|
||||
LineThickness = NewLineThickness;
|
||||
SynchronizeProperties();
|
||||
}
|
||||
|
||||
void URadialMenuWidget::SetLineColor(FLinearColor NewLineColor)
|
||||
{
|
||||
if (LineColor == NewLineColor) return;
|
||||
|
||||
LineColor = NewLineColor;
|
||||
SynchronizeProperties();
|
||||
}
|
||||
|
||||
void URadialMenuWidget::SetDotTexture(UTexture2D* NewDotTexture)
|
||||
{
|
||||
if (DotTexture == NewDotTexture) return;
|
||||
|
||||
DotTexture = NewDotTexture;
|
||||
SynchronizeProperties();
|
||||
}
|
||||
|
||||
void URadialMenuWidget::SetDotRadius(float NewDotRadius)
|
||||
{
|
||||
if (DotRadius == NewDotRadius) return;
|
||||
|
||||
DotRadius = NewDotRadius;
|
||||
Config.SelectedItem = NewSelectedItem;
|
||||
SynchronizeProperties();
|
||||
}
|
||||
|
||||
@@ -296,29 +330,64 @@ void URadialMenuWidget::SynchronizeProperties()
|
||||
{
|
||||
Super::SynchronizeProperties();
|
||||
|
||||
if (!Layout)
|
||||
if (Directions.Num() != Config.MenuItems.Num())
|
||||
{
|
||||
Layout = NewObject<URadialMenuLayout>(this);
|
||||
Directions = FRadialMenuItem::CalculateDirections(Config.MenuItems.Num());
|
||||
}
|
||||
Layout->Configure(NumItems, ItemHeight, InnerRadius, MinSpoke, Spread);
|
||||
if (MySlateWidget.IsValid())
|
||||
{
|
||||
MySlateWidget->SetLineThickness(LineThickness);
|
||||
MySlateWidget->SetLineColor(LineColor);
|
||||
MySlateWidget->SetDotTexture(DotTexture);
|
||||
MySlateWidget->SetDotRadius(DotRadius);
|
||||
MySlateWidget->Refresh();
|
||||
MySlateWidget->SetConfig(Config);
|
||||
MySlateWidget->SetPointer(PointerVector);
|
||||
}
|
||||
}
|
||||
|
||||
void URadialMenuWidget::AddPointer(FVector2D Direction, float Scale)
|
||||
{
|
||||
SetPointer(PointerVector + (Direction * Scale), 1.0);
|
||||
}
|
||||
|
||||
void URadialMenuWidget::SetPointer(FVector2D Direction, float Scale)
|
||||
{
|
||||
if ((!IsVisible()) || (Directions.Num() == 0))
|
||||
{
|
||||
PointerVector = FVector2D();
|
||||
SetSelectedItem(-1);
|
||||
if (MySlateWidget.IsValid()) MySlateWidget->SetPointer(PointerVector);
|
||||
return;
|
||||
}
|
||||
|
||||
PointerVector = (Direction * Scale);
|
||||
|
||||
if (PointerVector.Length() < 0.75)
|
||||
{
|
||||
SetSelectedItem(-1);
|
||||
if (MySlateWidget.IsValid()) MySlateWidget->SetPointer(PointerVector);
|
||||
return;
|
||||
}
|
||||
|
||||
if (PointerVector.Length() > 1.0)
|
||||
{
|
||||
PointerVector.Normalize();
|
||||
}
|
||||
|
||||
int32 BestIndex = -1;
|
||||
double BestDot = -2.0;
|
||||
for (int32 I = 0; I < Directions.Num(); I++)
|
||||
{
|
||||
FVector2D SpokeDir = Directions[I];
|
||||
if (!SpokeDir.Normalize()) continue;
|
||||
double Dot = FVector2D::DotProduct(SpokeDir, PointerVector);
|
||||
if (Dot <= BestDot) continue;
|
||||
BestDot = Dot;
|
||||
BestIndex = I;
|
||||
}
|
||||
SetSelectedItem(BestIndex);
|
||||
if (MySlateWidget.IsValid()) MySlateWidget->SetPointer(PointerVector);
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> URadialMenuWidget::RebuildWidget()
|
||||
{
|
||||
if (!Layout)
|
||||
{
|
||||
Layout = NewObject<URadialMenuLayout>(this);
|
||||
}
|
||||
MySlateWidget = SNew(SRadialMenu, Layout);
|
||||
SynchronizeProperties();
|
||||
MySlateWidget = SNew(SRadialMenu, Config);
|
||||
return MySlateWidget.ToSharedRef();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,41 +7,88 @@
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/Widget.h"
|
||||
#include "Fonts/SlateFontInfo.h"
|
||||
#include "RadialMenu.generated.h"
|
||||
|
||||
class SRadialMenu;
|
||||
class UTexture2D;
|
||||
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FRadialMenuConfig
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||
TArray<FString> MenuItems;
|
||||
|
||||
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||
FSlateFontInfo Font;
|
||||
|
||||
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||
float InnerRadius = 20.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||
float MinSpoke = 20.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||
float Spread = 20.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||
float LineThickness = 4.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||
FLinearColor ItemColor = FLinearColor::White;
|
||||
|
||||
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||
TObjectPtr<UTexture2D> DotTexture;
|
||||
|
||||
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||
float DotRadius = 3.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||
TObjectPtr<UTexture2D> Teardrop;
|
||||
|
||||
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||
float TeardropRadius = 0.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||
int32 SelectedItem = -1;
|
||||
|
||||
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||
FLinearColor SelectedColor = FLinearColor::Yellow;
|
||||
|
||||
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||
float SelectedThickness = 2.0f;
|
||||
};
|
||||
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FRadialMenuItem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(BlueprintReadOnly)
|
||||
FVector2D Point1 = {0,0};
|
||||
|
||||
UPROPERTY(BlueprintReadOnly)
|
||||
FVector2D Point2 = {0,0};
|
||||
|
||||
UPROPERTY(BlueprintReadOnly)
|
||||
FVector2D Point3 = {0,0};
|
||||
|
||||
UPROPERTY(BlueprintReadOnly)
|
||||
bool RightSide = false;
|
||||
};
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class URadialMenuLayout : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
UPROPERTY(BlueprintReadOnly)
|
||||
FVector2D TextSize = {0,0};
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable)
|
||||
void Configure(int32 NItems, float ItemHeight, float InnerRadius, float MinSpoke, float Spread);
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
int32 LeftNum() const { return NumLeft; }
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
int32 RightNum() const { return NumRight; }
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
const TArray<FRadialMenuItem> &GetItems() const { return Items; }
|
||||
static TArray<FRadialMenuItem> Calculate(const FRadialMenuConfig &Config);
|
||||
|
||||
// Like Calculate, but only produces the unit-vector direction of
|
||||
// each spoke. Cheaper to evaluate when only the spoke directions
|
||||
// are needed (e.g. for hit-testing a pointer against the wheel).
|
||||
static TArray<FVector2D> CalculateDirections(int32 NumItems);
|
||||
|
||||
private:
|
||||
using View = TArrayView<FRadialMenuItem>;
|
||||
@@ -52,31 +99,24 @@ private:
|
||||
// spokes on both sides. The spokes on a given side are
|
||||
// organized top-to-bottom, and the angle between the
|
||||
// spokes is always equal to (1/NTotal) of the circle.
|
||||
FVector2D SpokeVector(int32 I, int32 NSide, int32 NTotal);
|
||||
static FVector2D SpokeVector(int32 I, int32 NSide, int32 NTotal);
|
||||
|
||||
// Populate Point1 and Point2, these are the
|
||||
// endpoints of the spoke segment. Spokes are
|
||||
// designed to always be long enough to make room
|
||||
// for MinSpoke, but also to make enough room to
|
||||
// keep the menu items from overlapping.
|
||||
void CalculateSpokes(View V, float ItemHeight, float InnerRadius, float MinSpoke);
|
||||
static void CalculateSpokes(View V, int32 TotalItems, float ItemHeight, float InnerRadius, float MinSpoke);
|
||||
|
||||
// Search for the widest spoke, and return its X coordinate.
|
||||
double WidestSpoke(View V);
|
||||
static double WidestSpoke(View V);
|
||||
|
||||
// Populate Point3, this is the endpoint of the spread
|
||||
// line that goes horizontal.
|
||||
void CalculateSpread(View V, double Offset);
|
||||
static void CalculateSpread(View V, double Offset);
|
||||
|
||||
// Flip everything in the specified view horizontally.
|
||||
void FlipHorizontal(View V);
|
||||
|
||||
// The array of items.
|
||||
TArray<FRadialMenuItem> Items;
|
||||
|
||||
// Number of items on the left, and on the right.
|
||||
int32 NumLeft = 0;
|
||||
int32 NumRight = 0;
|
||||
static void FlipHorizontal(View V);
|
||||
};
|
||||
|
||||
|
||||
@@ -87,70 +127,38 @@ class URadialMenuWidget : public UWidget
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable)
|
||||
void SetNumItems(int32 NewNumItems);
|
||||
void SetMenuItems(const TArray<FString>& NewMenuItems);
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
void SetItemHeight(float NewItemHeight);
|
||||
void ClearMenuItems() { SetMenuItems({}); }
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
void SetInnerRadius(float NewInnerRadius);
|
||||
void SetSelectedItem(int32 NewSelectedItem);
|
||||
|
||||
// Returns true if the selected item changed.
|
||||
UFUNCTION(BlueprintCallable)
|
||||
void AddPointer(FVector2D Direction, float Scale);
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
void SetMinSpoke(float NewMinSpoke);
|
||||
void SetPointer(FVector2D Direction, float Scale);
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
void SetSpread(float NewSpread);
|
||||
FVector2D GetPointer() const { return PointerVector; }
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
void SetLineThickness(float NewLineThickness);
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
void SetLineColor(FLinearColor NewLineColor);
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
void SetDotTexture(UTexture2D* NewDotTexture);
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
void SetDotRadius(float NewDotRadius);
|
||||
|
||||
UFUNCTION(BlueprintCallable)
|
||||
URadialMenuLayout* GetLayout() const { return Layout; }
|
||||
FString GetSelectedMenuItem() const;
|
||||
|
||||
protected:
|
||||
UPROPERTY(EditAnywhere)
|
||||
FRadialMenuConfig Config;
|
||||
|
||||
TArray<FVector2D> Directions;
|
||||
|
||||
FVector2D PointerVector = {0,0};
|
||||
|
||||
virtual TSharedRef<SWidget> RebuildWidget() override;
|
||||
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
|
||||
virtual void SynchronizeProperties() override;
|
||||
|
||||
private:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(ClampMin="0", AllowPrivateAccess="true"))
|
||||
int32 NumItems = 8;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true"))
|
||||
float ItemHeight = 20.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true"))
|
||||
float InnerRadius = 20.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true"))
|
||||
float MinSpoke = 20.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true"))
|
||||
float Spread = 20.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true"))
|
||||
float LineThickness = 2.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true"))
|
||||
FLinearColor LineColor = FLinearColor::White;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true"))
|
||||
TObjectPtr<UTexture2D> DotTexture;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Setter, Category="RadialMenu", meta=(AllowPrivateAccess="true"))
|
||||
float DotRadius = 0.0f;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<URadialMenuLayout> Layout;
|
||||
|
||||
TSharedPtr<SRadialMenu> MySlateWidget;
|
||||
};
|
||||
|
||||
226
Source/Integration/ReadSlashCommand.cpp
Normal file
226
Source/Integration/ReadSlashCommand.cpp
Normal file
@@ -0,0 +1,226 @@
|
||||
|
||||
|
||||
#include "ReadSlashCommand.h"
|
||||
|
||||
#include "BlueprintActionDatabaseRegistrar.h"
|
||||
#include "BlueprintNodeSpawner.h"
|
||||
#include "EdGraphSchema_K2.h"
|
||||
#include "K2Node_CallFunction.h"
|
||||
#include "KismetCompiler.h"
|
||||
#include "LuaCall.h"
|
||||
#include "SlashCommand.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "ReadSlashCommand"
|
||||
|
||||
const FName UK2Node_ReadSlashCommand::PrototypePinName(TEXT("Prototype"));
|
||||
const FName UK2Node_ReadSlashCommand::InputValuesPinName(TEXT("Input Values"));
|
||||
const FName UK2Node_ReadSlashCommand::ErrorPinName(TEXT("Error"));
|
||||
|
||||
bool UK2Node_ReadSlashCommand::ParsePrototype(const FString &Prototype, TArray<ParsingStep>& Steps)
|
||||
{
|
||||
TArray<FString> Words;
|
||||
Prototype.ParseIntoArrayWS(Words);
|
||||
|
||||
// The command name must be a slash followed by alphanumerics.
|
||||
if (Words.Num() == 0 || !UlxSlashCommand::IsSlashCommand(Words[0]))
|
||||
{
|
||||
SetErrorMsg(TEXT("The prototype must start with a command name, e.g. \"/foo\"."));
|
||||
Steps.Empty();
|
||||
return false;
|
||||
}
|
||||
|
||||
Steps.SetNum(Words.Num());
|
||||
Steps[0].Word = Words[0];
|
||||
TSet<FName> UsedNames;
|
||||
|
||||
for (int32 i = 1; i < Words.Num(); i++)
|
||||
{
|
||||
const FString& Word = Words[i];
|
||||
FString FuncName = FString(TEXT("Read")) + Word;
|
||||
UFunction* ReadFunc = UlxSlashCommand::StaticClass()->FindFunctionByName(FName(*FuncName));
|
||||
if (ReadFunc == nullptr)
|
||||
{
|
||||
SetErrorMsg(FString::Printf(TEXT("Unknown value type: %s"), *Word));
|
||||
Steps.Empty();
|
||||
return false;
|
||||
}
|
||||
|
||||
FName PinName = AddPrefix(Word, 'R');
|
||||
for (int Suffix = 2; UsedNames.Contains(PinName); Suffix++)
|
||||
{
|
||||
PinName = AddPrefix(FString::Printf(TEXT("%s%d"), *Word, Suffix), 'R');
|
||||
}
|
||||
Steps[i].Word = Word;
|
||||
Steps[i].PinName = PinName;
|
||||
Steps[i].ReadFunction = ReadFunc;
|
||||
UsedNames.Add(PinName);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
FText UK2Node_ReadSlashCommand::GetTooltipText() const
|
||||
{
|
||||
static FText Tooltip = FText::FromString(TEXT(
|
||||
"Parse a slash command.\n"
|
||||
"\n"
|
||||
"The prototype must be a hardwired string. The first word\n"
|
||||
"is the command name; each remaining word names a value to\n"
|
||||
"read and becomes an output pin.\n"
|
||||
"\n"
|
||||
"For example:\n"
|
||||
"\n"
|
||||
" /command Integer Float\n"
|
||||
"\n"
|
||||
"Supported types: Token, Integer, Float, Rest\n"));
|
||||
return Tooltip;
|
||||
}
|
||||
|
||||
void UK2Node_ReadSlashCommand::ReconstructNode()
|
||||
{
|
||||
// Save the value of the Prototype Pin before it gets reconstructed.
|
||||
UEdGraphPin* PrototypePin = FindPin(PrototypePinName);
|
||||
if (PrototypePin != nullptr)
|
||||
{
|
||||
ValuePrototype = PrototypePin->DefaultValue;
|
||||
}
|
||||
|
||||
Super::ReconstructNode();
|
||||
}
|
||||
|
||||
void UK2Node_ReadSlashCommand::AllocateDefaultPins()
|
||||
{
|
||||
Pins.Reset();
|
||||
Super::AllocateDefaultPins();
|
||||
|
||||
TArray<ParsingStep> Steps;
|
||||
ParsePrototype(ValuePrototype, Steps);
|
||||
|
||||
CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Execute);
|
||||
CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Then);
|
||||
CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, ErrorPinName);
|
||||
|
||||
UEdGraphPin *PrototypePin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_String, PrototypePinName);
|
||||
PrototypePin->DefaultValue = ValuePrototype;
|
||||
|
||||
CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Object, UlxSlashCommand::StaticClass(), InputValuesPinName);
|
||||
|
||||
// Create an output pin for each value word.
|
||||
for (int32 i = 1; i < Steps.Num(); i++)
|
||||
{
|
||||
// The value comes back through the function's "Result" out-parameter.
|
||||
CreatePin(EGPD_Output, PropertyToPinType(Steps[i].ReadFunction->FindPropertyByName(TEXT("Result"))), Steps[i].PinName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FText UK2Node_ReadSlashCommand::GetNodeTitle(ENodeTitleType::Type TitleType) const
|
||||
{
|
||||
return LOCTEXT("ReadSlashCommand_Title", "Read Slash Command");
|
||||
}
|
||||
|
||||
FText UK2Node_ReadSlashCommand::GetPinDisplayName(const UEdGraphPin* Pin) const
|
||||
{
|
||||
// These pins don't need labels.
|
||||
if ((Pin->PinName == UEdGraphSchema_K2::PN_Execute) ||
|
||||
(Pin->PinName == UEdGraphSchema_K2::PN_Then) ||
|
||||
(Pin->PinName == PrototypePinName))
|
||||
{
|
||||
return FText::GetEmpty();
|
||||
}
|
||||
|
||||
// Return the pin name, removing R: prefix if present.
|
||||
return FText::FromName(RemovePrefix(Pin->PinName));
|
||||
}
|
||||
|
||||
void UK2Node_ReadSlashCommand::PinDefaultValueChanged(UEdGraphPin* Pin)
|
||||
{
|
||||
if ((Pin->PinName == PrototypePinName) && (Pin->DefaultValue != ValuePrototype))
|
||||
{
|
||||
ReconstructNode();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void UK2Node_ReadSlashCommand::ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
|
||||
{
|
||||
Super::ExpandNode(CompilerContext, SourceGraph);
|
||||
|
||||
TArray<ParsingStep> Steps;
|
||||
if (!ParsePrototype(ValuePrototype, Steps))
|
||||
{
|
||||
CompilerContext.MessageLog.Error(*ErrorMsg);
|
||||
BreakAllNodeLinks();
|
||||
return;
|
||||
}
|
||||
|
||||
UEdGraphPin *InputSlashCommandPin = FindPinChecked(InputValuesPinName);
|
||||
UEdGraphPin *ErrorExecPin = FindPinChecked(ErrorPinName);
|
||||
|
||||
UFunction *CheckCommandFunc = UlxSlashCommand::StaticClass()->FindFunctionByName(TEXT("CheckCommand"));
|
||||
UK2Node_CallFunction *CheckCommandNode = MakeCallFunctionNode(CompilerContext, SourceGraph, CheckCommandFunc);
|
||||
CompilerContext.CopyPinLinksToIntermediate(*InputSlashCommandPin, *CheckCommandNode->FindPinChecked(UEdGraphSchema_K2::PN_Self));
|
||||
CompilerContext.MovePinLinksToIntermediate(*GetExecPin(), *CheckCommandNode->GetExecPin());
|
||||
CheckCommandNode->FindPinChecked(TEXT("Literal"))->DefaultValue = Steps[0].Word;
|
||||
CheckCommandNode->FindPinChecked(TEXT("Prototype"))->DefaultValue = ValuePrototype;
|
||||
CompilerContext.CopyPinLinksToIntermediate(*ErrorExecPin, *CheckCommandNode->FindPinChecked(TEXT("Error")));
|
||||
UEdGraphPin *ThenPin = CheckCommandNode->FindPinChecked(TEXT("Success"));
|
||||
|
||||
for (int32 i = 1; i < Steps.Num(); i++)
|
||||
{
|
||||
UK2Node_CallFunction *ReadNode = MakeCallFunctionNode(CompilerContext, SourceGraph, Steps[i].ReadFunction);
|
||||
CompilerContext.CopyPinLinksToIntermediate(*InputSlashCommandPin, *ReadNode->FindPinChecked(UEdGraphSchema_K2::PN_Self));
|
||||
CompilerContext.CopyPinLinksToIntermediate(*ErrorExecPin, *ReadNode->FindPinChecked(TEXT("Error")));
|
||||
UEdGraphPin *OutputPin = FindPinChecked(Steps[i].PinName);
|
||||
CompilerContext.MovePinLinksToIntermediate(*OutputPin, *ReadNode->FindPinChecked(TEXT("Result")));
|
||||
ThenPin = ChainExecPin(ThenPin, ReadNode, TEXT("Success"));
|
||||
}
|
||||
|
||||
CompilerContext.MovePinLinksToIntermediate(*GetThenPin(), *ThenPin);
|
||||
|
||||
BreakAllNodeLinks();
|
||||
}
|
||||
|
||||
|
||||
UK2Node::ERedirectType UK2Node_ReadSlashCommand::DoPinsMatchForReconstruction(const UEdGraphPin* NewPin, int32 NewPinIndex, const UEdGraphPin* OldPin, int32 OldPinIndex) const
|
||||
{
|
||||
if (IsTemplate() || (GetGraph() == nullptr)) return ERedirectType_None;
|
||||
if ((NewPin->PinName == OldPin->PinName) &&
|
||||
(NewPin->Direction == OldPin->Direction) &&
|
||||
(NewPin->PinType == OldPin->PinType))
|
||||
{
|
||||
return ERedirectType_Name;
|
||||
}
|
||||
return ERedirectType_None;
|
||||
}
|
||||
|
||||
bool UK2Node_ReadSlashCommand::IsConnectionDisallowed(const UEdGraphPin* MyPin, const UEdGraphPin* OtherPin, FString& OutReason) const
|
||||
{
|
||||
// The prototype pin cannot be connected.
|
||||
if (MyPin->PinName == PrototypePinName)
|
||||
{
|
||||
OutReason = LOCTEXT("Error_PrototypeMustBeHardwired", "Value prototype must be a hardwired constant.").ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
return Super::IsConnectionDisallowed(MyPin, OtherPin, OutReason);
|
||||
}
|
||||
|
||||
void UK2Node_ReadSlashCommand::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const
|
||||
{
|
||||
UClass* ActionKey = GetClass();
|
||||
if (ActionRegistrar.IsOpenForRegistration(ActionKey))
|
||||
{
|
||||
UBlueprintNodeSpawner* NodeSpawner = UBlueprintNodeSpawner::Create(GetClass());
|
||||
check(NodeSpawner != nullptr);
|
||||
ActionRegistrar.AddBlueprintAction(ActionKey, NodeSpawner);
|
||||
}
|
||||
}
|
||||
|
||||
FText UK2Node_ReadSlashCommand::GetMenuCategory() const
|
||||
{
|
||||
return FText::FromString(FString(TEXT("Luprex|Slash Commands")));
|
||||
}
|
||||
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
74
Source/Integration/ReadSlashCommand.h
Normal file
74
Source/Integration/ReadSlashCommand.h
Normal file
@@ -0,0 +1,74 @@
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// ReadSlashCommand.h
|
||||
//
|
||||
// K2Node that reads typed values from a UlxLuaValues array.
|
||||
// Takes a prototype string like "string x, float y, int z"
|
||||
// and creates output pins with the appropriate types.
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "LuprexK2Node.h"
|
||||
|
||||
#include "ReadSlashCommand.generated.h"
|
||||
|
||||
class FBlueprintActionDatabaseRegistrar;
|
||||
class FString;
|
||||
class UEdGraph;
|
||||
class UObject;
|
||||
|
||||
UCLASS(MinimalAPI)
|
||||
class UK2Node_ReadSlashCommand : public UlxK2Node
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
//~ Begin UEdGraphNode Interface.
|
||||
virtual void AllocateDefaultPins() override;
|
||||
virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
|
||||
virtual bool ShouldShowNodeProperties() const override { return true; }
|
||||
virtual void PinDefaultValueChanged(UEdGraphPin* Pin) override;
|
||||
virtual FText GetTooltipText() const override;
|
||||
virtual FText GetPinDisplayName(const UEdGraphPin* Pin) const override;
|
||||
//~ End UEdGraphNode Interface.
|
||||
|
||||
//~ Begin UK2Node Interface.
|
||||
virtual bool IsNodePure() const override { return false; }
|
||||
virtual void ReconstructNode() override;
|
||||
virtual bool NodeCausesStructuralBlueprintChange() const override { return true; }
|
||||
virtual void ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) override;
|
||||
virtual ERedirectType DoPinsMatchForReconstruction(const UEdGraphPin* NewPin, int32 NewPinIndex, const UEdGraphPin* OldPin, int32 OldPinIndex) const override;
|
||||
virtual bool IsConnectionDisallowed(const UEdGraphPin* MyPin, const UEdGraphPin* OtherPin, FString& OutReason) const override;
|
||||
virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override;
|
||||
virtual FText GetMenuCategory() const override;
|
||||
virtual int32 GetNodeRefreshPriority() const override { return EBaseNodeRefreshPriority::Low_UsesDependentWildcard; }
|
||||
//~ End UK2Node Interface.
|
||||
|
||||
private:
|
||||
static const FName PrototypePinName;
|
||||
static const FName InputValuesPinName;
|
||||
static const FName ErrorPinName;
|
||||
|
||||
struct ParsingStep
|
||||
{
|
||||
FString Word;
|
||||
FName PinName;
|
||||
UFunction *ReadFunction;
|
||||
};
|
||||
|
||||
bool ParsePrototype(const FString &Prototype, TArray<ParsingStep>& Steps);
|
||||
|
||||
private:
|
||||
// Whenever the prototype pin value changes, we call
|
||||
// ReconstructNode, which backs up the value into this
|
||||
// property. This cache is needed because during
|
||||
// ReconstructNode, we blow away the prototype pin. The
|
||||
// prototype pin is also absent when the node is first
|
||||
// created.
|
||||
//
|
||||
UPROPERTY()
|
||||
FString ValuePrototype = TEXT("/command Integer Float");
|
||||
|
||||
};
|
||||
157
Source/Integration/SlashCommand.cpp
Normal file
157
Source/Integration/SlashCommand.cpp
Normal file
@@ -0,0 +1,157 @@
|
||||
#include "SlashCommand.h"
|
||||
#include "CoreMinimal.h"
|
||||
#include "Containers/StringView.h"
|
||||
#include "Misc/Char.h"
|
||||
#include "Misc/DefaultValueHelper.h"
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
//
|
||||
// SlashCommand
|
||||
//
|
||||
// A command line plus a cursor, exposed to blueprint.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
UlxSlashCommand* UlxSlashCommand::MakeSlashCommand(const FString& CommandLine)
|
||||
{
|
||||
UlxSlashCommand* Result = NewObject<UlxSlashCommand>();
|
||||
Result->CommandLine = CommandLine;
|
||||
return Result;
|
||||
}
|
||||
|
||||
|
||||
bool UlxSlashCommand::IsSlashCommand(const FString& Command)
|
||||
{
|
||||
if (Command.Len() < 2 || Command[0] != TEXT('/'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Every character after the slash must be alphanumeric.
|
||||
for (int32 i = 1; i < Command.Len(); i++)
|
||||
{
|
||||
if (!FChar::IsAlnum(Command[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
FStringView UlxSlashCommand::FetchToken()
|
||||
{
|
||||
const TCHAR* Data = *CommandLine;
|
||||
int32 Len = CommandLine.Len();
|
||||
|
||||
// Skip leading whitespace.
|
||||
while (Cursor < Len && FChar::IsWhitespace(Data[Cursor]))
|
||||
{
|
||||
Cursor++;
|
||||
}
|
||||
|
||||
// Read characters up to the next whitespace.
|
||||
int32 Start = Cursor;
|
||||
while (Cursor < Len && !FChar::IsWhitespace(Data[Cursor]))
|
||||
{
|
||||
Cursor++;
|
||||
}
|
||||
|
||||
return FStringView(Data + Start, Cursor - Start);
|
||||
}
|
||||
|
||||
ElxSuccessOrError UlxSlashCommand::CheckCommand(const FString& Literal, const FString& Prototype)
|
||||
{
|
||||
KnownCommands.Add(Literal);
|
||||
|
||||
// Checking the command always starts a fresh parse.
|
||||
Cursor = 0;
|
||||
FStringView Token = FetchToken();
|
||||
if (Token.Equals(Literal, ESearchCase::IgnoreCase))
|
||||
{
|
||||
MatchingPrototypes.Add(Prototype);
|
||||
return ElxSuccessOrError::Success;
|
||||
}
|
||||
|
||||
return ElxSuccessOrError::Error;
|
||||
}
|
||||
|
||||
ElxSuccessOrError UlxSlashCommand::ReadToken(FString& Result)
|
||||
{
|
||||
FStringView Token = FetchToken();
|
||||
if (Token.IsEmpty())
|
||||
{
|
||||
Result.Empty();
|
||||
return ElxSuccessOrError::Error;
|
||||
}
|
||||
|
||||
Result = FString(Token);
|
||||
return ElxSuccessOrError::Success;
|
||||
}
|
||||
|
||||
|
||||
ElxSuccessOrError UlxSlashCommand::ReadInteger(int32& Result)
|
||||
{
|
||||
// ParseInt validates the whole token and converts it, so "12abc"
|
||||
// is rejected rather than read as a partial number.
|
||||
FStringView Token = FetchToken();
|
||||
if (!FDefaultValueHelper::ParseInt(FString(Token), Result))
|
||||
{
|
||||
Result = 0;
|
||||
return ElxSuccessOrError::Error;
|
||||
}
|
||||
|
||||
return ElxSuccessOrError::Success;
|
||||
}
|
||||
|
||||
|
||||
ElxSuccessOrError UlxSlashCommand::ReadFloat(double& Result)
|
||||
{
|
||||
// ParseDouble validates the whole token and converts it, so
|
||||
// "12abc" is rejected rather than read as a partial number.
|
||||
FStringView Token = FetchToken();
|
||||
if (!FDefaultValueHelper::ParseDouble(FString(Token), Result))
|
||||
{
|
||||
Result = 0.0;
|
||||
return ElxSuccessOrError::Error;
|
||||
}
|
||||
|
||||
return ElxSuccessOrError::Success;
|
||||
}
|
||||
|
||||
|
||||
ElxSuccessOrError UlxSlashCommand::ReadRest(FString& Result)
|
||||
{
|
||||
const TCHAR* Data = *CommandLine;
|
||||
int32 Len = CommandLine.Len();
|
||||
|
||||
// Everything from the cursor to the end of the line, trimmed.
|
||||
Result = FString(FStringView(Data + Cursor, Len - Cursor));
|
||||
Result.TrimStartAndEndInline();
|
||||
Cursor = Len;
|
||||
return ElxSuccessOrError::Success;
|
||||
}
|
||||
|
||||
FString UlxSlashCommand::GetErrorMessage() const
|
||||
{
|
||||
if (MatchingPrototypes.Num() == 0)
|
||||
{
|
||||
TArray<FString> Commands;
|
||||
for (const FString& Command : KnownCommands)
|
||||
{
|
||||
Commands.Add(Command);
|
||||
}
|
||||
Commands.Sort();
|
||||
return FString(TEXT("No such slash command. Valid slash commands: ")) + FString::Join(Commands, TEXT(", "));
|
||||
}
|
||||
|
||||
FString Result = TEXT("Invalid parameters. Valid parameters:\n");
|
||||
for (const FString& Prototype : MatchingPrototypes)
|
||||
{
|
||||
Result += Prototype;
|
||||
Result += TEXT("\n");
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
121
Source/Integration/SlashCommand.h
Normal file
121
Source/Integration/SlashCommand.h
Normal file
@@ -0,0 +1,121 @@
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// SlashCommand.h
|
||||
//
|
||||
// A command line plus a cursor, exposed to blueprint.
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Containers/Array.h"
|
||||
#include "Containers/Set.h"
|
||||
#include "Containers/UnrealString.h"
|
||||
#include "Containers/StringFwd.h"
|
||||
#include "Common.h"
|
||||
#include "SlashCommand.generated.h"
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// UlxSlashCommand
|
||||
//
|
||||
// Holds a command line (a string the user has typed) together
|
||||
// with a cursor marking the current position within it. This
|
||||
// is the object that wraps Unreal's FParse facilities so that
|
||||
// blueprint can parse a typed command piece by piece, with the
|
||||
// cursor advancing as each token is consumed.
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class UlxSlashCommand : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
private:
|
||||
// The full command line that we are parsing.
|
||||
//
|
||||
FString CommandLine;
|
||||
|
||||
// The current parse position: an index into CommandLine.
|
||||
//
|
||||
int32 Cursor = 0;
|
||||
|
||||
// Known command names that have been registered while exploring
|
||||
// possible parses for this line.
|
||||
//
|
||||
TSet<FString> KnownCommands;
|
||||
|
||||
// Prototypes whose command name matches this line.
|
||||
//
|
||||
TArray<FString> MatchingPrototypes;
|
||||
|
||||
// Skip leading whitespace at the cursor, then read characters up
|
||||
// to (but not including) the next whitespace. The token is
|
||||
// returned as a view into CommandLine, and the cursor is advanced
|
||||
// past it.
|
||||
//
|
||||
// Returns an empty view if there is no nonwhitespace input left.
|
||||
//
|
||||
FStringView FetchToken();
|
||||
|
||||
public:
|
||||
// Construct a slash command from a command line string.
|
||||
// The cursor starts at the beginning.
|
||||
//
|
||||
UFUNCTION(BlueprintCallable)
|
||||
static UlxSlashCommand* MakeSlashCommand(const FString& CommandLine);
|
||||
|
||||
// Return true if the string is a slash followed by one or more
|
||||
// alphanumeric characters, and nothing else (e.g. "/foo").
|
||||
//
|
||||
static bool IsSlashCommand(const FString& Command);
|
||||
|
||||
// Reset the cursor to the start, then read the first token and
|
||||
// check whether it matches the given literal, case-insensitively.
|
||||
// The command name is recorded in KnownCommands either way. If it
|
||||
// matched, the prototype is added to MatchingPrototypes. The token
|
||||
// is consumed either way. Returns Success if it matched, Error
|
||||
// otherwise.
|
||||
//
|
||||
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", ExpandEnumAsExecs = "ReturnValue"))
|
||||
ElxSuccessOrError CheckCommand(const FString& Literal, const FString& Prototype);
|
||||
|
||||
// Read the next whitespace-delimited word from the command line.
|
||||
//
|
||||
// This is the blueprint-callable form of FetchToken: it returns
|
||||
// the token as an FString. Returns Error (with an empty Result)
|
||||
// if there is no nonwhitespace input left, Success otherwise.
|
||||
//
|
||||
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", ExpandEnumAsExecs = "ReturnValue"))
|
||||
ElxSuccessOrError ReadToken(FString& Result);
|
||||
|
||||
// Read the next token and interpret it as an integer, using C++
|
||||
// number syntax (optional sign; decimal, 0x hex, or leading-0
|
||||
// octal). The whole token must be valid; "12abc" is rejected.
|
||||
// Returns Error (with Result 0) on failure, Success otherwise.
|
||||
//
|
||||
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", ExpandEnumAsExecs = "ReturnValue"))
|
||||
ElxSuccessOrError ReadInteger(int32& Result);
|
||||
|
||||
// Read the next token and interpret it as a floating-point number,
|
||||
// using C++ number syntax (optional sign, digits, dot, e/E
|
||||
// exponent, trailing f). The whole token must be valid; "12abc"
|
||||
// is rejected. Returns Error (with Result 0) on failure, Success
|
||||
// otherwise.
|
||||
//
|
||||
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", ExpandEnumAsExecs = "ReturnValue"))
|
||||
ElxSuccessOrError ReadFloat(double& Result);
|
||||
|
||||
// Read the rest of the command line, from the cursor to the end,
|
||||
// trimmed of leading and trailing whitespace. The cursor is
|
||||
// advanced to the end. Always returns Success.
|
||||
//
|
||||
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", ExpandEnumAsExecs = "ReturnValue"))
|
||||
ElxSuccessOrError ReadRest(FString& Result);
|
||||
|
||||
// Return a user-facing error message describing why parsing failed.
|
||||
//
|
||||
UFUNCTION(BlueprintCallable)
|
||||
FString GetErrorMessage() const;
|
||||
};
|
||||
@@ -10,11 +10,12 @@
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "Components/GridPanel.h"
|
||||
#include "Components/CanvasPanelSlot.h"
|
||||
#include "Components/Widget.h"
|
||||
#include "InputMappingContext.h"
|
||||
#include "EnhancedInputComponent.h"
|
||||
#include "Animation/AnimSequenceBase.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
#include "GameFramework/InputDeviceSubsystem.h"
|
||||
#include "GameFramework/InputSettings.h"
|
||||
|
||||
|
||||
@@ -157,65 +158,28 @@ bool UlxUtilityLibrary::LineTraceThroughPixel(const APlayerController* PlayerCon
|
||||
return false;
|
||||
}
|
||||
|
||||
void UlxUtilityLibrary::SetPositionOfGridPanelMiddleCell(UGridPanel *GridPanel, FVector2D UpperLeftXY, FVector2D LowerRightXY)
|
||||
void UlxUtilityLibrary::ConfigureCanvasPanelSlot(UObject *Target, FAnchors Anchors, FVector2D Position, FVector2D Size, FVector2D Alignment, bool SizeToContent)
|
||||
{
|
||||
if ((GridPanel == nullptr) || (GridPanel->ColumnFill.Num() != 3) || (GridPanel->RowFill.Num() != 3))
|
||||
UCanvasPanelSlot *CanvasSlot = Cast<UCanvasPanelSlot>(Target);
|
||||
if (CanvasSlot == nullptr)
|
||||
{
|
||||
UE_LOG(LogBlueprint, Error, TEXT("SetPositionOfGridPanelMiddleCell only works on 3x3 GridPanels."));
|
||||
UWidget *Widget = Cast<UWidget>(Target);
|
||||
if (Widget != nullptr)
|
||||
{
|
||||
CanvasSlot = Cast<UCanvasPanelSlot>(Widget->Slot);
|
||||
}
|
||||
}
|
||||
if (CanvasSlot == nullptr)
|
||||
{
|
||||
UE_LOG(LogBlueprint, Error, TEXT("ConfigureCanvasPanelSlot: object is not a CanvasPanelSlot, and is not a Widget in a CanvasPanel."));
|
||||
return;
|
||||
}
|
||||
|
||||
if ((LowerRightXY.X < UpperLeftXY.X) || (LowerRightXY.Y < UpperLeftXY.Y))
|
||||
{
|
||||
UE_LOG(LogBlueprint, Error, TEXT("LowerRightXY must be greater than or equal to UpperLeftXY"));
|
||||
return;
|
||||
}
|
||||
|
||||
UpperLeftXY.X = FMath::Clamp(UpperLeftXY.X, 0.0f, 1.0f);
|
||||
UpperLeftXY.Y = FMath::Clamp(UpperLeftXY.Y, 0.0f, 1.0f);
|
||||
LowerRightXY.X = FMath::Clamp(LowerRightXY.X, 0.0f, 1.0f);
|
||||
LowerRightXY.Y = FMath::Clamp(LowerRightXY.Y, 0.0f, 1.0f);
|
||||
|
||||
GridPanel->SetRowFill(0, UpperLeftXY.Y);
|
||||
GridPanel->SetRowFill(1, LowerRightXY.Y - UpperLeftXY.Y);
|
||||
GridPanel->SetRowFill(2, 1.0 - LowerRightXY.Y);
|
||||
|
||||
GridPanel->SetColumnFill(0, UpperLeftXY.X);
|
||||
GridPanel->SetColumnFill(1, LowerRightXY.X - UpperLeftXY.X);
|
||||
GridPanel->SetColumnFill(2, 1.0 - LowerRightXY.X);
|
||||
}
|
||||
|
||||
void UlxUtilityLibrary::GetPositionOfGridPanelMiddleCell(UGridPanel *GridPanel, FVector2D &UpperLeftXY, FVector2D &LowerRightXY)
|
||||
{
|
||||
TArray<float> &Col = GridPanel->ColumnFill;
|
||||
TArray<float> &Row = GridPanel->RowFill;
|
||||
|
||||
// Set default return value for error situations.
|
||||
UpperLeftXY.X = 0.0;
|
||||
LowerRightXY.X = 1.0;
|
||||
UpperLeftXY.Y = 0.0;
|
||||
LowerRightXY.Y = 1.0;
|
||||
|
||||
if ((GridPanel == nullptr) || (Row.Num() != 3) || (Col.Num() != 3))
|
||||
{
|
||||
UE_LOG(LogBlueprint, Error, TEXT("SetPositionOfGridPanelMiddleCell only works on 3x3 GridPanels."));
|
||||
return;
|
||||
}
|
||||
|
||||
double TotalX = Col[0] + Col[1] + Col[2];
|
||||
double TotalY = Row[0] + Row[1] + Row[2];
|
||||
|
||||
if (TotalX > 0)
|
||||
{
|
||||
UpperLeftXY.X = Col[0] / TotalX;
|
||||
LowerRightXY.X = (Col[0] + Col[1]) / TotalX;
|
||||
}
|
||||
|
||||
if (TotalY > 0)
|
||||
{
|
||||
UpperLeftXY.Y = Row[0] / TotalY;
|
||||
LowerRightXY.Y = (Row[0] + Row[1]) / TotalY;
|
||||
}
|
||||
CanvasSlot->SetAnchors(Anchors);
|
||||
CanvasSlot->SetAlignment(Alignment);
|
||||
CanvasSlot->SetPosition(Position);
|
||||
CanvasSlot->SetSize(Size);
|
||||
CanvasSlot->SetAutoSize(SizeToContent);
|
||||
}
|
||||
|
||||
ElxUsedOrNotUsed UlxUtilityLibrary::IsKeyUsedByMappingContext(const FKey &Key, const UInputMappingContext *MappingContext)
|
||||
@@ -278,17 +242,3 @@ void UlxUtilityLibrary::ValidateLuaExpr(
|
||||
Status = w.ValidateLuaExpr(Code, ErrorMessage);
|
||||
}
|
||||
|
||||
ElxControllerType UlxUtilityLibrary::DetectControllerType(ULocalPlayer *Player)
|
||||
{
|
||||
UInputDeviceSubsystem *IDS = GEngine->GetEngineSubsystem<UInputDeviceSubsystem>();
|
||||
if (!IDS) return ElxControllerType::KeyboardMouse;
|
||||
|
||||
FName Id = IDS->GetMostRecentlyUsedHardwareDevice(Player->GetPlatformUserId()).HardwareDeviceIdentifier;
|
||||
|
||||
if (Id == TEXT("ps3") || Id == TEXT("ps4") || Id == TEXT("ps5")) return ElxControllerType::PlayStationGamepad;
|
||||
if (Id == TEXT("xbox360") || Id == TEXT("xboxone")) return ElxControllerType::XboxGamepad;
|
||||
|
||||
// Unknown or unrecognized device — assume keyboard/mouse
|
||||
return ElxControllerType::KeyboardMouse;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "Input/Events.h"
|
||||
#include "Common.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "Components/CanvasPanelSlot.h"
|
||||
|
||||
#include "UtilityLibrary.generated.h"
|
||||
|
||||
@@ -91,51 +92,15 @@ public:
|
||||
ETraceTypeQuery TraceChannel, bool bTraceComplex, EDrawDebugTrace::Type DrawDebugType, bool bIgnorePlayerPawn,
|
||||
const TArray<AActor*>& ActorsToIgnore, FHitResult& HitResult);
|
||||
|
||||
// Set Position of GridPanel Middle Cell
|
||||
//
|
||||
// Sometimes, you want to specify the position of a widget, and you
|
||||
// don't want to specify the position in slate units, instead, you
|
||||
// want to specify the position using fractions: ie, (0,0) is the
|
||||
// upper left corner of the screen, and (1,1) is the lower-right corner.
|
||||
//
|
||||
// One way to accomplish this is to put your widget in the middle cell
|
||||
// of a 3x3 GridPanel. Then, you can position it by adjusting the grid
|
||||
// fill rules. This utility routine can do the math necessary to
|
||||
// correctly populate those fill rules.
|
||||
//
|
||||
// This routine must be passed a 3x3 GridPanel. This will reposition
|
||||
// the middle cell. You must specify the upper-left and lower-right
|
||||
// corners of the middle cell as fractions between (0,0) and (1,1).
|
||||
//
|
||||
// Be aware that if the content of a grid cell overflows the amount of
|
||||
// space allocated for it, then the grid will adjust to make room.
|
||||
// But that will mean that the grid is no longer faithful to the
|
||||
// positions specified in its fill rules. One way to ensure that the
|
||||
// grid remains faithful to its fill rules is to put an Overlay
|
||||
// into the GridPanel cell, then put -1000 padding into the
|
||||
// Overlay's GridPanel slot, then put +1000 padding into the Overlay
|
||||
// slot. The two paddings cancel each other out, leaving the item in
|
||||
// the Overlay at the originally-intended position. But if the item
|
||||
// in the overlay overflows, it doesn't cause the Grid to deform.
|
||||
// Instead, the item exceeds the bounds of the grid cell, but it leaves
|
||||
// the grid cell where it belongs.
|
||||
//
|
||||
UFUNCTION(BlueprintCallable, Category="Widget")
|
||||
static void SetPositionOfGridPanelMiddleCell(UGridPanel *GridPanel, FVector2D UpperLeftXY, FVector2D LowerRightXY);
|
||||
|
||||
// Get Position of GridPanel Middle Cell
|
||||
// Configure a CanvasPanelSlot's parameters in a single call.
|
||||
//
|
||||
// The routine must be passed a 3x3 GridPanel. This will return the
|
||||
// position of the middle cell of the gridpanel, expressed on a scale
|
||||
// from (0,0) to (1,1).
|
||||
//
|
||||
// The numbers returned by this routine are based entirely on the
|
||||
// GridPanel fill rules. If an item in the grid is overflowing its
|
||||
// allocated space, causing the grid to deform, then that won't be
|
||||
// reflected in the output of this routine.
|
||||
// Target must be either a UCanvasPanelSlot directly, or a UWidget whose
|
||||
// Slot is a UCanvasPanelSlot. If it is neither, logs an error and
|
||||
// does nothing.
|
||||
//
|
||||
UFUNCTION(BlueprintPure, Category="Widget")
|
||||
static void GetPositionOfGridPanelMiddleCell(UGridPanel *GridPanel, FVector2D &UpperLeftXY, FVector2D &LowerRightXY);
|
||||
UFUNCTION(BlueprintCallable, Category = "Widget", meta = (SizeToContent = "true"))
|
||||
static void ConfigureCanvasPanelSlot(UObject *Target, FAnchors Anchors, FVector2D Position, FVector2D Size, FVector2D Alignment, bool SizeToContent);
|
||||
|
||||
// Check if a given key is used by the specified mapping context.
|
||||
//
|
||||
@@ -179,9 +144,4 @@ public:
|
||||
//
|
||||
UFUNCTION(BlueprintCallable, meta = (WorldContext = "context"), Category = "Luprex|Utility")
|
||||
static void ValidateLuaExpr(ElxLuaSyntaxCheck &Status, FString &ErrorMessage, UObject *context, const FString &Code);
|
||||
|
||||
// Determine what type of controller the game is currently using
|
||||
//
|
||||
UFUNCTION(BlueprintCallable, category="Luprex|Utility")
|
||||
static ElxControllerType DetectControllerType(ULocalPlayer *Player);
|
||||
};
|
||||
|
||||
@@ -336,7 +336,7 @@ eng::string LuaCoreStack::load(LuaSlot result, std::string_view code, std::strin
|
||||
const char *str = lua_tolstring(L_, -1, &len);
|
||||
eng::string message(str, len);
|
||||
lua_pop(L_, 1);
|
||||
if (sv::has_suffix(message, "near <eof>"))
|
||||
if (sv::has_suffix(message, "near <eof>") && sv::is_possible_long_lua_expression(code))
|
||||
{
|
||||
message = "truncated lua";
|
||||
}
|
||||
|
||||
@@ -192,7 +192,6 @@ bool PrintChanneler::channel(const PrintBuffer *printbuffer, StreamBuffer *sb) {
|
||||
line_ = printbuffer->first_line();
|
||||
}
|
||||
while (line_ < printbuffer->first_unchecked()) {
|
||||
sb->write_bytes("|");
|
||||
sb->write_bytes(printbuffer->nth(line_));
|
||||
sb->write_bytes("\n");
|
||||
line_ += 1;
|
||||
|
||||
@@ -278,7 +278,6 @@ void SourceDB::update(const util::LuaSourceVec &source) {
|
||||
for (int i = 0; i < int(source.size()); i++) {
|
||||
const eng::string &file = source[i].first;
|
||||
const eng::string &code = source[i].second;
|
||||
util::dprint("Compiling ", file);
|
||||
LS.newtable(info);
|
||||
LS.rawset(info, "name", file);
|
||||
LS.rawset(info, "code", code);
|
||||
|
||||
@@ -50,19 +50,25 @@ int traceback_coroutine(lua_State *L) {
|
||||
if ((ar.currentline > 0) || (*ar.namewhat != 0) || (*ar.what != 'C')) {
|
||||
any = true;
|
||||
lua_pushliteral(L, "\n\t");
|
||||
lua_pushfstring(L, "%s:", ar.short_src);
|
||||
if (ar.currentline > 0)
|
||||
lua_pushfstring(L, "%d:", ar.currentline);
|
||||
if (strcmp(ar.short_src, "<console>")==0)
|
||||
{
|
||||
lua_pushstring(L, "in the console");
|
||||
}
|
||||
else
|
||||
{
|
||||
lua_pushfstring(L, "in %s", ar.short_src);
|
||||
if (ar.currentline > 0)
|
||||
lua_pushfstring(L, " line %d", ar.currentline);
|
||||
}
|
||||
if (*ar.namewhat != '\0') /* is there a name? */
|
||||
lua_pushfstring(L, " in function " LUA_QS, ar.name);
|
||||
else {
|
||||
if (*ar.what == 'm') /* main? */
|
||||
lua_pushfstring(L, " in main chunk");
|
||||
lua_pushfstring(L, " in top-level expression ");
|
||||
else if (*ar.what == 'C' || *ar.what == 't')
|
||||
lua_pushliteral(L, " ?"); /* C function or tail call */
|
||||
lua_pushliteral(L, " in unknown C function");
|
||||
else
|
||||
lua_pushfstring(L, " in function <%s:%d>",
|
||||
ar.short_src, ar.linedefined);
|
||||
lua_pushfstring(L, " in function on line %d", ar.linedefined);
|
||||
}
|
||||
if (1 + lua_gettop(L) - top > 5) {
|
||||
lua_concat(L, 1 + lua_gettop(L) - top);
|
||||
|
||||
@@ -55,7 +55,7 @@ public:
|
||||
virtual void event_access(AccessKind kind, int64_t place_id, std::string_view datapk, StreamBuffer *retpk) override {
|
||||
switch (kind) {
|
||||
case AccessKind::INVOKE_LUA_SOURCE: {
|
||||
world_->update_source(datapk);
|
||||
world_->update_source(datapk, 0);
|
||||
run_unittests(world_->state());
|
||||
stop_driver();
|
||||
break;
|
||||
|
||||
@@ -200,6 +200,24 @@ bool is_lua_comment(string_view s) {
|
||||
return s.substr(start, 2) == "--";
|
||||
}
|
||||
|
||||
bool is_possible_long_lua_expression(string_view s) {
|
||||
read_space(s);
|
||||
string_view id = read_lua_identifier(s);
|
||||
if (id.empty()) return false;
|
||||
if ((id == "function") || (id == "if") || (id == "while") || (id == "for") || (id == "repeat") || (id == "do")) return true;
|
||||
if (id == "local")
|
||||
{
|
||||
read_space(s);
|
||||
id = read_lua_identifier(s);
|
||||
}
|
||||
read_space(s);
|
||||
read_prefix(s, "="); // If not present, returns false but we continue anyway.
|
||||
read_space(s);
|
||||
if (has_prefix(s, "[")) return true;
|
||||
if (has_prefix(s, "(")) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool is_whitespace(string_view s) {
|
||||
for (int i = 0; i < int(s.size()); i++) {
|
||||
if (!ascii_isspace(s[i])) {
|
||||
|
||||
@@ -106,6 +106,14 @@ bool is_lua_classname(string_view s);
|
||||
// Return true if the line of code is a lua comment.
|
||||
bool is_lua_comment(string_view s);
|
||||
|
||||
// Return true if the line of code could be the beginning of a long expression.
|
||||
// In a read-eval-print loop, if the user types something like "function foo",
|
||||
// that's not a complete lua expression. But we don't want to just print an error,
|
||||
// we want to give the user a chance to continue typing so that he can turn it
|
||||
// into a complete lua expression. This function returns true if the string looks
|
||||
// like the beginning of a long lua expression. This is only a heuristic.
|
||||
bool is_possible_long_lua_expression(string_view s);
|
||||
|
||||
// Return true if the line is entirely whitespace.
|
||||
bool is_whitespace(string_view s);
|
||||
|
||||
|
||||
@@ -523,6 +523,9 @@ void World::probe_lua_call(int64_t actor_id, int64_t place_id, std::string_view
|
||||
// This is called from World::update_source, and also
|
||||
// from World::patch_source in the difference transmitter.
|
||||
//
|
||||
// When called from the difference transmitter, we suppress
|
||||
// error messages.
|
||||
//
|
||||
// For the moment, errors are channeled to util::dprint,
|
||||
// and 'print' statements just go to std::cerr. Neither
|
||||
// of these is ideal. We need to get serious about setting
|
||||
@@ -532,7 +535,7 @@ void World::probe_lua_call(int64_t actor_id, int64_t place_id, std::string_view
|
||||
// some lua source file tries to modify, say, tangible state
|
||||
// in top-level code.
|
||||
//
|
||||
bool World::rebuild_sourcedb() {
|
||||
bool World::rebuild_sourcedb(int64_t actor_id) {
|
||||
bool ok = true;
|
||||
for (const eng::string &mod: source_db_.modules()) {
|
||||
open_lthread_state(0, 0, 0, false);
|
||||
@@ -540,30 +543,28 @@ bool World::rebuild_sourcedb() {
|
||||
eng::string prints = lthread_prints_.str();
|
||||
clear_lthread_state();
|
||||
if (!err.empty()) ok = false;
|
||||
if (!err.empty() || !prints.empty()) {
|
||||
util::dprint("Loading Module ", mod);
|
||||
if (!err.empty()) util::dprint(err);
|
||||
if (!prints.empty()) util::dprint(prints);
|
||||
if (actor_id >= 0) {
|
||||
lthread_prints_ << "Compiling " << mod << std::endl;
|
||||
if (!err.empty()) lthread_prints_ << err << std::endl;
|
||||
if (!prints.empty()) lthread_prints_ << prints;
|
||||
lthread_prints_to_actor(actor_id);
|
||||
}
|
||||
}
|
||||
if (actor_id > 0) {
|
||||
lthread_prints_ << (ok ? "Compilation Successful." : "Compilation Failed.") << std::endl;
|
||||
lthread_prints_to_actor(actor_id);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool World::update_source(const util::LuaSourceVec &source) {
|
||||
bool World::update_source(const util::LuaSourceVec &source, int64_t actor_id) {
|
||||
assert(stack_is_clear());
|
||||
source_db_.update(source);
|
||||
return rebuild_sourcedb();
|
||||
return rebuild_sourcedb(actor_id);
|
||||
assert(stack_is_clear());
|
||||
}
|
||||
|
||||
bool World::update_source(const util::LuaSourcePtr &source) {
|
||||
if (source == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return update_source(*source);
|
||||
}
|
||||
|
||||
bool World::update_source(std::string_view sourcepack) {
|
||||
bool World::update_source(std::string_view sourcepack, int64_t actor_id) {
|
||||
if (sourcepack.empty()) {
|
||||
return false;
|
||||
}
|
||||
@@ -571,7 +572,7 @@ bool World::update_source(std::string_view sourcepack) {
|
||||
StreamBuffer sb(sourcepack);
|
||||
util::LuaSourceVec sv;
|
||||
SourceDB::deserialize_source(&sv, &sb);
|
||||
return update_source(sv);
|
||||
return update_source(sv, actor_id);
|
||||
} catch (const StreamException &ex) {
|
||||
return false;
|
||||
}
|
||||
@@ -697,7 +698,7 @@ HttpServerResponse World::http_serve(const HttpParser &request) {
|
||||
open_lthread_state(0, 0, 0, false);
|
||||
eng::string msg = traceback_pcall(L, 1, LUA_MULTRET);
|
||||
if (!msg.empty()) lthread_prints_ << msg << std::endl;
|
||||
lthread_prints_to_dprint();
|
||||
lthread_prints_to_actor(0);
|
||||
clear_lthread_state();
|
||||
|
||||
// If the call threw an error, return
|
||||
@@ -859,7 +860,7 @@ void World::invoke_lua_expr(int64_t actor_id, int64_t place_id, std::string_view
|
||||
LuaExtStack LS(L, func);
|
||||
|
||||
// create the compiled closure.
|
||||
eng::string error = LS.load(func, datapack, "=invoke");
|
||||
eng::string error = LS.load(func, datapack, "<console>");
|
||||
if (!error.empty()) {
|
||||
// The closure is actually an error message. Do nothing.
|
||||
// This should normally not happen: LuaConsole should filter
|
||||
@@ -954,7 +955,7 @@ void World::invoke_lua_source(int64_t actor_id, int64_t place_id, std::string_vi
|
||||
bool brand_new = (source_db_.modules().size() == 1);
|
||||
|
||||
// Compile and load the source.
|
||||
bool success = update_source(datapack);
|
||||
bool success = update_source(datapack, actor_id);
|
||||
|
||||
// Call world.init
|
||||
if (brand_new) {
|
||||
@@ -976,7 +977,7 @@ void World::invoke_lua_source(int64_t actor_id, int64_t place_id, std::string_vi
|
||||
util::dprint("You will need to fix the errors then run it manually.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Run the new thread and return.
|
||||
assert(stack_is_clear());
|
||||
}
|
||||
@@ -1071,6 +1072,10 @@ void World::run_scheduled_threads() {
|
||||
PrettyPrint::Indented().print(LSCO, LuaSpecial(i), <hread_prints_);
|
||||
lthread_prints_ << std::endl;
|
||||
}
|
||||
if (lthread_prints_.view().empty())
|
||||
{
|
||||
lthread_prints_ << "ok\n";
|
||||
}
|
||||
}
|
||||
} else if (status == LUA_YIELD) {
|
||||
if (is_authoritative()) {
|
||||
@@ -1092,7 +1097,7 @@ void World::run_scheduled_threads() {
|
||||
}
|
||||
LS.rawset(threads, sched.thread_id(), LuaNil);
|
||||
}
|
||||
lthread_prints_to_printbuffer();
|
||||
lthread_prints_to_actor(lthread_actor_id_);
|
||||
clear_lthread_state();
|
||||
}
|
||||
}
|
||||
@@ -1158,22 +1163,20 @@ void World::open_lthread_state(int64_t actor, int64_t place, int64_t thread, boo
|
||||
lthread_prints_.clear();
|
||||
}
|
||||
|
||||
void World::lthread_prints_to_printbuffer()
|
||||
void World::lthread_prints_to_actor(int64_t actor_id)
|
||||
{
|
||||
const eng::string &output = lthread_prints_.str();
|
||||
if (output.size() > 0) {
|
||||
Tangible *actor = tangible_get(lthread_actor_id_);
|
||||
if (actor != nullptr) {
|
||||
actor->print_buffer_.add_string(output, is_authoritative());
|
||||
if (actor_id >= 0) {
|
||||
Tangible *actor = tangible_get(actor_id);
|
||||
if (actor != nullptr) {
|
||||
actor->print_buffer_.add_string(output, is_authoritative());
|
||||
} else {
|
||||
util::dprintview(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void World::lthread_prints_to_dprint()
|
||||
{
|
||||
const eng::string &output = lthread_prints_.str();
|
||||
if (output.size() > 0) {
|
||||
util::dprintview(output);
|
||||
lthread_prints_.str("");
|
||||
lthread_prints_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -292,7 +292,7 @@ void World::patch_source(StreamBuffer *sb, DebugCollector *dbc) {
|
||||
DebugBlock dbb(dbc, "patch_source");
|
||||
bool modified = source_db_.patch(sb, dbc);
|
||||
if (modified) {
|
||||
rebuild_sourcedb();
|
||||
rebuild_sourcedb(-1);
|
||||
DebugLine(dbc) << "Source DB rebuilt";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,19 +284,20 @@ public:
|
||||
|
||||
// Rebuild the global environment from the source database.
|
||||
//
|
||||
// Error messages go to the specified actor.
|
||||
//
|
||||
// Returns true if the rebuild goes without errors.
|
||||
//
|
||||
bool rebuild_sourcedb();
|
||||
bool rebuild_sourcedb(int64_t actor_id);
|
||||
|
||||
// Update the source database from disk, then rebuild the global environment.
|
||||
//
|
||||
// Special case: if the source pointer is nullptr, does not update.
|
||||
// Error messages go to the specified actor.
|
||||
//
|
||||
// Returns true if the update goes without errors.
|
||||
//
|
||||
bool update_source(const util::LuaSourceVec &source);
|
||||
bool update_source(const util::LuaSourcePtr &source);
|
||||
bool update_source(std::string_view sourcepk);
|
||||
bool update_source(const util::LuaSourceVec &source, int64_t actor_id);
|
||||
bool update_source(std::string_view sourcepk, int64_t actor_id);
|
||||
|
||||
// Supply an HTTP response to an outstanding HTTP request.
|
||||
//
|
||||
@@ -375,8 +376,13 @@ public:
|
||||
|
||||
std::ostream *lthread_print_stream() { return <hread_prints_; }
|
||||
|
||||
void lthread_prints_to_printbuffer();
|
||||
void lthread_prints_to_dprint();
|
||||
// Send the lthread_prints output to the specified actor.
|
||||
//
|
||||
// If actor_id == (-1) prints are discarded.
|
||||
// If actor_id == (0) prints go to dprint.
|
||||
// Anything else, and the prints go to a specific actor.
|
||||
//
|
||||
void lthread_prints_to_actor(int64_t actor_id);
|
||||
|
||||
// Set a lua global variable.
|
||||
//
|
||||
|
||||
@@ -34,9 +34,13 @@ function moveto(x, y)
|
||||
end
|
||||
|
||||
function cube.lookmenu(add)
|
||||
add("Cube A", function () dprint("Doing Cube A") end)
|
||||
add("Cube B", function () dprint("Doing Cube B") end)
|
||||
add("Cube C", function () dprint("Doing Cube C") end)
|
||||
add("Cube Hi", function () dprint("Doing Cube Hi") end)
|
||||
add("Cube Bye", function () dprint("Doing Cube Bye") end)
|
||||
add("Cube Yo", function () dprint("Doing Cube Yo") end)
|
||||
add("Cube Z", function () dprint("Doing Cube Z") end)
|
||||
end
|
||||
|
||||
function sphere.lookhotkeys(add)
|
||||
@@ -75,7 +79,7 @@ function engio.getlookat()
|
||||
return ""
|
||||
end
|
||||
|
||||
|
||||
print("Hello from login.lua")
|
||||
|
||||
function jp3()
|
||||
tangible.animate{tan=actor, anim={action="play", seq="jump"}}
|
||||
|
||||
Reference in New Issue
Block a user