Files
unreal-engine-mcp-system-pl…/Source/UEBlueprintMCPEditor/Private/MCPMaterialLibrary.cpp
Bonchellon eba71c4ca8 Unified, portable Unreal Engine MCP system plugin
Merge of the two project copies into one self-contained plugin (the superset:
variable_op + variables.py, full pcg_op runtime/declarative/preset ops, the
CreateWidgetFloatAnimation widget tool, and full Voxel graph authoring).

Made fully project- and machine-agnostic — no hardcoded paths:
- New src/projectPaths.js auto-detects the host .uproject (walk-up), project
  name, Editor build target, log file, and engine install (EngineAssociation
  via launcher manifest/registry, else installed-engine scan). All overridable
  via UE_* env vars.
- Rewired buildOrchestrator/insights/launcher/insightsExporter/server.js and the
  Python workers (cpp_scaffold, live_coding, apply_graph, console) off the old
  C:/Github/ihy, IHY*, E:/UE_Versions and UE_5.7 literals onto the resolver.
- Voxel made optional: Build.cs auto-detects the Voxel plugin (env
  UE_MCP_WITH_VOXEL override) and the C++ compiles to stubs under WITH_MCP_VOXEL,
  so the module builds in projects without Voxel; .uplugin marks Voxel optional.
- De-branded the agent-gateway and docs; scrubbed a leaked API key; excluded
  node_modules/Binaries/Intermediate/__pycache__/secrets from the repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 01:07:24 +03:00

339 lines
12 KiB
C++

