diff --git a/.mcp.json b/.mcp.json deleted file mode 100644 index ae94da6b..00000000 --- a/.mcp.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "mcpServers": { - "ue-wingman": { - "command": "python3", - "args": ["Plugins/UEWingman/ue-wingman-mcp.py"] - } - } -} diff --git a/AGENTS.md b/AGENTS.md index 9477ee9c..f1e323c2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,7 +25,7 @@ - `Docs/` — Documentation. - `Config/` — Unreal config files - `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 key=value ...` (values starting with `[` or `{` are parsed as JSON). - `../integration.UE/` - the unreal engine source tree ## Coding Conventions diff --git a/Content/testing/WB_radtest.uasset b/Content/testing/WB_radtest.uasset new file mode 100644 index 00000000..8d413db9 --- /dev/null +++ b/Content/testing/WB_radtest.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:028d277767553c79980e7f2d1841133f9c930641a7bdcaa3fbab9e8062977aa0 +size 37399 diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Details_SetMany.h b/Plugins/UEWingman/BrokenHandlers/Details_SetMany.h similarity index 100% rename from Plugins/UEWingman/Source/UEWingman/Handlers/Details_SetMany.h rename to Plugins/UEWingman/BrokenHandlers/Details_SetMany.h diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_Add.h b/Plugins/UEWingman/BrokenHandlers/GraphNode_Add.h similarity index 100% rename from Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_Add.h rename to Plugins/UEWingman/BrokenHandlers/GraphNode_Add.h diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_SearchTypes.h b/Plugins/UEWingman/BrokenHandlers/GraphNode_SearchTypes.h similarity index 100% rename from Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_SearchTypes.h rename to Plugins/UEWingman/BrokenHandlers/GraphNode_SearchTypes.h diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_SetDefaults.h b/Plugins/UEWingman/BrokenHandlers/GraphNode_SetDefaults.h similarity index 100% rename from Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_SetDefaults.h rename to Plugins/UEWingman/BrokenHandlers/GraphNode_SetDefaults.h diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_SetPositions.h b/Plugins/UEWingman/BrokenHandlers/GraphNode_SetPositions.h similarity index 100% rename from Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_SetPositions.h rename to Plugins/UEWingman/BrokenHandlers/GraphNode_SetPositions.h diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/GraphPin_Connect.h b/Plugins/UEWingman/BrokenHandlers/GraphPin_Connect.h similarity index 100% rename from Plugins/UEWingman/Source/UEWingman/Handlers/GraphPin_Connect.h rename to Plugins/UEWingman/BrokenHandlers/GraphPin_Connect.h diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/GraphPin_Disconnect.h b/Plugins/UEWingman/BrokenHandlers/GraphPin_Disconnect.h similarity index 100% rename from Plugins/UEWingman/Source/UEWingman/Handlers/GraphPin_Disconnect.h rename to Plugins/UEWingman/BrokenHandlers/GraphPin_Disconnect.h diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Sequence.h b/Plugins/UEWingman/BrokenHandlers/Sequence.h similarity index 100% rename from Plugins/UEWingman/Source/UEWingman/Handlers/Sequence.h rename to Plugins/UEWingman/BrokenHandlers/Sequence.h diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Widget_SearchTypes.h b/Plugins/UEWingman/BrokenHandlers/Widget_SearchTypes.h similarity index 100% rename from Plugins/UEWingman/Source/UEWingman/Handlers/Widget_SearchTypes.h rename to Plugins/UEWingman/BrokenHandlers/Widget_SearchTypes.h diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Delete.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Delete.h index d213bd04..d99c2147 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Delete.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Delete.h @@ -25,7 +25,7 @@ public: UPROPERTY(EditAnywhere, meta=(Description="Asset to delete")) 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; virtual void Register() override diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Search.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Search.h index 3672081e..2ba99ceb 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Search.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Search.h @@ -21,13 +21,13 @@ class UWing_Asset_Search : public UWingHandler GENERATED_BODY() 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; - 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; - UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results (default 50)")) + UPROPERTY(EditAnywhere, meta=(Description="Maximum number of results (default 50)")) int32 Limit = 50; virtual void Register() override diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/BlueprintGraph_Add.h b/Plugins/UEWingman/Source/UEWingman/Handlers/BlueprintGraph_Add.h index 3f464429..6f2bd076 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/BlueprintGraph_Add.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/BlueprintGraph_Add.h @@ -32,16 +32,15 @@ public: UPROPERTY(EditAnywhere, meta=(Description="Type of graph: function or macro")) FString GraphType; - 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=(Description="Variables")) + FWingRestOfArgv Variables; virtual void Register() override { 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 { @@ -79,8 +78,7 @@ public: // Parse and validate variables before making changes WingVariables Vars; - if (!Vars.InputVariables.ParseString(InputVariables, WingOut::Stdout)) return; - if (!Vars.OutputVariables.ParseString(OutputVariables, WingOut::Stdout)) return; + if (!Vars.Parse(Variables.Argv, false, WingOut::Stdout)) return; // Create the Graph UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, InternalID, diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/BlueprintInterface_Remove.h b/Plugins/UEWingman/Source/UEWingman/Handlers/BlueprintInterface_Remove.h index e1e69f7e..5902251a 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/BlueprintInterface_Remove.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/BlueprintInterface_Remove.h @@ -27,7 +27,7 @@ public: UPROPERTY(EditAnywhere, meta=(Description="Interface name to remove")) 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; virtual void Register() override diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Details_Get.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Details_Get.h index 1c91f169..4b8351f0 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/Details_Get.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Details_Get.h @@ -37,5 +37,6 @@ public: if (!P) return; WingOut::Stdout.Print(P->GetText()); + WingOut::Stdout.Print(TEXT("\n")); } }; diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Documentation_Command.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Documentation_Command.h new file mode 100644 index 00000000..aa2dc747 --- /dev/null +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Documentation_Command.h @@ -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); + } +}; diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Documentation_Commands.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Documentation_Commands.h index a3a7717b..8a084cb2 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/Documentation_Commands.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Documentation_Commands.h @@ -12,19 +12,13 @@ class UWing_Documentation_Commands : public UWingHandler GENERATED_BODY() 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 { 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 { - WingManual::Commands(EWingHandlerKind::Normal, Query, Verbose); + WingManual::Commands(EWingHandlerKind::Normal, TEXT(""), false); } }; diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Documentation_CreateAssets.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Documentation_CreateAssets.h index d26d33ec..07458e13 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/Documentation_CreateAssets.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Documentation_CreateAssets.h @@ -12,10 +12,10 @@ class UWing_Documentation_CreateAssets : public UWingHandler GENERATED_BODY() public: - UPROPERTY(EditAnywhere, meta=(Optional, Description="Substring filter for command names")) + UPROPERTY(EditAnywhere, meta=(Description="Substring filter for command names")) 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; virtual void Register() override diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Documentation_Manual.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Documentation_Manual.h index c3a431c8..ceb7b6e2 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/Documentation_Manual.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Documentation_Manual.h @@ -12,7 +12,7 @@ class UWing_Documentation_Manual : public UWingHandler GENERATED_BODY() public: - UPROPERTY(EditAnywhere, meta=(Optional, Description="section of the manual")) + UPROPERTY(EditAnywhere, meta=(Description="section of the manual")) FString Section; virtual void Register() override @@ -44,7 +44,7 @@ public: } 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); } } diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/EventDispatcher_Add.h b/Plugins/UEWingman/Source/UEWingman/Handlers/EventDispatcher_Add.h index 344510a1..10836b9d 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/EventDispatcher_Add.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/EventDispatcher_Add.h @@ -28,13 +28,16 @@ public: UPROPERTY(EditAnywhere, meta=(Description="Name of the new event dispatcher")) FString Dispatcher; - UPROPERTY(EditAnywhere, meta=(Description="Input Variables, one per line, expressed as: type var = value")) - FString InputVariables; + UPROPERTY(EditAnywhere, meta=(Description="Variables")) + FWingRestOfArgv Variables; virtual void Register() override { 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 { @@ -49,7 +52,7 @@ public: // Parse the arguments. WingVariables Vars; - if (!Vars.InputVariables.ParseString(InputVariables, WingOut::Stdout)) return; + if (!Vars.Parse(Variables.Argv, false, WingOut::Stdout)) return; // Add the delegate variable FEdGraphPinType DelegateType; diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_Dump.h b/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_Dump.h index 0647763e..04bc40ce 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_Dump.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_Dump.h @@ -21,7 +21,7 @@ public: UPROPERTY(EditAnywhere, meta=(Description="Target 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; virtual void Register() override diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Graph_Dump.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Graph_Dump.h index 63642d1b..3e454334 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/Graph_Dump.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Graph_Dump.h @@ -22,7 +22,7 @@ public: UPROPERTY(EditAnywhere, meta=(Description="Path to 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; virtual void Register() override diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/TypeName_Search.h b/Plugins/UEWingman/Source/UEWingman/Handlers/TypeName_Search.h index 04024433..aaee229a 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/TypeName_Search.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/TypeName_Search.h @@ -20,7 +20,7 @@ public: UPROPERTY(EditAnywhere, meta=(Description="Substring filter for type names")) FString Query; - UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results")) + UPROPERTY(EditAnywhere, meta=(Description="Maximum number of results")) int32 Limit = 100; virtual void Register() override diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Variables_Add.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Variables_Add.h index 4315789c..8dcf7b8c 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/Variables_Add.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Variables_Add.h @@ -21,22 +21,16 @@ public: UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node")) FString Object; - UPROPERTY(EditAnywhere, meta=(Optional, Description="Blueprint variables, one per line")) - FString BlueprintVariables; - - 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; + UPROPERTY(EditAnywhere, meta=(Description="Variable descriptions")) + FWingRestOfArgv Variables; virtual void Register() override { 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 { @@ -46,10 +40,7 @@ public: WingVariables Vars; if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return; - if (!Vars.BlueprintVariables.ParseString(BlueprintVariables, 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.Parse(Variables.Argv, false, WingOut::Stdout)) return; if (!Vars.Check(WingOut::Stdout)) return; if (!Vars.Create(WingOut::Stdout)) return; WingOut::Stdout.Printf(TEXT("Success.\n")); diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Variables_Modify.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Variables_Modify.h index 12df1448..47231041 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/Variables_Modify.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Variables_Modify.h @@ -21,23 +21,16 @@ public: UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node")) FString Object; - UPROPERTY(EditAnywhere, meta=(Optional, Description="Blueprint variables, one per line")) - FString BlueprintVariables; - - 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; + UPROPERTY(EditAnywhere, meta=(Description="Variable descriptions")) + FWingRestOfArgv Variables; virtual void Register() override { UWingServer::AddHandler(this, - TEXT("Modify variables of a blueprint, function graph, " - "macro graph, event dispatcher graph, or custom event node. ")); + 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 { @@ -47,10 +40,7 @@ public: WingVariables Vars; if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return; - if (!Vars.BlueprintVariables.ParseString(BlueprintVariables, 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.Parse(Variables.Argv, false, WingOut::Stdout)) return; if (!Vars.Check(WingOut::Stdout)) return; if (!Vars.Modify(WingOut::Stdout)) return; WingOut::Stdout.Printf(TEXT("Success.\n")); diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Variables_Remove.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Variables_Remove.h index 07db678d..6d5ab658 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/Variables_Remove.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Variables_Remove.h @@ -21,22 +21,15 @@ public: UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node")) FString Object; - UPROPERTY(EditAnywhere, meta=(Optional, Description="Blueprint variable names to remove, comma-separated")) - FString BlueprintVariables; - - 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; + UPROPERTY(EditAnywhere, meta=(Description="Variable descriptions")) + FWingRestOfArgv Variables; virtual void Register() override { 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 { @@ -46,10 +39,7 @@ public: WingVariables Vars; if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return; - if (!Vars.BlueprintVariables.ParseNamesString(BlueprintVariables, 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.Parse(Variables.Argv, true, WingOut::Stdout)) return; if (!Vars.Remove(WingOut::Stdout)) return; WingOut::Stdout.Printf(TEXT("Success.\n")); } diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Widget_Add.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Widget_Add.h index e003cd92..fb33c3d0 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/Widget_Add.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Widget_Add.h @@ -33,10 +33,10 @@ public: UPROPERTY(EditAnywhere, meta=(Description="Name for the new widget")) 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; - 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; virtual void Register() override @@ -117,6 +117,10 @@ public: 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); } }; diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingManual.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingManual.cpp index 742f4d3e..4e1fa225 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingManual.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingManual.cpp @@ -6,17 +6,23 @@ void WingManual::PrintHandlerPrototype(const FWingHandlerConfig& Handler) { + WingOut::Stdout.Print(TEXT("ue-wingman ")); WingOut::Stdout.Print(Handler.Name); - WingOut::Stdout.Print(TEXT("(")); - bool bFirst = true; for (TFieldIterator PropIt(Handler.HandlerClass.Get(), EFieldIterationFlags::None); PropIt; ++PropIt) { - if (!bFirst) WingOut::Stdout.Print(TEXT(",")); - bFirst = false; - if (PropIt->HasMetaData(TEXT("Optional"))) WingOut::Stdout.Print(TEXT("?")); - WingOut::Stdout.Print(PropIt->GetName()); + FStructProperty* StructProp = CastField(*PropIt); + const bool bIsRest = + StructProp && (StructProp->Struct == FWingRestOfArgv::StaticStruct()); + 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) @@ -26,197 +32,193 @@ void WingManual::PrintHandlerArguments(const FWingHandlerConfig& Handler) { FProperty* Prop = *PropIt; FString Name = Prop->GetName(); - FString Type = UWingTypes::TypeToText(Prop); - bool bOptional = Prop->HasMetaData(TEXT("Optional")); - const FString& Desc = Prop->GetMetaData(TEXT("Description")); + FString Desc = Prop->GetMetaData(TEXT("Description")); + if (Desc.IsEmpty()) Desc = TEXT("No documentation"); - if (bOptional) - { - 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")); + WingOut::Stdout.Printf(TEXT(" %s - %s\n"), *Name, *Desc); } } void WingManual::PrintHandlerDescription(const FWingHandlerConfig& Handler) { 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) { WingOut::Stdout.Print(TEXT("\n")); - PrintHandlerPrototype(Handler); - PrintHandlerArguments(Handler); - PrintHandlerDescription(Handler); + PrintHandlerPrototype(Handler); + PrintHandlerArguments(Handler); + PrintHandlerDescription(Handler); WingOut::Stdout.Print(TEXT("\n")); } void UWingManualSections::FetcherPaths() { - WingOut::Stdout.Print(TEXT( - "\n FETCHER PATHS:" - "\n" - "\n Most commands require you to specify a 'fetcher path'." - "\n A fetcher path starts with an asset name, followed by" - "\n steps that navigate into the asset. Some Examples:" - "\n" - "\n /Game/Widgets/WB_Hotkeys,widget:Canvas.122" - "\n /Game/Testing/BP_Test,graph:Rescale.Actor,node:K2Node_CallFunction_0,pin:Scale" - "\n /Game/Chars/BP_Manny,component:Camera.Boom" - "\n" - "\n The navigation steps supported are:" - "\n" - "\n graph — move from a blueprint or material to a graph." - "\n node — move from a graph to a graph node" - "\n pin — move from a graph node to a pin" - "\n component — move from a blueprint to a component" - "\n levelblueprint — move from a world to a blueprint" - "\n widget — move from a widget blueprint to a widget" - "\n structprop — move into a struct property of an object" - "\n" - "\n Notice that paths use escaped fnames. See the section" - "\n on escape sequences in fnames below sfor more information." - "\n" - "\n Steps do not always require a parameter. For example, materials" - "\n only have one graph, so you can just say:" - "\n" - "\n /Game/Materials/MyMaterial,graph" - "\n" - )); + WingOut::Stdout.Print(TEXT( + "\n FETCHER PATHS:" + "\n" + "\n Most commands require you to specify a 'fetcher path'." + "\n A fetcher path starts with an asset name, followed by" + "\n steps that navigate into the asset. Some Examples:" + "\n" + "\n /Game/Widgets/WB_Hotkeys,widget:Canvas.122" + "\n /Game/Testing/BP_Test,graph:Rescale.Actor,node:K2Node_CallFunction_0,pin:Scale" + "\n /Game/Chars/BP_Manny,component:Camera.Boom" + "\n" + "\n The navigation steps supported are:" + "\n" + "\n graph — move from a blueprint or material to a graph." + "\n node — move from a graph to a graph node" + "\n pin — move from a graph node to a pin" + "\n component — move from a blueprint to a component" + "\n levelblueprint — move from a world to a blueprint" + "\n widget — move from a widget blueprint to a widget" + "\n structprop — move into a struct property of an object" + "\n" + "\n Notice that paths use escaped fnames. See the section" + "\n on escape sequences in fnames below sfor more information." + "\n" + "\n Steps do not always require a parameter. For example, materials" + "\n only have one graph, so you can just say:" + "\n" + "\n /Game/Materials/MyMaterial,graph" + "\n" + )); } void UWingManualSections::ExpressingTypes() { - WingOut::Stdout.Print(TEXT( - "\n EXPRESSING TYPES:" - "\n" - "\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" - "\n Bool, String, Vector, Rotator, HitResult, Actor, Character," - "\n PlayerController, EBlendMode, EMovementMode, BP_Manny, BP_Quinn," - "\n Array, Set, Map" - "\n Soft, Class, SoftClass" - "\n" - "\n Notice that it's 'Actor', not 'AActor'. Type names are not" - "\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 for valid types using the TypeName_Search command." - "\n" - )); + WingOut::Stdout.Print(TEXT( + "\n EXPRESSING TYPES:" + "\n" + "\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" + "\n Bool, String, Vector, Rotator, HitResult, Actor, Character," + "\n PlayerController, EBlendMode, EMovementMode, BP_Manny, BP_Quinn," + "\n Array, Set, Map" + "\n Soft, Class, SoftClass" + "\n" + "\n Notice that it's 'Actor', not 'AActor'. Type names are not" + "\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 for valid types using the TypeName_Search command." + "\n" + )); } void UWingManualSections::VariableDeclarations() { - WingOut::Stdout.Print(TEXT( - "\n VARIABLE DECLARATIONS:" - "\n" - "\n We have our own syntax for variable declarations: a type," - "\n a name, optional flags, and an optional default value," - "\n always on one line:" - "\n" - "\n Array Actors" - "\n Float F (InstanceEditable)" - "\n 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" - )); + WingOut::Stdout.Print(TEXT( + "\n VARIABLE DECLARATIONS:" + "\n" + "\n We have our own syntax for variable declarations:" + "\n" + "\n kind type name (optional flags) = optional default value" + "\n" + "\n Kind can be:" + "\n" + "\n blueprint - eg, instance variables" + "\n input - a function argument or macro input" + "\n output - a function return value or macro output" + "\n local - local variables" + "\n" + "\n Here are some examples:" + "\n" + "\n input Array Actors" + "\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() { - WingOut::Stdout.Print(TEXT( - "\n ESCAPE SEQUENCES IN FNAMES:" - "\n" - "\n When we output FNames, we use HTML escape sequences for the" - "\n following marks: \\\"'(),.:;<=>&, and for certain other characters." - "\n We also translate spaces to periods." - "\n" - "\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 with a space in it, either use or a period." - "\n" - )); + WingOut::Stdout.Print(TEXT( + "\n ESCAPE SEQUENCES IN FNAMES:" + "\n" + "\n When we output FNames, we use HTML escape sequences for the" + "\n following marks: \\\"'(),.:;<=>&, and for certain other characters." + "\n We also translate spaces to periods." + "\n" + "\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 with a space in it, either use or a period." + "\n" + )); } void UWingManualSections::MaterialEditing() { - WingOut::Stdout.Print(TEXT( - "\n MATERIAL EDITING:" - "\n" - "\n We do not expose material expressions directly. Instead, you" - "\n will be editing the material graph. However, if you Graph_Dump" - "\n a material graph, you will see that the nodes contain" - "\n properties which actually come from the material expressions." - "\n You can edit these using Details_Set on the node." - "\n" - "\n Don't overlook custom HLSL nodes. These can accomplish in\n" - "\n a single node what would otherwise take many.\n" - "\n" - )); + WingOut::Stdout.Print(TEXT( + "\n MATERIAL EDITING:" + "\n" + "\n We do not expose material expressions directly. Instead, you" + "\n will be editing the material graph. However, if you Graph_Dump" + "\n a material graph, you will see that the nodes contain" + "\n properties which actually come from the material expressions." + "\n You can edit these using Details_Set on the node." + "\n" + "\n Don't overlook custom HLSL nodes. These can accomplish in\n" + "\n a single node what would otherwise take many.\n" + "\n" + )); } void UWingManualSections::NodeContextMenus() { - WingOut::Stdout.Print(TEXT( - "\n NODE CONTEXT MENUS:" - "\n" - "\n GraphNode_ShowMenu and GraphNode_ChooseMenu give access" - "\n to the node context menu. This menu includes both node" - "\n operations and pin operations (e.g. Split Struct Pin," - "\n Add Pin)." - "\n" - )); + WingOut::Stdout.Print(TEXT( + "\n NODE CONTEXT MENUS:" + "\n" + "\n GraphNode_ShowMenu and GraphNode_ChooseMenu give access" + "\n to the node context menu. This menu includes both node" + "\n operations and pin operations (e.g. Split Struct Pin," + "\n Add Pin)." + "\n" + )); } void UWingManualSections::VariableGettersAndSetters() { - WingOut::Stdout.Print(TEXT( - "\n VARIABLE GETTERS AND SETTERS:" - "\n" - "\n Access to local vars, function parameters, and " - "\n blueprint vars is through getter and setter nodes. " - "\n These can be found in GraphNode_SearchTypes by " - "\n searching for 'Variable'. Some examples:" - "\n" - "\n SKEL_WB_Menu_C|Variables|Default|GetPlaceTangible" - "\n SKEL_WB_Menu_C|Variables|Default|SetPlaceTangible" - "\n SKEL_WB_Menu_C|Variables|WB_Menu|GetMenuPanel" - "\n" - )); + WingOut::Stdout.Print(TEXT( + "\n VARIABLE GETTERS AND SETTERS:" + "\n" + "\n Access to local vars, function parameters, and " + "\n blueprint vars is through getter and setter nodes. " + "\n These can be found in GraphNode_SearchTypes by " + "\n searching for 'Variable'. Some examples:" + "\n" + "\n SKEL_WB_Menu_C|Variables|Default|GetPlaceTangible" + "\n SKEL_WB_Menu_C|Variables|Default|SetPlaceTangible" + "\n SKEL_WB_Menu_C|Variables|WB_Menu|GetMenuPanel" + "\n" + )); } void UWingManualSections::ImportantCommands() { - WingOut::Stdout.Print(TEXT( - "\n IMPORTANT COMMANDS:" - "\n" - "\n Documentation_Manual: print manual sections" - "\n Documentation_Commands: a list of all the main commands" - "\n Documentation_CreateAssets: Additional 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" - "\n Details_Set: Manipulate the details panel for a given object" - "\n Sequence: Batch commands together for faster execution" - "\n" - "\n You can use Documentation_Commands(Query=Command,Verbose=true)" - "\n to get detailed help for a specific command." - "\n" - )); + 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 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" + "\n Details_Set: Manipulate the details panel for a given object" + "\n" + )); } TSet WingManual::GetSections() @@ -232,7 +234,7 @@ TSet WingManual::GetSections() void WingManual::PrintSectionNames(const TCHAR *Prefix, const TSet& Sections, WingOut Output) { if (Sections.IsEmpty()) return; - if (Prefix) Output.Print(Prefix); + if (Prefix) Output.Print(Prefix); bool bFirst = true; for (const FName& Section : Sections) { @@ -240,7 +242,7 @@ void WingManual::PrintSectionNames(const TCHAR *Prefix, const TSet& Secti bFirst = false; Output.Printf(TEXT("%s"), *Section.ToString()); } - if (Prefix) Output.Print(TEXT("\n")); + if (Prefix) Output.Print(TEXT("\n")); } bool WingManual::PrintSection(FName Section) @@ -256,10 +258,12 @@ void WingManual::Commands(EWingHandlerKind Kind, const FString& Query, bool Verb FString QueryLower = Query.ToLower(); FString PrevGroup; + bool any = false; for (const FWingHandlerConfig& H : UWingServer::AllHandlers()) { if (H.Kind != Kind) continue; if (!H.Name.ToLower().Contains(QueryLower)) continue; + any = true; // Blank line between groups if (!Verbose) @@ -278,4 +282,9 @@ void WingManual::Commands(EWingHandlerKind Kind, const FString& Query, bool Verb else 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")); + } } diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp index 78b4741c..4e9bfe5e 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp @@ -194,57 +194,6 @@ bool FWingProperty::SetText(FString Value, WingOut Errors) const 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(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(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 FWingProperty::GetObject(WingOut Errors) const { FObjectPropertyBase *OProp = CastField(Prop); @@ -493,56 +442,54 @@ TArray FWingProperty::GetDetails(UObject* Obj, bool Mutable) return Result; } - -bool FWingProperty::PopulateFromJson(TArray& Props, const FJsonObject& Json, bool AllOptional, WingOut Errors) +bool FWingProperty::PopulateFromArgv(TArray& Props, TConstArrayView Argv, WingOut Errors) { - bool Ok = true; - - // Build a set of known property names for the unknown-field check. - TSet KnownKeys; - for (const FWingProperty& P : Props) KnownKeys.Add(P->GetFName()); - - // Check for unknown fields in the JSON - for (const auto& KV : Json.Values) + int32 ArgIndex = 0; + for (int32 PropIndex = 0; PropIndex < Props.Num(); ++PropIndex) { - FName Name = WingUtils::CheckInternalizeID(KV.Key, Errors); - if (!KnownKeys.Contains(Name)) - { - Errors.Printf(TEXT("ERROR: Unknown parameter '%s'\n"), *KV.Key); - Ok = false; - } - } + FWingProperty& P = Props[PropIndex]; + FStructProperty* StructProp = CastField(P.Prop); + const bool bIsRest = + StructProp && (StructProp->Struct == FWingRestOfArgv::StaticStruct()); - // Populate each property from JSON - for (FWingProperty& P : Props) - { - FString JsonKey = WingUtils::FormatName(P.Prop); - TSharedPtr Value = Json.TryGetField(JsonKey); - if (!Value) + if (bIsRest) { - bool Optional = AllOptional || P.Prop->HasMetaData(TEXT("Optional")); - if (!Optional) + if (PropIndex + 1 != Props.Num()) { - Errors.Printf(TEXT("ERROR: Missing required parameter '%s'\n"), *JsonKey); - Ok = false; + Errors.Printf(TEXT("ERROR: '%s' must be the last parameter\n"), + *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; } - if (!P.SetJson(*Value, Errors)) Ok = false; - } - return Ok; -} -bool FWingProperty::PopulateFromJson(TArray& Props, const FJsonValue& Json, bool AllOptional, WingOut Errors) -{ - // Make sure they passed in a JSON object. - TSharedPtr Obj = Json.AsObject(); - if (Obj == nullptr) + if (ArgIndex >= Argv.Num()) + { + Errors.Printf(TEXT("ERROR: Missing parameter '%s'\n"), + *WingUtils::FormatName(P.Prop)); + 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 PopulateFromJson(Props, *Obj, AllOptional, Errors); + + return true; } diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingServer.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingServer.cpp index 2e630235..4f78d8aa 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingServer.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingServer.cpp @@ -81,7 +81,7 @@ void UWingServer::Deinitialize() bShuttingDown = true; for (auto& Msg : PendingMessages) { - Msg->Response.SetValue(FString()); + Msg->Response.SetValue(TArray()); } PendingMessages.Empty(); } @@ -150,7 +150,7 @@ void UWingServer::Tick(float DeltaTime) // If we have a request, process it. if (Request.IsValid()) { - FString Response = HandleRequest(Request->Line); + TArray Response = HandleRequest(Request->Request); Request->Response.SetValue(Response); } } @@ -169,71 +169,24 @@ TStatId UWingServer::GetStatId() const // HandleRequest — Given a command, execute it. // ============================================================ -FString UWingServer::HandleRequest(const FString& Line) +TArray UWingServer::HandleRequest(const TArray& RequestBytes) { - // Parse the request as JSON before doing anything else. - TSharedPtr Value; - TSharedRef> Reader = TJsonReaderFactory<>::Create(Line); - if (!FJsonSerializer::Deserialize(Reader, Value)) - return PackageResponses({TEXT("Invalid Json")}); - - const TSharedPtr* RequestPtr = nullptr; - if (!Value->TryGetObject(RequestPtr)) - return PackageResponses({TEXT("Json must be an object")}); - TSharedPtr Request = *RequestPtr; + TArray Argv; + FString ResponseText; - FString Command; - Request->TryGetStringField(TEXT("command"), Command); - if (Command == TEXT("Sequence")) + if (DeserializeArgv(RequestBytes, Argv)) { - const TArray>* Subcommands = nullptr; - if (!Request->TryGetArrayField(TEXT("subcommands"), Subcommands)) - return PackageResponses({TEXT("Sequence requires a 'subcommands' array.")}); - - TArray Responses; - Responses.Reserve(Subcommands->Num()); - for (const TSharedPtr& Sub : *Subcommands) - { - const TSharedPtr* SubObjPtr = nullptr; - if (!Sub->TryGetObject(SubObjPtr)) - Responses.Add(TEXT("Subcommand must be a JSON object.")); - else - Responses.Add(HandleJsonRequest(*SubObjPtr)); - } - return PackageResponses(Responses); + PreCallHandler(); + TryCallHandler(Argv); + ResponseText = PostCallHandler(); } + else ResponseText = TEXT("Invalid argv encoding (bug in ue-wingman.py)\n"); - return PackageResponses({HandleJsonRequest(Request)}); + FTCHARToUTF8 Utf8(*ResponseText); + return TArray(reinterpret_cast(Utf8.Get()), Utf8.Length()); } -FString UWingServer::PackageResponses(const TArray& Responses) -{ - TArray> 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 Block = MakeShared(); - Block->SetStringField(TEXT("type"), TEXT("text")); - Block->SetStringField(TEXT("text"), Sanitized); - Blocks.Add(MakeShared(Block)); - } - - FString OutJson; - TSharedRef> Writer = TJsonWriterFactory<>::Create(&OutJson); - FJsonSerializer::Serialize(Blocks, Writer); - return OutJson; -} - -FString UWingServer::HandleJsonRequest(TSharedPtr Request) +void UWingServer::PreCallHandler() { LogCapture.CapturedErrors.Empty(); LogCapture.bEnabled = true; @@ -241,9 +194,10 @@ FString UWingServer::HandleJsonRequest(TSharedPtr Request) SuggestedManualSections.Empty(); bSuggestHandlerHelp = false; LastHandler = nullptr; +} - TryCallHandler(Request); - +FString UWingServer::PostCallHandler() +{ Notifier.SendNotifications(); LogCapture.bEnabled = false; for (const FString& Msg : LogCapture.CapturedErrors) @@ -269,17 +223,15 @@ FString UWingServer::HandleJsonRequest(TSharedPtr Request) return Result; } -void UWingServer::TryCallHandler(TSharedPtr Request) +void UWingServer::TryCallHandler(const TArray& Argv) { - // Extract the command from the request. - FString Command; - if (!Request->TryGetStringField(TEXT("command"), Command)) + if (Argv.Num() < 1) { - WingOut::Stdout.Printf(TEXT("Request does not contain 'command' parameter")); - WingOut::Stdout.Printf(TEXT("We recommend sending command='Documentation_Manual'.")); + WingOut::Stdout.Print(TEXT("Missing command\n")); return; } - Request->RemoveField(TEXT("command")); + + FString Command = Argv[0]; // Find the handler for the specified command. FWingHandlerConfig* Found = FindHandler(Command); @@ -296,9 +248,9 @@ void UWingServer::TryCallHandler(TSharedPtr Request) UWingHandler* Handler = Cast(HandlerObj.Get()); Handler->Configuration = Found; - // Populate the handler object with the request parameters. + // Populate the handler object with argv parameters. TArray 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(); return; @@ -356,102 +308,105 @@ void UWingServer::CleanupFinishedClients() void UWingServer::ClientThreadFunc(UWingServer* Server, TSharedPtr Client) { - constexpr int32 MaxRecvBufBytes = 1024 * 1024; - constexpr int32 MinUnusedRecvSpace = 4096; - FSocket* Socket = Client->Socket; - TArray RecvBuf; - RecvBuf.SetNumUninitialized(MinUnusedRecvSpace); - int32 RecvLen = 0; WaitForAssetRegistry(); - while (true) + TArray Request; + if (!ReceiveRequest(Socket, Request)) { - FString Request; - if (ExtractRequestFromBuffer(RecvBuf, RecvLen, Request)) - { - 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(Utf8.Get()), - Utf8.Length() + 1)) - { - Client->bDone = true; - return; - } - continue; - } - - if (!ReceiveMoreBytesIntoBuffer(Socket, RecvBuf, RecvLen)) - { - break; - } + Client->bDone = true; + return; } + TArray Response; + if (!ProcessRequestOnGameThread(Request, Response)) + { + Client->bDone = true; + return; + } + + SendAll(Socket, Response.GetData(), Response.Num()); Client->bDone = true; } -bool UWingServer::ExtractRequestFromBuffer( - TArray& RecvBuf, int32& RecvLen, FString& OutRequest) +uint32 UWingServer::UnpackBigEndian(const uint8 *Data) { - const uint8* EndOfRequest = static_cast( - memchr(RecvBuf.GetData(), '\0', RecvLen)); - if (EndOfRequest == nullptr) + return + ((uint32)Data[0] << 24) | + ((uint32)Data[1] << 16) | + ((uint32)Data[2] << 8) | + (uint32)Data[3]; +} + +bool UWingServer::DeserializeArgv( + const TArray& RequestBytes, TArray& 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(RequestBytes.GetData() + Offset), + Length)); + Offset += (int32)Length; } - const int32 MessageLen = - static_cast(EndOfRequest - RecvBuf.GetData()); - OutRequest = FString::ConstructFromPtrSize( - reinterpret_cast(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; } -bool UWingServer::ReceiveMoreBytesIntoBuffer( - FSocket* Socket, TArray& RecvBuf, int32& RecvLen) +bool UWingServer::ReceiveRequest(FSocket* Socket, TArray& OutRequest) { constexpr int32 MaxRecvBufBytes = 1024 * 1024; - constexpr int32 MinUnusedRecvSpace = 4096; + constexpr int32 ChunkSize = 8192; - int32 UnusedSpace = RecvBuf.Num() - RecvLen; - if (UnusedSpace < MinUnusedRecvSpace) + TArray RecvBuf; + 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; } - RecvBuf.SetNumUninitialized(RecvBuf.Num() * 2); - UnusedSpace = RecvBuf.Num() - RecvLen; + RecvBuf.Append(Temp, BytesRead); } - int32 BytesRead = 0; - if (!Socket->Recv(RecvBuf.GetData() + RecvLen, UnusedSpace, BytesRead)) - { - return false; - } - if (BytesRead <= 0) - { - return false; - } + if (RecvBuf.Num() < 4) return false; + uint32 Size = UnpackBigEndian(RecvBuf.GetData()); + if ((uint32)RecvBuf.Num() != (4u + Size)) return false; + RecvBuf.RemoveAt(0, 4); - RecvLen += BytesRead; + OutRequest = MoveTemp(RecvBuf); return true; } @@ -471,13 +426,13 @@ bool UWingServer::SendAll(FSocket* Socket, const uint8* Data, int32 BytesToSend) } bool UWingServer::ProcessRequestOnGameThread( - const FString& Request, FString& Response) + const TArray& Request, TArray& Response) { // Enqueue the message for game-thread processing. TSharedPtr Msg = MakeShared(); - Msg->Line = Request; - TFuture Future = Msg->Response.GetFuture(); + Msg->Request = Request; + TFuture> Future = Msg->Response.GetFuture(); { FScopeLock Lock(&GWingServer->Mutex); diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingVariables.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingVariables.cpp index 7bcf3d05..cc759761 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingVariables.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingVariables.cpp @@ -82,7 +82,7 @@ bool WingVariableList::CheckSanity(const TSet &GoodFlags, bool Allow, Win { 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; } for (const Var &Variable : Variables) @@ -112,116 +112,6 @@ bool WingVariableList::CheckSanity(const TSet &GoodFlags, bool Allow, Win return true; } -bool WingVariableList::ParseString(const FString &Input, WingOut Errors) -{ - Variables.Empty(); - - TArray 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 &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() { BlueprintVariables.Empty(); @@ -254,6 +144,107 @@ void WingVariables::Print(WingOut 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 &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 &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) { Empty(); diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingBasics.h b/Plugins/UEWingman/Source/UEWingman/Public/WingBasics.h index 893f47b8..d5e4eb0e 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingBasics.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingBasics.h @@ -62,32 +62,18 @@ public: //////////////////////////////////////////////////////////// // -// Json wrappers. -// -// 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. +// A simple type to store the remaining arguments in +// an Argv Array. // //////////////////////////////////////////////////////////// USTRUCT() -struct FWingJsonObject +struct FWingRestOfArgv { GENERATED_BODY() - TSharedPtr Json; -}; -// Marker struct for handler parameters that accept a JSON array. -// PopulateFromJson stashes the actual JSON array into the Array field. -// -USTRUCT() -struct FWingJsonArray -{ - GENERATED_BODY() - TArray> Array; + UPROPERTY() + TArray Argv; }; //////////////////////////////////////////////////////////// @@ -200,3 +186,4 @@ public: bool Editable; }; + diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingProperty.h b/Plugins/UEWingman/Source/UEWingman/Public/WingProperty.h index 5084258f..79c56b02 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingProperty.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingProperty.h @@ -41,7 +41,6 @@ struct FWingProperty bool SetInt64(int64 I, WingOut Errors) const; bool SetBool(bool B, 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 // 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. // static TArray GetVisible(UObject *Obj, void *Container, UStruct *Struct, bool Mutable); + static bool PopulateFromArgv(TArray& Props, TConstArrayView Argv, WingOut Errors); // Convenience versions of GetAll and GetVisible for UObjects. // @@ -121,16 +121,6 @@ struct FWingProperty // static TArray GetDetails(UObject* Obj, bool Mutable); - // Functions to populate properties from a JSON object. - // - static bool PopulateFromJson(TArray& Props, const FJsonObject& Json, - bool AllOptional, WingOut Errors); - static bool PopulateFromJson(TArray& Props, const FJsonValue& Json, - bool AllOptional, WingOut Errors); - - // Functions to populate properties from a JSON object. - // - private: static bool IsUnsigned(FNumericProperty* Prop); static bool IsPinTypeProperty(FProperty *Prop); diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingServer.h b/Plugins/UEWingman/Source/UEWingman/Public/WingServer.h index 938e213c..d1cc7963 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingServer.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingServer.h @@ -62,9 +62,6 @@ public: static void AddHandler(UObject* Obj, const FString& Name, UObject* Config, EWingHandlerKind Kind, UClass* FactoryClass, const FString& Documentation); static const TArray& AllHandlers() { return GWingServer->WingHandlerRegistry; } - /** Package a list of response texts into a single serialized JSON content-block array. */ - static FString PackageResponses(const TArray& Responses); - private: static UWingServer* GWingServer; @@ -79,10 +76,11 @@ private: FDelegateHandle LoadingPhasesCompleteHandle; FWingHandlerConfig* FindHandler(const FString& Name); - // Handle a complete JSON line and return the response JSON - FString HandleRequest(const FString& Line); - FString HandleJsonRequest(TSharedPtr Request); - void TryCallHandler(TSharedPtr Request); + // Handle a complete request and return the response bytes. + TArray HandleRequest(const TArray& RequestBytes); + void PreCallHandler(); + FString PostCallHandler(); + void TryCallHandler(const TArray& Argv); // ----- TCP server ----- FSocket* ListenSocket = nullptr; @@ -99,22 +97,23 @@ private: TArray> Clients; void AcceptNewConnections(); void CleanupFinishedClients(); + static uint32 UnpackBigEndian(const uint8 *Data); + static bool DeserializeArgv( + const TArray& RequestBytes, TArray& Argv); static void ClientThreadFunc(UWingServer* Server, TSharedPtr Client); - static bool ExtractRequestFromBuffer( - TArray& RecvBuf, int32& RecvLen, FString& OutRequest); - static bool ReceiveMoreBytesIntoBuffer( - FSocket* Socket, TArray& RecvBuf, int32& RecvLen); + static bool ReceiveRequest( + FSocket* Socket, TArray& OutRequest); static bool SendAll(FSocket* Socket, const uint8* Data, int32 BytesToSend); static bool ProcessRequestOnGameThread( - const FString& Request, FString& Response); + const TArray& Request, TArray& Response); static void WaitForAssetRegistry(); // ----- The Critical Section ----- struct FPendingMessage { - FString Line; - TPromise Response; - FPendingMessage() : Response(TPromise()) {} + TArray Request; + TPromise> Response; + FPendingMessage() : Response(TPromise>()) {} }; FCriticalSection Mutex; TArray> PendingMessages; diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingVariables.h b/Plugins/UEWingman/Source/UEWingman/Public/WingVariables.h index 6b3b80a3..4bda681d 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingVariables.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingVariables.h @@ -57,6 +57,9 @@ public: // Empty the variable list. void Empty() { Variables.Empty(); } + // Add a variable. + void Add(const Var &Var) { Variables.Add(Var); } + // Return true if the variables are empty. bool IsEmpty() { return Variables.IsEmpty(); } @@ -72,21 +75,11 @@ public: // Check the sanity of the vars in the array. If allow // is false, then no variables are allowed in the array. bool CheckSanity(const TSet &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 &Out, WingOut Errors); }; class WingVariables { -public: +public: using Var = WingVariableList::Var; WingVariables() {} @@ -125,6 +118,10 @@ public: void Print(WingOut Out); + // Parse variables. + + bool Parse(const TArray &Vars, bool NameOnly, WingOut Errors); + // Load: clear the workspace, then // copy everything from the backing store into the workspace. @@ -193,4 +190,8 @@ private: void AddUserPinInfo(const Var &V, EEdGraphPinDirection Dir, UK2Node_EditablePinBase *Node); bool ErrorNoBackingStore(WingOut Errors); + + bool ParseVariableFlags(WingTokenizer &Tok, TSet &Out, WingOut Errors); + bool ParseOneVariable(WingTokenizer &Tok, FName &Kind, Var &V, bool NameOnly, WingOut Errors); + WingVariableList *GetList(FName Name); }; diff --git a/Plugins/UEWingman/ue-wingman-mcp.py b/Plugins/UEWingman/ue-wingman-mcp.py deleted file mode 100644 index 0cc7c477..00000000 --- a/Plugins/UEWingman/ue-wingman-mcp.py +++ /dev/null @@ -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() diff --git a/Plugins/UEWingman/ue-wingman.py b/Plugins/UEWingman/ue-wingman.py index b4a5c4d6..825c2fdf 100755 --- a/Plugins/UEWingman/ue-wingman.py +++ b/Plugins/UEWingman/ue-wingman.py @@ -1,37 +1,23 @@ #!/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 [key=value ...] - -Values starting with '[' or '{' are parsed as JSON. +Usage: ue-wingman.py [arg2 ...] """ import sys -import json import socket +import struct HOST = "localhost" PORT = 9851 -TIMEOUT = 120 +TIMEOUT = 15 def main(): args = sys.argv[1:] - if not args: - print("Usage: ue-wingman.py [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.settimeout(TIMEOUT) @@ -41,7 +27,14 @@ def main(): print(f"Cannot connect to {HOST}:{PORT} — is the editor running?") 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"" while True: @@ -49,29 +42,9 @@ def main(): if not chunk: break result += chunk - if b"\0" in result: - break sock.close() - result = result[:result.index(b"\0")].decode() if b"\0" in result else result.decode() - - 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)) + print(result.decode(), end="") if __name__ == "__main__": main()