1594 lines
55 KiB
C++
1594 lines
55 KiB
C++
#pragma once
|
|
|
|
#include "CoreMinimal.h"
|
|
#include "MCPHandler.h"
|
|
#include "MCPAssetFinder.h"
|
|
#include "MCPUtils.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"
|
|
#include "MCPHandlers_MaterialMutation.generated.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
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="create_material_asset"))
|
|
class UMCPHandler_CreateMaterial : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Name for the new material asset"))
|
|
FString Name;
|
|
|
|
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
|
|
FString PackagePath;
|
|
|
|
UPROPERTY(meta=(Optional, Description="Material domain: Surface, DeferredDecal, LightFunction, Volume, PostProcess, UI"))
|
|
FString Domain;
|
|
|
|
UPROPERTY(meta=(Optional, Description="Blend mode: Opaque, Masked, Translucent, Additive, Modulate"))
|
|
FString BlendMode;
|
|
|
|
UPROPERTY(meta=(Optional, Description="Whether the material is two-sided"))
|
|
bool TwoSided = false;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Create a new UMaterial asset with optional domain, blend mode, and two-sided settings.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
if (!PackagePath.StartsWith(TEXT("/Game")))
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'"));
|
|
}
|
|
|
|
// Check if asset already exists
|
|
MCPAssets<UMaterial> ExistCheck;
|
|
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
|
|
|
|
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 MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create Material '%s' in '%s'"), *Name, *PackagePath));
|
|
}
|
|
|
|
UMaterial* MaterialObj = Cast<UMaterial>(NewAsset);
|
|
if (!MaterialObj)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UMaterial"));
|
|
}
|
|
|
|
// Apply optional properties
|
|
bool bHasTwoSided = Json->HasField(TEXT("twoSided"));
|
|
|
|
MaterialObj->PreEditChange(nullptr);
|
|
|
|
// Parse domain
|
|
if (!Domain.IsEmpty())
|
|
{
|
|
if (Domain == TEXT("Surface"))
|
|
MaterialObj->MaterialDomain = MD_Surface;
|
|
else if (Domain == TEXT("DeferredDecal"))
|
|
MaterialObj->MaterialDomain = MD_DeferredDecal;
|
|
else if (Domain == TEXT("LightFunction"))
|
|
MaterialObj->MaterialDomain = MD_LightFunction;
|
|
else if (Domain == TEXT("Volume"))
|
|
MaterialObj->MaterialDomain = MD_Volume;
|
|
else if (Domain == TEXT("PostProcess"))
|
|
MaterialObj->MaterialDomain = MD_PostProcess;
|
|
else if (Domain == TEXT("UI"))
|
|
MaterialObj->MaterialDomain = MD_UI;
|
|
}
|
|
|
|
// Parse blend mode
|
|
if (!BlendMode.IsEmpty())
|
|
{
|
|
if (BlendMode == TEXT("Opaque"))
|
|
MaterialObj->BlendMode = BLEND_Opaque;
|
|
else if (BlendMode == TEXT("Masked"))
|
|
MaterialObj->BlendMode = BLEND_Masked;
|
|
else if (BlendMode == TEXT("Translucent"))
|
|
MaterialObj->BlendMode = BLEND_Translucent;
|
|
else if (BlendMode == TEXT("Additive"))
|
|
MaterialObj->BlendMode = BLEND_Additive;
|
|
else if (BlendMode == TEXT("Modulate"))
|
|
MaterialObj->BlendMode = BLEND_Modulate;
|
|
}
|
|
|
|
if (bHasTwoSided)
|
|
{
|
|
MaterialObj->TwoSided = TwoSided;
|
|
}
|
|
|
|
MaterialObj->PostEditChange();
|
|
|
|
// Save
|
|
bool bSaved = MCPUtils::SaveMaterialPackage(MaterialObj);
|
|
|
|
|
|
// Map domain back to string for response
|
|
auto DomainToString = [](EMaterialDomain InDomain) -> FString
|
|
{
|
|
switch (InDomain)
|
|
{
|
|
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"));
|
|
|
|
Result->SetStringField(TEXT("path"), MaterialObj->GetPathName());
|
|
Result->SetStringField(TEXT("domain"), DomainToString(MaterialObj->MaterialDomain));
|
|
Result->SetStringField(TEXT("blendMode"), BlendModeToString(MaterialObj->BlendMode));
|
|
Result->SetBoolField(TEXT("twoSided"), MaterialObj->TwoSided != 0);
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="set_material_property"))
|
|
class UMCPHandler_SetMaterialProperty : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Material name or package path"))
|
|
FString Material;
|
|
|
|
UPROPERTY(meta=(Description="Property name to set (domain, blendMode, twoSided, shadingModel, opacity, opacityMaskClipValue, bUsedWithSkeletalMesh, bUsedWithMorphTargets, bUsedWithNiagaraSprites, ditheredLODTransition, bAllowNegativeEmissiveColor)"))
|
|
FString Property;
|
|
|
|
UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it"))
|
|
bool DryRun = false;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Set a top-level material property such as domain, blend mode, shading model, or usage flags. "
|
|
"The 'value' field in the JSON payload provides the new value.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
if (!Json->HasField(TEXT("value")))
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: value"));
|
|
}
|
|
|
|
// Load material
|
|
MCPAssets<UMaterial> Assets;
|
|
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
|
|
UMaterial* MaterialObj = Assets.Object();
|
|
|
|
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(MaterialObj->MaterialDomain);
|
|
|
|
EMaterialDomain NewDomain = MaterialObj->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 MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
TEXT("Invalid domain '%s'. Valid values: Surface, DeferredDecal, LightFunction, Volume, PostProcess, UI"),
|
|
*ValueStr));
|
|
}
|
|
|
|
NewValue = ValueStr;
|
|
|
|
if (!DryRun)
|
|
{
|
|
MaterialObj->PreEditChange(nullptr);
|
|
MaterialObj->MaterialDomain = NewDomain;
|
|
MaterialObj->PostEditChange();
|
|
}
|
|
}
|
|
else if (Property == TEXT("blendMode"))
|
|
{
|
|
FString ValueStr = Json->GetStringField(TEXT("value"));
|
|
OldValue = BlendModeToString(MaterialObj->BlendMode);
|
|
|
|
EBlendMode NewBlend = MaterialObj->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 MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
TEXT("Invalid blendMode '%s'. Valid values: Opaque, Masked, Translucent, Additive, Modulate"),
|
|
*ValueStr));
|
|
}
|
|
|
|
NewValue = ValueStr;
|
|
|
|
if (!DryRun)
|
|
{
|
|
MaterialObj->PreEditChange(nullptr);
|
|
MaterialObj->BlendMode = NewBlend;
|
|
MaterialObj->PostEditChange();
|
|
}
|
|
}
|
|
else if (Property == TEXT("twoSided"))
|
|
{
|
|
bool bValue = Json->GetBoolField(TEXT("value"));
|
|
OldValue = MaterialObj->TwoSided ? TEXT("true") : TEXT("false");
|
|
NewValue = bValue ? TEXT("true") : TEXT("false");
|
|
|
|
if (!DryRun)
|
|
{
|
|
MaterialObj->PreEditChange(nullptr);
|
|
MaterialObj->TwoSided = bValue ? 1 : 0;
|
|
MaterialObj->PostEditChange();
|
|
}
|
|
}
|
|
else if (Property == TEXT("shadingModel"))
|
|
{
|
|
FString ValueStr = Json->GetStringField(TEXT("value"));
|
|
OldValue = ShadingModelToString(MaterialObj->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 MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
TEXT("Invalid shadingModel '%s'. Valid values: Unlit, DefaultLit, Subsurface, PreintegratedSkin, ClearCoat, SubsurfaceProfile, TwoSidedFoliage, Hair, Cloth, Eye"),
|
|
*ValueStr));
|
|
}
|
|
|
|
NewValue = ValueStr;
|
|
|
|
if (!DryRun)
|
|
{
|
|
MaterialObj->PreEditChange(nullptr);
|
|
MaterialObj->SetShadingModel(NewModel);
|
|
MaterialObj->PostEditChange();
|
|
}
|
|
}
|
|
else if (Property == TEXT("opacity") || Property == TEXT("opacityMaskClipValue"))
|
|
{
|
|
double OpacityValue = Json->GetNumberField(TEXT("value"));
|
|
OldValue = FString::Printf(TEXT("%f"), MaterialObj->OpacityMaskClipValue);
|
|
NewValue = FString::Printf(TEXT("%f"), OpacityValue);
|
|
|
|
if (!DryRun)
|
|
{
|
|
MaterialObj->PreEditChange(nullptr);
|
|
MaterialObj->OpacityMaskClipValue = (float)OpacityValue;
|
|
MaterialObj->PostEditChange();
|
|
}
|
|
}
|
|
else if (Property == TEXT("bUsedWithSkeletalMesh"))
|
|
{
|
|
bool bValue = Json->GetBoolField(TEXT("value"));
|
|
OldValue = MaterialObj->bUsedWithSkeletalMesh ? TEXT("true") : TEXT("false");
|
|
NewValue = bValue ? TEXT("true") : TEXT("false");
|
|
|
|
if (!DryRun)
|
|
{
|
|
MaterialObj->PreEditChange(nullptr);
|
|
MaterialObj->bUsedWithSkeletalMesh = bValue ? 1 : 0;
|
|
MaterialObj->PostEditChange();
|
|
}
|
|
}
|
|
else if (Property == TEXT("bUsedWithMorphTargets"))
|
|
{
|
|
bool bValue = Json->GetBoolField(TEXT("value"));
|
|
OldValue = MaterialObj->bUsedWithMorphTargets ? TEXT("true") : TEXT("false");
|
|
NewValue = bValue ? TEXT("true") : TEXT("false");
|
|
|
|
if (!DryRun)
|
|
{
|
|
MaterialObj->PreEditChange(nullptr);
|
|
MaterialObj->bUsedWithMorphTargets = bValue ? 1 : 0;
|
|
MaterialObj->PostEditChange();
|
|
}
|
|
}
|
|
else if (Property == TEXT("bUsedWithNiagaraSprites"))
|
|
{
|
|
bool bValue = Json->GetBoolField(TEXT("value"));
|
|
OldValue = MaterialObj->bUsedWithNiagaraSprites ? TEXT("true") : TEXT("false");
|
|
NewValue = bValue ? TEXT("true") : TEXT("false");
|
|
|
|
if (!DryRun)
|
|
{
|
|
MaterialObj->PreEditChange(nullptr);
|
|
MaterialObj->bUsedWithNiagaraSprites = bValue ? 1 : 0;
|
|
MaterialObj->PostEditChange();
|
|
}
|
|
}
|
|
else if (Property == TEXT("ditheredLODTransition") || Property == TEXT("DitheredLODTransition"))
|
|
{
|
|
bool bValue = Json->GetBoolField(TEXT("value"));
|
|
OldValue = MaterialObj->DitheredLODTransition ? TEXT("true") : TEXT("false");
|
|
NewValue = bValue ? TEXT("true") : TEXT("false");
|
|
|
|
if (!DryRun)
|
|
{
|
|
MaterialObj->PreEditChange(nullptr);
|
|
MaterialObj->DitheredLODTransition = bValue ? 1 : 0;
|
|
MaterialObj->PostEditChange();
|
|
}
|
|
}
|
|
else if (Property == TEXT("bAllowNegativeEmissiveColor"))
|
|
{
|
|
bool bValue = Json->GetBoolField(TEXT("value"));
|
|
OldValue = MaterialObj->bAllowNegativeEmissiveColor ? TEXT("true") : TEXT("false");
|
|
NewValue = bValue ? TEXT("true") : TEXT("false");
|
|
|
|
if (!DryRun)
|
|
{
|
|
MaterialObj->PreEditChange(nullptr);
|
|
MaterialObj->bAllowNegativeEmissiveColor = bValue ? 1 : 0;
|
|
MaterialObj->PostEditChange();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, 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 (!DryRun)
|
|
{
|
|
bSaved = MCPUtils::SaveMaterialPackage(MaterialObj);
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %sSet material property '%s' on '%s': '%s' -> '%s'"),
|
|
DryRun ? TEXT("[DRY RUN] ") : TEXT(""),
|
|
*Property, *Material, *OldValue, *NewValue);
|
|
|
|
Result->SetStringField(TEXT("material"), MaterialObj->GetName());
|
|
Result->SetStringField(TEXT("oldValue"), OldValue);
|
|
Result->SetStringField(TEXT("newValue"), NewValue);
|
|
Result->SetBoolField(TEXT("dryRun"), DryRun);
|
|
if (!DryRun)
|
|
{
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
}
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="add_material_expression"))
|
|
class UMCPHandler_AddMaterialExpression : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction, not both)"))
|
|
FString Material;
|
|
|
|
UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material, not both)"))
|
|
FString MaterialFunction;
|
|
|
|
UPROPERTY(meta=(Description="Expression class name without 'MaterialExpression' prefix (e.g. 'Constant', 'ScalarParameter', 'Add', 'Multiply', 'Lerp')"))
|
|
FString ExpressionClass;
|
|
|
|
UPROPERTY(meta=(Optional, Description="X position in the material graph editor"))
|
|
int32 PosX = 0;
|
|
|
|
UPROPERTY(meta=(Optional, Description="Y position in the material graph editor"))
|
|
int32 PosY = 0;
|
|
|
|
UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it"))
|
|
bool DryRun = false;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Add a new expression node to a material or material function graph.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'"));
|
|
}
|
|
|
|
// 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 = ExpressionClass;
|
|
if (const FString* Alias = Aliases.Find(ExpressionClass))
|
|
{
|
|
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 MCPUtils::MakeErrorJson(Result, 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.)"),
|
|
*ExpressionClass));
|
|
}
|
|
if (ExprClass->HasAnyClassFlags(CLASS_Abstract))
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
TEXT("Expression class '%s' is abstract and cannot be instantiated."), *ExpressionClass));
|
|
}
|
|
|
|
// Load material or material function
|
|
UMaterial* MaterialObj = nullptr;
|
|
UMaterialFunction* MatFunc = nullptr;
|
|
UObject* Owner = nullptr;
|
|
FString AssetDisplayName;
|
|
|
|
if (!MaterialFunction.IsEmpty())
|
|
{
|
|
if (!Material.IsEmpty())
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("Specify either 'material' or 'materialFunction', not both"));
|
|
}
|
|
MCPAssets<UMaterialFunction> MFAssets;
|
|
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
|
|
MatFunc = MFAssets.Object();
|
|
Owner = MatFunc;
|
|
AssetDisplayName = MatFunc->GetName();
|
|
}
|
|
else
|
|
{
|
|
MCPAssets<UMaterial> MatAssets;
|
|
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
|
|
MaterialObj = MatAssets.Object();
|
|
Owner = MaterialObj;
|
|
AssetDisplayName = MaterialObj->GetName();
|
|
}
|
|
|
|
if (DryRun)
|
|
{
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would add expression '%s' to '%s' at (%d, %d)"),
|
|
*ExpressionClass, *AssetDisplayName, PosX, PosY);
|
|
|
|
Result->SetBoolField(TEXT("dryRun"), true);
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
return;
|
|
}
|
|
|
|
// Ensure the MaterialGraph exists (commandlet mode doesn't auto-create it)
|
|
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
|
|
|
|
// 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, MaterialObj, MatFunc, PosX, PosY, &NewExpr);
|
|
if (CreateResult != 0 || !NewExpr)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
TEXT("Expression class '%s' cannot be instantiated (may be abstract or have internal errors)."),
|
|
*ExpressionClass));
|
|
}
|
|
#else
|
|
NewExpr = NewObject<UMaterialExpression>(Owner, ExprClass);
|
|
if (!NewExpr)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create material expression object"));
|
|
}
|
|
NewExpr->MaterialExpressionEditorX = PosX;
|
|
NewExpr->MaterialExpressionEditorY = PosY;
|
|
if (MaterialObj)
|
|
{
|
|
MaterialObj->GetExpressionCollection().AddExpression(NewExpr);
|
|
if (MaterialObj->MaterialGraph)
|
|
{
|
|
MaterialObj->MaterialGraph->RebuildGraph();
|
|
}
|
|
MaterialObj->PreEditChange(nullptr);
|
|
MaterialObj->PostEditChange();
|
|
MaterialObj->MarkPackageDirty();
|
|
}
|
|
else if (MatFunc)
|
|
{
|
|
MatFunc->GetExpressionCollection().AddExpression(NewExpr);
|
|
MatFunc->PreEditChange(nullptr);
|
|
MatFunc->PostEditChange();
|
|
MatFunc->MarkPackageDirty();
|
|
}
|
|
#endif
|
|
|
|
// Save
|
|
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
|
|
|
|
// Find the node GUID from the material graph (only for materials)
|
|
FString NodeGuid;
|
|
if (MaterialObj && MaterialObj->MaterialGraph)
|
|
{
|
|
for (UEdGraphNode* Node : MaterialObj->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)"),
|
|
*ExpressionClass, *AssetDisplayName, *NodeGuid, bSaved ? TEXT("true") : TEXT("false"));
|
|
|
|
// Serialize the expression details
|
|
TSharedPtr<FJsonObject> ExprDetails = MCPUtils::SerializeMaterialExpression(NewExpr);
|
|
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
Result->SetStringField(TEXT("nodeId"), NodeGuid);
|
|
if (ExprDetails.IsValid())
|
|
{
|
|
Result->SetObjectField(TEXT("expression"), ExprDetails);
|
|
}
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="delete_material_expression"))
|
|
class UMCPHandler_DeleteMaterialExpression : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)"))
|
|
FString Material;
|
|
|
|
UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)"))
|
|
FString MaterialFunction;
|
|
|
|
UPROPERTY(meta=(Description="Node GUID of the expression to delete"))
|
|
FString Node;
|
|
|
|
UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it"))
|
|
bool DryRun = false;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Remove an expression node from a material or material function graph.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'"));
|
|
}
|
|
|
|
// Load material or material function
|
|
UMaterial* MaterialObj = nullptr;
|
|
UMaterialFunction* MatFunc = nullptr;
|
|
FString AssetDisplayName;
|
|
|
|
if (!MaterialFunction.IsEmpty())
|
|
{
|
|
MCPAssets<UMaterialFunction> MFAssets;
|
|
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
|
|
MatFunc = MFAssets.Object();
|
|
AssetDisplayName = MatFunc->GetName();
|
|
}
|
|
else
|
|
{
|
|
MCPAssets<UMaterial> MatAssets;
|
|
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
|
|
MaterialObj = MatAssets.Object();
|
|
AssetDisplayName = MaterialObj->GetName();
|
|
}
|
|
|
|
// For materials, we need the graph to find nodes by GUID
|
|
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
|
|
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
|
|
if (!Graph)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName));
|
|
}
|
|
|
|
// Find the node by GUID
|
|
UMaterialGraphNode* TargetMatNode = nullptr;
|
|
for (UEdGraphNode* GraphNode : Graph->Nodes)
|
|
{
|
|
if (!GraphNode) continue;
|
|
if (GraphNode->NodeGuid.ToString() == Node)
|
|
{
|
|
TargetMatNode = Cast<UMaterialGraphNode>(GraphNode);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!TargetMatNode)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *Node));
|
|
}
|
|
|
|
if (!TargetMatNode->MaterialExpression)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' has no associated material expression"), *Node));
|
|
}
|
|
|
|
// Capture info before deletion
|
|
FString DeletedNodeTitle = TargetMatNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
|
FString DeletedExprClass = TargetMatNode->MaterialExpression->GetClass()->GetName();
|
|
|
|
if (DryRun)
|
|
{
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would delete expression '%s' (nodeId: %s) from '%s'"),
|
|
*DeletedExprClass, *Node, *AssetDisplayName);
|
|
|
|
Result->SetBoolField(TEXT("dryRun"), true);
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
Result->SetStringField(TEXT("deletedNodeTitle"), DeletedNodeTitle);
|
|
Result->SetStringField(TEXT("deletedExpressionClass"), DeletedExprClass);
|
|
return;
|
|
}
|
|
|
|
// Remove the expression
|
|
UMaterialExpression* ExprToRemove = TargetMatNode->MaterialExpression;
|
|
if (MaterialObj)
|
|
{
|
|
MaterialObj->GetExpressionCollection().RemoveExpression(ExprToRemove);
|
|
}
|
|
else
|
|
{
|
|
MatFunc->GetExpressionCollection().RemoveExpression(ExprToRemove);
|
|
}
|
|
ExprToRemove->MarkAsGarbage();
|
|
|
|
// Rebuild graph
|
|
Graph->NotifyGraphChanged();
|
|
|
|
UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc;
|
|
Asset->PreEditChange(nullptr);
|
|
Asset->PostEditChange();
|
|
Asset->MarkPackageDirty();
|
|
|
|
// Save
|
|
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleted expression '%s' (nodeId: %s) from '%s' (saved: %s)"),
|
|
*DeletedExprClass, *Node, *AssetDisplayName, bSaved ? TEXT("true") : TEXT("false"));
|
|
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
Result->SetStringField(TEXT("deletedNodeTitle"), DeletedNodeTitle);
|
|
Result->SetStringField(TEXT("deletedExpressionClass"), DeletedExprClass);
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="connect_material_expression_pins"))
|
|
class UMCPHandler_ConnectMaterialPins : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)"))
|
|
FString Material;
|
|
|
|
UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)"))
|
|
FString MaterialFunction;
|
|
|
|
UPROPERTY(meta=(Description="Node GUID of the source (output) node"))
|
|
FString SourceNode;
|
|
|
|
UPROPERTY(meta=(Description="Pin name on the source node"))
|
|
FString SourcePinName;
|
|
|
|
UPROPERTY(meta=(Description="Node GUID of the target (input) node"))
|
|
FString TargetNode;
|
|
|
|
UPROPERTY(meta=(Description="Pin name on the target node"))
|
|
FString TargetPinName;
|
|
|
|
UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it"))
|
|
bool DryRun = false;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Connect two pins in a material or material function graph.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'"));
|
|
}
|
|
|
|
// Load material or material function
|
|
UMaterial* MaterialObj = nullptr;
|
|
UMaterialFunction* MatFunc = nullptr;
|
|
FString AssetDisplayName;
|
|
|
|
if (!MaterialFunction.IsEmpty())
|
|
{
|
|
MCPAssets<UMaterialFunction> MFAssets;
|
|
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
|
|
MatFunc = MFAssets.Object();
|
|
AssetDisplayName = MatFunc->GetName();
|
|
}
|
|
else
|
|
{
|
|
MCPAssets<UMaterial> MatAssets;
|
|
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
|
|
MaterialObj = MatAssets.Object();
|
|
AssetDisplayName = MaterialObj->GetName();
|
|
}
|
|
|
|
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
|
|
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
|
|
if (!Graph)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName));
|
|
}
|
|
|
|
// Find source and target nodes by GUID
|
|
UEdGraphNode* SourceGraphNode = nullptr;
|
|
UEdGraphNode* TargetGraphNode = nullptr;
|
|
|
|
for (UEdGraphNode* GraphNode : Graph->Nodes)
|
|
{
|
|
if (!GraphNode) continue;
|
|
if (GraphNode->NodeGuid.ToString() == SourceNode)
|
|
SourceGraphNode = GraphNode;
|
|
if (GraphNode->NodeGuid.ToString() == TargetNode)
|
|
TargetGraphNode = GraphNode;
|
|
if (SourceGraphNode && TargetGraphNode)
|
|
break;
|
|
}
|
|
|
|
if (!SourceGraphNode)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source node '%s' not found in material graph"), *SourceNode));
|
|
}
|
|
if (!TargetGraphNode)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target node '%s' not found in material graph"), *TargetNode));
|
|
}
|
|
|
|
// Find pins
|
|
UEdGraphPin* SourcePin = SourceGraphNode->FindPin(FName(*SourcePinName));
|
|
if (!SourcePin)
|
|
{
|
|
// List available pins for debugging
|
|
TArray<TSharedPtr<FJsonValue>> PinNames;
|
|
for (UEdGraphPin* P : SourceGraphNode->Pins)
|
|
{
|
|
if (P) PinNames.Add(MakeShared<FJsonValueString>(
|
|
FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(),
|
|
P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"))));
|
|
}
|
|
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source pin '%s' not found on node '%s'"),
|
|
*SourcePinName, *SourceNode));
|
|
Result->SetArrayField(TEXT("availablePins"), PinNames);
|
|
return;
|
|
}
|
|
|
|
UEdGraphPin* TargetPin = TargetGraphNode->FindPin(FName(*TargetPinName));
|
|
if (!TargetPin)
|
|
{
|
|
TArray<TSharedPtr<FJsonValue>> PinNames;
|
|
for (UEdGraphPin* P : TargetGraphNode->Pins)
|
|
{
|
|
if (P) PinNames.Add(MakeShared<FJsonValueString>(
|
|
FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(),
|
|
P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"))));
|
|
}
|
|
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target pin '%s' not found on node '%s'"),
|
|
*TargetPinName, *TargetNode));
|
|
Result->SetArrayField(TEXT("availablePins"), PinNames);
|
|
return;
|
|
}
|
|
|
|
if (DryRun)
|
|
{
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would connect %s.%s -> %s.%s in '%s'"),
|
|
*SourceNode, *SourcePinName, *TargetNode, *TargetPinName, *AssetDisplayName);
|
|
|
|
Result->SetBoolField(TEXT("dryRun"), true);
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
return;
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Connecting %s.%s -> %s.%s in '%s'"),
|
|
*SourceNode, *SourcePinName, *TargetNode, *TargetPinName, *AssetDisplayName);
|
|
|
|
// Try to connect via the schema
|
|
const UEdGraphSchema* Schema = Graph->GetSchema();
|
|
if (!Schema)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("Material graph schema not found"));
|
|
}
|
|
|
|
bool bConnected = Schema->TryCreateConnection(SourcePin, TargetPin);
|
|
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
|
|
if (!bConnected)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
TEXT("Cannot connect %s.%s to %s.%s — types may be incompatible"),
|
|
*SourceNode, *SourcePinName, *TargetNode, *TargetPinName));
|
|
}
|
|
|
|
// Save
|
|
UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc;
|
|
Asset->PreEditChange(nullptr);
|
|
Asset->PostEditChange();
|
|
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="disconnect_material_expression_pin"))
|
|
class UMCPHandler_DisconnectMaterialPin : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)"))
|
|
FString Material;
|
|
|
|
UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)"))
|
|
FString MaterialFunction;
|
|
|
|
UPROPERTY(meta=(Description="Node GUID of the node whose pin to disconnect"))
|
|
FString Node;
|
|
|
|
UPROPERTY(meta=(Description="Pin name to disconnect"))
|
|
FString PinName;
|
|
|
|
UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it"))
|
|
bool DryRun = false;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Break all connections on a specific pin in a material or material function graph.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'"));
|
|
}
|
|
|
|
// Load material or material function
|
|
UMaterial* MaterialObj = nullptr;
|
|
UMaterialFunction* MatFunc = nullptr;
|
|
FString AssetDisplayName;
|
|
|
|
if (!MaterialFunction.IsEmpty())
|
|
{
|
|
MCPAssets<UMaterialFunction> MFAssets;
|
|
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
|
|
MatFunc = MFAssets.Object();
|
|
AssetDisplayName = MatFunc->GetName();
|
|
}
|
|
else
|
|
{
|
|
MCPAssets<UMaterial> MatAssets;
|
|
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
|
|
MaterialObj = MatAssets.Object();
|
|
AssetDisplayName = MaterialObj->GetName();
|
|
}
|
|
|
|
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
|
|
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
|
|
if (!Graph)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName));
|
|
}
|
|
|
|
// Find node by GUID
|
|
UEdGraphNode* TargetGraphNode = nullptr;
|
|
for (UEdGraphNode* GraphNode : Graph->Nodes)
|
|
{
|
|
if (!GraphNode) continue;
|
|
if (GraphNode->NodeGuid.ToString() == Node)
|
|
{
|
|
TargetGraphNode = GraphNode;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!TargetGraphNode)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *Node));
|
|
}
|
|
|
|
// Find pin
|
|
UEdGraphPin* Pin = TargetGraphNode->FindPin(FName(*PinName));
|
|
if (!Pin)
|
|
{
|
|
TArray<TSharedPtr<FJsonValue>> PinNames;
|
|
for (UEdGraphPin* P : TargetGraphNode->Pins)
|
|
{
|
|
if (P) PinNames.Add(MakeShared<FJsonValueString>(
|
|
FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(),
|
|
P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"))));
|
|
}
|
|
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' not found on node '%s'"),
|
|
*PinName, *Node));
|
|
Result->SetArrayField(TEXT("availablePins"), PinNames);
|
|
return;
|
|
}
|
|
|
|
int32 BrokenCount = Pin->LinkedTo.Num();
|
|
|
|
if (DryRun)
|
|
{
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would disconnect pin '%s' on node '%s' in '%s' (%d links)"),
|
|
*PinName, *Node, *AssetDisplayName, BrokenCount);
|
|
|
|
Result->SetBoolField(TEXT("dryRun"), true);
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
Result->SetNumberField(TEXT("brokenLinkCount"), BrokenCount);
|
|
return;
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Disconnecting pin '%s' on node '%s' in '%s' (%d links)"),
|
|
*PinName, *Node, *AssetDisplayName, BrokenCount);
|
|
|
|
// Break all links
|
|
Pin->BreakAllPinLinks();
|
|
|
|
UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc;
|
|
Asset->PreEditChange(nullptr);
|
|
Asset->PostEditChange();
|
|
|
|
// Save
|
|
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
|
|
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
Result->SetNumberField(TEXT("brokenLinkCount"), BrokenCount);
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="set_material_expression_property"))
|
|
class UMCPHandler_SetExpressionValue : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)"))
|
|
FString Material;
|
|
|
|
UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)"))
|
|
FString MaterialFunction;
|
|
|
|
UPROPERTY(meta=(Description="Node GUID of the expression to modify"))
|
|
FString Node;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Set the value or properties on a material expression node. "
|
|
"The 'value' field in the JSON payload provides the new value, whose format depends on the expression type.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'"));
|
|
}
|
|
|
|
if (!Json->HasField(TEXT("value")))
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: value"));
|
|
}
|
|
|
|
// Load material or material function
|
|
UMaterial* MaterialObj = nullptr;
|
|
UMaterialFunction* MatFunc = nullptr;
|
|
FString AssetDisplayName;
|
|
|
|
if (!MaterialFunction.IsEmpty())
|
|
{
|
|
MCPAssets<UMaterialFunction> MFAssets;
|
|
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
|
|
MatFunc = MFAssets.Object();
|
|
AssetDisplayName = MatFunc->GetName();
|
|
}
|
|
else
|
|
{
|
|
MCPAssets<UMaterial> MatAssets;
|
|
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
|
|
MaterialObj = MatAssets.Object();
|
|
AssetDisplayName = MaterialObj->GetName();
|
|
}
|
|
|
|
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
|
|
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
|
|
if (!Graph)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName));
|
|
}
|
|
|
|
// Find the node by GUID
|
|
UMaterialGraphNode* TargetMatNode = nullptr;
|
|
for (UEdGraphNode* GraphNode : Graph->Nodes)
|
|
{
|
|
if (!GraphNode) continue;
|
|
if (GraphNode->NodeGuid.ToString() == Node)
|
|
{
|
|
TargetMatNode = Cast<UMaterialGraphNode>(GraphNode);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!TargetMatNode)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *Node));
|
|
}
|
|
|
|
UMaterialExpression* Expr = TargetMatNode->MaterialExpression;
|
|
if (!Expr)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' has no associated material expression"), *Node));
|
|
}
|
|
|
|
FString ExprType;
|
|
FString NewValueStr;
|
|
|
|
UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (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 MCPUtils::MakeErrorJson(Result, 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 MCPUtils::MakeErrorJson(Result, 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 MCPUtils::MakeErrorJson(Result, 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 MCPUtils::MakeErrorJson(Result, 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 MCPUtils::MakeErrorJson(Result, TEXT("ComponentMask requires value as object {r, g, b, a} (booleans)"));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Asset->PostEditChange();
|
|
return MCPUtils::MakeErrorJson(Result, 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 = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set expression value on node '%s' (%s) in '%s': %s"),
|
|
*Node, *ExprType, *AssetDisplayName, *NewValueStr);
|
|
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
Result->SetStringField(TEXT("expressionType"), ExprType);
|
|
Result->SetStringField(TEXT("newValue"), NewValueStr);
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="set_material_expression_position"))
|
|
class UMCPHandler_MoveMaterialExpression : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)"))
|
|
FString Material;
|
|
|
|
UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)"))
|
|
FString MaterialFunction;
|
|
|
|
UPROPERTY(meta=(Description="Node GUID of the expression to reposition"))
|
|
FString Node;
|
|
|
|
UPROPERTY(meta=(Description="New X position"))
|
|
int32 PosX = 0;
|
|
|
|
UPROPERTY(meta=(Description="New Y position"))
|
|
int32 PosY = 0;
|
|
|
|
UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it"))
|
|
bool DryRun = false;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Reposition a material expression node in the material graph editor.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'"));
|
|
}
|
|
|
|
// Load material or material function
|
|
UMaterial* MaterialObj = nullptr;
|
|
UMaterialFunction* MatFunc = nullptr;
|
|
FString AssetDisplayName;
|
|
|
|
if (!MaterialFunction.IsEmpty())
|
|
{
|
|
MCPAssets<UMaterialFunction> MFAssets;
|
|
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
|
|
MatFunc = MFAssets.Object();
|
|
AssetDisplayName = MatFunc->GetName();
|
|
}
|
|
else
|
|
{
|
|
MCPAssets<UMaterial> MatAssets;
|
|
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
|
|
MaterialObj = MatAssets.Object();
|
|
AssetDisplayName = MaterialObj->GetName();
|
|
}
|
|
|
|
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
|
|
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
|
|
if (!Graph)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName));
|
|
}
|
|
|
|
// Find node by GUID
|
|
UMaterialGraphNode* TargetMatNode = nullptr;
|
|
for (UEdGraphNode* GraphNode : Graph->Nodes)
|
|
{
|
|
if (!GraphNode) continue;
|
|
if (GraphNode->NodeGuid.ToString() == Node)
|
|
{
|
|
TargetMatNode = Cast<UMaterialGraphNode>(GraphNode);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!TargetMatNode)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *Node));
|
|
}
|
|
|
|
if (DryRun)
|
|
{
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would move node '%s' to (%d, %d) in '%s'"),
|
|
*Node, PosX, PosY, *AssetDisplayName);
|
|
|
|
Result->SetBoolField(TEXT("dryRun"), true);
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
return;
|
|
}
|
|
|
|
// 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 = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc;
|
|
Asset->PreEditChange(nullptr);
|
|
Asset->PostEditChange();
|
|
|
|
// Save
|
|
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Moved node '%s' to (%d, %d) in '%s' (saved: %s)"),
|
|
*Node, PosX, PosY, *AssetDisplayName, bSaved ? TEXT("true") : TEXT("false"));
|
|
|
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="create_material_function_asset"))
|
|
class UMCPHandler_CreateMaterialFunction : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Name for the new material function asset"))
|
|
FString Name;
|
|
|
|
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
|
|
FString PackagePath;
|
|
|
|
UPROPERTY(meta=(Optional, Description="Description for the material function"))
|
|
FString Description;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Create a new UMaterialFunction asset with an optional description.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
if (!PackagePath.StartsWith(TEXT("/Game")))
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'"));
|
|
}
|
|
|
|
// Check if asset already exists
|
|
MCPAssets<UMaterialFunction> ExistCheck;
|
|
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
|
|
|
|
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 MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create Material Function '%s' in '%s'"), *Name, *PackagePath));
|
|
}
|
|
|
|
UMaterialFunction* MF = Cast<UMaterialFunction>(NewAsset);
|
|
if (!MF)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UMaterialFunction"));
|
|
}
|
|
|
|
// Set optional description
|
|
if (!Description.IsEmpty())
|
|
{
|
|
MF->Description = Description;
|
|
}
|
|
|
|
// Save
|
|
bool bSaved = MCPUtils::SaveGenericPackage(MF);
|
|
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Material Function '%s' (saved: %s)"),
|
|
*Name, bSaved ? TEXT("true") : TEXT("false"));
|
|
|
|
Result->SetStringField(TEXT("path"), MF->GetPathName());
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
}
|
|
};
|