#include "MCPMaterialLibrary.h"
#include "Dom/JsonObject.h"
#include "Dom/JsonValue.h"
#include "Materials/Material.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialFunction.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h"
namespace
{
FString JsonStringifyMaterial(const TSharedRef<FJsonObject>& Object)
{
FString Out;
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Out);
FJsonSerializer::Serialize(Object, Writer);
return Out;
}
FString ExpressionId(const UMaterialExpression* Expression)
{
if (!Expression)
{
return FString();
}
#if WITH_EDITORONLY_DATA
if (Expression->MaterialExpressionGuid.IsValid())
{
return Expression->MaterialExpressionGuid.ToString(EGuidFormats::DigitsWithHyphensInBraces);
}
#endif
return Expression->GetPathName();
}
FString OutputNameForIndex(UMaterialExpression* Expression, int32 OutputIndex)
{
if (!Expression || OutputIndex < 0)
{
return FString();
}
TArray<FExpressionOutput>& Outputs = Expression->GetOutputs();
if (!Outputs.IsValidIndex(OutputIndex))
{
return FString();
}
return Outputs[OutputIndex].OutputName.ToString();
}
int32 ResolveOutputIndex(UMaterialExpression* Expression, const FString& OutputNameOrIndex)
{
if (!Expression)
{
return INDEX_NONE;
}
FString Needle = OutputNameOrIndex;
Needle.TrimStartAndEndInline();
if (Needle.IsEmpty() || Needle.Equals(TEXT("None"), ESearchCase::IgnoreCase))
{
return 0;
}
int32 NumericIndex = INDEX_NONE;
if (LexTryParseString(NumericIndex, *Needle))
{
return Expression->GetOutputs().IsValidIndex(NumericIndex) ? NumericIndex : INDEX_NONE;
}
TArray<FExpressionOutput>& Outputs = Expression->GetOutputs();
for (int32 OutputIndex = 0; OutputIndex < Outputs.Num(); ++OutputIndex)
{
if (Outputs[OutputIndex].OutputName.ToString().Equals(Needle, ESearchCase::IgnoreCase))
{
return OutputIndex;
}
}
return Outputs.Num() == 1 ? 0 : INDEX_NONE;
}
int32 ResolveInputIndex(UMaterialExpression* Expression, const FString& InputNameOrIndex)
{
if (!Expression)
{
return INDEX_NONE;
}
FString Needle = InputNameOrIndex;
Needle.TrimStartAndEndInline();
if (Needle.IsEmpty() || Needle.Equals(TEXT("None"), ESearchCase::IgnoreCase))
{
return Expression->CountInputs() == 1 ? 0 : INDEX_NONE;
}
int32 NumericIndex = INDEX_NONE;
if (LexTryParseString(NumericIndex, *Needle))
{
return NumericIndex >= 0 && NumericIndex < Expression->CountInputs() ? NumericIndex : INDEX_NONE;
}
for (int32 InputIndex = 0; InputIndex < Expression->CountInputs(); ++InputIndex)
{
if (Expression->GetInputName(InputIndex).ToString().Equals(Needle, ESearchCase::IgnoreCase))
{
return InputIndex;
}
}
return INDEX_NONE;
}
TSharedRef<FJsonObject> MakeStatusJson(bool bOk, const FString& Error = FString())
{
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
Root->SetBoolField(TEXT("ok"), bOk);
if (!Error.IsEmpty())
{
Root->SetStringField(TEXT("error"), Error);
}
return Root;
}
void AppendExpressionJson(UMaterialExpression* Expression, TArray<TSharedPtr<FJsonValue>>& Nodes, TArray<TSharedPtr<FJsonValue>>& Edges)
{
if (!Expression)
{
return;
}
TSharedRef<FJsonObject> Node = MakeShared<FJsonObject>();
Node->SetStringField(TEXT("guid"), ExpressionId(Expression));
Node->SetStringField(TEXT("name"), Expression->GetName());
Node->SetStringField(TEXT("class"), Expression->GetClass()->GetPathName());
#if WITH_EDITORONLY_DATA
Node->SetNumberField(TEXT("x"), Expression->MaterialExpressionEditorX);
Node->SetNumberField(TEXT("y"), Expression->MaterialExpressionEditorY);
if (!Expression->Desc.IsEmpty())
{
Node->SetStringField(TEXT("desc"), Expression->Desc);
}
#endif
TArray<TSharedPtr<FJsonValue>> Inputs;
const int32 InputCount = Expression->CountInputs();
for (int32 InputIndex = 0; InputIndex < InputCount; ++InputIndex)
{
const FExpressionInput* Input = Expression->GetInput(InputIndex);
const FName InputName = Expression->GetInputName(InputIndex);
TSharedRef<FJsonObject> InputJson = MakeShared<FJsonObject>();
InputJson->SetStringField(TEXT("name"), InputName.ToString());
InputJson->SetNumberField(TEXT("index"), InputIndex);
InputJson->SetBoolField(TEXT("connected"), Input && Input->Expression);
Inputs.Add(MakeShared<FJsonValueObject>(InputJson));
if (Input && Input->Expression)
{
TSharedRef<FJsonObject> Edge = MakeShared<FJsonObject>();
TArray<TSharedPtr<FJsonValue>> From;
From.Add(MakeShared<FJsonValueString>(ExpressionId(Input->Expression)));
From.Add(MakeShared<FJsonValueString>(OutputNameForIndex(Input->Expression, Input->OutputIndex)));
TArray<TSharedPtr<FJsonValue>> To;
To.Add(MakeShared<FJsonValueString>(ExpressionId(Expression)));
To.Add(MakeShared<FJsonValueString>(InputName.ToString()));
Edge->SetArrayField(TEXT("from"), From);
Edge->SetArrayField(TEXT("to"), To);
Edges.Add(MakeShared<FJsonValueObject>(Edge));
}
}
Node->SetArrayField(TEXT("inputs"), Inputs);
TArray<TSharedPtr<FJsonValue>> OutputsJson;
TArray<FExpressionOutput>& Outputs = Expression->GetOutputs();
for (int32 OutputIndex = 0; OutputIndex < Outputs.Num(); ++OutputIndex)
{
TSharedRef<FJsonObject> OutputJson = MakeShared<FJsonObject>();
OutputJson->SetStringField(TEXT("name"), Outputs[OutputIndex].OutputName.ToString());
OutputJson->SetNumberField(TEXT("index"), OutputIndex);
OutputsJson.Add(MakeShared<FJsonValueObject>(OutputJson));
}
Node->SetArrayField(TEXT("outputs"), OutputsJson);
Nodes.Add(MakeShared<FJsonValueObject>(Node));
}
}
TArray<UMaterialExpression*> UMCPMaterialLibrary::GetMaterialExpressions(UObject* MaterialOrFunction)
{
TArray<UMaterialExpression*> Out;
if (UMaterial* Material = Cast<UMaterial>(MaterialOrFunction))
{
for (TObjectPtr<UMaterialExpression> Expression : Material->GetExpressionCollection().Expressions)
{
if (Expression)
{
Out.Add(Expression.Get());
}
}
}
else if (UMaterialFunction* Function = Cast<UMaterialFunction>(MaterialOrFunction))
{
for (TObjectPtr<UMaterialExpression> Expression : Function->GetExpressionCollection().Expressions)
{
if (Expression)
{
Out.Add(Expression.Get());
}
}
}
return Out;
}
UMaterialExpression* UMCPMaterialLibrary::FindMaterialExpression(UObject* MaterialOrFunction, const FString& NodeId)
{
for (UMaterialExpression* Expression : GetMaterialExpressions(MaterialOrFunction))
{
if (!Expression)
{
continue;
}
if (ExpressionId(Expression) == NodeId || Expression->GetName() == NodeId || Expression->GetPathName() == NodeId)
{
return Expression;
}
}
return nullptr;
}
FString UMCPMaterialLibrary::DumpMaterialGraphToJson(UObject* MaterialOrFunction)
{
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
if (!MaterialOrFunction)
{
Root->SetBoolField(TEXT("ok"), false);
Root->SetStringField(TEXT("error"), TEXT("null material/function"));
return JsonStringifyMaterial(Root);
}
Root->SetBoolField(TEXT("ok"), true);
Root->SetStringField(TEXT("asset"), MaterialOrFunction->GetPathName());
Root->SetStringField(TEXT("class"), MaterialOrFunction->GetClass()->GetPathName());
TArray<TSharedPtr<FJsonValue>> Nodes;
TArray<TSharedPtr<FJsonValue>> Edges;
for (UMaterialExpression* Expression : GetMaterialExpressions(MaterialOrFunction))
{
AppendExpressionJson(Expression, Nodes, Edges);
}
Root->SetArrayField(TEXT("nodes"), Nodes);
Root->SetArrayField(TEXT("edges"), Edges);
if (UMaterial* Material = Cast<UMaterial>(MaterialOrFunction))
{
TSharedRef<FJsonObject> Outputs = MakeShared<FJsonObject>();
for (int32 PropertyIndex = 0; PropertyIndex < static_cast<int32>(MP_MAX); ++PropertyIndex)
{
const EMaterialProperty Property = static_cast<EMaterialProperty>(PropertyIndex);
FExpressionInput* Input = Material->GetExpressionInputForProperty(Property);
if (!Input || !Input->Expression)
{
continue;
}
TSharedRef<FJsonObject> Ref = MakeShared<FJsonObject>();
TArray<TSharedPtr<FJsonValue>> From;
From.Add(MakeShared<FJsonValueString>(ExpressionId(Input->Expression)));
From.Add(MakeShared<FJsonValueString>(OutputNameForIndex(Input->Expression, Input->OutputIndex)));
Ref->SetArrayField(TEXT("from"), From);
Outputs->SetObjectField(UEnum::GetValueAsString(Property), Ref);
}
Root->SetObjectField(TEXT("outputs"), Outputs);
}
return JsonStringifyMaterial(Root);
}
FString UMCPMaterialLibrary::ConnectMaterialExpressionsRaw(UObject* MaterialOrFunction, UMaterialExpression* FromExpression, const FString& FromOutput, UMaterialExpression* ToExpression, const FString& ToInput)
{
if (!MaterialOrFunction)
{
return JsonStringifyMaterial(MakeStatusJson(false, TEXT("null material/function")));
}
if (!FromExpression)
{
return JsonStringifyMaterial(MakeStatusJson(false, TEXT("null source expression")));
}
if (!ToExpression)
{
return JsonStringifyMaterial(MakeStatusJson(false, TEXT("null target expression")));
}
const int32 OutputIndex = ResolveOutputIndex(FromExpression, FromOutput);
if (OutputIndex == INDEX_NONE)
{
return JsonStringifyMaterial(MakeStatusJson(false, FString::Printf(TEXT("source output not found: %s.%s"), *FromExpression->GetName(), *FromOutput)));
}
const int32 InputIndex = ResolveInputIndex(ToExpression, ToInput);
if (InputIndex == INDEX_NONE)
{
return JsonStringifyMaterial(MakeStatusJson(false, FString::Printf(TEXT("target input not found: %s.%s"), *ToExpression->GetName(), *ToInput)));
}
FExpressionInput* Input = ToExpression->GetInput(InputIndex);
if (!Input)
{
return JsonStringifyMaterial(MakeStatusJson(false, FString::Printf(TEXT("target input is null: %s.%s"), *ToExpression->GetName(), *ToInput)));
}
Input->Expression = FromExpression;
Input->OutputIndex = OutputIndex;
if (UMaterial* Material = Cast<UMaterial>(MaterialOrFunction))
{
Material->PreEditChange(nullptr);
Material->PostEditChange();
Material->MarkPackageDirty();
}
else if (UMaterialFunction* Function = Cast<UMaterialFunction>(MaterialOrFunction))
{
Function->PreEditChange(nullptr);
Function->PostEditChange();
Function->MarkPackageDirty();
}
TSharedRef<FJsonObject> Root = MakeStatusJson(true);
Root->SetStringField(TEXT("from"), FromExpression->GetName());
Root->SetStringField(TEXT("from_output"), OutputNameForIndex(FromExpression, OutputIndex));
Root->SetStringField(TEXT("to"), ToExpression->GetName());
Root->SetStringField(TEXT("to_input"), ToExpression->GetInputName(InputIndex).ToString());
return JsonStringifyMaterial(Root);
}