Refactoring ue-wingman to be a command-line only tool
This commit is contained in:
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"ue-wingman": {
|
|
||||||
"command": "python3",
|
|
||||||
"args": ["Plugins/UEWingman/ue-wingman-mcp.py"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
- `Docs/` — Documentation.
|
- `Docs/` — Documentation.
|
||||||
- `Config/` — Unreal config files
|
- `Config/` — Unreal config files
|
||||||
- `EnginePatches/` — Custom engine modifications
|
- `EnginePatches/` — Custom engine modifications
|
||||||
- `Plugins/UEWingman/' - An MCP that gives you control over the unreal editor.
|
- `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).
|
||||||
- `../integration.UE/` - the unreal engine source tree
|
- `../integration.UE/` - the unreal engine source tree
|
||||||
|
|
||||||
## Coding Conventions
|
## Coding Conventions
|
||||||
|
|||||||
BIN
Content/testing/WB_radtest.uasset
LFS
Normal file
BIN
Content/testing/WB_radtest.uasset
LFS
Normal file
Binary file not shown.
@@ -25,7 +25,7 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Asset to delete"))
|
UPROPERTY(EditAnywhere, meta=(Description="Asset to delete"))
|
||||||
FString Asset;
|
FString Asset;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="If true, skip reference check and force delete"))
|
UPROPERTY(EditAnywhere, meta=(Description="If true, skip reference check and force delete"))
|
||||||
bool Force = false;
|
bool Force = false;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ class UWing_Asset_Search : public UWingHandler
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Substring to match against asset package paths"))
|
UPROPERTY(EditAnywhere, meta=(Description="Substring to match against asset package paths"))
|
||||||
FString Query;
|
FString Query;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Asset class name to filter by, e.g. Blueprint, Material, StaticMesh"))
|
UPROPERTY(EditAnywhere, meta=(Description="Asset class name to filter by, e.g. Blueprint, Material, StaticMesh"))
|
||||||
FString Type;
|
FString Type;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results (default 50)"))
|
UPROPERTY(EditAnywhere, meta=(Description="Maximum number of results (default 50)"))
|
||||||
int32 Limit = 50;
|
int32 Limit = 50;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
|
|||||||
@@ -32,16 +32,15 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Type of graph: function or macro"))
|
UPROPERTY(EditAnywhere, meta=(Description="Type of graph: function or macro"))
|
||||||
FString GraphType;
|
FString GraphType;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Input variables, one per line"))
|
UPROPERTY(EditAnywhere, meta=(Description="Variables"))
|
||||||
FString InputVariables;
|
FWingRestOfArgv Variables;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Output variables, one per line"))
|
|
||||||
FString OutputVariables;
|
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
{
|
{
|
||||||
UWingServer::AddHandler(this,
|
UWingServer::AddHandler(this,
|
||||||
TEXT("Create a new function or macro graph in a Blueprint."));
|
TEXT("Create a new function or macro graph in a Blueprint. "
|
||||||
|
"Variables must be expressed as 'kind type name (flags) = default'. "
|
||||||
|
"Kind can be input, output, or local."));
|
||||||
}
|
}
|
||||||
virtual void Handle() override
|
virtual void Handle() override
|
||||||
{
|
{
|
||||||
@@ -79,8 +78,7 @@ public:
|
|||||||
|
|
||||||
// Parse and validate variables before making changes
|
// Parse and validate variables before making changes
|
||||||
WingVariables Vars;
|
WingVariables Vars;
|
||||||
if (!Vars.InputVariables.ParseString(InputVariables, WingOut::Stdout)) return;
|
if (!Vars.Parse(Variables.Argv, false, WingOut::Stdout)) return;
|
||||||
if (!Vars.OutputVariables.ParseString(OutputVariables, WingOut::Stdout)) return;
|
|
||||||
|
|
||||||
// Create the Graph
|
// Create the Graph
|
||||||
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, InternalID,
|
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, InternalID,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Interface name to remove"))
|
UPROPERTY(EditAnywhere, meta=(Description="Interface name to remove"))
|
||||||
FString Interface;
|
FString Interface;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="If true, keep the function graphs as regular functions"))
|
UPROPERTY(EditAnywhere, meta=(Description="If true, keep the function graphs as regular functions"))
|
||||||
bool PreserveFunctions = false;
|
bool PreserveFunctions = false;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
|
|||||||
@@ -37,5 +37,6 @@ public:
|
|||||||
if (!P) return;
|
if (!P) return;
|
||||||
|
|
||||||
WingOut::Stdout.Print(P->GetText());
|
WingOut::Stdout.Print(P->GetText());
|
||||||
|
WingOut::Stdout.Print(TEXT("\n"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "WingBasics.h"
|
||||||
|
#include "WingServer.h"
|
||||||
|
#include "WingManual.h"
|
||||||
|
#include "Documentation_Command.generated.h"
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UWing_Documentation_Command : public UWingHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(EditAnywhere, meta=(Description="Substring filter for command names"))
|
||||||
|
FString Command;
|
||||||
|
|
||||||
|
virtual void Register() override
|
||||||
|
{
|
||||||
|
UWingServer::AddHandler(this,
|
||||||
|
TEXT("Detailed documentation for one or more commands."));
|
||||||
|
}
|
||||||
|
virtual void Handle() override
|
||||||
|
{
|
||||||
|
WingManual::Commands(EWingHandlerKind::Normal, Command, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -12,19 +12,13 @@ class UWing_Documentation_Commands : public UWingHandler
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Substring filter for command names"))
|
|
||||||
FString Query;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="If true, return full details including parameter types and descriptions"))
|
|
||||||
bool Verbose = false;
|
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
{
|
{
|
||||||
UWingServer::AddHandler(this,
|
UWingServer::AddHandler(this,
|
||||||
TEXT("List all the main commands with their descriptions."));
|
TEXT("A concise list of all ue-wingman commands."));
|
||||||
}
|
}
|
||||||
virtual void Handle() override
|
virtual void Handle() override
|
||||||
{
|
{
|
||||||
WingManual::Commands(EWingHandlerKind::Normal, Query, Verbose);
|
WingManual::Commands(EWingHandlerKind::Normal, TEXT(""), false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ class UWing_Documentation_CreateAssets : public UWingHandler
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Substring filter for command names"))
|
UPROPERTY(EditAnywhere, meta=(Description="Substring filter for command names"))
|
||||||
FString Query;
|
FString Query;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="If true, return full details including parameter types and descriptions"))
|
UPROPERTY(EditAnywhere, meta=(Description="If true, return full details including parameter types and descriptions"))
|
||||||
bool Verbose = false;
|
bool Verbose = false;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class UWing_Documentation_Manual : public UWingHandler
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="section of the manual"))
|
UPROPERTY(EditAnywhere, meta=(Description="section of the manual"))
|
||||||
FString Section;
|
FString Section;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
@@ -44,7 +44,7 @@ public:
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Printf(TEXT("Unknown manual section '%s'\n"));
|
WingOut::Stdout.Printf(TEXT("Unknown manual section '%s'\n"), *Section);
|
||||||
WingManual::PrintSectionNames(TEXT("Valid manual sections:"), Sections, WingOut::Stdout);
|
WingManual::PrintSectionNames(TEXT("Valid manual sections:"), Sections, WingOut::Stdout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,13 +28,16 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Name of the new event dispatcher"))
|
UPROPERTY(EditAnywhere, meta=(Description="Name of the new event dispatcher"))
|
||||||
FString Dispatcher;
|
FString Dispatcher;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Description="Input Variables, one per line, expressed as: type var = value"))
|
UPROPERTY(EditAnywhere, meta=(Description="Variables"))
|
||||||
FString InputVariables;
|
FWingRestOfArgv Variables;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
{
|
{
|
||||||
UWingServer::AddHandler(this,
|
UWingServer::AddHandler(this,
|
||||||
TEXT("Add a new event dispatcher to a Blueprint."));
|
TEXT("Add a new event dispatcher to a Blueprint. "
|
||||||
|
"Variables must be expressed as 'kind type name (flags) = default'. "
|
||||||
|
"Kind can only be 'input'."));
|
||||||
|
|
||||||
}
|
}
|
||||||
virtual void Handle() override
|
virtual void Handle() override
|
||||||
{
|
{
|
||||||
@@ -49,7 +52,7 @@ public:
|
|||||||
|
|
||||||
// Parse the arguments.
|
// Parse the arguments.
|
||||||
WingVariables Vars;
|
WingVariables Vars;
|
||||||
if (!Vars.InputVariables.ParseString(InputVariables, WingOut::Stdout)) return;
|
if (!Vars.Parse(Variables.Argv, false, WingOut::Stdout)) return;
|
||||||
|
|
||||||
// Add the delegate variable
|
// Add the delegate variable
|
||||||
FEdGraphPinType DelegateType;
|
FEdGraphPinType DelegateType;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Target node"))
|
UPROPERTY(EditAnywhere, meta=(Description="Target node"))
|
||||||
FString Node;
|
FString Node;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="True to show minor node properties"))
|
UPROPERTY(EditAnywhere, meta=(Description="True to show minor node properties"))
|
||||||
bool Details = false;
|
bool Details = false;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Path to graph"))
|
UPROPERTY(EditAnywhere, meta=(Description="Path to graph"))
|
||||||
FString Graph;
|
FString Graph;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="True to show minor node properties"))
|
UPROPERTY(EditAnywhere, meta=(Description="True to show minor node properties"))
|
||||||
bool Details = false;
|
bool Details = false;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Substring filter for type names"))
|
UPROPERTY(EditAnywhere, meta=(Description="Substring filter for type names"))
|
||||||
FString Query;
|
FString Query;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results"))
|
UPROPERTY(EditAnywhere, meta=(Description="Maximum number of results"))
|
||||||
int32 Limit = 100;
|
int32 Limit = 100;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
|
|||||||
@@ -21,22 +21,16 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node"))
|
UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node"))
|
||||||
FString Object;
|
FString Object;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Blueprint variables, one per line"))
|
UPROPERTY(EditAnywhere, meta=(Description="Variable descriptions"))
|
||||||
FString BlueprintVariables;
|
FWingRestOfArgv Variables;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Input variables, one per line"))
|
|
||||||
FString InputVariables;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Output variables, one per line"))
|
|
||||||
FString OutputVariables;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Local variables, one per line"))
|
|
||||||
FString LocalVariables;
|
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
{
|
{
|
||||||
UWingServer::AddHandler(this,
|
UWingServer::AddHandler(this,
|
||||||
TEXT("Add new variables. Format: 'type name (flags) = default', one per line."));
|
TEXT("Add variables to a blueprint, function graph, "
|
||||||
|
"macro graph, event dispatcher graph, or custom event node. "
|
||||||
|
"Each variable must be expressed as: 'kind type name (flags) = default'. "
|
||||||
|
"Kind can be blueprint, input, output, or local."));
|
||||||
}
|
}
|
||||||
virtual void Handle() override
|
virtual void Handle() override
|
||||||
{
|
{
|
||||||
@@ -46,10 +40,7 @@ public:
|
|||||||
|
|
||||||
WingVariables Vars;
|
WingVariables Vars;
|
||||||
if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return;
|
if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return;
|
||||||
if (!Vars.BlueprintVariables.ParseString(BlueprintVariables, WingOut::Stdout)) return;
|
if (!Vars.Parse(Variables.Argv, false, WingOut::Stdout)) return;
|
||||||
if (!Vars.InputVariables.ParseString(InputVariables, WingOut::Stdout)) return;
|
|
||||||
if (!Vars.OutputVariables.ParseString(OutputVariables, WingOut::Stdout)) return;
|
|
||||||
if (!Vars.LocalVariables.ParseString(LocalVariables, WingOut::Stdout)) return;
|
|
||||||
if (!Vars.Check(WingOut::Stdout)) return;
|
if (!Vars.Check(WingOut::Stdout)) return;
|
||||||
if (!Vars.Create(WingOut::Stdout)) return;
|
if (!Vars.Create(WingOut::Stdout)) return;
|
||||||
WingOut::Stdout.Printf(TEXT("Success.\n"));
|
WingOut::Stdout.Printf(TEXT("Success.\n"));
|
||||||
|
|||||||
@@ -21,23 +21,16 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node"))
|
UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node"))
|
||||||
FString Object;
|
FString Object;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Blueprint variables, one per line"))
|
UPROPERTY(EditAnywhere, meta=(Description="Variable descriptions"))
|
||||||
FString BlueprintVariables;
|
FWingRestOfArgv Variables;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Input variables, one per line"))
|
|
||||||
FString InputVariables;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Output variables, one per line"))
|
|
||||||
FString OutputVariables;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Local variables, one per line"))
|
|
||||||
FString LocalVariables;
|
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
{
|
{
|
||||||
UWingServer::AddHandler(this,
|
UWingServer::AddHandler(this,
|
||||||
TEXT("Modify variables of a blueprint, function graph, "
|
TEXT("Add variables to a blueprint, function graph, "
|
||||||
"macro graph, event dispatcher graph, or custom event node. "));
|
"macro graph, event dispatcher graph, or custom event node. "
|
||||||
|
"Each variable must be expressed as: 'kind type name (flags) = default'. "
|
||||||
|
"Kind can be blueprint, input, output, or local."));
|
||||||
}
|
}
|
||||||
virtual void Handle() override
|
virtual void Handle() override
|
||||||
{
|
{
|
||||||
@@ -47,10 +40,7 @@ public:
|
|||||||
|
|
||||||
WingVariables Vars;
|
WingVariables Vars;
|
||||||
if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return;
|
if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return;
|
||||||
if (!Vars.BlueprintVariables.ParseString(BlueprintVariables, WingOut::Stdout)) return;
|
if (!Vars.Parse(Variables.Argv, false, WingOut::Stdout)) return;
|
||||||
if (!Vars.InputVariables.ParseString(InputVariables, WingOut::Stdout)) return;
|
|
||||||
if (!Vars.OutputVariables.ParseString(OutputVariables, WingOut::Stdout)) return;
|
|
||||||
if (!Vars.LocalVariables.ParseString(LocalVariables, WingOut::Stdout)) return;
|
|
||||||
if (!Vars.Check(WingOut::Stdout)) return;
|
if (!Vars.Check(WingOut::Stdout)) return;
|
||||||
if (!Vars.Modify(WingOut::Stdout)) return;
|
if (!Vars.Modify(WingOut::Stdout)) return;
|
||||||
WingOut::Stdout.Printf(TEXT("Success.\n"));
|
WingOut::Stdout.Printf(TEXT("Success.\n"));
|
||||||
|
|||||||
@@ -21,22 +21,15 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node"))
|
UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node"))
|
||||||
FString Object;
|
FString Object;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Blueprint variable names to remove, comma-separated"))
|
UPROPERTY(EditAnywhere, meta=(Description="Variable descriptions"))
|
||||||
FString BlueprintVariables;
|
FWingRestOfArgv Variables;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Input variable names to remove, comma-separated"))
|
|
||||||
FString InputVariables;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Output variable names to remove, comma-separated"))
|
|
||||||
FString OutputVariables;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Local variable names to remove, comma-separated"))
|
|
||||||
FString LocalVariables;
|
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
{
|
{
|
||||||
UWingServer::AddHandler(this,
|
UWingServer::AddHandler(this,
|
||||||
TEXT("Remove variables from a blueprint, graph, or custom event node."));
|
TEXT("Remove variables from a blueprint, graph, or custom event node. "
|
||||||
|
"Each variable must be expressed as: 'kind name'. "
|
||||||
|
"Kind can be blueprint, input, output, or local."));
|
||||||
}
|
}
|
||||||
virtual void Handle() override
|
virtual void Handle() override
|
||||||
{
|
{
|
||||||
@@ -46,10 +39,7 @@ public:
|
|||||||
|
|
||||||
WingVariables Vars;
|
WingVariables Vars;
|
||||||
if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return;
|
if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return;
|
||||||
if (!Vars.BlueprintVariables.ParseNamesString(BlueprintVariables, WingOut::Stdout)) return;
|
if (!Vars.Parse(Variables.Argv, true, WingOut::Stdout)) return;
|
||||||
if (!Vars.InputVariables.ParseNamesString(InputVariables, WingOut::Stdout)) return;
|
|
||||||
if (!Vars.OutputVariables.ParseNamesString(OutputVariables, WingOut::Stdout)) return;
|
|
||||||
if (!Vars.LocalVariables.ParseNamesString(LocalVariables, WingOut::Stdout)) return;
|
|
||||||
if (!Vars.Remove(WingOut::Stdout)) return;
|
if (!Vars.Remove(WingOut::Stdout)) return;
|
||||||
WingOut::Stdout.Printf(TEXT("Success.\n"));
|
WingOut::Stdout.Printf(TEXT("Success.\n"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Name for the new widget"))
|
UPROPERTY(EditAnywhere, meta=(Description="Name for the new widget"))
|
||||||
FString Name;
|
FString Name;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Parent widget name. If omitted, sets as root."))
|
UPROPERTY(EditAnywhere, meta=(Description="Parent widget name. If omitted, sets as root."))
|
||||||
FString Parent;
|
FString Parent;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Whether to expose the widget as a variable in the blueprint (default false)"))
|
UPROPERTY(EditAnywhere, meta=(Description="Whether to expose the widget as a variable in the blueprint (default false)"))
|
||||||
bool IsVariable = false;
|
bool IsVariable = false;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
@@ -117,6 +117,10 @@ public:
|
|||||||
BP->WidgetTree->RootWidget = NewWidget;
|
BP->WidgetTree->RootWidget = NewWidget;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
|
||||||
WingOut::Stdout.Printf(TEXT("Created widget '%s' of type '%s'\n"), *Name, *Type);
|
WingOut::Stdout.Printf(TEXT("Created widget '%s' of type '%s'\n"), *Name, *Type);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,17 +6,23 @@
|
|||||||
|
|
||||||
void WingManual::PrintHandlerPrototype(const FWingHandlerConfig& Handler)
|
void WingManual::PrintHandlerPrototype(const FWingHandlerConfig& Handler)
|
||||||
{
|
{
|
||||||
|
WingOut::Stdout.Print(TEXT("ue-wingman "));
|
||||||
WingOut::Stdout.Print(Handler.Name);
|
WingOut::Stdout.Print(Handler.Name);
|
||||||
WingOut::Stdout.Print(TEXT("("));
|
|
||||||
bool bFirst = true;
|
|
||||||
for (TFieldIterator<FProperty> PropIt(Handler.HandlerClass.Get(), EFieldIterationFlags::None); PropIt; ++PropIt)
|
for (TFieldIterator<FProperty> PropIt(Handler.HandlerClass.Get(), EFieldIterationFlags::None); PropIt; ++PropIt)
|
||||||
{
|
{
|
||||||
if (!bFirst) WingOut::Stdout.Print(TEXT(","));
|
FStructProperty* StructProp = CastField<FStructProperty>(*PropIt);
|
||||||
bFirst = false;
|
const bool bIsRest =
|
||||||
if (PropIt->HasMetaData(TEXT("Optional"))) WingOut::Stdout.Print(TEXT("?"));
|
StructProp && (StructProp->Struct == FWingRestOfArgv::StaticStruct());
|
||||||
WingOut::Stdout.Print(PropIt->GetName());
|
if (bIsRest)
|
||||||
|
{
|
||||||
|
WingOut::Stdout.Printf(TEXT(" [%s...]"), *PropIt->GetName());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WingOut::Stdout.Printf(TEXT(" %s"), *PropIt->GetName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
WingOut::Stdout.Print(TEXT(")\n"));
|
WingOut::Stdout.Print(TEXT("\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void WingManual::PrintHandlerArguments(const FWingHandlerConfig& Handler)
|
void WingManual::PrintHandlerArguments(const FWingHandlerConfig& Handler)
|
||||||
@@ -26,197 +32,193 @@ void WingManual::PrintHandlerArguments(const FWingHandlerConfig& Handler)
|
|||||||
{
|
{
|
||||||
FProperty* Prop = *PropIt;
|
FProperty* Prop = *PropIt;
|
||||||
FString Name = Prop->GetName();
|
FString Name = Prop->GetName();
|
||||||
FString Type = UWingTypes::TypeToText(Prop);
|
FString Desc = Prop->GetMetaData(TEXT("Description"));
|
||||||
bool bOptional = Prop->HasMetaData(TEXT("Optional"));
|
if (Desc.IsEmpty()) Desc = TEXT("No documentation");
|
||||||
const FString& Desc = Prop->GetMetaData(TEXT("Description"));
|
|
||||||
|
|
||||||
if (bOptional)
|
WingOut::Stdout.Printf(TEXT(" %s - %s\n"), *Name, *Desc);
|
||||||
{
|
|
||||||
WingOut::Stdout.Printf(TEXT(" %s (optional %s)"), *Name, *Type);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
WingOut::Stdout.Printf(TEXT(" %s (%s)"), *Name, *Type);
|
|
||||||
}
|
|
||||||
if (!Desc.IsEmpty()) WingOut::Stdout.Printf(TEXT(" — %s"), *Desc);
|
|
||||||
WingOut::Stdout.Print(TEXT("\n"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WingManual::PrintHandlerDescription(const FWingHandlerConfig& Handler)
|
void WingManual::PrintHandlerDescription(const FWingHandlerConfig& Handler)
|
||||||
{
|
{
|
||||||
if (Handler.Documentation.IsEmpty()) return;
|
if (Handler.Documentation.IsEmpty()) return;
|
||||||
WingOut::Stdout.Print(WingUtils::WrapText(Handler.Documentation, 80, TEXT(" // ")));
|
WingOut::Stdout.Printf(TEXT("\n%s\n\n"), *Handler.Documentation);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WingManual::PrintHandlerHelp(const FWingHandlerConfig& Handler)
|
void WingManual::PrintHandlerHelp(const FWingHandlerConfig& Handler)
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Print(TEXT("\n"));
|
WingOut::Stdout.Print(TEXT("\n"));
|
||||||
PrintHandlerPrototype(Handler);
|
PrintHandlerPrototype(Handler);
|
||||||
PrintHandlerArguments(Handler);
|
PrintHandlerArguments(Handler);
|
||||||
PrintHandlerDescription(Handler);
|
PrintHandlerDescription(Handler);
|
||||||
WingOut::Stdout.Print(TEXT("\n"));
|
WingOut::Stdout.Print(TEXT("\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void UWingManualSections::FetcherPaths()
|
void UWingManualSections::FetcherPaths()
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Print(TEXT(
|
WingOut::Stdout.Print(TEXT(
|
||||||
"\n FETCHER PATHS:"
|
"\n FETCHER PATHS:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n Most commands require you to specify a 'fetcher path'."
|
"\n Most commands require you to specify a 'fetcher path'."
|
||||||
"\n A fetcher path starts with an asset name, followed by"
|
"\n A fetcher path starts with an asset name, followed by"
|
||||||
"\n steps that navigate into the asset. Some Examples:"
|
"\n steps that navigate into the asset. Some Examples:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n /Game/Widgets/WB_Hotkeys,widget:Canvas.122"
|
"\n /Game/Widgets/WB_Hotkeys,widget:Canvas.122"
|
||||||
"\n /Game/Testing/BP_Test,graph:Rescale.Actor,node:K2Node_CallFunction_0,pin:Scale"
|
"\n /Game/Testing/BP_Test,graph:Rescale.Actor,node:K2Node_CallFunction_0,pin:Scale"
|
||||||
"\n /Game/Chars/BP_Manny,component:Camera.Boom"
|
"\n /Game/Chars/BP_Manny,component:Camera.Boom"
|
||||||
"\n"
|
"\n"
|
||||||
"\n The navigation steps supported are:"
|
"\n The navigation steps supported are:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n graph — move from a blueprint or material to a graph."
|
"\n graph — move from a blueprint or material to a graph."
|
||||||
"\n node — move from a graph to a graph node"
|
"\n node — move from a graph to a graph node"
|
||||||
"\n pin — move from a graph node to a pin"
|
"\n pin — move from a graph node to a pin"
|
||||||
"\n component — move from a blueprint to a component"
|
"\n component — move from a blueprint to a component"
|
||||||
"\n levelblueprint — move from a world to a blueprint"
|
"\n levelblueprint — move from a world to a blueprint"
|
||||||
"\n widget — move from a widget blueprint to a widget"
|
"\n widget — move from a widget blueprint to a widget"
|
||||||
"\n structprop — move into a struct property of an object"
|
"\n structprop — move into a struct property of an object"
|
||||||
"\n"
|
"\n"
|
||||||
"\n Notice that paths use escaped fnames. See the section"
|
"\n Notice that paths use escaped fnames. See the section"
|
||||||
"\n on escape sequences in fnames below sfor more information."
|
"\n on escape sequences in fnames below sfor more information."
|
||||||
"\n"
|
"\n"
|
||||||
"\n Steps do not always require a parameter. For example, materials"
|
"\n Steps do not always require a parameter. For example, materials"
|
||||||
"\n only have one graph, so you can just say:"
|
"\n only have one graph, so you can just say:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n /Game/Materials/MyMaterial,graph"
|
"\n /Game/Materials/MyMaterial,graph"
|
||||||
"\n"
|
"\n"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void UWingManualSections::ExpressingTypes()
|
void UWingManualSections::ExpressingTypes()
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Print(TEXT(
|
WingOut::Stdout.Print(TEXT(
|
||||||
"\n EXPRESSING TYPES:"
|
"\n EXPRESSING TYPES:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n To change the type of a variable, or to express function parameters,"
|
"\n To change the type of a variable, or to express function parameters,"
|
||||||
"\n you will use our syntax for types. Here are some valid examples:"
|
"\n you will use our syntax for types. Here are some valid examples:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n Bool, String, Vector, Rotator, HitResult, Actor, Character,"
|
"\n Bool, String, Vector, Rotator, HitResult, Actor, Character,"
|
||||||
"\n PlayerController, EBlendMode, EMovementMode, BP_Manny, BP_Quinn,"
|
"\n PlayerController, EBlendMode, EMovementMode, BP_Manny, BP_Quinn,"
|
||||||
"\n Array<Int>, Set<String>, Map<Int,Actor>"
|
"\n Array<Int>, Set<String>, Map<Int,Actor>"
|
||||||
"\n Soft<ABP_Manny>, Class<Pawn>, SoftClass<Pawn>"
|
"\n Soft<ABP_Manny>, Class<Pawn>, SoftClass<Pawn>"
|
||||||
"\n"
|
"\n"
|
||||||
"\n Notice that it's 'Actor', not 'AActor'. Type names are not"
|
"\n Notice that it's 'Actor', not 'AActor'. Type names are not"
|
||||||
"\n case-sensitive. When a blueprint like /Game/Testing/BP_Foo"
|
"\n case-sensitive. When a blueprint like /Game/Testing/BP_Foo"
|
||||||
"\n is used as a type, the typename is just BP_Foo. You can search"
|
"\n is used as a type, the typename is just BP_Foo. You can search"
|
||||||
"\n for valid types using the TypeName_Search command."
|
"\n for valid types using the TypeName_Search command."
|
||||||
"\n"
|
"\n"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void UWingManualSections::VariableDeclarations()
|
void UWingManualSections::VariableDeclarations()
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Print(TEXT(
|
WingOut::Stdout.Print(TEXT(
|
||||||
"\n VARIABLE DECLARATIONS:"
|
"\n VARIABLE DECLARATIONS:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n We have our own syntax for variable declarations: a type,"
|
"\n We have our own syntax for variable declarations:"
|
||||||
"\n a name, optional flags, and an optional default value,"
|
"\n"
|
||||||
"\n always on one line:"
|
"\n kind type name (optional flags) = optional default value"
|
||||||
"\n"
|
"\n"
|
||||||
"\n Array<Actor> Actors"
|
"\n Kind can be:"
|
||||||
"\n Float F (InstanceEditable)"
|
"\n"
|
||||||
"\n String S = This is the default value"
|
"\n blueprint - eg, instance variables"
|
||||||
"\n"
|
"\n input - a function argument or macro input"
|
||||||
"\n The commands Variables_Add, Variables_Modify,"
|
"\n output - a function return value or macro output"
|
||||||
"\n and Variables_Remove can be used to edit "
|
"\n local - local variables"
|
||||||
"\n blueprint variables, graph local variables, graph input"
|
"\n"
|
||||||
"\n variables, graph output variables, and custom"
|
"\n Here are some examples:"
|
||||||
"\n event node input variables. Event dispatchers are"
|
"\n"
|
||||||
"\n also graphs, so they too can be edited."
|
"\n input Array<Actor> Actors"
|
||||||
"\n"
|
"\n output Float F (InstanceEditable)"
|
||||||
));
|
"\n blueprint String S = This is the default value"
|
||||||
|
"\n"
|
||||||
|
"\n The commands Variables_Add, Variables_Modify,"
|
||||||
|
"\n and Variables_Remove can be used to edit "
|
||||||
|
"\n blueprint variables, graph local variables, graph input"
|
||||||
|
"\n variables, graph output variables, and custom"
|
||||||
|
"\n event node input variables. Event dispatchers are"
|
||||||
|
"\n also graphs, so they too can be edited."
|
||||||
|
"\n"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void UWingManualSections::EscapeSequencesInFNames()
|
void UWingManualSections::EscapeSequencesInFNames()
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Print(TEXT(
|
WingOut::Stdout.Print(TEXT(
|
||||||
"\n ESCAPE SEQUENCES IN FNAMES:"
|
"\n ESCAPE SEQUENCES IN FNAMES:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n When we output FNames, we use HTML escape sequences for the"
|
"\n When we output FNames, we use HTML escape sequences for the"
|
||||||
"\n following marks: \\\"'(),.:;<=>&, and for certain other characters."
|
"\n following marks: \\\"'(),.:;<=>&, and for certain other characters."
|
||||||
"\n We also translate spaces to periods."
|
"\n We also translate spaces to periods."
|
||||||
"\n"
|
"\n"
|
||||||
"\n When sending FNames to UE Wingman, you *must* escape the marks"
|
"\n When sending FNames to UE Wingman, you *must* escape the marks"
|
||||||
"\n listed above, but you *may* escape any character. To send an FName"
|
"\n listed above, but you *may* escape any character. To send an FName"
|
||||||
"\n with a space in it, either use   or a period."
|
"\n with a space in it, either use   or a period."
|
||||||
"\n"
|
"\n"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void UWingManualSections::MaterialEditing()
|
void UWingManualSections::MaterialEditing()
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Print(TEXT(
|
WingOut::Stdout.Print(TEXT(
|
||||||
"\n MATERIAL EDITING:"
|
"\n MATERIAL EDITING:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n We do not expose material expressions directly. Instead, you"
|
"\n We do not expose material expressions directly. Instead, you"
|
||||||
"\n will be editing the material graph. However, if you Graph_Dump"
|
"\n will be editing the material graph. However, if you Graph_Dump"
|
||||||
"\n a material graph, you will see that the nodes contain"
|
"\n a material graph, you will see that the nodes contain"
|
||||||
"\n properties which actually come from the material expressions."
|
"\n properties which actually come from the material expressions."
|
||||||
"\n You can edit these using Details_Set on the node."
|
"\n You can edit these using Details_Set on the node."
|
||||||
"\n"
|
"\n"
|
||||||
"\n Don't overlook custom HLSL nodes. These can accomplish in\n"
|
"\n Don't overlook custom HLSL nodes. These can accomplish in\n"
|
||||||
"\n a single node what would otherwise take many.\n"
|
"\n a single node what would otherwise take many.\n"
|
||||||
"\n"
|
"\n"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void UWingManualSections::NodeContextMenus()
|
void UWingManualSections::NodeContextMenus()
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Print(TEXT(
|
WingOut::Stdout.Print(TEXT(
|
||||||
"\n NODE CONTEXT MENUS:"
|
"\n NODE CONTEXT MENUS:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n GraphNode_ShowMenu and GraphNode_ChooseMenu give access"
|
"\n GraphNode_ShowMenu and GraphNode_ChooseMenu give access"
|
||||||
"\n to the node context menu. This menu includes both node"
|
"\n to the node context menu. This menu includes both node"
|
||||||
"\n operations and pin operations (e.g. Split Struct Pin,"
|
"\n operations and pin operations (e.g. Split Struct Pin,"
|
||||||
"\n Add Pin)."
|
"\n Add Pin)."
|
||||||
"\n"
|
"\n"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void UWingManualSections::VariableGettersAndSetters()
|
void UWingManualSections::VariableGettersAndSetters()
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Print(TEXT(
|
WingOut::Stdout.Print(TEXT(
|
||||||
"\n VARIABLE GETTERS AND SETTERS:"
|
"\n VARIABLE GETTERS AND SETTERS:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n Access to local vars, function parameters, and "
|
"\n Access to local vars, function parameters, and "
|
||||||
"\n blueprint vars is through getter and setter nodes. "
|
"\n blueprint vars is through getter and setter nodes. "
|
||||||
"\n These can be found in GraphNode_SearchTypes by "
|
"\n These can be found in GraphNode_SearchTypes by "
|
||||||
"\n searching for 'Variable'. Some examples:"
|
"\n searching for 'Variable'. Some examples:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n SKEL_WB_Menu_C|Variables|Default|GetPlaceTangible"
|
"\n SKEL_WB_Menu_C|Variables|Default|GetPlaceTangible"
|
||||||
"\n SKEL_WB_Menu_C|Variables|Default|SetPlaceTangible"
|
"\n SKEL_WB_Menu_C|Variables|Default|SetPlaceTangible"
|
||||||
"\n SKEL_WB_Menu_C|Variables|WB_Menu|GetMenuPanel"
|
"\n SKEL_WB_Menu_C|Variables|WB_Menu|GetMenuPanel"
|
||||||
"\n"
|
"\n"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void UWingManualSections::ImportantCommands()
|
void UWingManualSections::ImportantCommands()
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Print(TEXT(
|
WingOut::Stdout.Print(TEXT(
|
||||||
"\n IMPORTANT COMMANDS:"
|
"\n IMPORTANT COMMANDS:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n Documentation_Manual: print manual sections"
|
"\n Documentation_Manual: print manual sections"
|
||||||
"\n Documentation_Commands: a list of all the main commands"
|
"\n Documentation_Commands: A concise list of all ue-wingman commands"
|
||||||
"\n Documentation_CreateAssets: Additional commands that create new assets"
|
"\n Documentation_Command: Detailed documentation for a single ue-wingman command"
|
||||||
"\n Blueprint_Dump: a summary of any blueprint"
|
"\n Documentation_CreateAssets: Additional commands that create new assets"
|
||||||
"\n Graph_Dump: a fairly detailed listing of any Graph"
|
"\n Blueprint_Dump: a summary of any blueprint"
|
||||||
"\n Details_Dump: Dump the details panel for a given object"
|
"\n Graph_Dump: a fairly detailed listing of any Graph"
|
||||||
"\n Details_Set: Manipulate the details panel for a given object"
|
"\n Details_Dump: Dump the details panel for a given object"
|
||||||
"\n Sequence: Batch commands together for faster execution"
|
"\n Details_Set: Manipulate the details panel for a given object"
|
||||||
"\n"
|
"\n"
|
||||||
"\n You can use Documentation_Commands(Query=Command,Verbose=true)"
|
));
|
||||||
"\n to get detailed help for a specific command."
|
|
||||||
"\n"
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TSet<FName> WingManual::GetSections()
|
TSet<FName> WingManual::GetSections()
|
||||||
@@ -232,7 +234,7 @@ TSet<FName> WingManual::GetSections()
|
|||||||
void WingManual::PrintSectionNames(const TCHAR *Prefix, const TSet<FName>& Sections, WingOut Output)
|
void WingManual::PrintSectionNames(const TCHAR *Prefix, const TSet<FName>& Sections, WingOut Output)
|
||||||
{
|
{
|
||||||
if (Sections.IsEmpty()) return;
|
if (Sections.IsEmpty()) return;
|
||||||
if (Prefix) Output.Print(Prefix);
|
if (Prefix) Output.Print(Prefix);
|
||||||
bool bFirst = true;
|
bool bFirst = true;
|
||||||
for (const FName& Section : Sections)
|
for (const FName& Section : Sections)
|
||||||
{
|
{
|
||||||
@@ -240,7 +242,7 @@ void WingManual::PrintSectionNames(const TCHAR *Prefix, const TSet<FName>& Secti
|
|||||||
bFirst = false;
|
bFirst = false;
|
||||||
Output.Printf(TEXT("%s"), *Section.ToString());
|
Output.Printf(TEXT("%s"), *Section.ToString());
|
||||||
}
|
}
|
||||||
if (Prefix) Output.Print(TEXT("\n"));
|
if (Prefix) Output.Print(TEXT("\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WingManual::PrintSection(FName Section)
|
bool WingManual::PrintSection(FName Section)
|
||||||
@@ -256,10 +258,12 @@ void WingManual::Commands(EWingHandlerKind Kind, const FString& Query, bool Verb
|
|||||||
FString QueryLower = Query.ToLower();
|
FString QueryLower = Query.ToLower();
|
||||||
FString PrevGroup;
|
FString PrevGroup;
|
||||||
|
|
||||||
|
bool any = false;
|
||||||
for (const FWingHandlerConfig& H : UWingServer::AllHandlers())
|
for (const FWingHandlerConfig& H : UWingServer::AllHandlers())
|
||||||
{
|
{
|
||||||
if (H.Kind != Kind) continue;
|
if (H.Kind != Kind) continue;
|
||||||
if (!H.Name.ToLower().Contains(QueryLower)) continue;
|
if (!H.Name.ToLower().Contains(QueryLower)) continue;
|
||||||
|
any = true;
|
||||||
|
|
||||||
// Blank line between groups
|
// Blank line between groups
|
||||||
if (!Verbose)
|
if (!Verbose)
|
||||||
@@ -278,4 +282,9 @@ void WingManual::Commands(EWingHandlerKind Kind, const FString& Query, bool Verb
|
|||||||
else
|
else
|
||||||
PrintHandlerPrototype(H);
|
PrintHandlerPrototype(H);
|
||||||
}
|
}
|
||||||
|
if (!any)
|
||||||
|
{
|
||||||
|
WingOut::Stdout.Print(TEXT("No matching commands. To see a full list, type:\n"));
|
||||||
|
WingOut::Stdout.Print(TEXT(" ue-wingman Documentation_Commands.\n"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,57 +194,6 @@ bool FWingProperty::SetText(FString Value, WingOut Errors) const
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FWingProperty::SetJson(const FJsonValue &JsonValue, WingOut Errors) const
|
|
||||||
{
|
|
||||||
if (!CheckEditable(Errors)) return false;
|
|
||||||
|
|
||||||
if (JsonValue.Type == EJson::String)
|
|
||||||
{
|
|
||||||
return SetText(JsonValue.AsString(), Errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (JsonValue.Type == EJson::Number)
|
|
||||||
{
|
|
||||||
return SetDouble(JsonValue.AsNumber(), Errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (JsonValue.Type == EJson::Boolean)
|
|
||||||
{
|
|
||||||
return SetBool(JsonValue.AsBool(), Errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (JsonValue.Type == EJson::Object)
|
|
||||||
{
|
|
||||||
FStructProperty* StructProp = CastField<FStructProperty>(Prop);
|
|
||||||
if (StructProp && (StructProp->Struct == FWingJsonObject::StaticStruct()))
|
|
||||||
{
|
|
||||||
FWingJsonObject Val;
|
|
||||||
Val.Json = JsonValue.AsObject();
|
|
||||||
Prop->SetValue_InContainer(Container, &Val);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
PrintExpectsReceived(TEXT("json object"), Errors);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (JsonValue.Type == EJson::Array)
|
|
||||||
{
|
|
||||||
FStructProperty* StructProp = CastField<FStructProperty>(Prop);
|
|
||||||
if (StructProp && (StructProp->Struct == FWingJsonArray::StaticStruct()))
|
|
||||||
{
|
|
||||||
FWingJsonArray Val;
|
|
||||||
Val.Array = JsonValue.AsArray();
|
|
||||||
Prop->SetValue_InContainer(Container, &Val);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
PrintExpectsReceived(TEXT("json array"), Errors);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
PrintExpectsReceived(TEXT("Unrecognized Json Data"), Errors);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
TOptional<UObject*> FWingProperty::GetObject(WingOut Errors) const
|
TOptional<UObject*> FWingProperty::GetObject(WingOut Errors) const
|
||||||
{
|
{
|
||||||
FObjectPropertyBase *OProp = CastField<FObjectPropertyBase>(Prop);
|
FObjectPropertyBase *OProp = CastField<FObjectPropertyBase>(Prop);
|
||||||
@@ -493,56 +442,54 @@ TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, bool Mutable)
|
|||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool FWingProperty::PopulateFromArgv(TArray<FWingProperty>& Props, TConstArrayView<FString> Argv, WingOut Errors)
|
||||||
bool FWingProperty::PopulateFromJson(TArray<FWingProperty>& Props, const FJsonObject& Json, bool AllOptional, WingOut Errors)
|
|
||||||
{
|
{
|
||||||
bool Ok = true;
|
int32 ArgIndex = 0;
|
||||||
|
for (int32 PropIndex = 0; PropIndex < Props.Num(); ++PropIndex)
|
||||||
// Build a set of known property names for the unknown-field check.
|
|
||||||
TSet<FName> KnownKeys;
|
|
||||||
for (const FWingProperty& P : Props) KnownKeys.Add(P->GetFName());
|
|
||||||
|
|
||||||
// Check for unknown fields in the JSON
|
|
||||||
for (const auto& KV : Json.Values)
|
|
||||||
{
|
{
|
||||||
FName Name = WingUtils::CheckInternalizeID(KV.Key, Errors);
|
FWingProperty& P = Props[PropIndex];
|
||||||
if (!KnownKeys.Contains(Name))
|
FStructProperty* StructProp = CastField<FStructProperty>(P.Prop);
|
||||||
{
|
const bool bIsRest =
|
||||||
Errors.Printf(TEXT("ERROR: Unknown parameter '%s'\n"), *KV.Key);
|
StructProp && (StructProp->Struct == FWingRestOfArgv::StaticStruct());
|
||||||
Ok = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate each property from JSON
|
if (bIsRest)
|
||||||
for (FWingProperty& P : Props)
|
|
||||||
{
|
|
||||||
FString JsonKey = WingUtils::FormatName(P.Prop);
|
|
||||||
TSharedPtr<FJsonValue> Value = Json.TryGetField(JsonKey);
|
|
||||||
if (!Value)
|
|
||||||
{
|
{
|
||||||
bool Optional = AllOptional || P.Prop->HasMetaData(TEXT("Optional"));
|
if (PropIndex + 1 != Props.Num())
|
||||||
if (!Optional)
|
|
||||||
{
|
{
|
||||||
Errors.Printf(TEXT("ERROR: Missing required parameter '%s'\n"), *JsonKey);
|
Errors.Printf(TEXT("ERROR: '%s' must be the last parameter\n"),
|
||||||
Ok = false;
|
*WingUtils::FormatName(P.Prop));
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FWingRestOfArgv Rest;
|
||||||
|
for (int32 I = ArgIndex; I < Argv.Num(); ++I)
|
||||||
|
{
|
||||||
|
Rest.Argv.Add(Argv[I]);
|
||||||
|
}
|
||||||
|
P.Prop->SetValue_InContainer(P.Container, &Rest);
|
||||||
|
ArgIndex = Argv.Num();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!P.SetJson(*Value, Errors)) Ok = false;
|
|
||||||
}
|
|
||||||
return Ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool FWingProperty::PopulateFromJson(TArray<FWingProperty>& Props, const FJsonValue& Json, bool AllOptional, WingOut Errors)
|
if (ArgIndex >= Argv.Num())
|
||||||
{
|
{
|
||||||
// Make sure they passed in a JSON object.
|
Errors.Printf(TEXT("ERROR: Missing parameter '%s'\n"),
|
||||||
TSharedPtr<FJsonObject> Obj = Json.AsObject();
|
*WingUtils::FormatName(P.Prop));
|
||||||
if (Obj == nullptr)
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!P.SetText(Argv[ArgIndex], Errors)) return false;
|
||||||
|
ArgIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ArgIndex < Argv.Num())
|
||||||
{
|
{
|
||||||
Errors.Printf(TEXT("property data should be stored in a json object\n"));
|
Errors.Printf(TEXT("ERROR: Too many parameters, starting with '%s'\n"),
|
||||||
|
*Argv[ArgIndex]);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return PopulateFromJson(Props, *Obj, AllOptional, Errors);
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ void UWingServer::Deinitialize()
|
|||||||
bShuttingDown = true;
|
bShuttingDown = true;
|
||||||
for (auto& Msg : PendingMessages)
|
for (auto& Msg : PendingMessages)
|
||||||
{
|
{
|
||||||
Msg->Response.SetValue(FString());
|
Msg->Response.SetValue(TArray<uint8>());
|
||||||
}
|
}
|
||||||
PendingMessages.Empty();
|
PendingMessages.Empty();
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,7 @@ void UWingServer::Tick(float DeltaTime)
|
|||||||
// If we have a request, process it.
|
// If we have a request, process it.
|
||||||
if (Request.IsValid())
|
if (Request.IsValid())
|
||||||
{
|
{
|
||||||
FString Response = HandleRequest(Request->Line);
|
TArray<uint8> Response = HandleRequest(Request->Request);
|
||||||
Request->Response.SetValue(Response);
|
Request->Response.SetValue(Response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,71 +169,24 @@ TStatId UWingServer::GetStatId() const
|
|||||||
// HandleRequest — Given a command, execute it.
|
// HandleRequest — Given a command, execute it.
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
FString UWingServer::HandleRequest(const FString& Line)
|
TArray<uint8> UWingServer::HandleRequest(const TArray<uint8>& RequestBytes)
|
||||||
{
|
{
|
||||||
// Parse the request as JSON before doing anything else.
|
TArray<FString> Argv;
|
||||||
TSharedPtr<FJsonValue> Value;
|
FString ResponseText;
|
||||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Line);
|
|
||||||
if (!FJsonSerializer::Deserialize(Reader, Value))
|
|
||||||
return PackageResponses({TEXT("Invalid Json")});
|
|
||||||
|
|
||||||
const TSharedPtr<FJsonObject>* RequestPtr = nullptr;
|
|
||||||
if (!Value->TryGetObject(RequestPtr))
|
|
||||||
return PackageResponses({TEXT("Json must be an object")});
|
|
||||||
TSharedPtr<FJsonObject> Request = *RequestPtr;
|
|
||||||
|
|
||||||
FString Command;
|
if (DeserializeArgv(RequestBytes, Argv))
|
||||||
Request->TryGetStringField(TEXT("command"), Command);
|
|
||||||
if (Command == TEXT("Sequence"))
|
|
||||||
{
|
{
|
||||||
const TArray<TSharedPtr<FJsonValue>>* Subcommands = nullptr;
|
PreCallHandler();
|
||||||
if (!Request->TryGetArrayField(TEXT("subcommands"), Subcommands))
|
TryCallHandler(Argv);
|
||||||
return PackageResponses({TEXT("Sequence requires a 'subcommands' array.")});
|
ResponseText = PostCallHandler();
|
||||||
|
|
||||||
TArray<FString> Responses;
|
|
||||||
Responses.Reserve(Subcommands->Num());
|
|
||||||
for (const TSharedPtr<FJsonValue>& Sub : *Subcommands)
|
|
||||||
{
|
|
||||||
const TSharedPtr<FJsonObject>* SubObjPtr = nullptr;
|
|
||||||
if (!Sub->TryGetObject(SubObjPtr))
|
|
||||||
Responses.Add(TEXT("Subcommand must be a JSON object."));
|
|
||||||
else
|
|
||||||
Responses.Add(HandleJsonRequest(*SubObjPtr));
|
|
||||||
}
|
|
||||||
return PackageResponses(Responses);
|
|
||||||
}
|
}
|
||||||
|
else ResponseText = TEXT("Invalid argv encoding (bug in ue-wingman.py)\n");
|
||||||
|
|
||||||
return PackageResponses({HandleJsonRequest(Request)});
|
FTCHARToUTF8 Utf8(*ResponseText);
|
||||||
|
return TArray<uint8>(reinterpret_cast<const uint8*>(Utf8.Get()), Utf8.Length());
|
||||||
}
|
}
|
||||||
|
|
||||||
FString UWingServer::PackageResponses(const TArray<FString>& Responses)
|
void UWingServer::PreCallHandler()
|
||||||
{
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Blocks;
|
|
||||||
Blocks.Reserve(Responses.Num());
|
|
||||||
for (const FString& Response : Responses)
|
|
||||||
{
|
|
||||||
// Unreal's JSON writer terminates string serialization at the first
|
|
||||||
// embedded null byte rather than escaping it, which would silently
|
|
||||||
// truncate output. Sanitize null bytes to spaces.
|
|
||||||
FString Sanitized = Response;
|
|
||||||
for (int32 i = 0; i < Sanitized.Len(); ++i)
|
|
||||||
{
|
|
||||||
if (Sanitized[i] == TEXT('\0')) Sanitized[i] = TEXT(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
TSharedPtr<FJsonObject> Block = MakeShared<FJsonObject>();
|
|
||||||
Block->SetStringField(TEXT("type"), TEXT("text"));
|
|
||||||
Block->SetStringField(TEXT("text"), Sanitized);
|
|
||||||
Blocks.Add(MakeShared<FJsonValueObject>(Block));
|
|
||||||
}
|
|
||||||
|
|
||||||
FString OutJson;
|
|
||||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutJson);
|
|
||||||
FJsonSerializer::Serialize(Blocks, Writer);
|
|
||||||
return OutJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
FString UWingServer::HandleJsonRequest(TSharedPtr<FJsonObject> Request)
|
|
||||||
{
|
{
|
||||||
LogCapture.CapturedErrors.Empty();
|
LogCapture.CapturedErrors.Empty();
|
||||||
LogCapture.bEnabled = true;
|
LogCapture.bEnabled = true;
|
||||||
@@ -241,9 +194,10 @@ FString UWingServer::HandleJsonRequest(TSharedPtr<FJsonObject> Request)
|
|||||||
SuggestedManualSections.Empty();
|
SuggestedManualSections.Empty();
|
||||||
bSuggestHandlerHelp = false;
|
bSuggestHandlerHelp = false;
|
||||||
LastHandler = nullptr;
|
LastHandler = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
TryCallHandler(Request);
|
FString UWingServer::PostCallHandler()
|
||||||
|
{
|
||||||
Notifier.SendNotifications();
|
Notifier.SendNotifications();
|
||||||
LogCapture.bEnabled = false;
|
LogCapture.bEnabled = false;
|
||||||
for (const FString& Msg : LogCapture.CapturedErrors)
|
for (const FString& Msg : LogCapture.CapturedErrors)
|
||||||
@@ -269,17 +223,15 @@ FString UWingServer::HandleJsonRequest(TSharedPtr<FJsonObject> Request)
|
|||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UWingServer::TryCallHandler(TSharedPtr<FJsonObject> Request)
|
void UWingServer::TryCallHandler(const TArray<FString>& Argv)
|
||||||
{
|
{
|
||||||
// Extract the command from the request.
|
if (Argv.Num() < 1)
|
||||||
FString Command;
|
|
||||||
if (!Request->TryGetStringField(TEXT("command"), Command))
|
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Printf(TEXT("Request does not contain 'command' parameter"));
|
WingOut::Stdout.Print(TEXT("Missing command\n"));
|
||||||
WingOut::Stdout.Printf(TEXT("We recommend sending command='Documentation_Manual'."));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Request->RemoveField(TEXT("command"));
|
|
||||||
|
FString Command = Argv[0];
|
||||||
|
|
||||||
// Find the handler for the specified command.
|
// Find the handler for the specified command.
|
||||||
FWingHandlerConfig* Found = FindHandler(Command);
|
FWingHandlerConfig* Found = FindHandler(Command);
|
||||||
@@ -296,9 +248,9 @@ void UWingServer::TryCallHandler(TSharedPtr<FJsonObject> Request)
|
|||||||
UWingHandler* Handler = Cast<UWingHandler>(HandlerObj.Get());
|
UWingHandler* Handler = Cast<UWingHandler>(HandlerObj.Get());
|
||||||
Handler->Configuration = Found;
|
Handler->Configuration = Found;
|
||||||
|
|
||||||
// Populate the handler object with the request parameters.
|
// Populate the handler object with argv parameters.
|
||||||
TArray<FWingProperty> Props = FWingProperty::GetVisible(Handler, true);
|
TArray<FWingProperty> Props = FWingProperty::GetVisible(Handler, true);
|
||||||
if (!FWingProperty::PopulateFromJson(Props, *Request, false, WingOut::Stdout))
|
if (!FWingProperty::PopulateFromArgv(Props, MakeArrayView(Argv).RightChop(1), WingOut::Stdout))
|
||||||
{
|
{
|
||||||
UWingServer::SuggestHandlerHelp();
|
UWingServer::SuggestHandlerHelp();
|
||||||
return;
|
return;
|
||||||
@@ -356,102 +308,105 @@ void UWingServer::CleanupFinishedClients()
|
|||||||
|
|
||||||
void UWingServer::ClientThreadFunc(UWingServer* Server, TSharedPtr<FClientConnection> Client)
|
void UWingServer::ClientThreadFunc(UWingServer* Server, TSharedPtr<FClientConnection> Client)
|
||||||
{
|
{
|
||||||
constexpr int32 MaxRecvBufBytes = 1024 * 1024;
|
|
||||||
constexpr int32 MinUnusedRecvSpace = 4096;
|
|
||||||
|
|
||||||
FSocket* Socket = Client->Socket;
|
FSocket* Socket = Client->Socket;
|
||||||
TArray<uint8> RecvBuf;
|
|
||||||
RecvBuf.SetNumUninitialized(MinUnusedRecvSpace);
|
|
||||||
int32 RecvLen = 0;
|
|
||||||
|
|
||||||
WaitForAssetRegistry();
|
WaitForAssetRegistry();
|
||||||
|
|
||||||
while (true)
|
TArray<uint8> Request;
|
||||||
|
if (!ReceiveRequest(Socket, Request))
|
||||||
{
|
{
|
||||||
FString Request;
|
Client->bDone = true;
|
||||||
if (ExtractRequestFromBuffer(RecvBuf, RecvLen, Request))
|
return;
|
||||||
{
|
|
||||||
FString Response;
|
|
||||||
if (!ProcessRequestOnGameThread(Request, Response))
|
|
||||||
{
|
|
||||||
Client->bDone = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the response back, null-terminated (blocking)
|
|
||||||
FTCHARToUTF8 Utf8(*Response);
|
|
||||||
if (!SendAll(Socket, reinterpret_cast<const uint8*>(Utf8.Get()),
|
|
||||||
Utf8.Length() + 1))
|
|
||||||
{
|
|
||||||
Client->bDone = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ReceiveMoreBytesIntoBuffer(Socket, RecvBuf, RecvLen))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TArray<uint8> Response;
|
||||||
|
if (!ProcessRequestOnGameThread(Request, Response))
|
||||||
|
{
|
||||||
|
Client->bDone = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SendAll(Socket, Response.GetData(), Response.Num());
|
||||||
Client->bDone = true;
|
Client->bDone = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UWingServer::ExtractRequestFromBuffer(
|
uint32 UWingServer::UnpackBigEndian(const uint8 *Data)
|
||||||
TArray<uint8>& RecvBuf, int32& RecvLen, FString& OutRequest)
|
|
||||||
{
|
{
|
||||||
const uint8* EndOfRequest = static_cast<const uint8*>(
|
return
|
||||||
memchr(RecvBuf.GetData(), '\0', RecvLen));
|
((uint32)Data[0] << 24) |
|
||||||
if (EndOfRequest == nullptr)
|
((uint32)Data[1] << 16) |
|
||||||
|
((uint32)Data[2] << 8) |
|
||||||
|
(uint32)Data[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UWingServer::DeserializeArgv(
|
||||||
|
const TArray<uint8>& RequestBytes, TArray<FString>& Argv)
|
||||||
|
{
|
||||||
|
Argv.Empty();
|
||||||
|
|
||||||
|
int32 Offset = 0;
|
||||||
|
while (Offset < RequestBytes.Num())
|
||||||
{
|
{
|
||||||
return false;
|
if (RequestBytes.Num() - Offset < 4)
|
||||||
|
{
|
||||||
|
Argv.Empty();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32 Length = UnpackBigEndian(RequestBytes.GetData() + Offset);
|
||||||
|
Offset += 4;
|
||||||
|
|
||||||
|
if ((uint32)(RequestBytes.Num() - Offset) < Length)
|
||||||
|
{
|
||||||
|
Argv.Empty();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Argv.Add(FString::ConstructFromPtrSize(
|
||||||
|
reinterpret_cast<const UTF8CHAR*>(RequestBytes.GetData() + Offset),
|
||||||
|
Length));
|
||||||
|
Offset += (int32)Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int32 MessageLen =
|
|
||||||
static_cast<int32>(EndOfRequest - RecvBuf.GetData());
|
|
||||||
OutRequest = FString::ConstructFromPtrSize(
|
|
||||||
reinterpret_cast<const UTF8CHAR*>(RecvBuf.GetData()), MessageLen);
|
|
||||||
const int32 RemainingBytes = RecvLen - (MessageLen + 1);
|
|
||||||
if (RemainingBytes > 0)
|
|
||||||
{
|
|
||||||
FMemory::Memmove(
|
|
||||||
RecvBuf.GetData(),
|
|
||||||
RecvBuf.GetData() + MessageLen + 1,
|
|
||||||
RemainingBytes);
|
|
||||||
}
|
|
||||||
RecvLen = RemainingBytes;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UWingServer::ReceiveMoreBytesIntoBuffer(
|
bool UWingServer::ReceiveRequest(FSocket* Socket, TArray<uint8>& OutRequest)
|
||||||
FSocket* Socket, TArray<uint8>& RecvBuf, int32& RecvLen)
|
|
||||||
{
|
{
|
||||||
constexpr int32 MaxRecvBufBytes = 1024 * 1024;
|
constexpr int32 MaxRecvBufBytes = 1024 * 1024;
|
||||||
constexpr int32 MinUnusedRecvSpace = 4096;
|
constexpr int32 ChunkSize = 8192;
|
||||||
|
|
||||||
int32 UnusedSpace = RecvBuf.Num() - RecvLen;
|
TArray<uint8> RecvBuf;
|
||||||
if (UnusedSpace < MinUnusedRecvSpace)
|
RecvBuf.Reserve(ChunkSize);
|
||||||
|
|
||||||
|
// Unreal's FSocket API is fundamentally broken: recv cannot
|
||||||
|
// differentiate between a socket that has been cleanly closed
|
||||||
|
// and a socket that has had an error. So we have no choice
|
||||||
|
// but to just read until recv returns false (which could be a
|
||||||
|
// clean close or an error). Then, we check if we have a cleanly
|
||||||
|
// encoded payload: if so, we assume everything is fine.
|
||||||
|
while (true)
|
||||||
{
|
{
|
||||||
if (RecvBuf.Num() >= MaxRecvBufBytes)
|
uint8 Temp[ChunkSize];
|
||||||
|
int32 BytesRead = 0;
|
||||||
|
if (!Socket->Recv(Temp, ChunkSize, BytesRead))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (BytesRead <= 0) break;
|
||||||
|
if (RecvBuf.Num() + BytesRead > MaxRecvBufBytes)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
RecvBuf.SetNumUninitialized(RecvBuf.Num() * 2);
|
RecvBuf.Append(Temp, BytesRead);
|
||||||
UnusedSpace = RecvBuf.Num() - RecvLen;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int32 BytesRead = 0;
|
if (RecvBuf.Num() < 4) return false;
|
||||||
if (!Socket->Recv(RecvBuf.GetData() + RecvLen, UnusedSpace, BytesRead))
|
uint32 Size = UnpackBigEndian(RecvBuf.GetData());
|
||||||
{
|
if ((uint32)RecvBuf.Num() != (4u + Size)) return false;
|
||||||
return false;
|
RecvBuf.RemoveAt(0, 4);
|
||||||
}
|
|
||||||
if (BytesRead <= 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
RecvLen += BytesRead;
|
OutRequest = MoveTemp(RecvBuf);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,13 +426,13 @@ bool UWingServer::SendAll(FSocket* Socket, const uint8* Data, int32 BytesToSend)
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool UWingServer::ProcessRequestOnGameThread(
|
bool UWingServer::ProcessRequestOnGameThread(
|
||||||
const FString& Request, FString& Response)
|
const TArray<uint8>& Request, TArray<uint8>& Response)
|
||||||
{
|
{
|
||||||
// Enqueue the message for game-thread processing.
|
// Enqueue the message for game-thread processing.
|
||||||
TSharedPtr<UWingServer::FPendingMessage> Msg =
|
TSharedPtr<UWingServer::FPendingMessage> Msg =
|
||||||
MakeShared<UWingServer::FPendingMessage>();
|
MakeShared<UWingServer::FPendingMessage>();
|
||||||
Msg->Line = Request;
|
Msg->Request = Request;
|
||||||
TFuture<FString> Future = Msg->Response.GetFuture();
|
TFuture<TArray<uint8>> Future = Msg->Response.GetFuture();
|
||||||
|
|
||||||
{
|
{
|
||||||
FScopeLock Lock(&GWingServer->Mutex);
|
FScopeLock Lock(&GWingServer->Mutex);
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ bool WingVariableList::CheckSanity(const TSet<FName> &GoodFlags, bool Allow, Win
|
|||||||
{
|
{
|
||||||
if ((!Allow) && (!Variables.IsEmpty()))
|
if ((!Allow) && (!Variables.IsEmpty()))
|
||||||
{
|
{
|
||||||
Errors.Printf(TEXT("In this context, %s must be empty."), ListName);
|
Errors.Printf(TEXT("This object does not support %s.\n"), ListName);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (const Var &Variable : Variables)
|
for (const Var &Variable : Variables)
|
||||||
@@ -112,116 +112,6 @@ bool WingVariableList::CheckSanity(const TSet<FName> &GoodFlags, bool Allow, Win
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WingVariableList::ParseString(const FString &Input, WingOut Errors)
|
|
||||||
{
|
|
||||||
Variables.Empty();
|
|
||||||
|
|
||||||
TArray<FString> Lines;
|
|
||||||
Input.ParseIntoArrayLines(Lines);
|
|
||||||
|
|
||||||
for (const FString& Line : Lines)
|
|
||||||
{
|
|
||||||
WingTokenizer Tok(Line);
|
|
||||||
if (Tok.NextType() == 0) continue;
|
|
||||||
Var V;
|
|
||||||
V.DefaultSpecified = false;
|
|
||||||
if (!ParseOneVariable(Tok, V, Errors)) return false;
|
|
||||||
Variables.Add(MoveTemp(V));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WingVariableList::ParseNamesString(const FString &Input, WingOut Errors)
|
|
||||||
{
|
|
||||||
Variables.Empty();
|
|
||||||
WingTokenizer Tok(Input);
|
|
||||||
while (Tok.TokenIs(Tok.Identifier))
|
|
||||||
{
|
|
||||||
FName Name = Tok.NextName();
|
|
||||||
Var V;
|
|
||||||
V.Name = Name;
|
|
||||||
Variables.Add(V);
|
|
||||||
V.DefaultSpecified = false;
|
|
||||||
Tok.Advance();
|
|
||||||
if (Tok.TokenIs(',')) Tok.Advance();
|
|
||||||
}
|
|
||||||
if (!Tok.TokenIs(0))
|
|
||||||
{
|
|
||||||
Tok.SaveCursor(NAME_None);
|
|
||||||
Errors.Printf(TEXT("Unexpected token %s in variable list"),
|
|
||||||
*FString(Tok.GetRange(NAME_None, 1)));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WingVariableList::ParseOneVariable(WingTokenizer &Tok, Var &V, WingOut Errors)
|
|
||||||
{
|
|
||||||
// Parse type.
|
|
||||||
UWingTypes::Requirements Req;
|
|
||||||
Req.BlueprintType = true;
|
|
||||||
Req.Blueprintable = false;
|
|
||||||
Req.AllowContainer = true;
|
|
||||||
if (!UWingTypes::TextToType(Tok, V.Type, Req, false, Errors))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Parse name.
|
|
||||||
if (Tok.NextType() != Tok.Identifier)
|
|
||||||
{
|
|
||||||
Errors.Print(TEXT("ERROR: Expected variable name after type\n"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
V.Name = Tok.NextName();
|
|
||||||
Tok.Advance();
|
|
||||||
|
|
||||||
// Parse optional flags: (flag1, flag2, ...)
|
|
||||||
if (Tok.TokenIs('('))
|
|
||||||
{
|
|
||||||
if (!ParseVariableFlags(Tok, V.Flags, Errors)) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse optional default value: = rest-of-line
|
|
||||||
if (Tok.NextType() == Tok.RestOfLine)
|
|
||||||
{
|
|
||||||
V.DefaultSpecified = true;
|
|
||||||
V.DefaultValue = FString(Tok.NextRest().TrimStartAndEnd());
|
|
||||||
Tok.Advance();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should be at end of line.
|
|
||||||
if (Tok.NextType() != 0)
|
|
||||||
{
|
|
||||||
Tok.SaveCursor(NAME_None);
|
|
||||||
Errors.Printf(TEXT("ERROR: Unexpected token after variable declaration: '%s'\n"),
|
|
||||||
*FString(Tok.GetRange(NAME_None, 1)));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WingVariableList::ParseVariableFlags(WingTokenizer &Tok, TSet<FName> &Out, WingOut Errors)
|
|
||||||
{
|
|
||||||
Tok.Advance(); // Step over open-paren
|
|
||||||
while (Tok.TokenIs(Tok.Identifier))
|
|
||||||
{
|
|
||||||
Out.Add(Tok.NextName());
|
|
||||||
Tok.Advance();
|
|
||||||
// Commas are optional.
|
|
||||||
if (Tok.TokenIs(',')) Tok.Advance();
|
|
||||||
}
|
|
||||||
if (!Tok.TokenIs(')'))
|
|
||||||
{
|
|
||||||
Tok.SaveCursor(NAME_None);
|
|
||||||
Errors.Printf(TEXT("ERROR: flag list contains invalid token '%s'\n"),
|
|
||||||
*FString(Tok.GetRange(NAME_None, 1)));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Tok.Advance(); // Step over close-paren
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void WingVariables::Empty()
|
void WingVariables::Empty()
|
||||||
{
|
{
|
||||||
BlueprintVariables.Empty();
|
BlueprintVariables.Empty();
|
||||||
@@ -254,6 +144,107 @@ void WingVariables::Print(WingOut Out)
|
|||||||
OutputVariables.Print(Out);
|
OutputVariables.Print(Out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WingVariableList *WingVariables::GetList(FName Name)
|
||||||
|
{
|
||||||
|
if (Name == TEXT("blueprint")) return &BlueprintVariables;
|
||||||
|
if (Name == TEXT("input")) return &InputVariables;
|
||||||
|
if (Name == TEXT("output")) return &OutputVariables;
|
||||||
|
if (Name == TEXT("local")) return &LocalVariables;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WingVariables::ParseOneVariable(WingTokenizer &Tok, FName &Kind, Var &V, bool NameOnly, WingOut Errors)
|
||||||
|
{
|
||||||
|
// Parse Kind.
|
||||||
|
if (GetList(Tok.NextName()) == nullptr)
|
||||||
|
{
|
||||||
|
Errors.Print(TEXT("ERROR: Variable description should start with 'blueprint', 'input', 'output', or 'local'"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Kind = Tok.NextName();
|
||||||
|
Tok.Advance();
|
||||||
|
|
||||||
|
// Parse type.
|
||||||
|
if (!NameOnly)
|
||||||
|
{
|
||||||
|
UWingTypes::Requirements Req;
|
||||||
|
Req.BlueprintType = true;
|
||||||
|
Req.Blueprintable = false;
|
||||||
|
Req.AllowContainer = true;
|
||||||
|
if (!UWingTypes::TextToType(Tok, V.Type, Req, false, Errors))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse name.
|
||||||
|
if (Tok.NextType() != Tok.Identifier)
|
||||||
|
{
|
||||||
|
Errors.Print(TEXT("ERROR: Expected variable name after type\n"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
V.Name = Tok.NextName();
|
||||||
|
Tok.Advance();
|
||||||
|
|
||||||
|
// Parse optional flags: (flag1, flag2, ...)
|
||||||
|
if ((!NameOnly) && Tok.TokenIs('('))
|
||||||
|
{
|
||||||
|
if (!ParseVariableFlags(Tok, V.Flags, Errors)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse optional default value: = rest-of-line
|
||||||
|
if (!NameOnly && (Tok.NextType() == Tok.RestOfLine))
|
||||||
|
{
|
||||||
|
V.DefaultSpecified = true;
|
||||||
|
V.DefaultValue = FString(Tok.NextRest().TrimStartAndEnd());
|
||||||
|
Tok.Advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be at end of line.
|
||||||
|
if (Tok.NextType() != 0)
|
||||||
|
{
|
||||||
|
Tok.SaveCursor(NAME_None);
|
||||||
|
Errors.Printf(TEXT("ERROR: Unexpected token after variable declaration: '%s'\n"),
|
||||||
|
*FString(Tok.GetRange(NAME_None, 1)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WingVariables::ParseVariableFlags(WingTokenizer &Tok, TSet<FName> &Out, WingOut Errors)
|
||||||
|
{
|
||||||
|
Tok.Advance(); // Step over open-paren
|
||||||
|
while (Tok.TokenIs(Tok.Identifier))
|
||||||
|
{
|
||||||
|
Out.Add(Tok.NextName());
|
||||||
|
Tok.Advance();
|
||||||
|
// Commas are optional.
|
||||||
|
if (Tok.TokenIs(',')) Tok.Advance();
|
||||||
|
}
|
||||||
|
if (!Tok.TokenIs(')'))
|
||||||
|
{
|
||||||
|
Tok.SaveCursor(NAME_None);
|
||||||
|
Errors.Printf(TEXT("ERROR: flag list contains invalid token '%s'\n"),
|
||||||
|
*FString(Tok.GetRange(NAME_None, 1)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Tok.Advance(); // Step over close-paren
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WingVariables::Parse(const TArray<FString> &Vars, bool NameOnly, WingOut Errors)
|
||||||
|
{
|
||||||
|
for (const FString& Onevar : Vars)
|
||||||
|
{
|
||||||
|
WingTokenizer Tok(Onevar);
|
||||||
|
FName Kind;
|
||||||
|
Var V;
|
||||||
|
if (!ParseOneVariable(Tok, Kind, V, NameOnly, Errors)) return false;
|
||||||
|
WingVariableList *List = GetList(Kind);
|
||||||
|
List->Add(V);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void WingVariables::Load(WingOut Errors)
|
void WingVariables::Load(WingOut Errors)
|
||||||
{
|
{
|
||||||
Empty();
|
Empty();
|
||||||
|
|||||||
@@ -62,32 +62,18 @@ public:
|
|||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Json wrappers.
|
// A simple type to store the remaining arguments in
|
||||||
//
|
// an Argv Array.
|
||||||
// Normally, the json request is automatically used to
|
|
||||||
// populate the properties of the handler, so the handler
|
|
||||||
// doesn't have to deal with json. However, in a few cases,
|
|
||||||
// the handler actually does want to see some json. These
|
|
||||||
// wrappers allow a handler to request raw json data instead
|
|
||||||
// of pre-processed values.
|
|
||||||
//
|
//
|
||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
USTRUCT()
|
USTRUCT()
|
||||||
struct FWingJsonObject
|
struct FWingRestOfArgv
|
||||||
{
|
{
|
||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
TSharedPtr<FJsonObject> Json;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Marker struct for handler parameters that accept a JSON array.
|
UPROPERTY()
|
||||||
// PopulateFromJson stashes the actual JSON array into the Array field.
|
TArray<FString> Argv;
|
||||||
//
|
|
||||||
USTRUCT()
|
|
||||||
struct FWingJsonArray
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Array;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
@@ -200,3 +186,4 @@ public:
|
|||||||
|
|
||||||
bool Editable;
|
bool Editable;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ struct FWingProperty
|
|||||||
bool SetInt64(int64 I, WingOut Errors) const;
|
bool SetInt64(int64 I, WingOut Errors) const;
|
||||||
bool SetBool(bool B, WingOut Errors) const;
|
bool SetBool(bool B, WingOut Errors) const;
|
||||||
bool SetText(FString Value, WingOut Errors) const;
|
bool SetText(FString Value, WingOut Errors) const;
|
||||||
bool SetJson(const FJsonValue &Value, WingOut Errors) const;
|
|
||||||
|
|
||||||
// Fetch a value. If an error occurs such as a type
|
// Fetch a value. If an error occurs such as a type
|
||||||
// mismatch, returns an empty optional and prints an
|
// mismatch, returns an empty optional and prints an
|
||||||
@@ -86,6 +85,7 @@ struct FWingProperty
|
|||||||
// If mutable is false, all properties will be marked non-editable.
|
// If mutable is false, all properties will be marked non-editable.
|
||||||
//
|
//
|
||||||
static TArray<FWingProperty> GetVisible(UObject *Obj, void *Container, UStruct *Struct, bool Mutable);
|
static TArray<FWingProperty> GetVisible(UObject *Obj, void *Container, UStruct *Struct, bool Mutable);
|
||||||
|
static bool PopulateFromArgv(TArray<FWingProperty>& Props, TConstArrayView<FString> Argv, WingOut Errors);
|
||||||
|
|
||||||
// Convenience versions of GetAll and GetVisible for UObjects.
|
// Convenience versions of GetAll and GetVisible for UObjects.
|
||||||
//
|
//
|
||||||
@@ -121,16 +121,6 @@ struct FWingProperty
|
|||||||
//
|
//
|
||||||
static TArray<FWingProperty> GetDetails(UObject* Obj, bool Mutable);
|
static TArray<FWingProperty> GetDetails(UObject* Obj, bool Mutable);
|
||||||
|
|
||||||
// Functions to populate properties from a JSON object.
|
|
||||||
//
|
|
||||||
static bool PopulateFromJson(TArray<FWingProperty>& Props, const FJsonObject& Json,
|
|
||||||
bool AllOptional, WingOut Errors);
|
|
||||||
static bool PopulateFromJson(TArray<FWingProperty>& Props, const FJsonValue& Json,
|
|
||||||
bool AllOptional, WingOut Errors);
|
|
||||||
|
|
||||||
// Functions to populate properties from a JSON object.
|
|
||||||
//
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static bool IsUnsigned(FNumericProperty* Prop);
|
static bool IsUnsigned(FNumericProperty* Prop);
|
||||||
static bool IsPinTypeProperty(FProperty *Prop);
|
static bool IsPinTypeProperty(FProperty *Prop);
|
||||||
|
|||||||
@@ -62,9 +62,6 @@ public:
|
|||||||
static void AddHandler(UObject* Obj, const FString& Name, UObject* Config, EWingHandlerKind Kind, UClass* FactoryClass, const FString& Documentation);
|
static void AddHandler(UObject* Obj, const FString& Name, UObject* Config, EWingHandlerKind Kind, UClass* FactoryClass, const FString& Documentation);
|
||||||
static const TArray<FWingHandlerConfig>& AllHandlers() { return GWingServer->WingHandlerRegistry; }
|
static const TArray<FWingHandlerConfig>& AllHandlers() { return GWingServer->WingHandlerRegistry; }
|
||||||
|
|
||||||
/** Package a list of response texts into a single serialized JSON content-block array. */
|
|
||||||
static FString PackageResponses(const TArray<FString>& Responses);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static UWingServer* GWingServer;
|
static UWingServer* GWingServer;
|
||||||
|
|
||||||
@@ -79,10 +76,11 @@ private:
|
|||||||
FDelegateHandle LoadingPhasesCompleteHandle;
|
FDelegateHandle LoadingPhasesCompleteHandle;
|
||||||
FWingHandlerConfig* FindHandler(const FString& Name);
|
FWingHandlerConfig* FindHandler(const FString& Name);
|
||||||
|
|
||||||
// Handle a complete JSON line and return the response JSON
|
// Handle a complete request and return the response bytes.
|
||||||
FString HandleRequest(const FString& Line);
|
TArray<uint8> HandleRequest(const TArray<uint8>& RequestBytes);
|
||||||
FString HandleJsonRequest(TSharedPtr<FJsonObject> Request);
|
void PreCallHandler();
|
||||||
void TryCallHandler(TSharedPtr<FJsonObject> Request);
|
FString PostCallHandler();
|
||||||
|
void TryCallHandler(const TArray<FString>& Argv);
|
||||||
|
|
||||||
// ----- TCP server -----
|
// ----- TCP server -----
|
||||||
FSocket* ListenSocket = nullptr;
|
FSocket* ListenSocket = nullptr;
|
||||||
@@ -99,22 +97,23 @@ private:
|
|||||||
TArray<TSharedPtr<FClientConnection>> Clients;
|
TArray<TSharedPtr<FClientConnection>> Clients;
|
||||||
void AcceptNewConnections();
|
void AcceptNewConnections();
|
||||||
void CleanupFinishedClients();
|
void CleanupFinishedClients();
|
||||||
|
static uint32 UnpackBigEndian(const uint8 *Data);
|
||||||
|
static bool DeserializeArgv(
|
||||||
|
const TArray<uint8>& RequestBytes, TArray<FString>& Argv);
|
||||||
static void ClientThreadFunc(UWingServer* Server, TSharedPtr<FClientConnection> Client);
|
static void ClientThreadFunc(UWingServer* Server, TSharedPtr<FClientConnection> Client);
|
||||||
static bool ExtractRequestFromBuffer(
|
static bool ReceiveRequest(
|
||||||
TArray<uint8>& RecvBuf, int32& RecvLen, FString& OutRequest);
|
FSocket* Socket, TArray<uint8>& OutRequest);
|
||||||
static bool ReceiveMoreBytesIntoBuffer(
|
|
||||||
FSocket* Socket, TArray<uint8>& RecvBuf, int32& RecvLen);
|
|
||||||
static bool SendAll(FSocket* Socket, const uint8* Data, int32 BytesToSend);
|
static bool SendAll(FSocket* Socket, const uint8* Data, int32 BytesToSend);
|
||||||
static bool ProcessRequestOnGameThread(
|
static bool ProcessRequestOnGameThread(
|
||||||
const FString& Request, FString& Response);
|
const TArray<uint8>& Request, TArray<uint8>& Response);
|
||||||
static void WaitForAssetRegistry();
|
static void WaitForAssetRegistry();
|
||||||
|
|
||||||
// ----- The Critical Section -----
|
// ----- The Critical Section -----
|
||||||
struct FPendingMessage
|
struct FPendingMessage
|
||||||
{
|
{
|
||||||
FString Line;
|
TArray<uint8> Request;
|
||||||
TPromise<FString> Response;
|
TPromise<TArray<uint8>> Response;
|
||||||
FPendingMessage() : Response(TPromise<FString>()) {}
|
FPendingMessage() : Response(TPromise<TArray<uint8>>()) {}
|
||||||
};
|
};
|
||||||
FCriticalSection Mutex;
|
FCriticalSection Mutex;
|
||||||
TArray<TSharedPtr<FPendingMessage>> PendingMessages;
|
TArray<TSharedPtr<FPendingMessage>> PendingMessages;
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ public:
|
|||||||
// Empty the variable list.
|
// Empty the variable list.
|
||||||
void Empty() { Variables.Empty(); }
|
void Empty() { Variables.Empty(); }
|
||||||
|
|
||||||
|
// Add a variable.
|
||||||
|
void Add(const Var &Var) { Variables.Add(Var); }
|
||||||
|
|
||||||
// Return true if the variables are empty.
|
// Return true if the variables are empty.
|
||||||
bool IsEmpty() { return Variables.IsEmpty(); }
|
bool IsEmpty() { return Variables.IsEmpty(); }
|
||||||
|
|
||||||
@@ -72,21 +75,11 @@ public:
|
|||||||
// Check the sanity of the vars in the array. If allow
|
// Check the sanity of the vars in the array. If allow
|
||||||
// is false, then no variables are allowed in the array.
|
// is false, then no variables are allowed in the array.
|
||||||
bool CheckSanity(const TSet<FName> &GoodFlags, bool Allow, WingOut Errors);
|
bool CheckSanity(const TSet<FName> &GoodFlags, bool Allow, WingOut Errors);
|
||||||
|
|
||||||
// Parse variables from a string.
|
|
||||||
bool ParseString(const FString &Input, WingOut Errors);
|
|
||||||
|
|
||||||
// Parse variable names only from a string.
|
|
||||||
bool ParseNamesString(const FString &Input, WingOut Errors);
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool ParseOneVariable(WingTokenizer &Tok, Var &V, WingOut Errors);
|
|
||||||
bool ParseVariableFlags(WingTokenizer &Tok, TSet<FName> &Out, WingOut Errors);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class WingVariables
|
class WingVariables
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
using Var = WingVariableList::Var;
|
using Var = WingVariableList::Var;
|
||||||
WingVariables() {}
|
WingVariables() {}
|
||||||
|
|
||||||
@@ -125,6 +118,10 @@ public:
|
|||||||
|
|
||||||
void Print(WingOut Out);
|
void Print(WingOut Out);
|
||||||
|
|
||||||
|
// Parse variables.
|
||||||
|
|
||||||
|
bool Parse(const TArray<FString> &Vars, bool NameOnly, WingOut Errors);
|
||||||
|
|
||||||
// Load: clear the workspace, then
|
// Load: clear the workspace, then
|
||||||
// copy everything from the backing store into the workspace.
|
// copy everything from the backing store into the workspace.
|
||||||
|
|
||||||
@@ -193,4 +190,8 @@ private:
|
|||||||
void AddUserPinInfo(const Var &V, EEdGraphPinDirection Dir, UK2Node_EditablePinBase *Node);
|
void AddUserPinInfo(const Var &V, EEdGraphPinDirection Dir, UK2Node_EditablePinBase *Node);
|
||||||
|
|
||||||
bool ErrorNoBackingStore(WingOut Errors);
|
bool ErrorNoBackingStore(WingOut Errors);
|
||||||
|
|
||||||
|
bool ParseVariableFlags(WingTokenizer &Tok, TSet<FName> &Out, WingOut Errors);
|
||||||
|
bool ParseOneVariable(WingTokenizer &Tok, FName &Kind, Var &V, bool NameOnly, WingOut Errors);
|
||||||
|
WingVariableList *GetList(FName Name);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,178 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
MCP stdio-to-TCP bridge for UE Wingman.
|
|
||||||
|
|
||||||
Exposes a single MCP tool "unreal" that forwards JSON commands to the
|
|
||||||
UE Wingman TCP server in the Unreal Editor.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import socket
|
|
||||||
|
|
||||||
HOST = "localhost"
|
|
||||||
PORT = 9851
|
|
||||||
CONNECT_TIMEOUT = 2
|
|
||||||
READ_TIMEOUT = 30
|
|
||||||
|
|
||||||
TOOL_DESCRIPTION = (
|
|
||||||
"Send a command to the Unreal Editor's UE Wingman plugin. "
|
|
||||||
"The 'command' field specifies which operation to perform; "
|
|
||||||
"additional fields are command-specific parameters. "
|
|
||||||
'Use {"command": "Documentation_Manual"} to get an overview. '
|
|
||||||
"If the editor is not running, the call will return an error; "
|
|
||||||
"just ask the user to start the editor and try again."
|
|
||||||
)
|
|
||||||
|
|
||||||
TOOL_SCHEMA = {
|
|
||||||
"name": "unreal",
|
|
||||||
"description": TOOL_DESCRIPTION,
|
|
||||||
"inputSchema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"command": {"type": "string", "description": "The command to execute"},
|
|
||||||
},
|
|
||||||
"required": ["command"],
|
|
||||||
"additionalProperties": True,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
sock = None
|
|
||||||
|
|
||||||
|
|
||||||
def connect():
|
|
||||||
"""Try to connect to the editor. Returns True on success."""
|
|
||||||
global sock
|
|
||||||
if sock is not None:
|
|
||||||
return True
|
|
||||||
try:
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
s.settimeout(CONNECT_TIMEOUT)
|
|
||||||
s.connect((HOST, PORT))
|
|
||||||
s.settimeout(READ_TIMEOUT)
|
|
||||||
sock = s
|
|
||||||
return True
|
|
||||||
except (ConnectionRefusedError, socket.timeout, OSError):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def disconnect():
|
|
||||||
global sock
|
|
||||||
if sock is not None:
|
|
||||||
try:
|
|
||||||
sock.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
sock = None
|
|
||||||
|
|
||||||
|
|
||||||
def send_and_receive(message):
|
|
||||||
"""Send a JSON message to the editor and return the null-terminated response."""
|
|
||||||
data = json.dumps(message) + "\0"
|
|
||||||
sock.sendall(data.encode())
|
|
||||||
|
|
||||||
result = b""
|
|
||||||
while True:
|
|
||||||
chunk = sock.recv(65536)
|
|
||||||
if not chunk:
|
|
||||||
raise ConnectionError("Connection closed")
|
|
||||||
result += chunk
|
|
||||||
if b"\0" in result:
|
|
||||||
break
|
|
||||||
|
|
||||||
return result[:result.index(b"\0")].decode()
|
|
||||||
|
|
||||||
|
|
||||||
def forward_to_editor(arguments):
|
|
||||||
"""Forward arguments to the editor, return the result dict."""
|
|
||||||
if not connect():
|
|
||||||
return {"error": "Unreal Editor is not running. Start the editor and try again."}
|
|
||||||
try:
|
|
||||||
return send_and_receive(arguments)
|
|
||||||
except Exception:
|
|
||||||
disconnect()
|
|
||||||
return {"error": "Lost connection to Unreal Editor."}
|
|
||||||
|
|
||||||
|
|
||||||
def make_jsonrpc(msg_id, result):
|
|
||||||
return {"jsonrpc": "2.0", "id": msg_id, "result": result}
|
|
||||||
|
|
||||||
|
|
||||||
def parse_editor_response(result):
|
|
||||||
"""Parse and validate a raw editor response into an MCP content list.
|
|
||||||
|
|
||||||
MCP expects `content` to be a list of objects, each with at least a
|
|
||||||
string "type" field (e.g. {"type": "text", "text": "..."}). Anything
|
|
||||||
else is replaced with a single error item so the client sees a clear
|
|
||||||
message instead of a schema violation.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
parsed = json.loads(result)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
return [{"type": "text", "text": "Malformed response from editor: invalid JSON."}]
|
|
||||||
if not isinstance(parsed, list):
|
|
||||||
return [{"type": "text", "text": "Malformed response from editor: expected a list."}]
|
|
||||||
for item in parsed:
|
|
||||||
if not isinstance(item, dict):
|
|
||||||
return [{"type": "text", "text": "Malformed response from editor: list item is not an object."}]
|
|
||||||
if not isinstance(item.get("type"), str):
|
|
||||||
return [{"type": "text", "text": "Malformed response from editor: item missing string 'type' field."}]
|
|
||||||
return parsed
|
|
||||||
|
|
||||||
|
|
||||||
def handle_message(msg):
|
|
||||||
"""Handle one JSON-RPC message from Claude Code."""
|
|
||||||
msg_id = msg.get("id")
|
|
||||||
method = msg.get("method", "")
|
|
||||||
|
|
||||||
# Notifications don't get responses
|
|
||||||
if msg_id is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if method == "initialize":
|
|
||||||
return make_jsonrpc(msg_id, {
|
|
||||||
"protocolVersion": "2024-11-05",
|
|
||||||
"capabilities": {"tools": {}},
|
|
||||||
"serverInfo": {"name": "ue-wingman", "version": "1.0.0"},
|
|
||||||
})
|
|
||||||
|
|
||||||
if method == "tools/list":
|
|
||||||
return make_jsonrpc(msg_id, {"tools": [TOOL_SCHEMA]})
|
|
||||||
|
|
||||||
if method == "tools/call":
|
|
||||||
params = msg.get("params", {})
|
|
||||||
arguments = params.get("arguments", {})
|
|
||||||
result = forward_to_editor(arguments)
|
|
||||||
if isinstance(result, dict) and "error" in result:
|
|
||||||
content = [{"type": "text", "text": result["error"]}]
|
|
||||||
else:
|
|
||||||
content = parse_editor_response(result)
|
|
||||||
return make_jsonrpc(msg_id, {
|
|
||||||
"content": content,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": msg_id,
|
|
||||||
"error": {"code": -32601, "message": f"Method not found: {method}"},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
for line in sys.stdin:
|
|
||||||
line = line.strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
msg = json.loads(line)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
response = handle_message(msg)
|
|
||||||
if response is not None:
|
|
||||||
sys.stdout.write(json.dumps(response) + "\n")
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,37 +1,23 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Human-friendly MCP test client.
|
UE Wingman command-line tool. This tool simply packages up its
|
||||||
|
argv and sends it to the plugin, and then prints whatever the
|
||||||
|
plugin sends back. All the real work is done in the plugin.
|
||||||
|
|
||||||
Usage: ue-wingman.py <command> [key=value ...]
|
Usage: ue-wingman.py <arg1> [arg2 ...]
|
||||||
|
|
||||||
Values starting with '[' or '{' are parsed as JSON.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import json
|
|
||||||
import socket
|
import socket
|
||||||
|
import struct
|
||||||
|
|
||||||
HOST = "localhost"
|
HOST = "localhost"
|
||||||
PORT = 9851
|
PORT = 9851
|
||||||
TIMEOUT = 120
|
TIMEOUT = 15
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
if not args:
|
|
||||||
print("Usage: ue-wingman.py <command> [key=value ...]")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
msg = {"command": args[0]}
|
|
||||||
for arg in args[1:]:
|
|
||||||
key, _, value = arg.partition("=")
|
|
||||||
if value and value[0] in ('[', '{'):
|
|
||||||
try:
|
|
||||||
value = json.loads(value)
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
print(f"Bad JSON in {key}: {e.msg}")
|
|
||||||
sys.exit(1)
|
|
||||||
msg[key] = value
|
|
||||||
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
sock.settimeout(TIMEOUT)
|
sock.settimeout(TIMEOUT)
|
||||||
@@ -41,7 +27,14 @@ def main():
|
|||||||
print(f"Cannot connect to {HOST}:{PORT} — is the editor running?")
|
print(f"Cannot connect to {HOST}:{PORT} — is the editor running?")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
sock.sendall((json.dumps(msg) + "\0").encode())
|
payload = bytearray()
|
||||||
|
for arg in args:
|
||||||
|
data = arg.encode()
|
||||||
|
payload += struct.pack("!I", len(data))
|
||||||
|
payload += data
|
||||||
|
sock.sendall(struct.pack("!I", len(payload)))
|
||||||
|
sock.sendall(payload)
|
||||||
|
sock.shutdown(socket.SHUT_WR)
|
||||||
|
|
||||||
result = b""
|
result = b""
|
||||||
while True:
|
while True:
|
||||||
@@ -49,29 +42,9 @@ def main():
|
|||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
result += chunk
|
result += chunk
|
||||||
if b"\0" in result:
|
|
||||||
break
|
|
||||||
|
|
||||||
sock.close()
|
sock.close()
|
||||||
result = result[:result.index(b"\0")].decode() if b"\0" in result else result.decode()
|
print(result.decode(), end="")
|
||||||
|
|
||||||
try:
|
|
||||||
parsed = json.loads(result)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
print("Error: response is not valid JSON.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if not isinstance(parsed, list):
|
|
||||||
print("Error: response is not a list of content blocks.")
|
|
||||||
sys.exit(1)
|
|
||||||
for block in parsed:
|
|
||||||
if not (isinstance(block, dict)
|
|
||||||
and block.get("type") == "text"
|
|
||||||
and isinstance(block.get("text"), str)):
|
|
||||||
print("Error: response contains a non-text block.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print("\n---\n".join(block["text"] for block in parsed))
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user