Sequences are now implemented in UE Wingman
This commit is contained in:
40
Plugins/UEWingman/Source/UEWingman/Handlers/Sequence.h
Normal file
40
Plugins/UEWingman/Source/UEWingman/Handlers/Sequence.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "WingServer.h"
|
||||||
|
#include "WingBasics.h"
|
||||||
|
#include "Sequence.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UWing_Sequence : public UWingHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(EditAnywhere, meta=(Description=
|
||||||
|
"Array of subcommand JSON objects to execute in order. Each must contain 'command' and its parameters."))
|
||||||
|
FWingJsonArray Subcommands;
|
||||||
|
|
||||||
|
virtual void Register() override
|
||||||
|
{
|
||||||
|
UWingServer::AddHandler(this,
|
||||||
|
TEXT("Execute multiple commands in one request. Each subcommand produces its own content block in the response. "
|
||||||
|
"Nested Sequence commands are not allowed."));
|
||||||
|
}
|
||||||
|
virtual void Handle() override
|
||||||
|
{
|
||||||
|
// The actual code that implements Sequence is hardwired into
|
||||||
|
// WingServer. Because of that, this handler is never actually called
|
||||||
|
// under normal conditions. The handler exists for two reasons: to
|
||||||
|
// provide documentation, and also to catch the case where somebody
|
||||||
|
// nests a sequence inside another sequence (WingServer doesn't catch
|
||||||
|
// that).
|
||||||
|
//
|
||||||
|
WingOut::Stdout.Print(TEXT("ERROR: Sequence inside a Sequence is not allowed.\n"));
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -170,6 +170,70 @@ TStatId UWingServer::GetStatId() const
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
FString UWingServer::HandleRequest(const FString& Line)
|
FString UWingServer::HandleRequest(const FString& Line)
|
||||||
|
{
|
||||||
|
// Parse the request as JSON before doing anything else.
|
||||||
|
TSharedPtr<FJsonValue> Value;
|
||||||
|
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Line);
|
||||||
|
if (!FJsonSerializer::Deserialize(Reader, Value))
|
||||||
|
return PackageResponses({TEXT("Invalid Json")});
|
||||||
|
|
||||||
|
const TSharedPtr<FJsonObject>* RequestPtr = nullptr;
|
||||||
|
if (!Value->TryGetObject(RequestPtr))
|
||||||
|
return PackageResponses({TEXT("Json must be an object")});
|
||||||
|
TSharedPtr<FJsonObject> Request = *RequestPtr;
|
||||||
|
|
||||||
|
FString Command;
|
||||||
|
Request->TryGetStringField(TEXT("command"), Command);
|
||||||
|
if (Command == TEXT("Sequence"))
|
||||||
|
{
|
||||||
|
const TArray<TSharedPtr<FJsonValue>>* Subcommands = nullptr;
|
||||||
|
if (!Request->TryGetArrayField(TEXT("subcommands"), Subcommands))
|
||||||
|
return PackageResponses({TEXT("Sequence requires a 'subcommands' array.")});
|
||||||
|
|
||||||
|
TArray<FString> Responses;
|
||||||
|
Responses.Reserve(Subcommands->Num());
|
||||||
|
for (const TSharedPtr<FJsonValue>& Sub : *Subcommands)
|
||||||
|
{
|
||||||
|
const TSharedPtr<FJsonObject>* SubObjPtr = nullptr;
|
||||||
|
if (!Sub->TryGetObject(SubObjPtr))
|
||||||
|
Responses.Add(TEXT("Subcommand must be a JSON object."));
|
||||||
|
else
|
||||||
|
Responses.Add(HandleJsonRequest(*SubObjPtr));
|
||||||
|
}
|
||||||
|
return PackageResponses(Responses);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PackageResponses({HandleJsonRequest(Request)});
|
||||||
|
}
|
||||||
|
|
||||||
|
FString UWingServer::PackageResponses(const TArray<FString>& Responses)
|
||||||
|
{
|
||||||
|
TArray<TSharedPtr<FJsonValue>> Blocks;
|
||||||
|
Blocks.Reserve(Responses.Num());
|
||||||
|
for (const FString& Response : Responses)
|
||||||
|
{
|
||||||
|
// Unreal's JSON writer terminates string serialization at the first
|
||||||
|
// embedded null byte rather than escaping it, which would silently
|
||||||
|
// truncate output. Sanitize null bytes to spaces.
|
||||||
|
FString Sanitized = Response;
|
||||||
|
for (int32 i = 0; i < Sanitized.Len(); ++i)
|
||||||
|
{
|
||||||
|
if (Sanitized[i] == TEXT('\0')) Sanitized[i] = TEXT(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedPtr<FJsonObject> Block = MakeShared<FJsonObject>();
|
||||||
|
Block->SetStringField(TEXT("type"), TEXT("text"));
|
||||||
|
Block->SetStringField(TEXT("text"), Sanitized);
|
||||||
|
Blocks.Add(MakeShared<FJsonValueObject>(Block));
|
||||||
|
}
|
||||||
|
|
||||||
|
FString OutJson;
|
||||||
|
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutJson);
|
||||||
|
FJsonSerializer::Serialize(Blocks, Writer);
|
||||||
|
return OutJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
FString UWingServer::HandleJsonRequest(TSharedPtr<FJsonObject> Request)
|
||||||
{
|
{
|
||||||
LogCapture.CapturedErrors.Empty();
|
LogCapture.CapturedErrors.Empty();
|
||||||
LogCapture.bEnabled = true;
|
LogCapture.bEnabled = true;
|
||||||
@@ -178,7 +242,7 @@ FString UWingServer::HandleRequest(const FString& Line)
|
|||||||
bSuggestHandlerHelp = false;
|
bSuggestHandlerHelp = false;
|
||||||
LastHandler = nullptr;
|
LastHandler = nullptr;
|
||||||
|
|
||||||
TryCallHandler(Line);
|
TryCallHandler(Request);
|
||||||
|
|
||||||
Notifier.SendNotifications();
|
Notifier.SendNotifications();
|
||||||
LogCapture.bEnabled = false;
|
LogCapture.bEnabled = false;
|
||||||
@@ -202,37 +266,11 @@ FString UWingServer::HandleRequest(const FString& Line)
|
|||||||
}
|
}
|
||||||
FString Result = WingOut::StdoutBuffer.ToString();
|
FString Result = WingOut::StdoutBuffer.ToString();
|
||||||
WingOut::StdoutBuffer.Reset();
|
WingOut::StdoutBuffer.Reset();
|
||||||
for (int32 i = 0; i < Result.Len(); ++i)
|
return Result;
|
||||||
{
|
|
||||||
if (Result[i] == TEXT('\0')) Result[i] = TEXT(' ');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap the text in an MCP content-block array: [{"type":"text","text":"..."}]
|
void UWingServer::TryCallHandler(TSharedPtr<FJsonObject> Request)
|
||||||
TSharedPtr<FJsonObject> Block = MakeShared<FJsonObject>();
|
|
||||||
Block->SetStringField(TEXT("type"), TEXT("text"));
|
|
||||||
Block->SetStringField(TEXT("text"), Result);
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Blocks;
|
|
||||||
Blocks.Add(MakeShared<FJsonValueObject>(Block));
|
|
||||||
|
|
||||||
FString OutJson;
|
|
||||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutJson);
|
|
||||||
FJsonSerializer::Serialize(Blocks, Writer);
|
|
||||||
return OutJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
void UWingServer::TryCallHandler(const FString &Line)
|
|
||||||
{
|
{
|
||||||
// Turn the request string into a JSON tree.
|
|
||||||
TSharedPtr<FJsonObject> Request;
|
|
||||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Line);
|
|
||||||
FJsonSerializer::Deserialize(Reader, Request);
|
|
||||||
if (!Request.IsValid())
|
|
||||||
{
|
|
||||||
WingOut::Stdout.Printf(TEXT("Request is not valid JSON"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the command from the request.
|
// Extract the command from the request.
|
||||||
FString Command;
|
FString Command;
|
||||||
if (!Request->TryGetStringField(TEXT("command"), Command))
|
if (!Request->TryGetStringField(TEXT("command"), Command))
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "WingServer.generated.h"
|
#include "WingServer.generated.h"
|
||||||
|
|
||||||
class FSocket;
|
class FSocket;
|
||||||
|
class FJsonObject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UWingServer — editor subsystem that listens on a TCP socket and dispatches
|
* UWingServer — editor subsystem that listens on a TCP socket and dispatches
|
||||||
@@ -61,6 +62,9 @@ public:
|
|||||||
static void AddHandler(UObject* Obj, const FString& Name, UObject* Config, EWingHandlerKind Kind, UClass* FactoryClass, const FString& Documentation);
|
static void AddHandler(UObject* Obj, const FString& Name, UObject* Config, EWingHandlerKind Kind, UClass* FactoryClass, const FString& Documentation);
|
||||||
static const TArray<FWingHandlerConfig>& AllHandlers() { return GWingServer->WingHandlerRegistry; }
|
static const TArray<FWingHandlerConfig>& AllHandlers() { return GWingServer->WingHandlerRegistry; }
|
||||||
|
|
||||||
|
/** Package a list of response texts into a single serialized JSON content-block array. */
|
||||||
|
static FString PackageResponses(const TArray<FString>& Responses);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static UWingServer* GWingServer;
|
static UWingServer* GWingServer;
|
||||||
|
|
||||||
@@ -78,7 +82,8 @@ private:
|
|||||||
|
|
||||||
// Handle a complete JSON line and return the response JSON
|
// Handle a complete JSON line and return the response JSON
|
||||||
FString HandleRequest(const FString& Line);
|
FString HandleRequest(const FString& Line);
|
||||||
void TryCallHandler(const FString &Line);
|
FString HandleJsonRequest(TSharedPtr<FJsonObject> Request);
|
||||||
|
void TryCallHandler(TSharedPtr<FJsonObject> Request);
|
||||||
|
|
||||||
// ----- TCP server -----
|
// ----- TCP server -----
|
||||||
FSocket* ListenSocket = nullptr;
|
FSocket* ListenSocket = nullptr;
|
||||||
|
|||||||
Reference in New Issue
Block a user