Compare commits

...

13 Commits

Author SHA1 Message Date
d737879ed6 More slash command stuff 2026-05-26 18:42:48 -04:00
46051526e6 New slash command parser 2026-05-26 18:20:35 -04:00
8dab0d16b7 CPL command 2026-05-26 15:07:42 -04:00
933c1ac6c3 More tweaks on tracebacks 2026-05-21 19:40:30 -04:00
521d4726ad Make lua tracebacks a little better 2026-05-21 19:24:11 -04:00
2bfa3024f1 Lots of work on the lua read-eval-print loop 2026-05-21 18:41:09 -04:00
f7983b1f02 Final form of radial menus 2026-05-19 02:55:31 -04:00
36ec4a3b9b Add new input device processor 2026-05-18 19:26:11 -04:00
1c2be1b4d8 Radial menus are in good shape 2026-05-18 16:12:26 -04:00
0b23c82e73 Progress on radial menus 2026-05-18 02:26:28 -04:00
b1defd821b More work on radial menus 2026-05-16 02:27:18 -04:00
e17f5417f2 Real progress on radial menus 2026-05-16 01:49:26 -04:00
c0848c2670 Completing downgrade to 5.5.4 2026-05-15 19:37:34 -04:00
54 changed files with 1304 additions and 475 deletions

View File

@@ -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).

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Content/Widgets/teardrop.uasset LFS Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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

View File

@@ -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::NodeContextMenus();
UWingManualSections::VariableGettersAndSetters();
UWingManualSections::BestPerformance();
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);
}
}
}
};

View File

@@ -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);
}
}
};

View File

@@ -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)
{

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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"

View File

@@ -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;

View File

@@ -1,7 +1,7 @@
#pragma once
#include "CoreMinimal.h"
#include "Materials/MaterialParameters.h"
#include "MaterialTypes.h"
#include "WingBasics.h"
#include "WingParameterEditor.generated.h"

View File

@@ -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;

View File

@@ -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.

View File

@@ -105,6 +105,13 @@ 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

View File

@@ -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.

View 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;
}

View 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;
};

View File

@@ -31,6 +31,7 @@ public class Integration : ModuleRules
"UMG",
"UMGEditor",
"EditorSubsystem",
"ApplicationCore",
});
PrivateDependencyModuleNames.Add("Slate");

View File

@@ -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;
}

View File

@@ -63,6 +63,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")
ElxBreakToDebuggerThreshold BreakToDebuggerLogVerbosity;
@@ -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;

View File

@@ -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));
}

View File

@@ -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++)
{
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 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,
AllottedGeometry.ToPaintGeometry(DotSize, FSlateLayoutTransform(LocalPoint - FVector2D(DotRadius))),
LayerId + 1,
AllottedGeometry.ToPaintGeometry(DotSize, FSlateLayoutTransform(LocalPoint - FVector2D(Config.DotRadius))),
&DotBrush,
ESlateDrawEffect::None,
LineColor);
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();
}

View File

@@ -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;
};

View 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

View 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");
};

View 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;
}

View 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;
};

View File

@@ -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;
}

View File

@@ -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).
// 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.
//
// 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.
//
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);
};

View File

@@ -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";
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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 (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, "%d:", ar.currentline);
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);

View File

@@ -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;

View File

@@ -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])) {

View File

@@ -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);

View File

@@ -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) {
@@ -1071,6 +1072,10 @@ void World::run_scheduled_threads() {
PrettyPrint::Indented().print(LSCO, LuaSpecial(i), &lthread_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,23 +1163,21 @@ 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_id >= 0) {
Tangible *actor = tangible_get(actor_id);
if (actor != nullptr) {
actor->print_buffer_.add_string(output, is_authoritative());
}
}
}
void World::lthread_prints_to_dprint()
{
const eng::string &output = lthread_prints_.str();
if (output.size() > 0) {
} else {
util::dprintview(output);
}
}
lthread_prints_.str("");
lthread_prints_.clear();
}
}

View File

@@ -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";
}
}

View File

@@ -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 &lthread_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.
//

View File

@@ -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"}}