unreal-engine-mcp-server 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish-mcp.yml +1 -4
- package/.github/workflows/release-drafter.yml +2 -1
- package/CHANGELOG.md +38 -0
- package/dist/automation/bridge.d.ts +1 -2
- package/dist/automation/bridge.js +24 -23
- package/dist/automation/connection-manager.d.ts +1 -0
- package/dist/automation/connection-manager.js +10 -0
- package/dist/automation/message-handler.js +5 -4
- package/dist/automation/request-tracker.d.ts +4 -0
- package/dist/automation/request-tracker.js +11 -3
- package/dist/tools/actors.d.ts +19 -1
- package/dist/tools/actors.js +15 -5
- package/dist/tools/assets.js +1 -1
- package/dist/tools/blueprint.d.ts +12 -0
- package/dist/tools/blueprint.js +43 -14
- package/dist/tools/consolidated-tool-definitions.js +2 -1
- package/dist/tools/editor.js +3 -2
- package/dist/tools/handlers/actor-handlers.d.ts +1 -1
- package/dist/tools/handlers/actor-handlers.js +14 -8
- package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
- package/dist/tools/handlers/sequence-handlers.js +24 -13
- package/dist/tools/introspection.d.ts +1 -1
- package/dist/tools/introspection.js +1 -1
- package/dist/tools/level.js +3 -3
- package/dist/tools/lighting.d.ts +54 -7
- package/dist/tools/lighting.js +4 -4
- package/dist/tools/materials.d.ts +1 -1
- package/dist/types/tool-types.d.ts +2 -0
- package/dist/unreal-bridge.js +4 -4
- package/dist/utils/command-validator.js +6 -5
- package/dist/utils/error-handler.d.ts +24 -2
- package/dist/utils/error-handler.js +58 -23
- package/dist/utils/normalize.d.ts +7 -4
- package/dist/utils/normalize.js +12 -10
- package/dist/utils/response-validator.js +88 -73
- package/dist/utils/unreal-command-queue.d.ts +2 -0
- package/dist/utils/unreal-command-queue.js +8 -1
- package/docs/handler-mapping.md +4 -2
- package/package.json +1 -1
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +298 -33
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +7 -8
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +229 -319
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +98 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +24 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +96 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +52 -5
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +5 -268
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +57 -2
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +0 -1
- package/server.json +3 -3
- package/src/automation/bridge.ts +27 -25
- package/src/automation/connection-manager.ts +18 -0
- package/src/automation/message-handler.ts +33 -8
- package/src/automation/request-tracker.ts +39 -7
- package/src/server/tool-registry.ts +3 -3
- package/src/tools/actors.ts +44 -19
- package/src/tools/assets.ts +3 -3
- package/src/tools/blueprint.ts +115 -49
- package/src/tools/consolidated-tool-definitions.ts +2 -1
- package/src/tools/editor.ts +4 -3
- package/src/tools/handlers/actor-handlers.ts +14 -9
- package/src/tools/handlers/sequence-handlers.ts +86 -63
- package/src/tools/introspection.ts +7 -7
- package/src/tools/level.ts +6 -6
- package/src/tools/lighting.ts +19 -19
- package/src/tools/materials.ts +1 -1
- package/src/tools/sequence.ts +1 -1
- package/src/tools/ui.ts +1 -1
- package/src/types/tool-types.ts +4 -0
- package/src/unreal-bridge.ts +71 -26
- package/src/utils/command-validator.ts +46 -5
- package/src/utils/error-handler.ts +128 -45
- package/src/utils/normalize.ts +38 -16
- package/src/utils/response-validator.ts +103 -87
- package/src/utils/unreal-command-queue.ts +13 -1
|
@@ -40,19 +40,27 @@
|
|
|
40
40
|
#endif
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* Process a "manage_blueprint_graph" automation request to inspect or modify a
|
|
43
|
+
* Process a "manage_blueprint_graph" automation request to inspect or modify a
|
|
44
|
+
* Blueprint graph.
|
|
44
45
|
*
|
|
45
|
-
* The Payload JSON controls the specific operation via the "subAction" field
|
|
46
|
-
* connect_pins, get_nodes, break_pin_links,
|
|
47
|
-
*
|
|
48
|
-
*
|
|
46
|
+
* The Payload JSON controls the specific operation via the "subAction" field
|
|
47
|
+
* (examples: create_node, connect_pins, get_nodes, break_pin_links,
|
|
48
|
+
* delete_node, create_reroute_node, set_node_property, get_node_details,
|
|
49
|
+
* get_graph_details, get_pin_details). In editor builds this function performs
|
|
50
|
+
* graph/blueprint lookups and edits; in non-editor builds it reports an
|
|
51
|
+
* editor-only error.
|
|
49
52
|
*
|
|
50
|
-
* @param RequestId Unique identifier for the automation request (used in
|
|
51
|
-
*
|
|
52
|
-
* @param
|
|
53
|
-
*
|
|
54
|
-
* @param
|
|
55
|
-
*
|
|
53
|
+
* @param RequestId Unique identifier for the automation request (used in
|
|
54
|
+
* responses).
|
|
55
|
+
* @param Action The requested action name; this handler only processes
|
|
56
|
+
* "manage_blueprint_graph".
|
|
57
|
+
* @param Payload JSON object containing action options such as
|
|
58
|
+
* "assetPath"/"blueprintPath", "graphName", "subAction" and subaction-specific
|
|
59
|
+
* fields (nodeType, nodeId, pin names, positions, etc.).
|
|
60
|
+
* @param RequestingSocket WebSocket used to send responses and errors back to
|
|
61
|
+
* the requester.
|
|
62
|
+
* @return `true` if the request was handled by this function (Action ==
|
|
63
|
+
* "manage_blueprint_graph"), `false` otherwise.
|
|
56
64
|
*/
|
|
57
65
|
bool UMcpAutomationBridgeSubsystem::HandleBlueprintGraphAction(
|
|
58
66
|
const FString &RequestId, const FString &Action,
|
|
@@ -339,76 +347,39 @@ bool UMcpAutomationBridgeSubsystem::HandleBlueprintGraphAction(
|
|
|
339
347
|
return true;
|
|
340
348
|
}
|
|
341
349
|
|
|
342
|
-
//
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
Payload->TryGetStringField(TEXT("inputAxisName"), InputAxisName);
|
|
350
|
+
// ========================================================================
|
|
351
|
+
// DYNAMIC NODE CREATION - Find node classes at runtime
|
|
352
|
+
// ========================================================================
|
|
346
353
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
FGraphNodeCreator<UK2Node_InputAxisEvent> NodeCreator(*TargetGraph);
|
|
355
|
-
UK2Node_InputAxisEvent *InputNode = NodeCreator.CreateNode(false);
|
|
356
|
-
InputNode->InputAxisName = FName(*InputAxisName);
|
|
354
|
+
// Helper: Try to find a UK2Node subclass by name
|
|
355
|
+
auto FindNodeClassByName = [](const FString &TypeName) -> UClass * {
|
|
356
|
+
TArray<FString> NamesToTry;
|
|
357
|
+
NamesToTry.Add(TypeName);
|
|
358
|
+
NamesToTry.Add(FString::Printf(TEXT("K2Node_%s"), *TypeName));
|
|
359
|
+
NamesToTry.Add(FString::Printf(TEXT("UK2Node_%s"), *TypeName));
|
|
357
360
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
Payload->TryGetStringField(TEXT("memberName"), MemberName);
|
|
364
|
-
FString MemberClass;
|
|
365
|
-
Payload->TryGetStringField(TEXT("memberClass"),
|
|
366
|
-
MemberClass); // Optional, for static functions
|
|
361
|
+
for (TObjectIterator<UClass> It; It; ++It) {
|
|
362
|
+
if (!It->IsChildOf(UEdGraphNode::StaticClass()))
|
|
363
|
+
continue;
|
|
364
|
+
if (It->HasAnyClassFlags(CLASS_Abstract))
|
|
365
|
+
continue;
|
|
367
366
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
Func = Class->FindFunctionByName(*MemberName);
|
|
373
|
-
}
|
|
374
|
-
} else {
|
|
375
|
-
// Try to find in blueprint context
|
|
376
|
-
Func = Blueprint->GeneratedClass->FindFunctionByName(*MemberName);
|
|
377
|
-
if (!Func) {
|
|
378
|
-
// Try global search if simple name, or check common libraries
|
|
379
|
-
Func = FindObject<UFunction>(nullptr, *MemberName);
|
|
380
|
-
if (!Func) {
|
|
381
|
-
// Fallback: Check common libraries
|
|
382
|
-
if (UClass *KSL = UKismetSystemLibrary::StaticClass())
|
|
383
|
-
Func = KSL->FindFunctionByName(*MemberName);
|
|
384
|
-
if (!Func)
|
|
385
|
-
if (UClass *GPS = UGameplayStatics::StaticClass())
|
|
386
|
-
Func = GPS->FindFunctionByName(*MemberName);
|
|
387
|
-
if (!Func)
|
|
388
|
-
if (UClass *KML = UKismetMathLibrary::StaticClass())
|
|
389
|
-
Func = KML->FindFunctionByName(*MemberName);
|
|
367
|
+
FString ClassName = It->GetName();
|
|
368
|
+
for (const FString &NameToMatch : NamesToTry) {
|
|
369
|
+
if (ClassName.Equals(NameToMatch, ESearchCase::IgnoreCase)) {
|
|
370
|
+
return *It;
|
|
390
371
|
}
|
|
391
372
|
}
|
|
392
373
|
}
|
|
374
|
+
return nullptr;
|
|
375
|
+
};
|
|
393
376
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
CallFuncNode->SetFromFunction(Func);
|
|
398
|
-
FinalizeAndReport(NodeCreator, CallFuncNode);
|
|
399
|
-
} else {
|
|
400
|
-
SendAutomationError(
|
|
401
|
-
RequestingSocket, RequestId,
|
|
402
|
-
FString::Printf(TEXT("Could not find function '%s'"), *MemberName),
|
|
403
|
-
TEXT("FUNCTION_NOT_FOUND"));
|
|
404
|
-
return true;
|
|
405
|
-
}
|
|
406
|
-
} else if (NodeType == TEXT("VariableGet")) {
|
|
377
|
+
// Special nodes requiring extra parameters
|
|
378
|
+
if (NodeType == TEXT("VariableGet") ||
|
|
379
|
+
NodeType == TEXT("K2Node_VariableGet")) {
|
|
407
380
|
FString VarName;
|
|
408
381
|
Payload->TryGetStringField(TEXT("variableName"), VarName);
|
|
409
382
|
FName VarFName(*VarName);
|
|
410
|
-
|
|
411
|
-
// Validation BEFORE creation
|
|
412
383
|
bool bFound = false;
|
|
413
384
|
for (const FBPVariableDescription &VarDesc : Blueprint->NewVariables) {
|
|
414
385
|
if (VarDesc.VarName == VarFName) {
|
|
@@ -420,25 +391,25 @@ bool UMcpAutomationBridgeSubsystem::HandleBlueprintGraphAction(
|
|
|
420
391
|
Blueprint->GeneratedClass->FindPropertyByName(VarFName)) {
|
|
421
392
|
bFound = true;
|
|
422
393
|
}
|
|
423
|
-
|
|
424
394
|
if (!bFound) {
|
|
425
395
|
SendAutomationError(
|
|
426
396
|
RequestingSocket, RequestId,
|
|
427
|
-
FString::Printf(TEXT("
|
|
397
|
+
FString::Printf(TEXT("Variable '%s' not found"), *VarName),
|
|
428
398
|
TEXT("VARIABLE_NOT_FOUND"));
|
|
429
399
|
return true;
|
|
430
400
|
}
|
|
431
|
-
|
|
432
401
|
FGraphNodeCreator<UK2Node_VariableGet> NodeCreator(*TargetGraph);
|
|
433
402
|
UK2Node_VariableGet *VarGet = NodeCreator.CreateNode(false);
|
|
434
403
|
VarGet->VariableReference.SetSelfMember(VarFName);
|
|
435
404
|
FinalizeAndReport(NodeCreator, VarGet);
|
|
436
|
-
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (NodeType == TEXT("VariableSet") ||
|
|
409
|
+
NodeType == TEXT("K2Node_VariableSet")) {
|
|
437
410
|
FString VarName;
|
|
438
411
|
Payload->TryGetStringField(TEXT("variableName"), VarName);
|
|
439
412
|
FName VarFName(*VarName);
|
|
440
|
-
|
|
441
|
-
// Validation BEFORE creation
|
|
442
413
|
bool bFound = false;
|
|
443
414
|
for (const FBPVariableDescription &VarDesc : Blueprint->NewVariables) {
|
|
444
415
|
if (VarDesc.VarName == VarFName) {
|
|
@@ -450,97 +421,89 @@ bool UMcpAutomationBridgeSubsystem::HandleBlueprintGraphAction(
|
|
|
450
421
|
Blueprint->GeneratedClass->FindPropertyByName(VarFName)) {
|
|
451
422
|
bFound = true;
|
|
452
423
|
}
|
|
453
|
-
|
|
454
424
|
if (!bFound) {
|
|
455
425
|
SendAutomationError(
|
|
456
426
|
RequestingSocket, RequestId,
|
|
457
|
-
FString::Printf(TEXT("
|
|
427
|
+
FString::Printf(TEXT("Variable '%s' not found"), *VarName),
|
|
458
428
|
TEXT("VARIABLE_NOT_FOUND"));
|
|
459
429
|
return true;
|
|
460
430
|
}
|
|
461
|
-
|
|
462
431
|
FGraphNodeCreator<UK2Node_VariableSet> NodeCreator(*TargetGraph);
|
|
463
432
|
UK2Node_VariableSet *VarSet = NodeCreator.CreateNode(false);
|
|
464
433
|
VarSet->VariableReference.SetSelfMember(VarFName);
|
|
465
434
|
FinalizeAndReport(NodeCreator, VarSet);
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
Payload->TryGetStringField(TEXT("eventName"), EventName);
|
|
469
|
-
|
|
470
|
-
FGraphNodeCreator<UK2Node_CustomEvent> NodeCreator(*TargetGraph);
|
|
471
|
-
UK2Node_CustomEvent *EventNode = NodeCreator.CreateNode(false);
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
472
437
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
FString
|
|
477
|
-
Payload->TryGetStringField(
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
438
|
+
if (NodeType == TEXT("CallFunction") ||
|
|
439
|
+
NodeType == TEXT("K2Node_CallFunction") ||
|
|
440
|
+
NodeType == TEXT("FunctionCall")) {
|
|
441
|
+
FString MemberName, MemberClass;
|
|
442
|
+
Payload->TryGetStringField(TEXT("memberName"), MemberName);
|
|
443
|
+
Payload->TryGetStringField(TEXT("memberClass"), MemberClass);
|
|
444
|
+
UFunction *Func = nullptr;
|
|
445
|
+
if (!MemberClass.IsEmpty()) {
|
|
446
|
+
if (UClass *Class = ResolveUClass(MemberClass))
|
|
447
|
+
Func = Class->FindFunctionByName(*MemberName);
|
|
448
|
+
} else {
|
|
449
|
+
Func = Blueprint->GeneratedClass->FindFunctionByName(*MemberName);
|
|
450
|
+
if (!Func) {
|
|
451
|
+
if (UClass *KSL = UKismetSystemLibrary::StaticClass())
|
|
452
|
+
Func = KSL->FindFunctionByName(*MemberName);
|
|
453
|
+
if (!Func)
|
|
454
|
+
if (UClass *GPS = UGameplayStatics::StaticClass())
|
|
455
|
+
Func = GPS->FindFunctionByName(*MemberName);
|
|
456
|
+
if (!Func)
|
|
457
|
+
if (UClass *KML = UKismetMathLibrary::StaticClass())
|
|
458
|
+
Func = KML->FindFunctionByName(*MemberName);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (Func) {
|
|
462
|
+
FGraphNodeCreator<UK2Node_CallFunction> NodeCreator(*TargetGraph);
|
|
463
|
+
UK2Node_CallFunction *CallFuncNode = NodeCreator.CreateNode(false);
|
|
464
|
+
CallFuncNode->SetFromFunction(Func);
|
|
465
|
+
FinalizeAndReport(NodeCreator, CallFuncNode);
|
|
466
|
+
} else {
|
|
467
|
+
SendAutomationError(
|
|
468
|
+
RequestingSocket, RequestId,
|
|
469
|
+
FString::Printf(TEXT("Function '%s' not found"), *MemberName),
|
|
470
|
+
TEXT("FUNCTION_NOT_FOUND"));
|
|
471
|
+
}
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
483
474
|
|
|
475
|
+
if (NodeType == TEXT("Event") || NodeType == TEXT("K2Node_Event")) {
|
|
476
|
+
FString EventName, MemberClass;
|
|
477
|
+
Payload->TryGetStringField(TEXT("eventName"), EventName);
|
|
478
|
+
Payload->TryGetStringField(TEXT("memberClass"), MemberClass);
|
|
484
479
|
if (EventName.IsEmpty()) {
|
|
485
480
|
SendAutomationError(RequestingSocket, RequestId,
|
|
486
|
-
TEXT("eventName required
|
|
481
|
+
TEXT("eventName required"),
|
|
487
482
|
TEXT("INVALID_ARGUMENT"));
|
|
488
483
|
return true;
|
|
489
484
|
}
|
|
490
|
-
|
|
491
|
-
// Map common event name aliases to their actual function names
|
|
492
|
-
static TMap<FString, FString> EventNameAliases = {
|
|
485
|
+
static TMap<FString, FString> Aliases = {
|
|
493
486
|
{TEXT("BeginPlay"), TEXT("ReceiveBeginPlay")},
|
|
494
487
|
{TEXT("Tick"), TEXT("ReceiveTick")},
|
|
495
|
-
{TEXT("EndPlay"), TEXT("ReceiveEndPlay")}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
{TEXT("Hit"), TEXT("ReceiveHit")},
|
|
499
|
-
{TEXT("BeginCursorOver"), TEXT("ReceiveBeginCursorOver")},
|
|
500
|
-
{TEXT("EndCursorOver"), TEXT("ReceiveEndCursorOver")},
|
|
501
|
-
{TEXT("Clicked"), TEXT("ReceiveClicked")},
|
|
502
|
-
{TEXT("Released"), TEXT("ReceiveReleased")},
|
|
503
|
-
{TEXT("Destroyed"), TEXT("ReceiveDestroyed")},
|
|
504
|
-
};
|
|
505
|
-
|
|
506
|
-
if (const FString *Alias = EventNameAliases.Find(EventName)) {
|
|
507
|
-
EventName = *Alias;
|
|
508
|
-
}
|
|
488
|
+
{TEXT("EndPlay"), TEXT("ReceiveEndPlay")}};
|
|
489
|
+
if (const FString *A = Aliases.Find(EventName))
|
|
490
|
+
EventName = *A;
|
|
509
491
|
|
|
510
|
-
// Determine target class: use explicit MemberClass or search hierarchy
|
|
511
492
|
UClass *TargetClass = nullptr;
|
|
512
493
|
UFunction *EventFunc = nullptr;
|
|
513
|
-
|
|
514
494
|
if (!MemberClass.IsEmpty()) {
|
|
515
|
-
// Explicit class specified
|
|
516
495
|
TargetClass = ResolveUClass(MemberClass);
|
|
517
|
-
if (TargetClass)
|
|
496
|
+
if (TargetClass)
|
|
518
497
|
EventFunc = TargetClass->FindFunctionByName(*EventName);
|
|
519
|
-
}
|
|
520
498
|
} else {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
*EventName, EIncludeSuperFlag::ExcludeSuper);
|
|
528
|
-
if (EventFunc) {
|
|
529
|
-
TargetClass = SearchClass;
|
|
530
|
-
break;
|
|
531
|
-
}
|
|
532
|
-
SearchClass = SearchClass->GetSuperClass();
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// If not found in hierarchy, try the generated class
|
|
536
|
-
if (!EventFunc && Blueprint->GeneratedClass) {
|
|
537
|
-
EventFunc = Blueprint->GeneratedClass->FindFunctionByName(*EventName);
|
|
538
|
-
if (EventFunc) {
|
|
539
|
-
TargetClass = Blueprint->GeneratedClass;
|
|
540
|
-
}
|
|
499
|
+
for (UClass *C = Blueprint->ParentClass; C && !EventFunc;
|
|
500
|
+
C = C->GetSuperClass()) {
|
|
501
|
+
EventFunc = C->FindFunctionByName(*EventName,
|
|
502
|
+
EIncludeSuperFlag::ExcludeSuper);
|
|
503
|
+
if (EventFunc)
|
|
504
|
+
TargetClass = C;
|
|
541
505
|
}
|
|
542
506
|
}
|
|
543
|
-
|
|
544
507
|
if (EventFunc && TargetClass) {
|
|
545
508
|
FGraphNodeCreator<UK2Node_Event> NodeCreator(*TargetGraph);
|
|
546
509
|
UK2Node_Event *EventNode = NodeCreator.CreateNode(false);
|
|
@@ -548,214 +511,92 @@ bool UMcpAutomationBridgeSubsystem::HandleBlueprintGraphAction(
|
|
|
548
511
|
EventNode->bOverrideFunction = true;
|
|
549
512
|
FinalizeAndReport(NodeCreator, EventNode);
|
|
550
513
|
} else {
|
|
551
|
-
// Provide helpful error message
|
|
552
|
-
FString SearchedClasses;
|
|
553
|
-
UClass *C = Blueprint->ParentClass;
|
|
554
|
-
int ClassCount = 0;
|
|
555
|
-
while (C && ClassCount < 5) {
|
|
556
|
-
if (!SearchedClasses.IsEmpty())
|
|
557
|
-
SearchedClasses += TEXT(", ");
|
|
558
|
-
SearchedClasses += C->GetName();
|
|
559
|
-
C = C->GetSuperClass();
|
|
560
|
-
ClassCount++;
|
|
561
|
-
}
|
|
562
514
|
SendAutomationError(
|
|
563
515
|
RequestingSocket, RequestId,
|
|
564
|
-
FString::Printf(TEXT("
|
|
565
|
-
"%s. Try using the full name like "
|
|
566
|
-
"'ReceiveBeginPlay' instead of 'BeginPlay'."),
|
|
567
|
-
*EventName, *SearchedClasses),
|
|
516
|
+
FString::Printf(TEXT("Event '%s' not found"), *EventName),
|
|
568
517
|
TEXT("EVENT_NOT_FOUND"));
|
|
569
518
|
}
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
570
521
|
|
|
571
|
-
|
|
572
|
-
|
|
522
|
+
if (NodeType == TEXT("CustomEvent") ||
|
|
523
|
+
NodeType == TEXT("K2Node_CustomEvent")) {
|
|
524
|
+
FString EventName;
|
|
525
|
+
Payload->TryGetStringField(TEXT("eventName"), EventName);
|
|
526
|
+
FGraphNodeCreator<UK2Node_CustomEvent> NodeCreator(*TargetGraph);
|
|
527
|
+
UK2Node_CustomEvent *EventNode = NodeCreator.CreateNode(false);
|
|
528
|
+
EventNode->CustomFunctionName = FName(*EventName);
|
|
529
|
+
FinalizeAndReport(NodeCreator, EventNode);
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (NodeType == TEXT("Cast") || NodeType.StartsWith(TEXT("CastTo"))) {
|
|
573
534
|
FString TargetClassName;
|
|
574
535
|
Payload->TryGetStringField(TEXT("targetClass"), TargetClassName);
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
// "CastTo<ClassName>"
|
|
578
|
-
if (TargetClassName.IsEmpty() && NodeType.StartsWith(TEXT("CastTo"))) {
|
|
579
|
-
TargetClassName = NodeType.Mid(6); // Remove "CastTo" prefix
|
|
580
|
-
}
|
|
581
|
-
|
|
536
|
+
if (TargetClassName.IsEmpty() && NodeType.StartsWith(TEXT("CastTo")))
|
|
537
|
+
TargetClassName = NodeType.Mid(6);
|
|
582
538
|
UClass *TargetClass = ResolveUClass(TargetClassName);
|
|
583
539
|
if (!TargetClass) {
|
|
584
540
|
SendAutomationError(
|
|
585
541
|
RequestingSocket, RequestId,
|
|
586
|
-
FString::Printf(
|
|
587
|
-
TEXT("Could not resolve target class '%s' for Cast node"),
|
|
588
|
-
*TargetClassName),
|
|
542
|
+
FString::Printf(TEXT("Class '%s' not found"), *TargetClassName),
|
|
589
543
|
TEXT("CLASS_NOT_FOUND"));
|
|
590
544
|
return true;
|
|
591
545
|
}
|
|
592
|
-
|
|
593
546
|
FGraphNodeCreator<UK2Node_DynamicCast> NodeCreator(*TargetGraph);
|
|
594
547
|
UK2Node_DynamicCast *CastNode = NodeCreator.CreateNode(false);
|
|
595
548
|
CastNode->TargetType = TargetClass;
|
|
596
549
|
FinalizeAndReport(NodeCreator, CastNode);
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
UK2Node_ExecutionSequence *NewNode = NodeCreator.CreateNode(false);
|
|
600
|
-
FinalizeAndReport(NodeCreator, NewNode);
|
|
601
|
-
} else if (NodeType == TEXT("Branch") || NodeType == TEXT("IfThenElse") ||
|
|
602
|
-
NodeType == TEXT("K2Node_IfThenElse")) {
|
|
603
|
-
FGraphNodeCreator<UK2Node_IfThenElse> NodeCreator(*TargetGraph);
|
|
604
|
-
UK2Node_IfThenElse *BranchNode = NodeCreator.CreateNode(false);
|
|
605
|
-
FinalizeAndReport(NodeCreator, BranchNode);
|
|
606
|
-
} else if (NodeType == TEXT("Literal")) {
|
|
607
|
-
// Create a literal node that can hold an object reference. This is a
|
|
608
|
-
// fully functional K2 literal node that returns the referenced asset
|
|
609
|
-
// or object when executed in the graph.
|
|
610
|
-
FString LiteralType;
|
|
611
|
-
Payload->TryGetStringField(TEXT("literalType"), LiteralType);
|
|
612
|
-
LiteralType.TrimStartAndEndInline();
|
|
613
|
-
const FString LiteralTypeLower =
|
|
614
|
-
LiteralType.IsEmpty() ? TEXT("object") : LiteralType.ToLower();
|
|
615
|
-
|
|
616
|
-
if (LiteralTypeLower == TEXT("object") ||
|
|
617
|
-
LiteralTypeLower == TEXT("asset")) {
|
|
618
|
-
FString ObjectPath;
|
|
619
|
-
Payload->TryGetStringField(TEXT("objectPath"), ObjectPath);
|
|
620
|
-
if (ObjectPath.IsEmpty()) {
|
|
621
|
-
// As a convenience, allow callers to use assetPath as the
|
|
622
|
-
// literal source when objectPath is omitted.
|
|
623
|
-
Payload->TryGetStringField(TEXT("assetPath"), ObjectPath);
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
if (ObjectPath.IsEmpty()) {
|
|
627
|
-
SendAutomationError(RequestingSocket, RequestId,
|
|
628
|
-
TEXT("Literal object creation requires "
|
|
629
|
-
"'objectPath' or 'assetPath'."),
|
|
630
|
-
TEXT("INVALID_LITERAL"));
|
|
631
|
-
return true;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
UObject *LoadedObject = LoadObject<UObject>(nullptr, *ObjectPath);
|
|
635
|
-
if (!LoadedObject) {
|
|
636
|
-
SendAutomationError(
|
|
637
|
-
RequestingSocket, RequestId,
|
|
638
|
-
FString::Printf(TEXT("Literal object not found at path '%s'"),
|
|
639
|
-
*ObjectPath),
|
|
640
|
-
TEXT("OBJECT_NOT_FOUND"));
|
|
641
|
-
return true;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// Create the node only after successful validation
|
|
645
|
-
FGraphNodeCreator<UK2Node_Literal> NodeCreator(*TargetGraph);
|
|
646
|
-
UK2Node_Literal *LiteralNode = NodeCreator.CreateNode(false);
|
|
647
|
-
if (!LiteralNode) {
|
|
648
|
-
SendAutomationError(RequestingSocket, RequestId,
|
|
649
|
-
TEXT("Failed to allocate Literal node."),
|
|
650
|
-
TEXT("CREATE_FAILED"));
|
|
651
|
-
return true;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// UK2Node_Literal stores the referenced UObject in a private
|
|
655
|
-
// member; use its public setter rather than touching the
|
|
656
|
-
// field directly so we respect engine encapsulation.
|
|
657
|
-
LiteralNode->SetObjectRef(LoadedObject);
|
|
658
|
-
FinalizeAndReport(NodeCreator, LiteralNode);
|
|
659
|
-
} else {
|
|
660
|
-
// Primitive literal support (float/int/bool/strings) can be
|
|
661
|
-
// added later by wiring value pins. For now, fail fast rather
|
|
662
|
-
// than pretending success.
|
|
663
|
-
SendAutomationError(
|
|
664
|
-
RequestingSocket, RequestId,
|
|
665
|
-
FString::Printf(TEXT("Unsupported literalType '%s' (only "
|
|
666
|
-
"'object'/'asset' supported)."),
|
|
667
|
-
*LiteralType),
|
|
668
|
-
TEXT("UNSUPPORTED_LITERAL_TYPE"));
|
|
669
|
-
return true;
|
|
670
|
-
}
|
|
671
|
-
} else if (NodeType == TEXT("Comment")) {
|
|
672
|
-
FGraphNodeCreator<UEdGraphNode_Comment> NodeCreator(*TargetGraph);
|
|
673
|
-
UEdGraphNode_Comment *CommentNode = NodeCreator.CreateNode(false);
|
|
674
|
-
|
|
675
|
-
FString CommentText;
|
|
676
|
-
if (Payload->TryGetStringField(TEXT("comment"), CommentText) &&
|
|
677
|
-
!CommentText.IsEmpty()) {
|
|
678
|
-
CommentNode->NodeComment = CommentText;
|
|
679
|
-
} else {
|
|
680
|
-
CommentNode->NodeComment = TEXT("Comment");
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
CommentNode->NodeWidth = 400;
|
|
684
|
-
CommentNode->NodeHeight = 100;
|
|
685
|
-
|
|
686
|
-
FinalizeAndReport(NodeCreator, CommentNode);
|
|
687
|
-
} else if (NodeType == TEXT("MakeArray")) {
|
|
688
|
-
FGraphNodeCreator<UK2Node_MakeArray> NodeCreator(*TargetGraph);
|
|
689
|
-
UK2Node_MakeArray *MakeArrayNode = NodeCreator.CreateNode(false);
|
|
690
|
-
FinalizeAndReport(NodeCreator, MakeArrayNode);
|
|
691
|
-
} else if (NodeType == TEXT("Return")) {
|
|
692
|
-
FGraphNodeCreator<UK2Node_FunctionResult> NodeCreator(*TargetGraph);
|
|
693
|
-
UK2Node_FunctionResult *ReturnNode = NodeCreator.CreateNode(false);
|
|
694
|
-
FinalizeAndReport(NodeCreator, ReturnNode);
|
|
695
|
-
} else if (NodeType == TEXT("Self")) {
|
|
696
|
-
FGraphNodeCreator<UK2Node_Self> NodeCreator(*TargetGraph);
|
|
697
|
-
UK2Node_Self *SelfNode = NodeCreator.CreateNode(false);
|
|
698
|
-
FinalizeAndReport(NodeCreator, SelfNode);
|
|
699
|
-
} else if (NodeType == TEXT("Select")) {
|
|
700
|
-
FGraphNodeCreator<UK2Node_Select> NodeCreator(*TargetGraph);
|
|
701
|
-
UK2Node_Select *SelectNode = NodeCreator.CreateNode(false);
|
|
702
|
-
FinalizeAndReport(NodeCreator, SelectNode);
|
|
703
|
-
} else if (NodeType == TEXT("Timeline")) {
|
|
704
|
-
FGraphNodeCreator<UK2Node_Timeline> NodeCreator(*TargetGraph);
|
|
705
|
-
UK2Node_Timeline *TimelineNode = NodeCreator.CreateNode(false);
|
|
706
|
-
|
|
707
|
-
FString TimelineName;
|
|
708
|
-
if (Payload->TryGetStringField(TEXT("timelineName"), TimelineName) &&
|
|
709
|
-
!TimelineName.IsEmpty()) {
|
|
710
|
-
TimelineNode->TimelineName = FName(*TimelineName);
|
|
711
|
-
}
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
712
552
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
FString
|
|
716
|
-
Payload->TryGetStringField(TEXT("
|
|
717
|
-
if (
|
|
553
|
+
if (NodeType == TEXT("InputAxisEvent") ||
|
|
554
|
+
NodeType == TEXT("K2Node_InputAxisEvent")) {
|
|
555
|
+
FString InputAxisName;
|
|
556
|
+
Payload->TryGetStringField(TEXT("inputAxisName"), InputAxisName);
|
|
557
|
+
if (InputAxisName.IsEmpty()) {
|
|
718
558
|
SendAutomationError(RequestingSocket, RequestId,
|
|
719
|
-
TEXT("
|
|
559
|
+
TEXT("inputAxisName required"),
|
|
720
560
|
TEXT("INVALID_ARGUMENT"));
|
|
721
561
|
return true;
|
|
722
562
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
563
|
+
FGraphNodeCreator<UK2Node_InputAxisEvent> NodeCreator(*TargetGraph);
|
|
564
|
+
UK2Node_InputAxisEvent *InputNode = NodeCreator.CreateNode(false);
|
|
565
|
+
InputNode->InputAxisName = FName(*InputAxisName);
|
|
566
|
+
FinalizeAndReport(NodeCreator, InputNode);
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
729
569
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
570
|
+
// ========== DYNAMIC FALLBACK: Create ANY node class by name ==========
|
|
571
|
+
UClass *NodeClass = FindNodeClassByName(NodeType);
|
|
572
|
+
if (NodeClass) {
|
|
573
|
+
UEdGraphNode *NewNode = NewObject<UEdGraphNode>(TargetGraph, NodeClass);
|
|
574
|
+
if (NewNode) {
|
|
575
|
+
TargetGraph->AddNode(NewNode, false, false);
|
|
576
|
+
NewNode->CreateNewGuid();
|
|
577
|
+
NewNode->PostPlacedNewNode();
|
|
578
|
+
NewNode->AllocateDefaultPins();
|
|
579
|
+
NewNode->NodePosX = X;
|
|
580
|
+
NewNode->NodePosY = Y;
|
|
581
|
+
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
|
|
582
|
+
TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
583
|
+
Result->SetStringField(TEXT("nodeId"), NewNode->NodeGuid.ToString());
|
|
584
|
+
Result->SetStringField(TEXT("nodeName"), NewNode->GetName());
|
|
585
|
+
Result->SetStringField(TEXT("nodeClass"), NodeClass->GetName());
|
|
586
|
+
SendAutomationResponse(RequestingSocket, RequestId, true,
|
|
587
|
+
TEXT("Node created."), Result);
|
|
588
|
+
} else {
|
|
745
589
|
SendAutomationError(RequestingSocket, RequestId,
|
|
746
|
-
TEXT("
|
|
747
|
-
|
|
590
|
+
TEXT("Failed to instantiate node."),
|
|
591
|
+
TEXT("CREATE_FAILED"));
|
|
748
592
|
}
|
|
749
|
-
|
|
750
|
-
FGraphNodeCreator<UK2Node_BreakStruct> NodeCreator(*TargetGraph);
|
|
751
|
-
UK2Node_BreakStruct *BreakStructNode = NodeCreator.CreateNode(false);
|
|
752
|
-
BreakStructNode->StructType = Struct;
|
|
753
|
-
FinalizeAndReport(NodeCreator, BreakStructNode);
|
|
754
593
|
} else {
|
|
755
594
|
SendAutomationError(
|
|
756
595
|
RequestingSocket, RequestId,
|
|
757
|
-
TEXT("
|
|
758
|
-
|
|
596
|
+
FString::Printf(TEXT("Node type '%s' not found. Use list_node_types "
|
|
597
|
+
"to see available types."),
|
|
598
|
+
*NodeType),
|
|
599
|
+
TEXT("NODE_TYPE_NOT_FOUND"));
|
|
759
600
|
}
|
|
760
601
|
return true;
|
|
761
602
|
} else if (SubAction == TEXT("connect_pins")) {
|
|
@@ -1168,6 +1009,75 @@ bool UMcpAutomationBridgeSubsystem::HandleBlueprintGraphAction(
|
|
|
1168
1009
|
SendAutomationResponse(RequestingSocket, RequestId, true,
|
|
1169
1010
|
TEXT("Pin details retrieved."), Result);
|
|
1170
1011
|
return true;
|
|
1012
|
+
} else if (SubAction == TEXT("list_node_types")) {
|
|
1013
|
+
// List all available UK2Node types for AI discoverability
|
|
1014
|
+
TArray<TSharedPtr<FJsonValue>> NodeTypes;
|
|
1015
|
+
for (TObjectIterator<UClass> It; It; ++It) {
|
|
1016
|
+
if (!It->IsChildOf(UK2Node::StaticClass()))
|
|
1017
|
+
continue;
|
|
1018
|
+
if (It->HasAnyClassFlags(CLASS_Abstract))
|
|
1019
|
+
continue;
|
|
1020
|
+
|
|
1021
|
+
TSharedPtr<FJsonObject> TypeObj = MakeShared<FJsonObject>();
|
|
1022
|
+
TypeObj->SetStringField(TEXT("className"), It->GetName());
|
|
1023
|
+
TypeObj->SetStringField(TEXT("displayName"),
|
|
1024
|
+
It->GetDisplayNameText().ToString());
|
|
1025
|
+
NodeTypes.Add(MakeShared<FJsonValueObject>(TypeObj));
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
1029
|
+
Result->SetArrayField(TEXT("nodeTypes"), NodeTypes);
|
|
1030
|
+
Result->SetNumberField(TEXT("count"), NodeTypes.Num());
|
|
1031
|
+
SendAutomationResponse(RequestingSocket, RequestId, true,
|
|
1032
|
+
TEXT("Node types listed."), Result);
|
|
1033
|
+
return true;
|
|
1034
|
+
} else if (SubAction == TEXT("set_pin_default_value")) {
|
|
1035
|
+
// Set a default value on a node's input pin
|
|
1036
|
+
FString NodeId, PinName, Value;
|
|
1037
|
+
Payload->TryGetStringField(TEXT("nodeId"), NodeId);
|
|
1038
|
+
Payload->TryGetStringField(TEXT("pinName"), PinName);
|
|
1039
|
+
Payload->TryGetStringField(TEXT("value"), Value);
|
|
1040
|
+
|
|
1041
|
+
UEdGraphNode *TargetNode = FindNodeByIdOrName(NodeId);
|
|
1042
|
+
if (!TargetNode) {
|
|
1043
|
+
SendAutomationError(RequestingSocket, RequestId, TEXT("Node not found."),
|
|
1044
|
+
TEXT("NODE_NOT_FOUND"));
|
|
1045
|
+
return true;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
UEdGraphPin *Pin = TargetNode->FindPin(*PinName);
|
|
1049
|
+
if (!Pin) {
|
|
1050
|
+
SendAutomationError(RequestingSocket, RequestId, TEXT("Pin not found."),
|
|
1051
|
+
TEXT("PIN_NOT_FOUND"));
|
|
1052
|
+
return true;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
if (Pin->Direction != EGPD_Input) {
|
|
1056
|
+
SendAutomationError(RequestingSocket, RequestId,
|
|
1057
|
+
TEXT("Can only set default values on input pins."),
|
|
1058
|
+
TEXT("INVALID_PIN_DIRECTION"));
|
|
1059
|
+
return true;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
const FScopedTransaction Transaction(
|
|
1063
|
+
FText::FromString(TEXT("Set Pin Default Value")));
|
|
1064
|
+
Blueprint->Modify();
|
|
1065
|
+
TargetGraph->Modify();
|
|
1066
|
+
TargetNode->Modify();
|
|
1067
|
+
|
|
1068
|
+
// Use the schema to properly set the default value
|
|
1069
|
+
const UEdGraphSchema *Schema = TargetGraph->GetSchema();
|
|
1070
|
+
Schema->TrySetDefaultValue(*Pin, Value);
|
|
1071
|
+
|
|
1072
|
+
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
|
|
1073
|
+
|
|
1074
|
+
TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
1075
|
+
Result->SetStringField(TEXT("nodeId"), NodeId);
|
|
1076
|
+
Result->SetStringField(TEXT("pinName"), PinName);
|
|
1077
|
+
Result->SetStringField(TEXT("value"), Value);
|
|
1078
|
+
SendAutomationResponse(RequestingSocket, RequestId, true,
|
|
1079
|
+
TEXT("Pin default value set."), Result);
|
|
1080
|
+
return true;
|
|
1171
1081
|
}
|
|
1172
1082
|
|
|
1173
1083
|
SendAutomationError(
|