2096 lines
70 KiB
C++
2096 lines
70 KiB
C++
#include "BlueprintMCPServer.h"
|
|
#include "Materials/Material.h"
|
|
#include "MaterialDomain.h"
|
|
#include "Materials/MaterialInstanceConstant.h"
|
|
#include "Materials/MaterialFunction.h"
|
|
#include "Materials/MaterialExpression.h"
|
|
#include "Materials/MaterialExpressionScalarParameter.h"
|
|
#include "Materials/MaterialExpressionVectorParameter.h"
|
|
#include "Materials/MaterialExpressionTextureObjectParameter.h"
|
|
#include "Materials/MaterialExpressionTextureSampleParameter2D.h"
|
|
#include "Materials/MaterialExpressionStaticSwitchParameter.h"
|
|
#include "Materials/MaterialExpressionConstant.h"
|
|
#include "Materials/MaterialExpressionConstant3Vector.h"
|
|
#include "Materials/MaterialExpressionConstant4Vector.h"
|
|
#include "Materials/MaterialExpressionTextureSample.h"
|
|
#include "Materials/MaterialExpressionTextureCoordinate.h"
|
|
#include "Materials/MaterialExpressionComponentMask.h"
|
|
#include "Materials/MaterialExpressionCustom.h"
|
|
#include "Materials/MaterialExpressionFunctionInput.h"
|
|
#include "Materials/MaterialExpressionFunctionOutput.h"
|
|
#include "Materials/MaterialExpressionMaterialFunctionCall.h"
|
|
#include "MaterialGraph/MaterialGraph.h"
|
|
#include "MaterialGraph/MaterialGraphNode.h"
|
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
|
#include "Factories/MaterialFactoryNew.h"
|
|
#include "Factories/MaterialFunctionFactoryNew.h"
|
|
#include "AssetToolsModule.h"
|
|
#include "IAssetTools.h"
|
|
#include "AssetRegistry/AssetRegistryModule.h"
|
|
#include "EdGraph/EdGraph.h"
|
|
#include "EdGraph/EdGraphNode.h"
|
|
#include "Serialization/JsonReader.h"
|
|
#include "Serialization/JsonWriter.h"
|
|
#include "Serialization/JsonSerializer.h"
|
|
#include "Misc/Guid.h"
|
|
#include "Misc/FileHelper.h"
|
|
#include "Misc/Paths.h"
|
|
#include "UObject/SavePackage.h"
|
|
#include "UObject/UObjectIterator.h"
|
|
#include "Kismet2/BlueprintEditorUtils.h"
|
|
|
|
// SEH wrapper defined in BlueprintMCPServer.cpp — catches crashes from abstract/invalid expression classes.
|
|
// Wraps the entire creation + registration + PostEditChange flow so that if the expression crashes
|
|
// (e.g. UMaterialExpressionParameter), it cleans up and returns -1 instead of terminating the process.
|
|
#if PLATFORM_WINDOWS
|
|
extern int32 TryAddMaterialExpressionSEH(
|
|
UObject* Owner, UClass* ExprClass, UMaterial* Material, UMaterialFunction* MatFunc,
|
|
int32 PosX, int32 PosY, UMaterialExpression** OutExpr);
|
|
#endif
|
|
|
|
// ============================================================
|
|
// Phase 2: Material Mutations
|
|
// ============================================================
|
|
|
|
// ============================================================
|
|
// HandleCreateMaterial — create a new UMaterial asset
|
|
// ============================================================
|
|
|
|
FString FBlueprintMCPServer::HandleCreateMaterial(const FString& Body)
|
|
{
|
|
TSharedPtr<FJsonObject> Json = ParseBodyJson(Body);
|
|
if (!Json.IsValid())
|
|
{
|
|
return MakeErrorJson(TEXT("Invalid JSON body"));
|
|
}
|
|
|
|
FString Name = Json->GetStringField(TEXT("name"));
|
|
FString PackagePath = Json->GetStringField(TEXT("packagePath"));
|
|
|
|
if (Name.IsEmpty() || PackagePath.IsEmpty())
|
|
{
|
|
return MakeErrorJson(TEXT("Missing required fields: name, packagePath"));
|
|
}
|
|
|
|
if (!PackagePath.StartsWith(TEXT("/Game")))
|
|
{
|
|
return MakeErrorJson(TEXT("packagePath must start with '/Game'"));
|
|
}
|
|
|
|
// Check if asset already exists
|
|
FString FullAssetPath = PackagePath / Name;
|
|
if (FindMaterialAsset(Name) || FindMaterialAsset(FullAssetPath))
|
|
{
|
|
return MakeErrorJson(FString::Printf(
|
|
TEXT("Material '%s' already exists. Use a different name or delete the existing asset first."),
|
|
*Name));
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Material '%s' in '%s'"), *Name, *PackagePath);
|
|
|
|
// Create via IAssetTools + factory
|
|
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
|
|
UMaterialFactoryNew* Factory = NewObject<UMaterialFactoryNew>();
|
|
UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterial::StaticClass(), Factory);
|
|
|
|
if (!NewAsset)
|
|
{
|
|
return MakeErrorJson(FString::Printf(TEXT("Failed to create Material '%s' in '%s'"), *Name, *PackagePath));
|
|
}
|
|
|
|
UMaterial* Material = Cast<UMaterial>(NewAsset);
|
|
if (!Material)
|
|
{
|
|
return MakeErrorJson(TEXT("Created asset is not a UMaterial"));
|
|
}
|
|
|
|
// Apply optional properties
|
|
FString DomainStr;
|
|
Json->TryGetStringField(TEXT("domain"), DomainStr);
|
|
|
|
FString BlendModeStr;
|
|
Json->TryGetStringField(TEXT("blendMode"), BlendModeStr);
|
|
|
|
bool bTwoSided = false;
|
|
bool bHasTwoSided = Json->TryGetBoolField(TEXT("twoSided"), bTwoSided);
|
|
|
|
Material->PreEditChange(nullptr);
|
|
|
|
// Parse domain
|
|
if (!DomainStr.IsEmpty())
|
|
{
|
|
if (DomainStr == TEXT("Surface"))
|
|
Material->MaterialDomain = MD_Surface;
|
|
else if (DomainStr == TEXT("DeferredDecal"))
|
|
Material->MaterialDomain = MD_DeferredDecal;
|
|
else if (DomainStr == TEXT("LightFunction"))
|
|
Material->MaterialDomain = MD_LightFunction;
|
|
else if (DomainStr == TEXT("Volume"))
|
|
Material->MaterialDomain = MD_Volume;
|
|
else if (DomainStr == TEXT("PostProcess"))
|
|
Material->MaterialDomain = MD_PostProcess;
|
|
else if (DomainStr == TEXT("UI"))
|
|
Material->MaterialDomain = MD_UI;
|
|
}
|
|
|
|
// Parse blend mode
|
|
if (!BlendModeStr.IsEmpty())
|
|
{
|
|
if (BlendModeStr == TEXT("Opaque"))
|
|
Material->BlendMode = BLEND_Opaque;
|
|
else if (BlendModeStr == TEXT("Masked"))
|
|
Material->BlendMode = BLEND_Masked;
|
|
else if (BlendModeStr == TEXT("Translucent"))
|
|
Material->BlendMode = BLEND_Translucent;
|
|
else if (BlendModeStr == TEXT("Additive"))
|
|
Material->BlendMode = BLEND_Additive;
|
|
else if (BlendModeStr == TEXT("Modulate"))
|
|
Material->BlendMode = BLEND_Modulate;
|
|
}
|
|
|
|
if (bHasTwoSided)
|
|
{
|
|
Material->TwoSided = bTwoSided;
|
|
}
|
|
|
|
Material->PostEditChange();
|
|
|
|
// Save
|
|
bool bSaved = SaveMaterialPackage(Material);
|
|
|
|
// Refresh asset cache
|
|
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
|
|
AllMaterialAssets.Empty();
|
|
ARM.Get().GetAssetsByClass(UMaterial::StaticClass()->GetClassPathName(), AllMaterialAssets, false);
|
|
|
|
// Map domain back to string for response
|
|
auto DomainToString = [](EMaterialDomain Domain) -> FString
|
|
{
|
|
switch (Domain)
|
|
{
|
|
case MD_Surface: return TEXT("Surface");
|
|
case MD_DeferredDecal: return TEXT("DeferredDecal");
|
|
case MD_LightFunction: return TEXT("LightFunction");
|
|
case MD_Volume: return TEXT("Volume");
|
|
case MD_PostProcess: return TEXT("PostProcess");
|
|
case MD_UI: return TEXT("UI");
|
|
default: return TEXT("Surface");
|
|
}
|
|
};
|
|
|
|
auto BlendModeToString = [](EBlendMode Mode) -> FString
|
|
{
|
|
switch (Mode)
|
|
{
|
|
case BLEND_Opaque: return TEXT("Opaque");
|
|
case BLEND_Masked: return TEXT("Masked");
|
|
case BLEND_Translucent: return TEXT("Translucent");
|
|
case BLEND_Additive: return TEXT("Additive");
|
|
case BLEND_Modulate: return TEXT("Modulate");
|
|
default: return TEXT("Opaque");
|
|
}
|
|
};
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Material '%s' (saved: %s)"),
|
|
*Name, bSaved ? TEXT("true") : TEXT("false"));
|
|
|
|
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
Result->SetBoolField(TEXT("success"), true);
|
|
Result->SetStringField(TEXT("name"), Name);
|
|
Result->SetStringField(TEXT("path"), Material->GetPathName());
|
|
Result->SetStringField(TEXT("domain"), DomainToString(Material->MaterialDomain));
|
|
Result->SetStringField(TEXT("blendMode"), BlendModeToString(Material->BlendMode));
|
|
Result->SetBoolField(TEXT("twoSided"), Material->TwoSided != 0);
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
return JsonToString(Result);
|
|
}
|
|
|
|
// ============================================================
|
|
// HandleSetMaterialProperty — set a top-level material property
|
|
// ============================================================
|
|
|
|
FString FBlueprintMCPServer::HandleSetMaterialProperty(const FString& Body)
|
|
{
|
|
TSharedPtr<FJsonObject> Json = ParseBodyJson(Body);
|
|
if (!Json.IsValid())
|
|
{
|
|
return MakeErrorJson(TEXT("Invalid JSON body"));
|
|
}
|
|
|
|
FString MaterialName = Json->GetStringField(TEXT("material"));
|
|
FString Property = Json->GetStringField(TEXT("property"));
|
|
|
|
if (MaterialName.IsEmpty() || Property.IsEmpty())
|
|
{
|
|
return MakeErrorJson(TEXT("Missing required fields: material, property"));
|
|
}
|
|
|
|
if (!Json->HasField(TEXT("value")))
|
|
{
|
|
return MakeErrorJson(TEXT("Missing required field: value"));
|
|
}
|
|
|
|
bool bDryRun = false;
|
|
Json->TryGetBoolField(TEXT("dryRun"), bDryRun);
|
|
|
|
// Load material
|
|
FString LoadError;
|
|
UMaterial* Material = LoadMaterialByName(MaterialName, LoadError);
|
|
if (!Material)
|
|
{
|
|
return MakeErrorJson(LoadError);
|
|
}
|
|
|
|
FString OldValue;
|
|
FString NewValue;
|
|
|
|
// Helper lambdas for converting enum values to strings
|
|
auto DomainToString = [](EMaterialDomain Domain) -> FString
|
|
{
|
|
switch (Domain)
|
|
{
|
|
case MD_Surface: return TEXT("Surface");
|
|
case MD_DeferredDecal: return TEXT("DeferredDecal");
|
|
case MD_LightFunction: return TEXT("LightFunction");
|
|
case MD_Volume: return TEXT("Volume");
|
|
case MD_PostProcess: return TEXT("PostProcess");
|
|
case MD_UI: return TEXT("UI");
|
|
default: return TEXT("Unknown");
|
|
}
|
|
};
|
|
|
|
auto BlendModeToString = [](EBlendMode Mode) -> FString
|
|
{
|
|
switch (Mode)
|
|
{
|
|
case BLEND_Opaque: return TEXT("Opaque");
|
|
case BLEND_Masked: return TEXT("Masked");
|
|
case BLEND_Translucent: return TEXT("Translucent");
|
|
case BLEND_Additive: return TEXT("Additive");
|
|
case BLEND_Modulate: return TEXT("Modulate");
|
|
default: return TEXT("Unknown");
|
|
}
|
|
};
|
|
|
|
auto ShadingModelToString = [](EMaterialShadingModel Model) -> FString
|
|
{
|
|
switch (Model)
|
|
{
|
|
case MSM_Unlit: return TEXT("Unlit");
|
|
case MSM_DefaultLit: return TEXT("DefaultLit");
|
|
case MSM_Subsurface: return TEXT("Subsurface");
|
|
case MSM_PreintegratedSkin: return TEXT("PreintegratedSkin");
|
|
case MSM_ClearCoat: return TEXT("ClearCoat");
|
|
case MSM_SubsurfaceProfile: return TEXT("SubsurfaceProfile");
|
|
case MSM_TwoSidedFoliage: return TEXT("TwoSidedFoliage");
|
|
case MSM_Hair: return TEXT("Hair");
|
|
case MSM_Cloth: return TEXT("Cloth");
|
|
case MSM_Eye: return TEXT("Eye");
|
|
default: return TEXT("DefaultLit");
|
|
}
|
|
};
|
|
|
|
if (Property == TEXT("domain"))
|
|
{
|
|
FString ValueStr = Json->GetStringField(TEXT("value"));
|
|
OldValue = DomainToString(Material->MaterialDomain);
|
|
|
|
EMaterialDomain NewDomain = Material->MaterialDomain;
|
|
if (ValueStr == TEXT("Surface")) NewDomain = MD_Surface;
|
|
else if (ValueStr == TEXT("DeferredDecal")) NewDomain = MD_DeferredDecal;
|
|
else if (ValueStr == TEXT("LightFunction")) NewDomain = MD_LightFunction;
|
|
else if (ValueStr == TEXT("Volume")) NewDomain = MD_Volume;
|
|
else if (ValueStr == TEXT("PostProcess")) NewDomain = MD_PostProcess;
|
|
else if (ValueStr == TEXT("UI")) NewDomain = MD_UI;
|
|
else
|
|
{
|
|
return MakeErrorJson(FString::Printf(
|
|
TEXT("Invalid domain '%s'. Valid values: Surface, DeferredDecal, LightFunction, Volume, PostProcess, UI"),
|
|
*ValueStr));
|
|
}
|
|
|
|
NewValue = ValueStr;
|
|
|
|
if (!bDryRun)
|
|
{
|
|
Material->PreEditChange(nullptr);
|
|
Material->MaterialDomain = NewDomain;
|
|
Material->PostEditChange();
|
|
}
|
|
}
|
|
else if (Property == TEXT("blendMode"))
|
|
{
|
|
FString ValueStr = Json->GetStringField(TEXT("value"));
|
|
OldValue = BlendModeToString(Material->BlendMode);
|
|
|
|
EBlendMode NewBlend = Material->BlendMode;
|
|
if (ValueStr == TEXT("Opaque")) NewBlend = BLEND_Opaque;
|
|
else if (ValueStr == TEXT("Masked")) NewBlend = BLEND_Masked;
|
|
else if (ValueStr == TEXT("Translucent")) NewBlend = BLEND_Translucent;
|
|
else if (ValueStr == TEXT("Additive")) NewBlend = BLEND_Additive;
|
|
else if (ValueStr == TEXT("Modulate")) NewBlend = BLEND_Modulate;
|
|
else
|
|
{
|
|
return MakeErrorJson(FString::Printf(
|
|
TEXT("Invalid blendMode '%s'. Valid values: Opaque, Masked, Translucent, Additive, Modulate"),
|
|
*ValueStr));
|
|
}
|
|
|
|
NewValue = ValueStr;
|
|
|
|
if (!bDryRun)
|
|
{
|
|
Material->PreEditChange(nullptr);
|
|
Material->BlendMode = NewBlend;
|
|
Material->PostEditChange();
|
|
}
|
|
}
|
|
else if (Property == TEXT("twoSided"))
|
|
{
|
|
bool bValue = Json->GetBoolField(TEXT("value"));
|
|
OldValue = Material->TwoSided ? TEXT("true") : TEXT("false");
|
|
NewValue = bValue ? TEXT("true") : TEXT("false");
|
|
|
|
if (!bDryRun)
|
|
{
|
|
Material->PreEditChange(nullptr);
|
|
Material->TwoSided = bValue ? 1 : 0;
|
|
Material->PostEditChange();
|
|
}
|
|
}
|
|
else if (Property == TEXT("shadingModel"))
|
|
{
|
|
FString ValueStr = Json->GetStringField(TEXT("value"));
|
|
OldValue = ShadingModelToString(Material->GetShadingModels().GetFirstShadingModel());
|
|
|
|
EMaterialShadingModel NewModel = MSM_DefaultLit;
|
|
if (ValueStr == TEXT("Unlit")) NewModel = MSM_Unlit;
|
|
else if (ValueStr == TEXT("DefaultLit")) NewModel = MSM_DefaultLit;
|
|
else if (ValueStr == TEXT("Subsurface")) NewModel = MSM_Subsurface;
|
|
else if (ValueStr == TEXT("PreintegratedSkin")) NewModel = MSM_PreintegratedSkin;
|
|
else if (ValueStr == TEXT("ClearCoat")) NewModel = MSM_ClearCoat;
|
|
else if (ValueStr == TEXT("SubsurfaceProfile")) NewModel = MSM_SubsurfaceProfile;
|
|
else if (ValueStr == TEXT("TwoSidedFoliage")) NewModel = MSM_TwoSidedFoliage;
|
|
else if (ValueStr == TEXT("Hair")) NewModel = MSM_Hair;
|
|
else if (ValueStr == TEXT("Cloth")) NewModel = MSM_Cloth;
|
|
else if (ValueStr == TEXT("Eye")) NewModel = MSM_Eye;
|
|
else
|
|
{
|
|
return MakeErrorJson(FString::Printf(
|
|
TEXT("Invalid shadingModel '%s'. Valid values: Unlit, DefaultLit, Subsurface, PreintegratedSkin, ClearCoat, SubsurfaceProfile, TwoSidedFoliage, Hair, Cloth, Eye"),
|
|
*ValueStr));
|
|
}
|
|
|
|
NewValue = ValueStr;
|
|
|
|
if (!bDryRun)
|
|
{
|
|
Material->PreEditChange(nullptr);
|
|
Material->SetShadingModel(NewModel);
|
|
Material->PostEditChange();
|
|
}
|
|
}
|
|
else if (Property == TEXT("opacity") || Property == TEXT("opacityMaskClipValue"))
|
|
{
|
|
double OpacityValue = Json->GetNumberField(TEXT("value"));
|
|
OldValue = FString::Printf(TEXT("%f"), Material->OpacityMaskClipValue);
|
|
NewValue = FString::Printf(TEXT("%f"), OpacityValue);
|
|
|
|
if (!bDryRun)
|
|
{
|
|
Material->PreEditChange(nullptr);
|
|
Material->OpacityMaskClipValue = (float)OpacityValue;
|
|
Material->PostEditChange();
|
|
}
|
|
}
|
|
else if (Property == TEXT("bUsedWithSkeletalMesh"))
|
|
{
|
|
bool bValue = Json->GetBoolField(TEXT("value"));
|
|
OldValue = Material->bUsedWithSkeletalMesh ? TEXT("true") : TEXT("false");
|
|
NewValue = bValue ? TEXT("true") : TEXT("false");
|
|
|
|
if (!bDryRun)
|
|
{
|
|
Material->PreEditChange(nullptr);
|
|
Material->bUsedWithSkeletalMesh = bValue ? 1 : 0;
|
|
Material->PostEditChange();
|
|
}
|
|
}
|
|
else if (Property == TEXT("bUsedWithMorphTargets"))
|
|
{
|
|
bool bValue = Json->GetBoolField(TEXT("value"));
|
|
OldValue = Material->bUsedWithMorphTargets ? TEXT("true") : TEXT("false");
|
|
NewValue = bValue ? TEXT("true") : TEXT("false");
|
|
|
|
if (!bDryRun)
|
|
{
|
|
Material->PreEditChange(nullptr);
|
|
Material->bUsedWithMorphTargets = bValue ? 1 : 0;
|
|
Material->PostEditChange();
|
|
}
|
|
}
|
|
else if (Property == TEXT("bUsedWithNiagaraSprites"))
|
|
{
|
|
bool bValue = Json->GetBoolField(TEXT("value"));
|
|
OldValue = Material->bUsedWithNiagaraSprites ? TEXT("true") : TEXT("false");
|
|
NewValue = bValue ? TEXT("true") : TEXT("false");
|
|
|
|
if (!bDryRun)
|
|
{
|
|
Material->PreEditChange(nullptr);
|
|
Material->bUsedWithNiagaraSprites = bValue ? 1 : 0;
|
|
Material->PostEditChange();
|
|
}
|
|
}
|
|
else if (Property == TEXT("ditheredLODTransition") || Property == TEXT("DitheredLODTransition"))
|
|
{
|
|
bool bValue = Json->GetBoolField(TEXT("value"));
|
|
OldValue = Material->DitheredLODTransition ? TEXT("true") : TEXT("false");
|
|
NewValue = bValue ? TEXT("true") : TEXT("false");
|
|
|
|
if (!bDryRun)
|
|
{
|
|
Material->PreEditChange(nullptr);
|
|
Material->DitheredLODTransition = bValue ? 1 : 0;
|
|
Material->PostEditChange();
|
|
}
|
|
}
|
|
else if (Property == TEXT("bAllowNegativeEmissiveColor"))
|
|
{
|
|
bool bValue = Json->GetBoolField(TEXT("value"));
|
|
OldValue = Material->bAllowNegativeEmissiveColor ? TEXT("true") : TEXT("false");
|
|
NewValue = bValue ? TEXT("true") : TEXT("false");
|
|
|
|
if (!bDryRun)
|
|
{
|
|
Material->PreEditChange(nullptr);
|
|
Material->bAllowNegativeEmissiveColor = bValue ? 1 : 0;
|
|
Material->PostEditChange();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return MakeErrorJson(FString::Printf(
|
|
TEXT("Unknown property '%s'. Valid properties: domain, blendMode, twoSided, shadingModel, opacity, "
|
|
"opacityMaskClipValue, bUsedWithSkeletalMesh, bUsedWithMorphTargets, bUsedWithNiagaraSprites, "
|
|
"ditheredLODTransition, bAllowNegativeEmissiveColor"),
|
|
*Property));
|
|
}
|
|
|
|
// Save if not dry run
|
|
bool bSaved = false;
|
|
if (!bDryRun)
|
|
{
|
|
bSaved = SaveMaterialPackage(Material);
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %sSet material property '%s' on '%s': '%s' -> '%s'"),
|
|
bDryRun ? TEXT("[DRY RUN] ") : TEXT(""),
|
|
*Property, *MaterialName, *OldValue, *NewValue);
|
|
|
|
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
Result->SetBoolField(TEXT("success"), true);
|
|
Result->SetStringField(TEXT("material"), Material->GetName());
|
|
Result->SetStringField(TEXT("property"), Property);
|
|
Result->SetStringField(TEXT("oldValue"), OldValue);
|
|
Result->SetStringField(TEXT("newValue"), NewValue);
|
|
Result->SetBoolField(TEXT("dryRun"), bDryRun);
|
|
if (!bDryRun)
|
|
{
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
}
|
|
return JsonToString(Result);
|
|
}
|
|
|
|
// ============================================================
|
|
// HandleAddMaterialExpression — add a new expression to a material
|
|
// ============================================================
|
|
|
|
FString FBlueprintMCPServer::HandleAddMaterialExpression(const FString& Body)
|
|
{
|
|
TSharedPtr<FJsonObject> Json = ParseBodyJson(Body);
|
|
if (!Json.IsValid())
|
|
{
|
|
return MakeErrorJson(TEXT("Invalid JSON body"));
|
|
}
|
|
|
|
FString MaterialName = Json->GetStringField(TEXT("material"));
|
|
FString ExpressionClassName = Json->GetStringField(TEXT("expressionClass"));
|
|
|
|
if (MaterialName.IsEmpty() && !Json->HasField(TEXT("materialFunction")))
|
|
{
|
|
return MakeErrorJson(TEXT("Missing required field: 'material' or 'materialFunction'"));
|
|
}
|
|
if (ExpressionClassName.IsEmpty())
|
|
{
|
|
return MakeErrorJson(TEXT("Missing required field: expressionClass"));
|
|
}
|
|
|
|
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"));
|
|
|
|
bool bDryRun = false;
|
|
Json->TryGetBoolField(TEXT("dryRun"), bDryRun);
|
|
|
|
// Map string class name to UClass via dynamic lookup
|
|
UClass* ExprClass = nullptr;
|
|
|
|
// Convenience aliases for backward compatibility
|
|
static TMap<FString, FString> Aliases = {
|
|
{TEXT("Lerp"), TEXT("LinearInterpolate")},
|
|
};
|
|
|
|
FString LookupName = ExpressionClassName;
|
|
if (const FString* Alias = Aliases.Find(ExpressionClassName))
|
|
{
|
|
LookupName = *Alias;
|
|
}
|
|
|
|
// Dynamic lookup: find UMaterialExpression<Name> via UClass iteration
|
|
FString FullClassName = FString::Printf(TEXT("MaterialExpression%s"), *LookupName);
|
|
for (TObjectIterator<UClass> It; It; ++It)
|
|
{
|
|
if (It->GetName() == FullClassName && It->IsChildOf(UMaterialExpression::StaticClass()))
|
|
{
|
|
ExprClass = *It;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!ExprClass)
|
|
{
|
|
return MakeErrorJson(FString::Printf(
|
|
TEXT("Unknown expression class '%s'. Use the UMaterialExpression subclass name without the 'MaterialExpression' prefix "
|
|
"(e.g. 'Constant', 'ScalarParameter', 'Add', 'Multiply', 'Lerp', 'Subtract', 'Fresnel', 'Comment', etc.)"),
|
|
*ExpressionClassName));
|
|
}
|
|
if (ExprClass->HasAnyClassFlags(CLASS_Abstract))
|
|
{
|
|
return MakeErrorJson(FString::Printf(
|
|
TEXT("Expression class '%s' is abstract and cannot be instantiated."), *ExpressionClassName));
|
|
}
|
|
|
|
// Load material or material function
|
|
FString MaterialFunctionName = Json->GetStringField(TEXT("materialFunction"));
|
|
UMaterial* Material = nullptr;
|
|
UMaterialFunction* MatFunc = nullptr;
|
|
UObject* Owner = nullptr;
|
|
FString AssetDisplayName;
|
|
|
|
if (!MaterialFunctionName.IsEmpty())
|
|
{
|
|
if (!MaterialName.IsEmpty())
|
|
{
|
|
return MakeErrorJson(TEXT("Specify either 'material' or 'materialFunction', not both"));
|
|
}
|
|
FString LoadError;
|
|
MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError);
|
|
if (!MatFunc) return MakeErrorJson(LoadError);
|
|
Owner = MatFunc;
|
|
AssetDisplayName = MatFunc->GetName();
|
|
}
|
|
else
|
|
{
|
|
FString LoadError;
|
|
Material = LoadMaterialByName(MaterialName, LoadError);
|
|
if (!Material) return MakeErrorJson(LoadError);
|
|
Owner = Material;
|
|
AssetDisplayName = Material->GetName();
|
|
}
|
|
|
|
if (bDryRun)
|
|
{
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would add expression '%s' to '%s' at (%d, %d)"),
|
|
*ExpressionClassName, *AssetDisplayName, PosX, PosY);
|
|
|
|
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
Result->SetBoolField(TEXT("success"), true);
|
|
Result->SetBoolField(TEXT("dryRun"), true);
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
Result->SetStringField(TEXT("expressionClass"), ExpressionClassName);
|
|
Result->SetNumberField(TEXT("posX"), PosX);
|
|
Result->SetNumberField(TEXT("posY"), PosY);
|
|
return JsonToString(Result);
|
|
}
|
|
|
|
// Ensure the MaterialGraph exists (commandlet mode doesn't auto-create it)
|
|
if (Material) EnsureMaterialGraph(Material);
|
|
|
|
// Create, register, and PostEditChange the expression — all inside an SEH wrapper because
|
|
// some classes (e.g. UMaterialExpressionParameter) lack CLASS_Abstract but crash during
|
|
// PostEditChange. The SEH wrapper cleans up the bad expression on crash.
|
|
UMaterialExpression* NewExpr = nullptr;
|
|
#if PLATFORM_WINDOWS
|
|
int32 CreateResult = TryAddMaterialExpressionSEH(Owner, ExprClass, Material, MatFunc, PosX, PosY, &NewExpr);
|
|
if (CreateResult != 0 || !NewExpr)
|
|
{
|
|
return MakeErrorJson(FString::Printf(
|
|
TEXT("Expression class '%s' cannot be instantiated (may be abstract or have internal errors)."),
|
|
*ExpressionClassName));
|
|
}
|
|
#else
|
|
NewExpr = NewObject<UMaterialExpression>(Owner, ExprClass);
|
|
if (!NewExpr)
|
|
{
|
|
return MakeErrorJson(TEXT("Failed to create material expression object"));
|
|
}
|
|
NewExpr->MaterialExpressionEditorX = PosX;
|
|
NewExpr->MaterialExpressionEditorY = PosY;
|
|
if (Material)
|
|
{
|
|
Material->GetExpressionCollection().AddExpression(NewExpr);
|
|
if (Material->MaterialGraph)
|
|
{
|
|
Material->MaterialGraph->RebuildGraph();
|
|
}
|
|
Material->PreEditChange(nullptr);
|
|
Material->PostEditChange();
|
|
Material->MarkPackageDirty();
|
|
}
|
|
else if (MatFunc)
|
|
{
|
|
MatFunc->GetExpressionCollection().AddExpression(NewExpr);
|
|
MatFunc->PreEditChange(nullptr);
|
|
MatFunc->PostEditChange();
|
|
MatFunc->MarkPackageDirty();
|
|
}
|
|
#endif
|
|
|
|
// Save
|
|
bool bSaved = Material ? SaveMaterialPackage(Material) : SaveGenericPackage(MatFunc);
|
|
|
|
// Find the node GUID from the material graph (only for materials)
|
|
FString NodeGuid;
|
|
if (Material && Material->MaterialGraph)
|
|
{
|
|
for (UEdGraphNode* Node : Material->MaterialGraph->Nodes)
|
|
{
|
|
UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(Node);
|
|
if (MatNode && MatNode->MaterialExpression == NewExpr)
|
|
{
|
|
NodeGuid = Node->NodeGuid.ToString();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added expression '%s' to '%s' (nodeId: %s, saved: %s)"),
|
|
*ExpressionClassName, *AssetDisplayName, *NodeGuid, bSaved ? TEXT("true") : TEXT("false"));
|
|
|
|
// Serialize the expression details
|
|
TSharedPtr<FJsonObject> ExprDetails = SerializeMaterialExpression(NewExpr);
|
|
|
|
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
Result->SetBoolField(TEXT("success"), true);
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
Result->SetStringField(TEXT("expressionClass"), ExpressionClassName);
|
|
Result->SetStringField(TEXT("nodeId"), NodeGuid);
|
|
Result->SetNumberField(TEXT("posX"), PosX);
|
|
Result->SetNumberField(TEXT("posY"), PosY);
|
|
if (ExprDetails.IsValid())
|
|
{
|
|
Result->SetObjectField(TEXT("expression"), ExprDetails);
|
|
}
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
return JsonToString(Result);
|
|
}
|
|
|
|
// ============================================================
|
|
// HandleDeleteMaterialExpression — remove an expression from a material
|
|
// ============================================================
|
|
|
|
FString FBlueprintMCPServer::HandleDeleteMaterialExpression(const FString& Body)
|
|
{
|
|
TSharedPtr<FJsonObject> Json = ParseBodyJson(Body);
|
|
if (!Json.IsValid())
|
|
{
|
|
return MakeErrorJson(TEXT("Invalid JSON body"));
|
|
}
|
|
|
|
FString MaterialName = Json->GetStringField(TEXT("material"));
|
|
FString MaterialFunctionName = Json->GetStringField(TEXT("materialFunction"));
|
|
FString NodeId = Json->GetStringField(TEXT("nodeId"));
|
|
|
|
if (MaterialName.IsEmpty() && MaterialFunctionName.IsEmpty())
|
|
{
|
|
return MakeErrorJson(TEXT("Missing required field: 'material' or 'materialFunction'"));
|
|
}
|
|
if (NodeId.IsEmpty())
|
|
{
|
|
return MakeErrorJson(TEXT("Missing required field: nodeId"));
|
|
}
|
|
|
|
bool bDryRun = false;
|
|
Json->TryGetBoolField(TEXT("dryRun"), bDryRun);
|
|
|
|
// Load material or material function
|
|
UMaterial* Material = nullptr;
|
|
UMaterialFunction* MatFunc = nullptr;
|
|
FString AssetDisplayName;
|
|
|
|
if (!MaterialFunctionName.IsEmpty())
|
|
{
|
|
FString LoadError;
|
|
MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError);
|
|
if (!MatFunc) return MakeErrorJson(LoadError);
|
|
AssetDisplayName = MatFunc->GetName();
|
|
}
|
|
else
|
|
{
|
|
FString LoadError;
|
|
Material = LoadMaterialByName(MaterialName, LoadError);
|
|
if (!Material) return MakeErrorJson(LoadError);
|
|
AssetDisplayName = Material->GetName();
|
|
}
|
|
|
|
// For materials, we need the graph to find nodes by GUID
|
|
if (Material) EnsureMaterialGraph(Material);
|
|
UEdGraph* Graph = Material ? (UEdGraph*)Material->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
|
|
if (!Graph)
|
|
{
|
|
return MakeErrorJson(FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName));
|
|
}
|
|
|
|
// Find the node by GUID
|
|
UMaterialGraphNode* TargetMatNode = nullptr;
|
|
for (UEdGraphNode* Node : Graph->Nodes)
|
|
{
|
|
if (!Node) continue;
|
|
if (Node->NodeGuid.ToString() == NodeId)
|
|
{
|
|
TargetMatNode = Cast<UMaterialGraphNode>(Node);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!TargetMatNode)
|
|
{
|
|
return MakeErrorJson(FString::Printf(TEXT("Node '%s' not found in material graph"), *NodeId));
|
|
}
|
|
|
|
if (!TargetMatNode->MaterialExpression)
|
|
{
|
|
return MakeErrorJson(FString::Printf(TEXT("Node '%s' has no associated material expression"), *NodeId));
|
|
}
|
|
|
|
// Capture info before deletion
|
|
FString DeletedNodeTitle = TargetMatNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
|
FString DeletedExprClass = TargetMatNode->MaterialExpression->GetClass()->GetName();
|
|
|
|
if (bDryRun)
|
|
{
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would delete expression '%s' (nodeId: %s) from '%s'"),
|
|
*DeletedExprClass, *NodeId, *AssetDisplayName);
|
|
|
|
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
Result->SetBoolField(TEXT("success"), true);
|
|
Result->SetBoolField(TEXT("dryRun"), true);
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
Result->SetStringField(TEXT("deletedNode"), NodeId);
|
|
Result->SetStringField(TEXT("deletedNodeTitle"), DeletedNodeTitle);
|
|
Result->SetStringField(TEXT("deletedExpressionClass"), DeletedExprClass);
|
|
return JsonToString(Result);
|
|
}
|
|
|
|
// Remove the expression
|
|
UMaterialExpression* ExprToRemove = TargetMatNode->MaterialExpression;
|
|
if (Material)
|
|
{
|
|
Material->GetExpressionCollection().RemoveExpression(ExprToRemove);
|
|
}
|
|
else
|
|
{
|
|
MatFunc->GetExpressionCollection().RemoveExpression(ExprToRemove);
|
|
}
|
|
ExprToRemove->MarkAsGarbage();
|
|
|
|
// Rebuild graph
|
|
Graph->NotifyGraphChanged();
|
|
|
|
UObject* Asset = Material ? (UObject*)Material : (UObject*)MatFunc;
|
|
Asset->PreEditChange(nullptr);
|
|
Asset->PostEditChange();
|
|
Asset->MarkPackageDirty();
|
|
|
|
// Save
|
|
bool bSaved = Material ? SaveMaterialPackage(Material) : SaveGenericPackage(MatFunc);
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleted expression '%s' (nodeId: %s) from '%s' (saved: %s)"),
|
|
*DeletedExprClass, *NodeId, *AssetDisplayName, bSaved ? TEXT("true") : TEXT("false"));
|
|
|
|
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
Result->SetBoolField(TEXT("success"), true);
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
Result->SetStringField(TEXT("deletedNode"), NodeId);
|
|
Result->SetStringField(TEXT("deletedNodeTitle"), DeletedNodeTitle);
|
|
Result->SetStringField(TEXT("deletedExpressionClass"), DeletedExprClass);
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
return JsonToString(Result);
|
|
}
|
|
|
|
// ============================================================
|
|
// HandleConnectMaterialPins — connect two pins in a material graph
|
|
// ============================================================
|
|
|
|
FString FBlueprintMCPServer::HandleConnectMaterialPins(const FString& Body)
|
|
{
|
|
TSharedPtr<FJsonObject> Json = ParseBodyJson(Body);
|
|
if (!Json.IsValid())
|
|
{
|
|
return MakeErrorJson(TEXT("Invalid JSON body"));
|
|
}
|
|
|
|
FString MaterialName = Json->GetStringField(TEXT("material"));
|
|
FString MaterialFunctionName = Json->GetStringField(TEXT("materialFunction"));
|
|
FString SourceNodeId = Json->GetStringField(TEXT("sourceNodeId"));
|
|
FString SourcePinName = Json->GetStringField(TEXT("sourcePinName"));
|
|
FString TargetNodeId = Json->GetStringField(TEXT("targetNodeId"));
|
|
FString TargetPinName = Json->GetStringField(TEXT("targetPinName"));
|
|
|
|
if (MaterialName.IsEmpty() && MaterialFunctionName.IsEmpty())
|
|
{
|
|
return MakeErrorJson(TEXT("Missing required field: 'material' or 'materialFunction'"));
|
|
}
|
|
if (SourceNodeId.IsEmpty() || SourcePinName.IsEmpty() || TargetNodeId.IsEmpty() || TargetPinName.IsEmpty())
|
|
{
|
|
return MakeErrorJson(TEXT("Missing required fields: sourceNodeId, sourcePinName, targetNodeId, targetPinName"));
|
|
}
|
|
|
|
bool bDryRun = false;
|
|
Json->TryGetBoolField(TEXT("dryRun"), bDryRun);
|
|
|
|
// Load material or material function
|
|
UMaterial* Material = nullptr;
|
|
UMaterialFunction* MatFunc = nullptr;
|
|
FString AssetDisplayName;
|
|
|
|
if (!MaterialFunctionName.IsEmpty())
|
|
{
|
|
FString LoadError;
|
|
MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError);
|
|
if (!MatFunc) return MakeErrorJson(LoadError);
|
|
AssetDisplayName = MatFunc->GetName();
|
|
}
|
|
else
|
|
{
|
|
FString LoadError;
|
|
Material = LoadMaterialByName(MaterialName, LoadError);
|
|
if (!Material) return MakeErrorJson(LoadError);
|
|
AssetDisplayName = Material->GetName();
|
|
}
|
|
|
|
if (Material) EnsureMaterialGraph(Material);
|
|
UEdGraph* Graph = Material ? (UEdGraph*)Material->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
|
|
if (!Graph)
|
|
{
|
|
return MakeErrorJson(FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName));
|
|
}
|
|
|
|
// Find source and target nodes by GUID
|
|
UEdGraphNode* SourceNode = nullptr;
|
|
UEdGraphNode* TargetNode = nullptr;
|
|
|
|
for (UEdGraphNode* Node : Graph->Nodes)
|
|
{
|
|
if (!Node) continue;
|
|
if (Node->NodeGuid.ToString() == SourceNodeId)
|
|
SourceNode = Node;
|
|
if (Node->NodeGuid.ToString() == TargetNodeId)
|
|
TargetNode = Node;
|
|
if (SourceNode && TargetNode)
|
|
break;
|
|
}
|
|
|
|
if (!SourceNode)
|
|
{
|
|
return MakeErrorJson(FString::Printf(TEXT("Source node '%s' not found in material graph"), *SourceNodeId));
|
|
}
|
|
if (!TargetNode)
|
|
{
|
|
return MakeErrorJson(FString::Printf(TEXT("Target node '%s' not found in material graph"), *TargetNodeId));
|
|
}
|
|
|
|
// Find pins
|
|
UEdGraphPin* SourcePin = SourceNode->FindPin(FName(*SourcePinName));
|
|
if (!SourcePin)
|
|
{
|
|
// List available pins for debugging
|
|
TArray<TSharedPtr<FJsonValue>> PinNames;
|
|
for (UEdGraphPin* P : SourceNode->Pins)
|
|
{
|
|
if (P) PinNames.Add(MakeShared<FJsonValueString>(
|
|
FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(),
|
|
P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"))));
|
|
}
|
|
TSharedRef<FJsonObject> E = MakeShared<FJsonObject>();
|
|
E->SetStringField(TEXT("error"), FString::Printf(TEXT("Source pin '%s' not found on node '%s'"),
|
|
*SourcePinName, *SourceNodeId));
|
|
E->SetArrayField(TEXT("availablePins"), PinNames);
|
|
return JsonToString(E);
|
|
}
|
|
|
|
UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*TargetPinName));
|
|
if (!TargetPin)
|
|
{
|
|
TArray<TSharedPtr<FJsonValue>> PinNames;
|
|
for (UEdGraphPin* P : TargetNode->Pins)
|
|
{
|
|
if (P) PinNames.Add(MakeShared<FJsonValueString>(
|
|
FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(),
|
|
P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"))));
|
|
}
|
|
TSharedRef<FJsonObject> E = MakeShared<FJsonObject>();
|
|
E->SetStringField(TEXT("error"), FString::Printf(TEXT("Target pin '%s' not found on node '%s'"),
|
|
*TargetPinName, *TargetNodeId));
|
|
E->SetArrayField(TEXT("availablePins"), PinNames);
|
|
return JsonToString(E);
|
|
}
|
|
|
|
if (bDryRun)
|
|
{
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would connect %s.%s -> %s.%s in '%s'"),
|
|
*SourceNodeId, *SourcePinName, *TargetNodeId, *TargetPinName, *AssetDisplayName);
|
|
|
|
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
Result->SetBoolField(TEXT("success"), true);
|
|
Result->SetBoolField(TEXT("dryRun"), true);
|
|
Result->SetBoolField(TEXT("connected"), false);
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
return JsonToString(Result);
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Connecting %s.%s -> %s.%s in '%s'"),
|
|
*SourceNodeId, *SourcePinName, *TargetNodeId, *TargetPinName, *AssetDisplayName);
|
|
|
|
// Try to connect via the schema
|
|
const UEdGraphSchema* Schema = Graph->GetSchema();
|
|
if (!Schema)
|
|
{
|
|
return MakeErrorJson(TEXT("Material graph schema not found"));
|
|
}
|
|
|
|
bool bConnected = Schema->TryCreateConnection(SourcePin, TargetPin);
|
|
|
|
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
Result->SetBoolField(TEXT("success"), bConnected);
|
|
Result->SetBoolField(TEXT("connected"), bConnected);
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
|
|
if (!bConnected)
|
|
{
|
|
Result->SetStringField(TEXT("error"), FString::Printf(
|
|
TEXT("Cannot connect %s.%s to %s.%s — types may be incompatible"),
|
|
*SourceNodeId, *SourcePinName, *TargetNodeId, *TargetPinName));
|
|
return JsonToString(Result);
|
|
}
|
|
|
|
// Save
|
|
UObject* Asset = Material ? (UObject*)Material : (UObject*)MatFunc;
|
|
Asset->PreEditChange(nullptr);
|
|
Asset->PostEditChange();
|
|
bool bSaved = Material ? SaveMaterialPackage(Material) : SaveGenericPackage(MatFunc);
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
|
|
return JsonToString(Result);
|
|
}
|
|
|
|
// ============================================================
|
|
// HandleDisconnectMaterialPin — break connections on a pin in a material graph
|
|
// ============================================================
|
|
|
|
FString FBlueprintMCPServer::HandleDisconnectMaterialPin(const FString& Body)
|
|
{
|
|
TSharedPtr<FJsonObject> Json = ParseBodyJson(Body);
|
|
if (!Json.IsValid())
|
|
{
|
|
return MakeErrorJson(TEXT("Invalid JSON body"));
|
|
}
|
|
|
|
FString MaterialName = Json->GetStringField(TEXT("material"));
|
|
FString MaterialFunctionName = Json->GetStringField(TEXT("materialFunction"));
|
|
FString NodeId = Json->GetStringField(TEXT("nodeId"));
|
|
FString PinName = Json->GetStringField(TEXT("pinName"));
|
|
|
|
if (MaterialName.IsEmpty() && MaterialFunctionName.IsEmpty())
|
|
{
|
|
return MakeErrorJson(TEXT("Missing required field: 'material' or 'materialFunction'"));
|
|
}
|
|
if (NodeId.IsEmpty() || PinName.IsEmpty())
|
|
{
|
|
return MakeErrorJson(TEXT("Missing required fields: nodeId, pinName"));
|
|
}
|
|
|
|
bool bDryRun = false;
|
|
Json->TryGetBoolField(TEXT("dryRun"), bDryRun);
|
|
|
|
// Load material or material function
|
|
UMaterial* Material = nullptr;
|
|
UMaterialFunction* MatFunc = nullptr;
|
|
FString AssetDisplayName;
|
|
|
|
if (!MaterialFunctionName.IsEmpty())
|
|
{
|
|
FString LoadError;
|
|
MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError);
|
|
if (!MatFunc) return MakeErrorJson(LoadError);
|
|
AssetDisplayName = MatFunc->GetName();
|
|
}
|
|
else
|
|
{
|
|
FString LoadError;
|
|
Material = LoadMaterialByName(MaterialName, LoadError);
|
|
if (!Material) return MakeErrorJson(LoadError);
|
|
AssetDisplayName = Material->GetName();
|
|
}
|
|
|
|
if (Material) EnsureMaterialGraph(Material);
|
|
UEdGraph* Graph = Material ? (UEdGraph*)Material->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
|
|
if (!Graph)
|
|
{
|
|
return MakeErrorJson(FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName));
|
|
}
|
|
|
|
// Find node by GUID
|
|
UEdGraphNode* TargetNode = nullptr;
|
|
for (UEdGraphNode* Node : Graph->Nodes)
|
|
{
|
|
if (!Node) continue;
|
|
if (Node->NodeGuid.ToString() == NodeId)
|
|
{
|
|
TargetNode = Node;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!TargetNode)
|
|
{
|
|
return MakeErrorJson(FString::Printf(TEXT("Node '%s' not found in material graph"), *NodeId));
|
|
}
|
|
|
|
// Find pin
|
|
UEdGraphPin* Pin = TargetNode->FindPin(FName(*PinName));
|
|
if (!Pin)
|
|
{
|
|
TArray<TSharedPtr<FJsonValue>> PinNames;
|
|
for (UEdGraphPin* P : TargetNode->Pins)
|
|
{
|
|
if (P) PinNames.Add(MakeShared<FJsonValueString>(
|
|
FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(),
|
|
P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"))));
|
|
}
|
|
TSharedRef<FJsonObject> E = MakeShared<FJsonObject>();
|
|
E->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' not found on node '%s'"),
|
|
*PinName, *NodeId));
|
|
E->SetArrayField(TEXT("availablePins"), PinNames);
|
|
return JsonToString(E);
|
|
}
|
|
|
|
int32 BrokenCount = Pin->LinkedTo.Num();
|
|
|
|
if (bDryRun)
|
|
{
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would disconnect pin '%s' on node '%s' in '%s' (%d links)"),
|
|
*PinName, *NodeId, *AssetDisplayName, BrokenCount);
|
|
|
|
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
Result->SetBoolField(TEXT("success"), true);
|
|
Result->SetBoolField(TEXT("dryRun"), true);
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
Result->SetStringField(TEXT("nodeId"), NodeId);
|
|
Result->SetStringField(TEXT("pinName"), PinName);
|
|
Result->SetNumberField(TEXT("brokenLinkCount"), BrokenCount);
|
|
return JsonToString(Result);
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Disconnecting pin '%s' on node '%s' in '%s' (%d links)"),
|
|
*PinName, *NodeId, *AssetDisplayName, BrokenCount);
|
|
|
|
// Break all links
|
|
Pin->BreakAllPinLinks();
|
|
|
|
UObject* Asset = Material ? (UObject*)Material : (UObject*)MatFunc;
|
|
Asset->PreEditChange(nullptr);
|
|
Asset->PostEditChange();
|
|
|
|
// Save
|
|
bool bSaved = Material ? SaveMaterialPackage(Material) : SaveGenericPackage(MatFunc);
|
|
|
|
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
Result->SetBoolField(TEXT("success"), true);
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
Result->SetStringField(TEXT("nodeId"), NodeId);
|
|
Result->SetStringField(TEXT("pinName"), PinName);
|
|
Result->SetNumberField(TEXT("brokenLinkCount"), BrokenCount);
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
return JsonToString(Result);
|
|
}
|
|
|
|
// ============================================================
|
|
// HandleSetExpressionValue — set value on a material expression
|
|
// ============================================================
|
|
|
|
FString FBlueprintMCPServer::HandleSetExpressionValue(const FString& Body)
|
|
{
|
|
TSharedPtr<FJsonObject> Json = ParseBodyJson(Body);
|
|
if (!Json.IsValid())
|
|
{
|
|
return MakeErrorJson(TEXT("Invalid JSON body"));
|
|
}
|
|
|
|
FString MaterialName = Json->GetStringField(TEXT("material"));
|
|
FString MaterialFunctionName = Json->GetStringField(TEXT("materialFunction"));
|
|
FString NodeId = Json->GetStringField(TEXT("nodeId"));
|
|
|
|
if (MaterialName.IsEmpty() && MaterialFunctionName.IsEmpty())
|
|
{
|
|
return MakeErrorJson(TEXT("Missing required field: 'material' or 'materialFunction'"));
|
|
}
|
|
if (NodeId.IsEmpty())
|
|
{
|
|
return MakeErrorJson(TEXT("Missing required field: nodeId"));
|
|
}
|
|
|
|
if (!Json->HasField(TEXT("value")))
|
|
{
|
|
return MakeErrorJson(TEXT("Missing required field: value"));
|
|
}
|
|
|
|
// Load material or material function
|
|
UMaterial* Material = nullptr;
|
|
UMaterialFunction* MatFunc = nullptr;
|
|
FString AssetDisplayName;
|
|
|
|
if (!MaterialFunctionName.IsEmpty())
|
|
{
|
|
FString LoadError;
|
|
MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError);
|
|
if (!MatFunc) return MakeErrorJson(LoadError);
|
|
AssetDisplayName = MatFunc->GetName();
|
|
}
|
|
else
|
|
{
|
|
FString LoadError;
|
|
Material = LoadMaterialByName(MaterialName, LoadError);
|
|
if (!Material) return MakeErrorJson(LoadError);
|
|
AssetDisplayName = Material->GetName();
|
|
}
|
|
|
|
if (Material) EnsureMaterialGraph(Material);
|
|
UEdGraph* Graph = Material ? (UEdGraph*)Material->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
|
|
if (!Graph)
|
|
{
|
|
return MakeErrorJson(FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName));
|
|
}
|
|
|
|
// Find the node by GUID
|
|
UMaterialGraphNode* TargetMatNode = nullptr;
|
|
for (UEdGraphNode* Node : Graph->Nodes)
|
|
{
|
|
if (!Node) continue;
|
|
if (Node->NodeGuid.ToString() == NodeId)
|
|
{
|
|
TargetMatNode = Cast<UMaterialGraphNode>(Node);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!TargetMatNode)
|
|
{
|
|
return MakeErrorJson(FString::Printf(TEXT("Node '%s' not found in material graph"), *NodeId));
|
|
}
|
|
|
|
UMaterialExpression* Expr = TargetMatNode->MaterialExpression;
|
|
if (!Expr)
|
|
{
|
|
return MakeErrorJson(FString::Printf(TEXT("Node '%s' has no associated material expression"), *NodeId));
|
|
}
|
|
|
|
FString ExprType;
|
|
FString NewValueStr;
|
|
|
|
UObject* Asset = Material ? (UObject*)Material : (UObject*)MatFunc;
|
|
Asset->PreEditChange(nullptr);
|
|
|
|
// Handle based on expression type
|
|
if (UMaterialExpressionConstant* ConstExpr = Cast<UMaterialExpressionConstant>(Expr))
|
|
{
|
|
ExprType = TEXT("Constant");
|
|
double Value = Json->GetNumberField(TEXT("value"));
|
|
ConstExpr->R = (float)Value;
|
|
NewValueStr = FString::Printf(TEXT("%f"), Value);
|
|
}
|
|
else if (UMaterialExpressionConstant3Vector* C3Expr = Cast<UMaterialExpressionConstant3Vector>(Expr))
|
|
{
|
|
ExprType = TEXT("Constant3Vector");
|
|
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
|
|
if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid())
|
|
{
|
|
double R = 0, G = 0, B = 0;
|
|
(*ValueObj)->TryGetNumberField(TEXT("r"), R);
|
|
(*ValueObj)->TryGetNumberField(TEXT("g"), G);
|
|
(*ValueObj)->TryGetNumberField(TEXT("b"), B);
|
|
C3Expr->Constant = FLinearColor((float)R, (float)G, (float)B);
|
|
NewValueStr = FString::Printf(TEXT("(%f, %f, %f)"), R, G, B);
|
|
}
|
|
else
|
|
{
|
|
Asset->PostEditChange();
|
|
return MakeErrorJson(TEXT("Constant3Vector requires value as object {r, g, b}"));
|
|
}
|
|
}
|
|
else if (UMaterialExpressionConstant4Vector* C4Expr = Cast<UMaterialExpressionConstant4Vector>(Expr))
|
|
{
|
|
ExprType = TEXT("Constant4Vector");
|
|
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
|
|
if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid())
|
|
{
|
|
double R = 0, G = 0, B = 0, A = 1;
|
|
(*ValueObj)->TryGetNumberField(TEXT("r"), R);
|
|
(*ValueObj)->TryGetNumberField(TEXT("g"), G);
|
|
(*ValueObj)->TryGetNumberField(TEXT("b"), B);
|
|
(*ValueObj)->TryGetNumberField(TEXT("a"), A);
|
|
C4Expr->Constant = FLinearColor((float)R, (float)G, (float)B, (float)A);
|
|
NewValueStr = FString::Printf(TEXT("(%f, %f, %f, %f)"), R, G, B, A);
|
|
}
|
|
else
|
|
{
|
|
Asset->PostEditChange();
|
|
return MakeErrorJson(TEXT("Constant4Vector requires value as object {r, g, b, a}"));
|
|
}
|
|
}
|
|
else if (UMaterialExpressionScalarParameter* SPExpr = Cast<UMaterialExpressionScalarParameter>(Expr))
|
|
{
|
|
ExprType = TEXT("ScalarParameter");
|
|
double Value = Json->GetNumberField(TEXT("value"));
|
|
SPExpr->DefaultValue = (float)Value;
|
|
NewValueStr = FString::Printf(TEXT("%f"), Value);
|
|
|
|
FString ParamName;
|
|
if (Json->TryGetStringField(TEXT("parameterName"), ParamName) && !ParamName.IsEmpty())
|
|
{
|
|
SPExpr->ParameterName = FName(*ParamName);
|
|
}
|
|
}
|
|
else if (UMaterialExpressionVectorParameter* VPExpr = Cast<UMaterialExpressionVectorParameter>(Expr))
|
|
{
|
|
ExprType = TEXT("VectorParameter");
|
|
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
|
|
if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid())
|
|
{
|
|
double R = 0, G = 0, B = 0, A = 1;
|
|
(*ValueObj)->TryGetNumberField(TEXT("r"), R);
|
|
(*ValueObj)->TryGetNumberField(TEXT("g"), G);
|
|
(*ValueObj)->TryGetNumberField(TEXT("b"), B);
|
|
(*ValueObj)->TryGetNumberField(TEXT("a"), A);
|
|
VPExpr->DefaultValue = FLinearColor((float)R, (float)G, (float)B, (float)A);
|
|
NewValueStr = FString::Printf(TEXT("(%f, %f, %f, %f)"), R, G, B, A);
|
|
}
|
|
else
|
|
{
|
|
Asset->PostEditChange();
|
|
return MakeErrorJson(TEXT("VectorParameter requires value as object {r, g, b, a}"));
|
|
}
|
|
|
|
FString ParamName;
|
|
if (Json->TryGetStringField(TEXT("parameterName"), ParamName) && !ParamName.IsEmpty())
|
|
{
|
|
VPExpr->ParameterName = FName(*ParamName);
|
|
}
|
|
}
|
|
else if (UMaterialExpressionTextureCoordinate* TCExpr = Cast<UMaterialExpressionTextureCoordinate>(Expr))
|
|
{
|
|
ExprType = TEXT("TextureCoordinate");
|
|
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
|
|
if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid())
|
|
{
|
|
double CoordIndex = 0, UTiling = 1, VTiling = 1;
|
|
(*ValueObj)->TryGetNumberField(TEXT("coordinateIndex"), CoordIndex);
|
|
(*ValueObj)->TryGetNumberField(TEXT("uTiling"), UTiling);
|
|
(*ValueObj)->TryGetNumberField(TEXT("vTiling"), VTiling);
|
|
TCExpr->CoordinateIndex = (int32)CoordIndex;
|
|
TCExpr->UTiling = (float)UTiling;
|
|
TCExpr->VTiling = (float)VTiling;
|
|
NewValueStr = FString::Printf(TEXT("(index=%d, uTiling=%f, vTiling=%f)"), (int32)CoordIndex, UTiling, VTiling);
|
|
}
|
|
else
|
|
{
|
|
Asset->PostEditChange();
|
|
return MakeErrorJson(TEXT("TextureCoordinate requires value as object {coordinateIndex, uTiling, vTiling}"));
|
|
}
|
|
}
|
|
else if (UMaterialExpressionCustom* CustomExpr = Cast<UMaterialExpressionCustom>(Expr))
|
|
{
|
|
ExprType = TEXT("Custom");
|
|
FString Code;
|
|
if (Json->TryGetStringField(TEXT("code"), Code))
|
|
{
|
|
CustomExpr->Code = Code;
|
|
NewValueStr = FString::Printf(TEXT("Code: %d chars"), Code.Len());
|
|
}
|
|
else if (Json->HasField(TEXT("value")))
|
|
{
|
|
// Also accept code via value field as string
|
|
FString ValueStr = Json->GetStringField(TEXT("value"));
|
|
if (!ValueStr.IsEmpty())
|
|
{
|
|
CustomExpr->Code = ValueStr;
|
|
NewValueStr = FString::Printf(TEXT("Code: %d chars"), ValueStr.Len());
|
|
}
|
|
}
|
|
|
|
FString OutputTypeStr;
|
|
if (Json->TryGetStringField(TEXT("outputType"), OutputTypeStr) && !OutputTypeStr.IsEmpty())
|
|
{
|
|
int64 EnumVal = StaticEnum<ECustomMaterialOutputType>()->GetValueByNameString(OutputTypeStr);
|
|
if (EnumVal != INDEX_NONE)
|
|
{
|
|
CustomExpr->OutputType = (ECustomMaterialOutputType)EnumVal;
|
|
}
|
|
}
|
|
}
|
|
else if (UMaterialExpressionComponentMask* CMExpr = Cast<UMaterialExpressionComponentMask>(Expr))
|
|
{
|
|
ExprType = TEXT("ComponentMask");
|
|
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
|
|
if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid())
|
|
{
|
|
bool bR = false, bG = false, bB = false, bA = false;
|
|
(*ValueObj)->TryGetBoolField(TEXT("r"), bR);
|
|
(*ValueObj)->TryGetBoolField(TEXT("g"), bG);
|
|
(*ValueObj)->TryGetBoolField(TEXT("b"), bB);
|
|
(*ValueObj)->TryGetBoolField(TEXT("a"), bA);
|
|
CMExpr->R = bR ? 1 : 0;
|
|
CMExpr->G = bG ? 1 : 0;
|
|
CMExpr->B = bB ? 1 : 0;
|
|
CMExpr->A = bA ? 1 : 0;
|
|
NewValueStr = FString::Printf(TEXT("(R=%s, G=%s, B=%s, A=%s)"),
|
|
bR ? TEXT("true") : TEXT("false"),
|
|
bG ? TEXT("true") : TEXT("false"),
|
|
bB ? TEXT("true") : TEXT("false"),
|
|
bA ? TEXT("true") : TEXT("false"));
|
|
}
|
|
else
|
|
{
|
|
Asset->PostEditChange();
|
|
return MakeErrorJson(TEXT("ComponentMask requires value as object {r, g, b, a} (booleans)"));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Asset->PostEditChange();
|
|
return MakeErrorJson(FString::Printf(
|
|
TEXT("Expression type '%s' does not support direct value setting. Supported types: Constant, "
|
|
"Constant3Vector, Constant4Vector, ScalarParameter, VectorParameter, TextureCoordinate, "
|
|
"Custom, ComponentMask"),
|
|
*Expr->GetClass()->GetName()));
|
|
}
|
|
|
|
Asset->PostEditChange();
|
|
Asset->MarkPackageDirty();
|
|
|
|
// Save
|
|
bool bSaved = Material ? SaveMaterialPackage(Material) : SaveGenericPackage(MatFunc);
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set expression value on node '%s' (%s) in '%s': %s"),
|
|
*NodeId, *ExprType, *AssetDisplayName, *NewValueStr);
|
|
|
|
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
Result->SetBoolField(TEXT("success"), true);
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
Result->SetStringField(TEXT("nodeId"), NodeId);
|
|
Result->SetStringField(TEXT("expressionType"), ExprType);
|
|
Result->SetStringField(TEXT("newValue"), NewValueStr);
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
return JsonToString(Result);
|
|
}
|
|
|
|
// ============================================================
|
|
// HandleMoveMaterialExpression — reposition a material graph node
|
|
// ============================================================
|
|
|
|
FString FBlueprintMCPServer::HandleMoveMaterialExpression(const FString& Body)
|
|
{
|
|
TSharedPtr<FJsonObject> Json = ParseBodyJson(Body);
|
|
if (!Json.IsValid())
|
|
{
|
|
return MakeErrorJson(TEXT("Invalid JSON body"));
|
|
}
|
|
|
|
FString MaterialName = Json->GetStringField(TEXT("material"));
|
|
FString MaterialFunctionName = Json->GetStringField(TEXT("materialFunction"));
|
|
FString NodeId = Json->GetStringField(TEXT("nodeId"));
|
|
|
|
if (MaterialName.IsEmpty() && MaterialFunctionName.IsEmpty())
|
|
{
|
|
return MakeErrorJson(TEXT("Missing required field: 'material' or 'materialFunction'"));
|
|
}
|
|
if (NodeId.IsEmpty())
|
|
{
|
|
return MakeErrorJson(TEXT("Missing required field: nodeId"));
|
|
}
|
|
|
|
if (!Json->HasField(TEXT("posX")) || !Json->HasField(TEXT("posY")))
|
|
{
|
|
return MakeErrorJson(TEXT("Missing required fields: posX, posY"));
|
|
}
|
|
|
|
int32 PosX = (int32)Json->GetNumberField(TEXT("posX"));
|
|
int32 PosY = (int32)Json->GetNumberField(TEXT("posY"));
|
|
|
|
bool bDryRun = false;
|
|
Json->TryGetBoolField(TEXT("dryRun"), bDryRun);
|
|
|
|
// Load material or material function
|
|
UMaterial* Material = nullptr;
|
|
UMaterialFunction* MatFunc = nullptr;
|
|
FString AssetDisplayName;
|
|
|
|
if (!MaterialFunctionName.IsEmpty())
|
|
{
|
|
FString LoadError;
|
|
MatFunc = LoadMaterialFunctionByName(MaterialFunctionName, LoadError);
|
|
if (!MatFunc) return MakeErrorJson(LoadError);
|
|
AssetDisplayName = MatFunc->GetName();
|
|
}
|
|
else
|
|
{
|
|
FString LoadError;
|
|
Material = LoadMaterialByName(MaterialName, LoadError);
|
|
if (!Material) return MakeErrorJson(LoadError);
|
|
AssetDisplayName = Material->GetName();
|
|
}
|
|
|
|
if (Material) EnsureMaterialGraph(Material);
|
|
UEdGraph* Graph = Material ? (UEdGraph*)Material->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
|
|
if (!Graph)
|
|
{
|
|
return MakeErrorJson(FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName));
|
|
}
|
|
|
|
// Find node by GUID
|
|
UMaterialGraphNode* TargetMatNode = nullptr;
|
|
for (UEdGraphNode* Node : Graph->Nodes)
|
|
{
|
|
if (!Node) continue;
|
|
if (Node->NodeGuid.ToString() == NodeId)
|
|
{
|
|
TargetMatNode = Cast<UMaterialGraphNode>(Node);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!TargetMatNode)
|
|
{
|
|
return MakeErrorJson(FString::Printf(TEXT("Node '%s' not found in material graph"), *NodeId));
|
|
}
|
|
|
|
if (bDryRun)
|
|
{
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would move node '%s' to (%d, %d) in '%s'"),
|
|
*NodeId, PosX, PosY, *AssetDisplayName);
|
|
|
|
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
Result->SetBoolField(TEXT("success"), true);
|
|
Result->SetBoolField(TEXT("dryRun"), true);
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
Result->SetStringField(TEXT("nodeId"), NodeId);
|
|
Result->SetNumberField(TEXT("posX"), PosX);
|
|
Result->SetNumberField(TEXT("posY"), PosY);
|
|
return JsonToString(Result);
|
|
}
|
|
|
|
// Set position on the graph node
|
|
TargetMatNode->NodePosX = PosX;
|
|
TargetMatNode->NodePosY = PosY;
|
|
|
|
// Also update the underlying expression position
|
|
if (TargetMatNode->MaterialExpression)
|
|
{
|
|
TargetMatNode->MaterialExpression->MaterialExpressionEditorX = PosX;
|
|
TargetMatNode->MaterialExpression->MaterialExpressionEditorY = PosY;
|
|
}
|
|
|
|
UObject* Asset = Material ? (UObject*)Material : (UObject*)MatFunc;
|
|
Asset->PreEditChange(nullptr);
|
|
Asset->PostEditChange();
|
|
|
|
// Save
|
|
bool bSaved = Material ? SaveMaterialPackage(Material) : SaveGenericPackage(MatFunc);
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Moved node '%s' to (%d, %d) in '%s' (saved: %s)"),
|
|
*NodeId, PosX, PosY, *AssetDisplayName, bSaved ? TEXT("true") : TEXT("false"));
|
|
|
|
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
Result->SetBoolField(TEXT("success"), true);
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
Result->SetStringField(TEXT("nodeId"), NodeId);
|
|
Result->SetNumberField(TEXT("posX"), PosX);
|
|
Result->SetNumberField(TEXT("posY"), PosY);
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
return JsonToString(Result);
|
|
}
|
|
|
|
// ============================================================
|
|
// Phase 4: Create Material Function
|
|
// ============================================================
|
|
|
|
// ============================================================
|
|
// HandleCreateMaterialFunction — create a new UMaterialFunction asset
|
|
// ============================================================
|
|
|
|
FString FBlueprintMCPServer::HandleCreateMaterialFunction(const FString& Body)
|
|
{
|
|
TSharedPtr<FJsonObject> Json = ParseBodyJson(Body);
|
|
if (!Json.IsValid())
|
|
{
|
|
return MakeErrorJson(TEXT("Invalid JSON body"));
|
|
}
|
|
|
|
FString Name = Json->GetStringField(TEXT("name"));
|
|
FString PackagePath = Json->GetStringField(TEXT("packagePath"));
|
|
|
|
if (Name.IsEmpty() || PackagePath.IsEmpty())
|
|
{
|
|
return MakeErrorJson(TEXT("Missing required fields: name, packagePath"));
|
|
}
|
|
|
|
if (!PackagePath.StartsWith(TEXT("/Game")))
|
|
{
|
|
return MakeErrorJson(TEXT("packagePath must start with '/Game'"));
|
|
}
|
|
|
|
// Check if asset already exists
|
|
FString FullAssetPath = PackagePath / Name;
|
|
if (FindMaterialFunctionAsset(Name) || FindMaterialFunctionAsset(FullAssetPath))
|
|
{
|
|
return MakeErrorJson(FString::Printf(
|
|
TEXT("Material Function '%s' already exists. Use a different name or delete the existing asset first."),
|
|
*Name));
|
|
}
|
|
|
|
FString Description;
|
|
Json->TryGetStringField(TEXT("description"), Description);
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Material Function '%s' in '%s'"), *Name, *PackagePath);
|
|
|
|
// Create via IAssetTools + factory
|
|
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
|
|
UMaterialFunctionFactoryNew* Factory = NewObject<UMaterialFunctionFactoryNew>();
|
|
UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterialFunction::StaticClass(), Factory);
|
|
|
|
if (!NewAsset)
|
|
{
|
|
return MakeErrorJson(FString::Printf(TEXT("Failed to create Material Function '%s' in '%s'"), *Name, *PackagePath));
|
|
}
|
|
|
|
UMaterialFunction* MF = Cast<UMaterialFunction>(NewAsset);
|
|
if (!MF)
|
|
{
|
|
return MakeErrorJson(TEXT("Created asset is not a UMaterialFunction"));
|
|
}
|
|
|
|
// Set optional description
|
|
if (!Description.IsEmpty())
|
|
{
|
|
MF->Description = Description;
|
|
}
|
|
|
|
// Save
|
|
bool bSaved = SaveGenericPackage(MF);
|
|
|
|
// Refresh asset cache
|
|
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
|
|
AllMaterialFunctionAssets.Empty();
|
|
ARM.Get().GetAssetsByClass(UMaterialFunction::StaticClass()->GetClassPathName(), AllMaterialFunctionAssets, false);
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Material Function '%s' (saved: %s)"),
|
|
*Name, bSaved ? TEXT("true") : TEXT("false"));
|
|
|
|
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
Result->SetBoolField(TEXT("success"), true);
|
|
Result->SetStringField(TEXT("name"), Name);
|
|
Result->SetStringField(TEXT("path"), MF->GetPathName());
|
|
if (!Description.IsEmpty())
|
|
{
|
|
Result->SetStringField(TEXT("description"), Description);
|
|
}
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
return JsonToString(Result);
|
|
}
|
|
|
|
// ============================================================
|
|
// Phase 5: Material Snapshot/Diff/Restore
|
|
// ============================================================
|
|
|
|
// ============================================================
|
|
// HandleSnapshotMaterialGraph — snapshot current material graph state
|
|
// ============================================================
|
|
|
|
FString FBlueprintMCPServer::HandleSnapshotMaterialGraph(const FString& Body)
|
|
{
|
|
TSharedPtr<FJsonObject> Json = ParseBodyJson(Body);
|
|
if (!Json.IsValid())
|
|
{
|
|
return MakeErrorJson(TEXT("Invalid JSON body"));
|
|
}
|
|
|
|
FString MaterialName = Json->GetStringField(TEXT("material"));
|
|
if (MaterialName.IsEmpty())
|
|
{
|
|
return MakeErrorJson(TEXT("Missing required field: material"));
|
|
}
|
|
|
|
// Load material
|
|
FString LoadError;
|
|
UMaterial* Material = LoadMaterialByName(MaterialName, LoadError);
|
|
if (!Material)
|
|
{
|
|
return MakeErrorJson(LoadError);
|
|
}
|
|
|
|
EnsureMaterialGraph(Material);
|
|
if (!Material->MaterialGraph)
|
|
{
|
|
return MakeErrorJson(FString::Printf(TEXT("Material '%s' has no material graph"), *MaterialName));
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating snapshot for material '%s'"), *MaterialName);
|
|
|
|
// Build the snapshot
|
|
FGraphSnapshot Snapshot;
|
|
Snapshot.SnapshotId = GenerateSnapshotId(MaterialName);
|
|
Snapshot.BlueprintName = Material->GetName();
|
|
Snapshot.BlueprintPath = Material->GetPathName();
|
|
Snapshot.CreatedAt = FDateTime::Now();
|
|
|
|
// Capture the material graph
|
|
FGraphSnapshotData GraphData = CaptureGraphSnapshot(Material->MaterialGraph);
|
|
|
|
int32 NodeCount = GraphData.Nodes.Num();
|
|
int32 ConnectionCount = GraphData.Connections.Num();
|
|
|
|
Snapshot.Graphs.Add(TEXT("MaterialGraph"), MoveTemp(GraphData));
|
|
|
|
// Store in material snapshots (separate from blueprint snapshots)
|
|
MaterialSnapshots.Add(Snapshot.SnapshotId, Snapshot);
|
|
|
|
// Prune old material snapshots
|
|
while (MaterialSnapshots.Num() > MaxSnapshots)
|
|
{
|
|
FString OldestId;
|
|
FDateTime OldestTime = FDateTime::MaxValue();
|
|
|
|
for (const auto& Pair : MaterialSnapshots)
|
|
{
|
|
if (Pair.Value.CreatedAt < OldestTime)
|
|
{
|
|
OldestTime = Pair.Value.CreatedAt;
|
|
OldestId = Pair.Key;
|
|
}
|
|
}
|
|
|
|
if (!OldestId.IsEmpty())
|
|
{
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Pruning old material snapshot '%s'"), *OldestId);
|
|
MaterialSnapshots.Remove(OldestId);
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Save to disk
|
|
SaveSnapshotToDisk(Snapshot.SnapshotId, Snapshot);
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Material snapshot '%s' created with %d nodes, %d connections"),
|
|
*Snapshot.SnapshotId, NodeCount, ConnectionCount);
|
|
|
|
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
Result->SetStringField(TEXT("status"), TEXT("ok"));
|
|
Result->SetStringField(TEXT("snapshotId"), Snapshot.SnapshotId);
|
|
Result->SetStringField(TEXT("material"), Material->GetName());
|
|
Result->SetNumberField(TEXT("nodeCount"), NodeCount);
|
|
Result->SetNumberField(TEXT("connectionCount"), ConnectionCount);
|
|
return JsonToString(Result);
|
|
}
|
|
|
|
// ============================================================
|
|
// HandleDiffMaterialGraph — diff current material graph against snapshot
|
|
// ============================================================
|
|
|
|
FString FBlueprintMCPServer::HandleDiffMaterialGraph(const FString& Body)
|
|
{
|
|
TSharedPtr<FJsonObject> Json = ParseBodyJson(Body);
|
|
if (!Json.IsValid())
|
|
{
|
|
return MakeErrorJson(TEXT("Invalid JSON body"));
|
|
}
|
|
|
|
FString MaterialName = Json->GetStringField(TEXT("material"));
|
|
FString SnapshotId = Json->GetStringField(TEXT("snapshotId"));
|
|
|
|
if (MaterialName.IsEmpty() || SnapshotId.IsEmpty())
|
|
{
|
|
return MakeErrorJson(TEXT("Missing required fields: material, snapshotId"));
|
|
}
|
|
|
|
// Load snapshot from material snapshots (memory or disk)
|
|
FGraphSnapshot* SnapshotPtr = MaterialSnapshots.Find(SnapshotId);
|
|
FGraphSnapshot LoadedSnapshot;
|
|
if (!SnapshotPtr)
|
|
{
|
|
if (!LoadSnapshotFromDisk(SnapshotId, LoadedSnapshot))
|
|
{
|
|
return MakeErrorJson(FString::Printf(TEXT("Snapshot '%s' not found in memory or on disk"), *SnapshotId));
|
|
}
|
|
SnapshotPtr = &LoadedSnapshot;
|
|
}
|
|
|
|
// Load material
|
|
FString LoadError;
|
|
UMaterial* Material = LoadMaterialByName(MaterialName, LoadError);
|
|
if (!Material)
|
|
{
|
|
return MakeErrorJson(LoadError);
|
|
}
|
|
|
|
EnsureMaterialGraph(Material);
|
|
if (!Material->MaterialGraph)
|
|
{
|
|
return MakeErrorJson(FString::Printf(TEXT("Material '%s' has no material graph"), *MaterialName));
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Diffing material '%s' against snapshot '%s'"), *MaterialName, *SnapshotId);
|
|
|
|
// Capture current state
|
|
FGraphSnapshotData CurrentData = CaptureGraphSnapshot(Material->MaterialGraph);
|
|
|
|
auto MakeConnKey = [](const FString& SrcGuid, const FString& SrcPin, const FString& TgtGuid, const FString& TgtPin) -> FString
|
|
{
|
|
return FString::Printf(TEXT("%s|%s|%s|%s"), *SrcGuid, *SrcPin, *TgtGuid, *TgtPin);
|
|
};
|
|
|
|
TArray<TSharedPtr<FJsonValue>> SeveredArr;
|
|
TArray<TSharedPtr<FJsonValue>> NewConnsArr;
|
|
TArray<TSharedPtr<FJsonValue>> MissingNodesArr;
|
|
|
|
// Process the MaterialGraph from the snapshot
|
|
const FGraphSnapshotData* SnapDataPtr = SnapshotPtr->Graphs.Find(TEXT("MaterialGraph"));
|
|
if (!SnapDataPtr)
|
|
{
|
|
return MakeErrorJson(TEXT("Snapshot does not contain a MaterialGraph"));
|
|
}
|
|
|
|
const FGraphSnapshotData& SnapData = *SnapDataPtr;
|
|
|
|
// Build node lookup maps
|
|
TMap<FString, const FNodeRecord*> SnapNodeLookup;
|
|
for (const FNodeRecord& NR : SnapData.Nodes)
|
|
{
|
|
SnapNodeLookup.Add(NR.NodeGuid, &NR);
|
|
}
|
|
|
|
TMap<FString, const FNodeRecord*> CurNodeLookup;
|
|
for (const FNodeRecord& NR : CurrentData.Nodes)
|
|
{
|
|
CurNodeLookup.Add(NR.NodeGuid, &NR);
|
|
}
|
|
|
|
// Build connection sets
|
|
TSet<FString> SnapConnSet;
|
|
for (const FPinConnectionRecord& Conn : SnapData.Connections)
|
|
{
|
|
SnapConnSet.Add(MakeConnKey(Conn.SourceNodeGuid, Conn.SourcePinName, Conn.TargetNodeGuid, Conn.TargetPinName));
|
|
}
|
|
|
|
TSet<FString> CurrentConnSet;
|
|
for (const FPinConnectionRecord& Conn : CurrentData.Connections)
|
|
{
|
|
CurrentConnSet.Add(MakeConnKey(Conn.SourceNodeGuid, Conn.SourcePinName, Conn.TargetNodeGuid, Conn.TargetPinName));
|
|
}
|
|
|
|
// Find severed connections: in snapshot but not in current
|
|
for (const FPinConnectionRecord& Conn : SnapData.Connections)
|
|
{
|
|
FString Key = MakeConnKey(Conn.SourceNodeGuid, Conn.SourcePinName, Conn.TargetNodeGuid, Conn.TargetPinName);
|
|
if (!CurrentConnSet.Contains(Key))
|
|
{
|
|
TSharedRef<FJsonObject> SJ = MakeShared<FJsonObject>();
|
|
SJ->SetStringField(TEXT("sourceNodeGuid"), Conn.SourceNodeGuid);
|
|
SJ->SetStringField(TEXT("sourcePinName"), Conn.SourcePinName);
|
|
SJ->SetStringField(TEXT("targetNodeGuid"), Conn.TargetNodeGuid);
|
|
SJ->SetStringField(TEXT("targetPinName"), Conn.TargetPinName);
|
|
|
|
const FNodeRecord** SrcRec = SnapNodeLookup.Find(Conn.SourceNodeGuid);
|
|
if (SrcRec) SJ->SetStringField(TEXT("sourceNodeName"), (*SrcRec)->NodeTitle);
|
|
const FNodeRecord** TgtRec = SnapNodeLookup.Find(Conn.TargetNodeGuid);
|
|
if (TgtRec) SJ->SetStringField(TEXT("targetNodeName"), (*TgtRec)->NodeTitle);
|
|
|
|
SeveredArr.Add(MakeShared<FJsonValueObject>(SJ));
|
|
}
|
|
}
|
|
|
|
// Find new connections: in current but not in snapshot
|
|
for (const FPinConnectionRecord& Conn : CurrentData.Connections)
|
|
{
|
|
FString Key = MakeConnKey(Conn.SourceNodeGuid, Conn.SourcePinName, Conn.TargetNodeGuid, Conn.TargetPinName);
|
|
if (!SnapConnSet.Contains(Key))
|
|
{
|
|
TSharedRef<FJsonObject> NJ = MakeShared<FJsonObject>();
|
|
NJ->SetStringField(TEXT("sourceNodeGuid"), Conn.SourceNodeGuid);
|
|
NJ->SetStringField(TEXT("sourcePinName"), Conn.SourcePinName);
|
|
NJ->SetStringField(TEXT("targetNodeGuid"), Conn.TargetNodeGuid);
|
|
NJ->SetStringField(TEXT("targetPinName"), Conn.TargetPinName);
|
|
|
|
const FNodeRecord** SrcRec = CurNodeLookup.Find(Conn.SourceNodeGuid);
|
|
if (SrcRec) NJ->SetStringField(TEXT("sourceNodeName"), (*SrcRec)->NodeTitle);
|
|
const FNodeRecord** TgtRec = CurNodeLookup.Find(Conn.TargetNodeGuid);
|
|
if (TgtRec) NJ->SetStringField(TEXT("targetNodeName"), (*TgtRec)->NodeTitle);
|
|
|
|
NewConnsArr.Add(MakeShared<FJsonValueObject>(NJ));
|
|
}
|
|
}
|
|
|
|
// Find missing nodes: in snapshot but not in current
|
|
for (const FNodeRecord& SnapNode : SnapData.Nodes)
|
|
{
|
|
const FNodeRecord** CurNodePtr = CurNodeLookup.Find(SnapNode.NodeGuid);
|
|
if (!CurNodePtr)
|
|
{
|
|
TSharedRef<FJsonObject> MJ = MakeShared<FJsonObject>();
|
|
MJ->SetStringField(TEXT("nodeGuid"), SnapNode.NodeGuid);
|
|
MJ->SetStringField(TEXT("nodeClass"), SnapNode.NodeClass);
|
|
MJ->SetStringField(TEXT("nodeTitle"), SnapNode.NodeTitle);
|
|
MissingNodesArr.Add(MakeShared<FJsonValueObject>(MJ));
|
|
}
|
|
}
|
|
|
|
// Build result
|
|
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
Result->SetStringField(TEXT("status"), TEXT("ok"));
|
|
Result->SetStringField(TEXT("material"), Material->GetName());
|
|
Result->SetStringField(TEXT("snapshotId"), SnapshotId);
|
|
Result->SetArrayField(TEXT("severedConnections"), SeveredArr);
|
|
Result->SetArrayField(TEXT("newConnections"), NewConnsArr);
|
|
Result->SetArrayField(TEXT("missingNodes"), MissingNodesArr);
|
|
|
|
TSharedRef<FJsonObject> Summary = MakeShared<FJsonObject>();
|
|
Summary->SetNumberField(TEXT("severedConnections"), SeveredArr.Num());
|
|
Summary->SetNumberField(TEXT("newConnections"), NewConnsArr.Num());
|
|
Summary->SetNumberField(TEXT("missingNodes"), MissingNodesArr.Num());
|
|
Result->SetObjectField(TEXT("summary"), Summary);
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Material diff complete — %d severed, %d new, %d missing nodes"),
|
|
SeveredArr.Num(), NewConnsArr.Num(), MissingNodesArr.Num());
|
|
|
|
return JsonToString(Result);
|
|
}
|
|
|
|
// ============================================================
|
|
// HandleRestoreMaterialGraph — restore material graph connections from snapshot
|
|
// ============================================================
|
|
|
|
FString FBlueprintMCPServer::HandleRestoreMaterialGraph(const FString& Body)
|
|
{
|
|
TSharedPtr<FJsonObject> Json = ParseBodyJson(Body);
|
|
if (!Json.IsValid())
|
|
{
|
|
return MakeErrorJson(TEXT("Invalid JSON body"));
|
|
}
|
|
|
|
FString MaterialName = Json->GetStringField(TEXT("material"));
|
|
FString SnapshotId = Json->GetStringField(TEXT("snapshotId"));
|
|
|
|
if (MaterialName.IsEmpty() || SnapshotId.IsEmpty())
|
|
{
|
|
return MakeErrorJson(TEXT("Missing required fields: material, snapshotId"));
|
|
}
|
|
|
|
bool bDryRun = false;
|
|
Json->TryGetBoolField(TEXT("dryRun"), bDryRun);
|
|
|
|
// Load snapshot from material snapshots (memory or disk)
|
|
FGraphSnapshot* SnapshotPtr = MaterialSnapshots.Find(SnapshotId);
|
|
FGraphSnapshot LoadedSnapshot;
|
|
if (!SnapshotPtr)
|
|
{
|
|
if (!LoadSnapshotFromDisk(SnapshotId, LoadedSnapshot))
|
|
{
|
|
return MakeErrorJson(FString::Printf(TEXT("Snapshot '%s' not found in memory or on disk"), *SnapshotId));
|
|
}
|
|
SnapshotPtr = &LoadedSnapshot;
|
|
}
|
|
|
|
// Load material
|
|
FString LoadError;
|
|
UMaterial* Material = LoadMaterialByName(MaterialName, LoadError);
|
|
if (!Material)
|
|
{
|
|
return MakeErrorJson(LoadError);
|
|
}
|
|
|
|
EnsureMaterialGraph(Material);
|
|
if (!Material->MaterialGraph)
|
|
{
|
|
return MakeErrorJson(FString::Printf(TEXT("Material '%s' has no material graph"), *MaterialName));
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Restoring material connections from snapshot '%s' for material '%s' (dryRun=%s)"),
|
|
*SnapshotId, *MaterialName, bDryRun ? TEXT("true") : TEXT("false"));
|
|
|
|
// Capture current state for comparison
|
|
FGraphSnapshotData CurrentData = CaptureGraphSnapshot(Material->MaterialGraph);
|
|
|
|
auto MakeConnKey = [](const FString& SrcGuid, const FString& SrcPin, const FString& TgtGuid, const FString& TgtPin) -> FString
|
|
{
|
|
return FString::Printf(TEXT("%s|%s|%s|%s"), *SrcGuid, *SrcPin, *TgtGuid, *TgtPin);
|
|
};
|
|
|
|
// Build current connection set
|
|
TSet<FString> CurrentConnSet;
|
|
for (const FPinConnectionRecord& Conn : CurrentData.Connections)
|
|
{
|
|
CurrentConnSet.Add(MakeConnKey(Conn.SourceNodeGuid, Conn.SourcePinName, Conn.TargetNodeGuid, Conn.TargetPinName));
|
|
}
|
|
|
|
// Build node lookup for the material graph
|
|
TMap<FString, UEdGraphNode*> NodeLookup;
|
|
for (UEdGraphNode* Node : Material->MaterialGraph->Nodes)
|
|
{
|
|
if (Node)
|
|
{
|
|
NodeLookup.Add(Node->NodeGuid.ToString(), Node);
|
|
}
|
|
}
|
|
|
|
const FGraphSnapshotData* SnapDataPtr = SnapshotPtr->Graphs.Find(TEXT("MaterialGraph"));
|
|
if (!SnapDataPtr)
|
|
{
|
|
return MakeErrorJson(TEXT("Snapshot does not contain a MaterialGraph"));
|
|
}
|
|
|
|
int32 Reconnected = 0;
|
|
int32 Failed = 0;
|
|
TArray<TSharedPtr<FJsonValue>> DetailsArr;
|
|
|
|
for (const FPinConnectionRecord& Conn : SnapDataPtr->Connections)
|
|
{
|
|
FString Key = MakeConnKey(Conn.SourceNodeGuid, Conn.SourcePinName, Conn.TargetNodeGuid, Conn.TargetPinName);
|
|
if (CurrentConnSet.Contains(Key)) continue; // Still connected, skip
|
|
|
|
TSharedRef<FJsonObject> Detail = MakeShared<FJsonObject>();
|
|
Detail->SetStringField(TEXT("sourcePinName"), Conn.SourcePinName);
|
|
Detail->SetStringField(TEXT("targetPinName"), Conn.TargetPinName);
|
|
Detail->SetStringField(TEXT("sourceNodeGuid"), Conn.SourceNodeGuid);
|
|
Detail->SetStringField(TEXT("targetNodeGuid"), Conn.TargetNodeGuid);
|
|
|
|
// Find source and target nodes
|
|
UEdGraphNode** SourceNodePtr = NodeLookup.Find(Conn.SourceNodeGuid);
|
|
UEdGraphNode** TargetNodePtr = NodeLookup.Find(Conn.TargetNodeGuid);
|
|
|
|
if (!SourceNodePtr || !*SourceNodePtr)
|
|
{
|
|
Detail->SetStringField(TEXT("result"), TEXT("failed"));
|
|
Detail->SetStringField(TEXT("reason"), FString::Printf(TEXT("Source node '%s' no longer exists"), *Conn.SourceNodeGuid));
|
|
Failed++;
|
|
DetailsArr.Add(MakeShared<FJsonValueObject>(Detail));
|
|
continue;
|
|
}
|
|
if (!TargetNodePtr || !*TargetNodePtr)
|
|
{
|
|
Detail->SetStringField(TEXT("result"), TEXT("failed"));
|
|
Detail->SetStringField(TEXT("reason"), FString::Printf(TEXT("Target node '%s' no longer exists"), *Conn.TargetNodeGuid));
|
|
Failed++;
|
|
DetailsArr.Add(MakeShared<FJsonValueObject>(Detail));
|
|
continue;
|
|
}
|
|
|
|
UEdGraphNode* SourceNode = *SourceNodePtr;
|
|
UEdGraphNode* TargetNode = *TargetNodePtr;
|
|
|
|
Detail->SetStringField(TEXT("sourceNodeName"), SourceNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
|
|
Detail->SetStringField(TEXT("targetNodeName"), TargetNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
|
|
|
|
// Find pins
|
|
UEdGraphPin* SourcePin = SourceNode->FindPin(FName(*Conn.SourcePinName));
|
|
UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*Conn.TargetPinName));
|
|
|
|
if (!SourcePin)
|
|
{
|
|
Detail->SetStringField(TEXT("result"), TEXT("failed"));
|
|
Detail->SetStringField(TEXT("reason"), FString::Printf(TEXT("Source pin '%s' not found on node"), *Conn.SourcePinName));
|
|
Failed++;
|
|
DetailsArr.Add(MakeShared<FJsonValueObject>(Detail));
|
|
continue;
|
|
}
|
|
if (!TargetPin)
|
|
{
|
|
Detail->SetStringField(TEXT("result"), TEXT("failed"));
|
|
Detail->SetStringField(TEXT("reason"), FString::Printf(TEXT("Target pin '%s' not found on node"), *Conn.TargetPinName));
|
|
Failed++;
|
|
DetailsArr.Add(MakeShared<FJsonValueObject>(Detail));
|
|
continue;
|
|
}
|
|
|
|
if (bDryRun)
|
|
{
|
|
Detail->SetStringField(TEXT("result"), TEXT("would_reconnect"));
|
|
Reconnected++;
|
|
DetailsArr.Add(MakeShared<FJsonValueObject>(Detail));
|
|
continue;
|
|
}
|
|
|
|
// Try to reconnect via the schema
|
|
const UEdGraphSchema* Schema = Material->MaterialGraph->GetSchema();
|
|
if (!Schema)
|
|
{
|
|
Detail->SetStringField(TEXT("result"), TEXT("failed"));
|
|
Detail->SetStringField(TEXT("reason"), TEXT("Material graph schema not found"));
|
|
Failed++;
|
|
DetailsArr.Add(MakeShared<FJsonValueObject>(Detail));
|
|
continue;
|
|
}
|
|
|
|
bool bConnected = Schema->TryCreateConnection(SourcePin, TargetPin);
|
|
if (bConnected)
|
|
{
|
|
Detail->SetStringField(TEXT("result"), TEXT("reconnected"));
|
|
Reconnected++;
|
|
}
|
|
else
|
|
{
|
|
Detail->SetStringField(TEXT("result"), TEXT("failed"));
|
|
Detail->SetStringField(TEXT("reason"), TEXT("TryCreateConnection failed — types may be incompatible"));
|
|
Failed++;
|
|
}
|
|
DetailsArr.Add(MakeShared<FJsonValueObject>(Detail));
|
|
}
|
|
|
|
// Save if not dry run and we reconnected something
|
|
bool bSaved = false;
|
|
if (!bDryRun && Reconnected > 0)
|
|
{
|
|
Material->PreEditChange(nullptr);
|
|
Material->PostEditChange();
|
|
bSaved = SaveMaterialPackage(Material);
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Material restore complete — %d reconnected, %d failed, saved=%s"),
|
|
Reconnected, Failed, bSaved ? TEXT("true") : TEXT("false"));
|
|
|
|
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
Result->SetStringField(TEXT("status"), TEXT("ok"));
|
|
Result->SetStringField(TEXT("material"), Material->GetName());
|
|
Result->SetStringField(TEXT("snapshotId"), SnapshotId);
|
|
Result->SetNumberField(TEXT("reconnected"), Reconnected);
|
|
Result->SetNumberField(TEXT("failed"), Failed);
|
|
Result->SetArrayField(TEXT("details"), DetailsArr);
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
Result->SetBoolField(TEXT("dryRun"), bDryRun);
|
|
return JsonToString(Result);
|
|
}
|