Implemented new framework for MCP Handlers. Made one handler with the new framework.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
#include "BlueprintMCPHandlers_Mutation.h"
|
||||
#include "BlueprintMCPServer.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "Materials/Material.h"
|
||||
@@ -2594,42 +2595,29 @@ void FBlueprintMCPServer::HandleSearchNodeActions(const FJsonObject* Json, FJson
|
||||
// Takes a full action name, finds the spawner, and calls Invoke().
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleSpawnNode(const FJsonObject* Json, FJsonObject* Result)
|
||||
void UMCPHandler_SpawnNode::Handle(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString GraphName = Json->GetStringField(TEXT("graph"));
|
||||
FString ActionName = Json->GetStringField(TEXT("actionName"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || ActionName.IsEmpty())
|
||||
{
|
||||
return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, actionName"));
|
||||
}
|
||||
|
||||
int32 PosX = 0, PosY = 0;
|
||||
if (Json->HasField(TEXT("posX")))
|
||||
PosX = (int32)Json->GetNumberField(TEXT("posX"));
|
||||
if (Json->HasField(TEXT("posY")))
|
||||
PosY = (int32)Json->GetNumberField(TEXT("posY"));
|
||||
MCPHelper* Helper = MCPHelper::Get();
|
||||
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
|
||||
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MakeErrorJson(Result, LoadError);
|
||||
return Helper->MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Find the target graph
|
||||
FString DecodedGraphName = UrlDecode(GraphName);
|
||||
FString DecodedGraphName = MCPHelper::UrlDecode(Graph);
|
||||
UEdGraph* TargetGraph = nullptr;
|
||||
TArray<UEdGraph*> AllGraphs;
|
||||
BP->GetAllGraphs(AllGraphs);
|
||||
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
for (UEdGraph* G : AllGraphs)
|
||||
{
|
||||
if (Graph && Graph->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
|
||||
if (G && G->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
TargetGraph = Graph;
|
||||
TargetGraph = G;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -2637,11 +2625,11 @@ void FBlueprintMCPServer::HandleSpawnNode(const FJsonObject* Json, FJsonObject*
|
||||
if (!TargetGraph)
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> GraphNames;
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
for (UEdGraph* G : AllGraphs)
|
||||
{
|
||||
if (Graph) GraphNames.Add(MakeShared<FJsonValueString>(Graph->GetName()));
|
||||
if (G) GraphNames.Add(MakeShared<FJsonValueString>(G->GetName()));
|
||||
}
|
||||
MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
|
||||
Helper->MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
|
||||
Result->SetArrayField(TEXT("availableGraphs"), GraphNames);
|
||||
return;
|
||||
}
|
||||
@@ -2650,13 +2638,13 @@ void FBlueprintMCPServer::HandleSpawnNode(const FJsonObject* Json, FJsonObject*
|
||||
TArray<UBlueprintNodeSpawner*> Matches = FNodeActionSearch::FindSpawner(ActionName);
|
||||
if (Matches.Num() == 0)
|
||||
{
|
||||
return MakeErrorJson(Result, FString::Printf(
|
||||
return Helper->MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("No action found matching '%s'. Use search_node_actions to find available actions."),
|
||||
*ActionName));
|
||||
}
|
||||
if (Matches.Num() > 1)
|
||||
{
|
||||
return MakeErrorJson(Result, FString::Printf(
|
||||
return Helper->MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Ambiguous: %d spawners match '%s'. Cannot determine which one to use."),
|
||||
Matches.Num(), *ActionName));
|
||||
}
|
||||
@@ -2669,7 +2657,7 @@ void FBlueprintMCPServer::HandleSpawnNode(const FJsonObject* Json, FJsonObject*
|
||||
|
||||
if (!NewNode)
|
||||
{
|
||||
return MakeErrorJson(Result, TEXT("Spawner Invoke() returned null — node creation failed."));
|
||||
return Helper->MakeErrorJson(Result, TEXT("Spawner Invoke() returned null — node creation failed."));
|
||||
}
|
||||
|
||||
// Ensure valid GUID
|
||||
@@ -2678,22 +2666,20 @@ void FBlueprintMCPServer::HandleSpawnNode(const FJsonObject* Json, FJsonObject*
|
||||
NewNode->CreateNewGuid();
|
||||
}
|
||||
|
||||
// Mark as modified and save
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Spawned node '%s' (class %s) via action '%s' in graph '%s' of '%s'"),
|
||||
*NewNode->NodeGuid.ToString(),
|
||||
*NewNode->GetClass()->GetName(),
|
||||
*ActionName,
|
||||
*DecodedGraphName,
|
||||
*BlueprintName);
|
||||
*Blueprint);
|
||||
|
||||
// Serialize result
|
||||
TSharedPtr<FJsonObject> NodeState = SerializeNode(NewNode);
|
||||
TSharedPtr<FJsonObject> NodeState = Helper->SerializeNode(NewNode);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("graph"), DecodedGraphName);
|
||||
Result->SetStringField(TEXT("actionName"), ActionName);
|
||||
Result->SetStringField(TEXT("nodeId"), NewNode->NodeGuid.ToString());
|
||||
@@ -2703,5 +2689,5 @@ void FBlueprintMCPServer::HandleSpawnNode(const FJsonObject* Json, FJsonObject*
|
||||
{
|
||||
Result->SetObjectField(TEXT("node"), NodeState);
|
||||
}
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "BlueprintMCPServer.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "Materials/MaterialExpression.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "AssetRegistry/IAssetRegistry.h"
|
||||
@@ -96,6 +97,19 @@
|
||||
#include "AnimationGraph.h"
|
||||
#include "AnimationTransitionGraph.h"
|
||||
|
||||
// ============================================================
|
||||
// Get() — retrieve the active server via the editor subsystem
|
||||
// ============================================================
|
||||
|
||||
#include "BlueprintMCPEditorSubsystem.h"
|
||||
|
||||
FBlueprintMCPServer* FBlueprintMCPServer::Get()
|
||||
{
|
||||
if (!GEditor) return nullptr;
|
||||
auto* Sub = GEditor->GetEditorSubsystem<UBlueprintMCPEditorSubsystem>();
|
||||
return Sub ? Sub->GetServer() : nullptr;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Helpers
|
||||
// ============================================================
|
||||
@@ -638,7 +652,7 @@ bool FBlueprintMCPServer::Start(int32 InPort, bool bEditorMode)
|
||||
Router->BindRoute(FHttpPath(TEXT("/api/search-node-actions")), EHttpServerRequestVerbs::VERB_POST,
|
||||
QueuedHandler(TEXT("searchNodeActions")));
|
||||
Router->BindRoute(FHttpPath(TEXT("/api/spawn-node")), EHttpServerRequestVerbs::VERB_POST,
|
||||
QueuedHandler(TEXT("spawnNode")));
|
||||
QueuedHandler(TEXT("spawn_node")));
|
||||
Router->BindRoute(FHttpPath(TEXT("/api/rename-asset")), EHttpServerRequestVerbs::VERB_POST,
|
||||
QueuedHandler(TEXT("renameAsset")));
|
||||
Router->BindRoute(FHttpPath(TEXT("/api/reparent-blueprint")), EHttpServerRequestVerbs::VERB_POST,
|
||||
@@ -827,6 +841,9 @@ bool FBlueprintMCPServer::Start(int32 InPort, bool bEditorMode)
|
||||
// Register TMap dispatch handlers
|
||||
RegisterHandlers();
|
||||
|
||||
// Build new-style handler registry from UMCPHandler subclasses
|
||||
BuildMCPHandlerRegistry();
|
||||
|
||||
HttpModule.StartAllListeners();
|
||||
|
||||
// Verify the listener actually bound by attempting a TCP connection
|
||||
@@ -901,7 +918,26 @@ bool FBlueprintMCPServer::ProcessOneRequest()
|
||||
|
||||
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
||||
|
||||
if (FRequestHandler* Handler = HandlerMap.Find(Req->Endpoint))
|
||||
if (UClass** HandlerClass = MCPHandlerRegistry.Find(Req->Endpoint))
|
||||
{
|
||||
const bool bIsMutation = MutationEndpoints.Contains(Req->Endpoint);
|
||||
if (bIsMutation && GEditor)
|
||||
{
|
||||
GEditor->BeginTransaction(FText::FromString(FString::Printf(TEXT("BlueprintMCP: %s"), *Req->Endpoint)));
|
||||
}
|
||||
|
||||
UMCPHandler* Handler = NewObject<UMCPHandler>(GetTransientPackage(), *HandlerClass);
|
||||
if (PopulateHandlerFromJson(Handler, Params.Get(), &*Result))
|
||||
{
|
||||
Handler->Handle(Params.Get(), &*Result);
|
||||
}
|
||||
|
||||
if (bIsMutation && GEditor)
|
||||
{
|
||||
GEditor->EndTransaction();
|
||||
}
|
||||
}
|
||||
else if (FRequestHandler* Handler = HandlerMap.Find(Req->Endpoint))
|
||||
{
|
||||
// Wrap mutation endpoints in an undo transaction so users can Ctrl+Z
|
||||
const bool bIsMutation = MutationEndpoints.Contains(Req->Endpoint);
|
||||
@@ -949,7 +985,7 @@ void FBlueprintMCPServer::RegisterHandlers()
|
||||
TEXT("deleteNode"),
|
||||
TEXT("duplicateNodes"),
|
||||
TEXT("addNode"),
|
||||
TEXT("spawnNode"),
|
||||
TEXT("spawn_node"),
|
||||
TEXT("setNodeComment"),
|
||||
TEXT("renameAsset"),
|
||||
TEXT("reparentBlueprint"),
|
||||
@@ -1032,7 +1068,7 @@ void FBlueprintMCPServer::RegisterHandlers()
|
||||
H(TEXT("validateAllBlueprints"), &FBlueprintMCPServer::HandleValidateAllBlueprints);
|
||||
H(TEXT("addNode"), &FBlueprintMCPServer::HandleAddNode);
|
||||
H(TEXT("searchNodeActions"), &FBlueprintMCPServer::HandleSearchNodeActions);
|
||||
H(TEXT("spawnNode"), &FBlueprintMCPServer::HandleSpawnNode);
|
||||
// spawn_node is now handled by UMCPHandler_SpawnNode (new-style registry)
|
||||
H(TEXT("renameAsset"), &FBlueprintMCPServer::HandleRenameAsset);
|
||||
H(TEXT("reparentBlueprint"), &FBlueprintMCPServer::HandleReparentBlueprint);
|
||||
H(TEXT("setBlueprintDefault"), &FBlueprintMCPServer::HandleSetBlueprintDefault);
|
||||
@@ -1100,6 +1136,35 @@ void FBlueprintMCPServer::RegisterHandlers()
|
||||
H(TEXT("setStateBlendSpace"), &FBlueprintMCPServer::HandleSetStateBlendSpace);
|
||||
}
|
||||
|
||||
void FBlueprintMCPServer::BuildMCPHandlerRegistry()
|
||||
{
|
||||
TArray<UClass*> HandlerClasses;
|
||||
GetDerivedClasses(UMCPHandler::StaticClass(), HandlerClasses);
|
||||
|
||||
for (UClass* Class : HandlerClasses)
|
||||
{
|
||||
if (Class->HasAnyClassFlags(CLASS_Abstract))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const FString& ToolName = Class->GetMetaData(TEXT("ToolName"));
|
||||
if (ToolName.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: %s has no ToolName meta — skipping."), *Class->GetName());
|
||||
continue;
|
||||
}
|
||||
if (MCPHandlerRegistry.Contains(ToolName))
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Duplicate ToolName '%s' on %s — skipping."), *ToolName, *Class->GetName());
|
||||
continue;
|
||||
}
|
||||
MCPHandlerRegistry.Add(ToolName, Class);
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Registered handler '%s' → %s"), *ToolName, *Class->GetName());
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %d new-style handlers registered."), MCPHandlerRegistry.Num());
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleRescan — refresh cached asset lists from asset registry
|
||||
// ============================================================
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
#include "MCPHandler.h"
|
||||
#include "BlueprintMCPServer.h"
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "UObject/UnrealType.h"
|
||||
#include "UObject/EnumProperty.h"
|
||||
|
||||
namespace MCPPopulate
|
||||
{
|
||||
|
||||
// Try to set a single FProperty on a handler from a JSON field.
|
||||
// Returns an empty string on success, or an error message on failure.
|
||||
FString SetPropertyFromJson(
|
||||
UMCPHandler* Handler,
|
||||
FProperty* Prop,
|
||||
const FString& FieldName,
|
||||
const FJsonObject* Json)
|
||||
{
|
||||
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Handler);
|
||||
|
||||
// FString
|
||||
if (FStrProperty* StrProp = CastField<FStrProperty>(Prop))
|
||||
{
|
||||
if (!Json->HasTypedField<EJson::String>(FieldName))
|
||||
{
|
||||
return FString::Printf(TEXT("'%s' must be a string"), *FieldName);
|
||||
}
|
||||
StrProp->SetPropertyValue(ValuePtr, Json->GetStringField(FieldName));
|
||||
return FString();
|
||||
}
|
||||
|
||||
// int32
|
||||
if (FIntProperty* IntProp = CastField<FIntProperty>(Prop))
|
||||
{
|
||||
if (!Json->HasTypedField<EJson::Number>(FieldName))
|
||||
{
|
||||
return FString::Printf(TEXT("'%s' must be a number"), *FieldName);
|
||||
}
|
||||
IntProp->SetPropertyValue(ValuePtr, (int32)Json->GetNumberField(FieldName));
|
||||
return FString();
|
||||
}
|
||||
|
||||
// float
|
||||
if (FFloatProperty* FloatProp = CastField<FFloatProperty>(Prop))
|
||||
{
|
||||
if (!Json->HasTypedField<EJson::Number>(FieldName))
|
||||
{
|
||||
return FString::Printf(TEXT("'%s' must be a number"), *FieldName);
|
||||
}
|
||||
FloatProp->SetPropertyValue(ValuePtr, (float)Json->GetNumberField(FieldName));
|
||||
return FString();
|
||||
}
|
||||
|
||||
// double
|
||||
if (FDoubleProperty* DoubleProp = CastField<FDoubleProperty>(Prop))
|
||||
{
|
||||
if (!Json->HasTypedField<EJson::Number>(FieldName))
|
||||
{
|
||||
return FString::Printf(TEXT("'%s' must be a number"), *FieldName);
|
||||
}
|
||||
DoubleProp->SetPropertyValue(ValuePtr, Json->GetNumberField(FieldName));
|
||||
return FString();
|
||||
}
|
||||
|
||||
// bool
|
||||
if (FBoolProperty* BoolProp = CastField<FBoolProperty>(Prop))
|
||||
{
|
||||
if (!Json->HasTypedField<EJson::Boolean>(FieldName))
|
||||
{
|
||||
return FString::Printf(TEXT("'%s' must be a boolean"), *FieldName);
|
||||
}
|
||||
BoolProp->SetPropertyValue(ValuePtr, Json->GetBoolField(FieldName));
|
||||
return FString();
|
||||
}
|
||||
|
||||
// Enum (FEnumProperty — C++ enum class)
|
||||
if (FEnumProperty* EnumProp = CastField<FEnumProperty>(Prop))
|
||||
{
|
||||
if (!Json->HasTypedField<EJson::String>(FieldName))
|
||||
{
|
||||
return FString::Printf(TEXT("'%s' must be a string"), *FieldName);
|
||||
}
|
||||
FString ValueStr = Json->GetStringField(FieldName);
|
||||
UEnum* Enum = EnumProp->GetEnum();
|
||||
int64 EnumVal = Enum->GetValueByNameString(ValueStr);
|
||||
if (EnumVal == INDEX_NONE)
|
||||
{
|
||||
return FString::Printf(TEXT("'%s': unknown enum value '%s'"), *FieldName, *ValueStr);
|
||||
}
|
||||
FNumericProperty* UnderlyingProp = EnumProp->GetUnderlyingProperty();
|
||||
UnderlyingProp->SetIntPropertyValue(ValuePtr, EnumVal);
|
||||
return FString();
|
||||
}
|
||||
|
||||
// Enum (FByteProperty with Enum — old-style UENUM)
|
||||
if (FByteProperty* ByteProp = CastField<FByteProperty>(Prop))
|
||||
{
|
||||
if (ByteProp->Enum)
|
||||
{
|
||||
if (!Json->HasTypedField<EJson::String>(FieldName))
|
||||
{
|
||||
return FString::Printf(TEXT("'%s' must be a string"), *FieldName);
|
||||
}
|
||||
FString ValueStr = Json->GetStringField(FieldName);
|
||||
int64 EnumVal = ByteProp->Enum->GetValueByNameString(ValueStr);
|
||||
if (EnumVal == INDEX_NONE)
|
||||
{
|
||||
return FString::Printf(TEXT("'%s': unknown enum value '%s'"), *FieldName, *ValueStr);
|
||||
}
|
||||
ByteProp->SetPropertyValue(ValuePtr, (uint8)EnumVal);
|
||||
return FString();
|
||||
}
|
||||
// Plain byte without enum — treat as number
|
||||
if (!Json->HasTypedField<EJson::Number>(FieldName))
|
||||
{
|
||||
return FString::Printf(TEXT("'%s' must be a number"), *FieldName);
|
||||
}
|
||||
ByteProp->SetPropertyValue(ValuePtr, (uint8)Json->GetNumberField(FieldName));
|
||||
return FString();
|
||||
}
|
||||
|
||||
// FMCPSubtree — stash the JSON subtree into the struct
|
||||
if (FStructProperty* StructProp = CastField<FStructProperty>(Prop))
|
||||
{
|
||||
if (StructProp->Struct == FMCPSubtree::StaticStruct())
|
||||
{
|
||||
if (!Json->HasTypedField<EJson::Object>(FieldName))
|
||||
{
|
||||
return FString::Printf(TEXT("'%s' must be an object"), *FieldName);
|
||||
}
|
||||
FMCPSubtree* Subtree = StructProp->ContainerPtrToValuePtr<FMCPSubtree>(Handler);
|
||||
Subtree->Json = Json->GetObjectField(FieldName);
|
||||
return FString();
|
||||
}
|
||||
}
|
||||
|
||||
return FString::Printf(TEXT("'%s': unsupported property type '%s'"),
|
||||
*FieldName, *Prop->GetCPPType());
|
||||
}
|
||||
|
||||
// Convert a property name from PascalCase to camelCase, matching JSON conventions.
|
||||
// e.g. "BlueprintName" -> "blueprintName", "PosX" -> "posX"
|
||||
FString PropertyNameToJsonKey(const FString& PropName)
|
||||
{
|
||||
if (PropName.IsEmpty())
|
||||
{
|
||||
return PropName;
|
||||
}
|
||||
FString Result = PropName;
|
||||
Result[0] = FChar::ToLower(Result[0]);
|
||||
return Result;
|
||||
}
|
||||
|
||||
} // namespace MCPPopulate
|
||||
|
||||
bool FBlueprintMCPServer::PopulateHandlerFromJson(
|
||||
UMCPHandler* Handler,
|
||||
const FJsonObject* Json,
|
||||
FJsonObject* Result)
|
||||
{
|
||||
UClass* HandlerClass = Handler->GetClass();
|
||||
|
||||
// Build a set of known property names (as JSON keys) for the unknown-field check.
|
||||
TSet<FString> KnownKeys;
|
||||
TArray<FProperty*> Properties;
|
||||
|
||||
for (TFieldIterator<FProperty> It(HandlerClass, EFieldIterationFlags::None); It; ++It)
|
||||
{
|
||||
FProperty* Prop = *It;
|
||||
Properties.Add(Prop);
|
||||
KnownKeys.Add(MCPPopulate::PropertyNameToJsonKey(Prop->GetName()));
|
||||
}
|
||||
|
||||
// Check for unknown fields in the JSON
|
||||
for (const auto& KV : Json->Values)
|
||||
{
|
||||
if (!KnownKeys.Contains(KV.Key))
|
||||
{
|
||||
MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Unknown parameter '%s'"), *KV.Key));
|
||||
Result->SetArrayField(TEXT("validParameters"),
|
||||
[&]() {
|
||||
TArray<TSharedPtr<FJsonValue>> Arr;
|
||||
for (const FString& Key : KnownKeys)
|
||||
{
|
||||
Arr.Add(MakeShared<FJsonValueString>(Key));
|
||||
}
|
||||
return Arr;
|
||||
}());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate each property from JSON
|
||||
for (FProperty* Prop : Properties)
|
||||
{
|
||||
FString JsonKey = MCPPopulate::PropertyNameToJsonKey(Prop->GetName());
|
||||
bool bOptional = Prop->HasMetaData(TEXT("Optional"));
|
||||
|
||||
if (!Json->HasField(JsonKey))
|
||||
{
|
||||
if (!bOptional)
|
||||
{
|
||||
MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Missing required parameter '%s'"), *JsonKey));
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
FString Error = MCPPopulate::SetPropertyFromJson(Handler, Prop, JsonKey, Json);
|
||||
if (!Error.IsEmpty())
|
||||
{
|
||||
MakeErrorJson(Result, Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user