Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1297,21 +1297,6 @@ static inline bool McpSafeLoadMap(const FString& MapPath, bool bForceCleanup = t
// STEP 4: Flush rendering commands to ensure all GPU work is complete
FlushRenderingCommands();

// STEP 4a: CRITICAL FIX - Call EndFrame() to clear FTickTaskManager's LevelList
// The FTickTaskManager maintains a LevelList that's populated by FillLevelList() during
// StartFrame() and cleared by LevelList.Reset() in EndFrame(). When LoadMap destroys
// the old world, FreeTickTaskLevel() asserts that the TickTaskLevel is NOT in LevelList.
// By calling EndFrame(), we ensure LevelList is cleared before world destruction.
//
// This is safe because:
// 1. We've already unregistered all tick functions (STEP 2)
// 2. We've set bIsVisible=false on all levels (STEP 1)
// 3. EndFrame() doesn't have assertions that would fail if called outside a tick frame
// 4. The TickTaskSequencer.EndFrame() just clears batched tick data
// 5. The LevelList.Reset() is the critical operation we need
FTickTaskManagerInterface::Get().EndFrame();
UE_LOG(LogTemp, Log, TEXT("McpSafeLoadMap: Called EndFrame() to clear TickTaskManager LevelList"));

// STEP 5: Unload streaming levels explicitly
// This prevents UE-197643 where tick prerequisites cross level boundaries
TArray<ULevelStreaming*> StreamingLevels = CurrentWorld->GetStreamingLevels();
Expand Down Expand Up @@ -1351,7 +1336,6 @@ static inline bool McpSafeLoadMap(const FString& MapPath, bool bForceCleanup = t
// EditorServer.cpp detects the existing package can't be GC'd → Fatal Error.
//
// Reference: EditorServer.cpp line 2524 - "World Memory Leaks: %d leaks objects"
// Reference: World.cpp line 1488-1491 - CleanupWorld must be called for initialized Inactive worlds
{
FString NormalizedMapPath = MapPath;
if (NormalizedMapPath.EndsWith(TEXT(".umap")))
Expand All @@ -1369,15 +1353,6 @@ static inline bool McpSafeLoadMap(const FString& MapPath, bool bForceCleanup = t
{
UE_LOG(LogTemp, Warning, TEXT("McpSafeLoadMap: Target world '%s' exists in memory from previous creation - cleaning up"), *NormalizedMapPath);

// STEP 11a: Call CleanupWorld() if the world was initialized
// This is CRITICAL - without this, HasEverBeenInitialized() remains true
// and the world can't be reused, causing "World Memory Leaks" crash.
if (ExistingWorld->IsInitialized())
{
UE_LOG(LogTemp, Log, TEXT("McpSafeLoadMap: Calling CleanupWorld() for initialized world"));
ExistingWorld->CleanupWorld();
}

// Mark the world for destruction
ExistingWorld->bIsTearingDown = true;

Expand Down Expand Up @@ -1405,19 +1380,8 @@ static inline bool McpSafeLoadMap(const FString& MapPath, bool bForceCleanup = t
}
}

// Remove from root if needed
if (ExistingWorld->IsRooted())
{
UE_LOG(LogTemp, Log, TEXT("McpSafeLoadMap: Removing world from root"));
ExistingWorld->RemoveFromRoot();
}

// Mark the world and its package for garbage collection
ExistingWorld->SetFlags(RF_Transient);
if (ExistingPackage->IsRooted())
{
ExistingPackage->RemoveFromRoot();
}
ExistingPackage->SetFlags(RF_Transient);

// Force garbage collection to clean up the existing world
Comment on lines 1383 to 1387
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Removed RemoveFromRoot() in McpSafeLoadMap target world cleanup — same GC failure

Same issue as in HandleCreateLevel: the McpSafeLoadMap Step 11 cleanup of a pre-existing target world package removed the RemoveFromRoot() calls for both ExistingWorld and ExistingPackage. The code at McpAutomationBridgeHelpers.h:1383-1388 sets RF_Transient and calls CollectGarbage, but without removing root flags first, the GC will not collect these objects. This means LoadMap will find the stale world in memory and trigger the "World Memory Leaks" fatal error described in the comments.

(Refers to lines 1383-1389)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,11 @@ bool UMcpAutomationBridgeSubsystem::HandleBlueprintGraphAction(
// Loops
{TEXT("ForLoop"), TEXT("K2Node_ForLoop")},
{TEXT("ForLoopWithBreak"), TEXT("K2Node_ForLoopWithBreak")},
{TEXT("ForEachLoop"), TEXT("K2Node_ForEachElementInEnum")},
// NOTE: ForEachLoop for arrays is a macro node (K2Node_MacroInstance),
// not an enum node. We handle it explicitly below via CallFunction on
// KismetArrayLibrary. The alias here is intentionally removed to prevent
// routing to the enum-only variant.
{TEXT("ForEachElementInEnum"), TEXT("K2Node_ForEachElementInEnum")},
{TEXT("WhileLoop"), TEXT("K2Node_WhileLoop")},
// Data
{TEXT("MakeArray"), TEXT("K2Node_MakeArray")},
Expand Down Expand Up @@ -724,23 +728,136 @@ bool UMcpAutomationBridgeSubsystem::HandleBlueprintGraphAction(
return true;
}

if (NodeType == TEXT("Cast") || NodeType.StartsWith(TEXT("CastTo"))) {
if (NodeType == TEXT("Cast") || NodeType == TEXT("K2Node_DynamicCast") ||
NodeType.StartsWith(TEXT("CastTo"))) {
FString TargetClassName;
Payload->TryGetStringField(TEXT("targetClass"), TargetClassName);
if (TargetClassName.IsEmpty() && NodeType.StartsWith(TEXT("CastTo")))
TargetClassName = NodeType.Mid(6);

// 1. Try native C++ class lookup first
UClass *TargetClass = ResolveUClass(TargetClassName);

// 2. If that failed, try loading it as a Blueprint asset and using its
// GeneratedClass. This is required for Blueprint targets like
// "BP_CharacterBase" which resolve to "BP_CharacterBase_C".
if (!TargetClass) {
// Try common path prefixes if no slash is present
TArray<FString> PathsToTry;
if (TargetClassName.Contains(TEXT("/"))) {
PathsToTry.Add(TargetClassName);
} else {
PathsToTry.Add(FString::Printf(TEXT("/Game/Blueprints/%s"), *TargetClassName));
PathsToTry.Add(FString::Printf(TEXT("/Game/%s"), *TargetClassName));
PathsToTry.Add(FString::Printf(TEXT("/Game/Blueprints/Characters/%s"), *TargetClassName));
PathsToTry.Add(FString::Printf(TEXT("/Game/Blueprints/Combat/%s"), *TargetClassName));
}
for (const FString &TryPath : PathsToTry) {
FString NormPath, LoadErr;
UBlueprint *CastBP = LoadBlueprintAsset(TryPath, NormPath, LoadErr);
if (CastBP && CastBP->GeneratedClass) {
TargetClass = CastBP->GeneratedClass;
break;
}
}
}

// 3. Last resort: search loaded objects for a GeneratedClass matching name
if (!TargetClass) {
FString SearchName = TargetClassName + TEXT("_C");
for (TObjectIterator<UClass> It; It; ++It) {
if (It->GetName().Equals(SearchName, ESearchCase::IgnoreCase) ||
It->GetName().Equals(TargetClassName, ESearchCase::IgnoreCase)) {
TargetClass = *It;
break;
}
}
}

if (!TargetClass) {
SendAutomationError(
RequestingSocket, RequestId,
FString::Printf(TEXT("Class '%s' not found"), *TargetClassName),
FString::Printf(TEXT("Class '%s' not found. For Blueprint classes, "
"provide the full asset path e.g. '/Game/Blueprints/BP_CharacterBase'."),
*TargetClassName),
TEXT("CLASS_NOT_FOUND"));
return true;
}

// Set TargetType BEFORE calling CreateNode(false) so that
// AllocateDefaultPins (called inside Finalize) generates the typed
// "As <ClassName>" output pin. Then ReconstructNode to flush pin state.
FGraphNodeCreator<UK2Node_DynamicCast> NodeCreator(*TargetGraph);
UK2Node_DynamicCast *CastNode = NodeCreator.CreateNode(false);
CastNode->TargetType = TargetClass;
FinalizeAndReport(NodeCreator, CastNode);
if (CastNode) {
CastNode->ReconstructNode();
SaveLoadedAssetThrottled(Blueprint);
}
return true;
}

if (NodeType == TEXT("CallArrayFunction") ||
NodeType == TEXT("K2Node_CallArrayFunction")) {
// Array function nodes require special handling: they must be created via
// FGraphNodeCreator<UK2Node_CallFunction> (not the dynamic NewObject path)
// and ReconstructNode must be called after finalization so the wildcard
// array pins resolve to the correct typed pins once a TargetArray is wired.
FString MemberName, MemberClass;
Payload->TryGetStringField(TEXT("memberName"), MemberName);
Payload->TryGetStringField(TEXT("memberClass"), MemberClass);

// Default to KismetArrayLibrary if no class specified
if (MemberClass.IsEmpty()) {
MemberClass = TEXT("KismetArrayLibrary");
}

UClass *ArrayLibClass = ResolveUClass(MemberClass);
if (!ArrayLibClass) {
// Try UKismetArrayLibrary directly
for (TObjectIterator<UClass> It; It; ++It) {
if (It->GetName().Equals(TEXT("KismetArrayLibrary"), ESearchCase::IgnoreCase) ||
It->GetName().Equals(TEXT("UKismetArrayLibrary"), ESearchCase::IgnoreCase)) {
ArrayLibClass = *It;
break;
}
}
}

UFunction *ArrayFunc = nullptr;
if (ArrayLibClass) {
ArrayFunc = ArrayLibClass->FindFunctionByName(*MemberName);
}

if (!ArrayFunc) {
SendAutomationError(
RequestingSocket, RequestId,
FString::Printf(TEXT("Array function '%s' not found in '%s'"),
*MemberName, *MemberClass),
TEXT("FUNCTION_NOT_FOUND"));
return true;
}

FGraphNodeCreator<UK2Node_CallFunction> NodeCreator(*TargetGraph);
UK2Node_CallFunction *CallFuncNode = NodeCreator.CreateNode(false);
CallFuncNode->SetFromFunction(ArrayFunc);
// Finalize allocates default pins (wildcard at this point)
NodeCreator.Finalize();
CallFuncNode->NodePosX = X;
CallFuncNode->NodePosY = Y;
Comment on lines +846 to +848
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Position set after Finalize() is inconsistent with the established pattern.

The FinalizeAndReport lambda (lines 335-342) explicitly sets position before Finalize() with the comment "Set position BEFORE finalization per FGraphNodeCreator pattern". However, this handler sets position after finalization.

While this may work functionally for UK2Node_CallFunction, it deviates from the documented pattern and could cause subtle issues with node positioning in the graph editor.

🔧 Proposed fix to match established pattern
       FGraphNodeCreator<UK2Node_CallFunction> NodeCreator(*TargetGraph);
       UK2Node_CallFunction *CallFuncNode = NodeCreator.CreateNode(false);
       CallFuncNode->SetFromFunction(ArrayFunc);
-      // Finalize allocates default pins (wildcard at this point)
-      NodeCreator.Finalize();
+      // Set position BEFORE finalization per FGraphNodeCreator pattern
       CallFuncNode->NodePosX = X;
       CallFuncNode->NodePosY = Y;
+      // Finalize allocates default pins (wildcard at this point)
+      NodeCreator.Finalize();
       // ReconstructNode forces pin re-evaluation which is required for array
       // function nodes so that TargetArray and ReturnValue pins appear correctly.
       CallFuncNode->ReconstructNode();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
NodeCreator.Finalize();
CallFuncNode->NodePosX = X;
CallFuncNode->NodePosY = Y;
FGraphNodeCreator<UK2Node_CallFunction> NodeCreator(*TargetGraph);
UK2Node_CallFunction *CallFuncNode = NodeCreator.CreateNode(false);
CallFuncNode->SetFromFunction(ArrayFunc);
// Set position BEFORE finalization per FGraphNodeCreator pattern
CallFuncNode->NodePosX = X;
CallFuncNode->NodePosY = Y;
// Finalize allocates default pins (wildcard at this point)
NodeCreator.Finalize();
// ReconstructNode forces pin re-evaluation which is required for array
// function nodes so that TargetArray and ReturnValue pins appear correctly.
CallFuncNode->ReconstructNode();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp`
around lines 846 - 848, The node position is being assigned after
NodeCreator.Finalize() (CallFuncNode->NodePosX and CallFuncNode->NodePosY),
which violates the established FGraphNodeCreator pattern; move the position
assignments to before calling NodeCreator.Finalize() so the node's
NodePosX/NodePosY are set prior to finalization (follow the same approach as the
FinalizeAndReport lambda that sets position before Finalize()).

// ReconstructNode forces pin re-evaluation which is required for array
// function nodes so that TargetArray and ReturnValue pins appear correctly.
CallFuncNode->ReconstructNode();
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
SaveLoadedAssetThrottled(Blueprint);

TSharedPtr<FJsonObject> Result = McpHandlerUtils::CreateResultObject();
Result->SetStringField(TEXT("nodeId"), CallFuncNode->NodeGuid.ToString());
Result->SetStringField(TEXT("nodeName"), CallFuncNode->GetName());
McpHandlerUtils::AddVerification(Result, Blueprint);
SendAutomationResponse(RequestingSocket, RequestId, true,
TEXT("Array function node created."), Result);
return true;
}

Expand Down Expand Up @@ -844,19 +961,79 @@ bool UMcpAutomationBridgeSubsystem::HandleBlueprintGraphAction(
FromNode->Modify();
ToNode->Modify();

if (TargetGraph->GetSchema()->TryCreateConnection(FromPin, ToPin)) {
const UEdGraphSchema *Schema = TargetGraph->GetSchema();

// Attempt 1: Connect as-is (correct direction expected by caller)
bool bConnected = Schema->TryCreateConnection(FromPin, ToPin);

// Attempt 2: Try reversed direction. This handles cases where the caller
// accidentally specifies output→input in the wrong order, and also fixes
// the common case where an Input pin is passed as FromPin.
if (!bConnected) {
bConnected = Schema->TryCreateConnection(ToPin, FromPin);
}

// Attempt 3: For object-typed pins connecting to a self/target pin,
// the Kismet schema may reject due to an exact type mismatch even when
// UE5 would accept the connection (e.g. SCS component ref → function self).
// Try BreakAllPinLinks on the target self pin first to clear stale state,
// then reattempt. Also try ReconstructNode on both nodes to force pin
// type refresh before the final attempt.
if (!bConnected) {
const bool bFromIsObject = (FromPin->PinType.PinCategory == TEXT("object") ||
FromPin->PinType.PinCategory == TEXT("Object"));
const bool bToIsObject = (ToPin->PinType.PinCategory == TEXT("object") ||
ToPin->PinType.PinCategory == TEXT("Object"));

if (bFromIsObject || bToIsObject) {
// Reconstruct both nodes to refresh their pin type information
FromNode->ReconstructNode();
ToNode->ReconstructNode();

// Re-fetch pins after reconstruction (pointers may have changed)
FromPin = FromNode->FindPin(*FromPinClean);
ToPin = ToNode->FindPin(*ToPinClean);

if (FromPin && ToPin) {
bConnected = Schema->TryCreateConnection(FromPin, ToPin);
if (!bConnected) {
bConnected = Schema->TryCreateConnection(ToPin, FromPin);
}
}
}
}

if (bConnected) {
// After a successful connection, reconstruct both nodes so that
// wildcard/typed pins (e.g. array function nodes) update their type
// information based on the newly connected pin type.
FromNode->ReconstructNode();
ToNode->ReconstructNode();

FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);

// CRITICAL: Save the blueprint to persist changes.
SaveLoadedAssetThrottled(Blueprint);

TSharedPtr<FJsonObject> Result = McpHandlerUtils::CreateResultObject();
McpHandlerUtils::AddVerification(Result, Blueprint);
SendAutomationResponse(RequestingSocket, RequestId, true,
TEXT("Pins connected."), Result);
} else {
// Provide a diagnostic message that includes pin type information
// to help the caller understand why the connection was rejected.
FString FromTypeStr = FromPin ? FromPin->PinType.PinCategory.ToString() : TEXT("?");
FString ToTypeStr = ToPin ? ToPin->PinType.PinCategory.ToString() : TEXT("?");
if (FromPin && FromPin->PinType.PinSubCategoryObject.IsValid()) {
FromTypeStr += TEXT(" (") + FromPin->PinType.PinSubCategoryObject->GetName() + TEXT(")");
}
if (ToPin && ToPin->PinType.PinSubCategoryObject.IsValid()) {
ToTypeStr += TEXT(" (") + ToPin->PinType.PinSubCategoryObject->GetName() + TEXT(")");
}
SendAutomationError(RequestingSocket, RequestId,
TEXT("Failed to connect pins (schema rejection)."),
FString::Printf(TEXT("Failed to connect pins (schema rejection). "
"FromPin type: %s [%s], ToPin type: %s [%s]. "
"Ensure pin directions and types are compatible."),
*FromPinClean, *FromTypeStr,
*ToPinClean, *ToTypeStr),
TEXT("CONNECTION_FAILED"));
}
return true;
Expand Down Expand Up @@ -1207,6 +1384,14 @@ bool UMcpAutomationBridgeSubsystem::HandleBlueprintGraphAction(
PinObj->SetStringField(TEXT("pinType"),
Pin->PinType.PinCategory.ToString());

// Always report the sub-category object name for object/class/struct pins.
// This is critical for cast nodes whose typed output pin ("As ClassName")
// must expose the target class so callers can wire it correctly.
if (Pin->PinType.PinSubCategoryObject.IsValid()) {
PinObj->SetStringField(TEXT("pinSubType"),
Pin->PinType.PinSubCategoryObject->GetName());
}

if (Pin->LinkedTo.Num() > 0) {
TArray<TSharedPtr<FJsonValue>> LinkedArray;
for (UEdGraphPin *LinkedPin : Pin->LinkedTo) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2742,10 +2742,45 @@ bool UMcpAutomationBridgeSubsystem::HandleBlueprintAction(
PinType.PinSubCategoryObject = TBaseStructure<FTransform>::Get();
} else if (LowerType == TEXT("object")) {
PinType.PinCategory = MCP_PC_Object;
PinType.PinSubCategoryObject = UObject::StaticClass();
// Read typed subclass from variablePinType JSON if provided.
// Supports: variablePinType={"PinSubCategoryObject":"AoStatGeneratorComponent"}
// or variablePinType={"objectClass":"AoStatGeneratorComponent"}
{
const TSharedPtr<FJsonObject> *PinTypeObj = nullptr;
if (LocalPayload->TryGetObjectField(TEXT("variablePinType"), PinTypeObj) && PinTypeObj) {
FString SubClassName;
if (!(*PinTypeObj)->TryGetStringField(TEXT("PinSubCategoryObject"), SubClassName) || SubClassName.IsEmpty()) {
(*PinTypeObj)->TryGetStringField(TEXT("objectClass"), SubClassName);
}
if (!SubClassName.IsEmpty()) {
if (UClass *SubClass = ResolveUClass(SubClassName)) {
PinType.PinSubCategoryObject = SubClass;
}
}
}
if (!PinType.PinSubCategoryObject.IsValid()) {
PinType.PinSubCategoryObject = UObject::StaticClass();
}
Comment on lines +2755 to +2763
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify current subtype resolution + fallback behavior in add_variable branch
rg -n -C6 'variablePinType|PinSubCategoryObject|objectClass|ResolveUClass|UObject::StaticClass' \
  plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp

# Verify ResolveUClass capabilities/limits used by this handler
rg -n -C10 'static inline UClass \*ResolveUClass|FindObject<UClass>|LoadObject<UClass>|TObjectIterator<UClass>' \
  plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h

Repository: ChiR24/Unreal_mcp

Length of output: 8217


Fail fast when an explicit variablePinType subtype cannot be resolved

When caller supplies a variablePinType with a PinSubCategoryObject or objectClass field (lines 2755–2758 and 2774–2777), the code attempts to resolve it via ResolveUClass(). On resolution failure, both the "object" and "class" type handlers silently default to UObject::StaticClass() rather than failing. This is inconsistent with the main type resolution path (lines 2790–2793), which sends a SendAutomationError() when the primary type cannot be resolved. An explicit but unresolvable subtype should not silently succeed with an incorrect, overly broad type; instead, return an error to the caller.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp`
around lines 2755 - 2763, The code silently falls back to UObject::StaticClass()
when ResolveUClass() fails for an explicitly supplied subtype (variablePinType's
PinSubCategoryObject or objectClass); change this so that if
ResolveUClass(SubClassName) or ResolveUClass(objectClass) returns null AND the
caller explicitly provided that subtype, call SendAutomationError(...) and
return failure instead of assigning UObject::StaticClass(); ensure both the
PinSubCategoryObject branch and the objectClass branch use the same fail-fast
behavior and reference variablePinType/PinType when deciding if the subtype was
explicitly provided.

}
} else if (LowerType == TEXT("class")) {
PinType.PinCategory = MCP_PC_Class;
PinType.PinSubCategoryObject = UObject::StaticClass();
{
const TSharedPtr<FJsonObject> *PinTypeObj = nullptr;
if (LocalPayload->TryGetObjectField(TEXT("variablePinType"), PinTypeObj) && PinTypeObj) {
FString SubClassName;
if (!(*PinTypeObj)->TryGetStringField(TEXT("PinSubCategoryObject"), SubClassName) || SubClassName.IsEmpty()) {
(*PinTypeObj)->TryGetStringField(TEXT("objectClass"), SubClassName);
}
if (!SubClassName.IsEmpty()) {
if (UClass *SubClass = ResolveUClass(SubClassName)) {
PinType.PinSubCategoryObject = SubClass;
}
}
}
if (!PinType.PinSubCategoryObject.IsValid()) {
PinType.PinSubCategoryObject = UObject::StaticClass();
}
}
} else if (!VarType.TrimStartAndEnd().IsEmpty()) {
PinType.PinCategory = MCP_PC_Object;
UClass *FoundClass = ResolveUClass(VarType);
Expand Down
Loading
Loading