ue-mcp 1.1.21 → 1.1.23
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/README.md +1 -1
- package/dist/plugin-cli.js +164 -3
- package/dist/plugin-cli.js.map +1 -1
- package/dist/tool-counts.json +9 -9
- package/dist/tools/asset.js +1 -0
- package/dist/tools/asset.js.map +1 -1
- package/dist/tools/audio.js +6 -1
- package/dist/tools/audio.js.map +1 -1
- package/dist/tools/editor.js +16 -5
- package/dist/tools/editor.js.map +1 -1
- package/dist/tools/gameplay.js +1 -1
- package/dist/tools/gameplay.js.map +1 -1
- package/dist/tools/landscape.js +4 -0
- package/dist/tools/landscape.js.map +1 -1
- package/dist/tools/level.js +7 -2
- package/dist/tools/level.js.map +1 -1
- package/dist/ue-mcp.default.yml +62 -8
- package/package.json +2 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/BridgeServer.cpp +6 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.cpp +108 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.h +3 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AudioHandlers.cpp +127 -17
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AudioHandlers.h +2 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.cpp +258 -5
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.h +7 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_Input.cpp +75 -32
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +100 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.h +4 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.cpp +79 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.h +2 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Lights.cpp +15 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +7 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.cpp +15 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/UE_MCP_Bridge.Build.cs +2 -0
- package/skills/ue-mcp-blueprint/SKILL.md +13 -0
- package/skills/ue-mcp-epic-routing/SKILL.md +37 -0
package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_Input.cpp
CHANGED
|
@@ -490,21 +490,40 @@ TSharedPtr<FJsonValue> FGameplayHandlers::SetMappingModifiers(const TSharedPtr<F
|
|
|
490
490
|
}
|
|
491
491
|
|
|
492
492
|
// ── Triggers ──
|
|
493
|
+
// #725: triggers previously accepted only {type:"Hold"}; the more obvious
|
|
494
|
+
// {class:"/Script/EnhancedInput.InputTriggerHold"} was rejected. Worse, an
|
|
495
|
+
// unresolvable shape could leave a NULL entry in Mapping.Triggers, which
|
|
496
|
+
// trips AssetCheck on save. Build into a temp array, accept class OR type
|
|
497
|
+
// (mirroring modifiers, #649), apply nested "properties", and never append
|
|
498
|
+
// a null; report any failed specs instead of silently corrupting the asset.
|
|
499
|
+
TArray<TSharedPtr<FJsonValue>> FailedTriggers;
|
|
493
500
|
const TArray<TSharedPtr<FJsonValue>>* TriggersArr = nullptr;
|
|
494
501
|
if (Params->TryGetArrayField(TEXT("triggers"), TriggersArr) && TriggersArr)
|
|
495
502
|
{
|
|
496
|
-
|
|
503
|
+
TArray<TObjectPtr<UInputTrigger>> NewTriggers;
|
|
497
504
|
for (const auto& TrigVal : *TriggersArr)
|
|
498
505
|
{
|
|
499
506
|
const TSharedPtr<FJsonObject>* TrigObj = nullptr;
|
|
500
|
-
if (!TrigVal->TryGetObject(TrigObj) || !TrigObj)
|
|
507
|
+
if (!TrigVal->TryGetObject(TrigObj) || !TrigObj)
|
|
508
|
+
{
|
|
509
|
+
FailedTriggers.Add(MakeShared<FJsonValueString>(TEXT("(non-object trigger entry)")));
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
FString ClassPath;
|
|
514
|
+
(*TrigObj)->TryGetStringField(TEXT("class"), ClassPath);
|
|
515
|
+
UClass* TrigClass = nullptr;
|
|
516
|
+
if (!ClassPath.IsEmpty())
|
|
517
|
+
{
|
|
518
|
+
TrigClass = LoadClass<UInputTrigger>(nullptr, *ClassPath);
|
|
519
|
+
if (!TrigClass) TrigClass = LoadObject<UClass>(nullptr, *ClassPath);
|
|
520
|
+
if (!TrigClass) TrigClass = FindClassByShortName(ClassPath);
|
|
521
|
+
if (TrigClass && !TrigClass->IsChildOf(UInputTrigger::StaticClass())) TrigClass = nullptr;
|
|
522
|
+
}
|
|
501
523
|
|
|
502
524
|
FString TypeName;
|
|
503
525
|
(*TrigObj)->TryGetStringField(TEXT("type"), TypeName);
|
|
504
|
-
if (TypeName.IsEmpty())
|
|
505
|
-
|
|
506
|
-
// Resolve trigger class: try multiple patterns (#169 fix)
|
|
507
|
-
UClass* TrigClass = nullptr;
|
|
526
|
+
if (!TrigClass && !TypeName.IsEmpty())
|
|
508
527
|
{
|
|
509
528
|
TArray<FString> Candidates;
|
|
510
529
|
if (TypeName.StartsWith(TEXT("UInputTrigger")) || TypeName.StartsWith(TEXT("InputTrigger")))
|
|
@@ -524,45 +543,63 @@ TSharedPtr<FJsonValue> FGameplayHandlers::SetMappingModifiers(const TSharedPtr<F
|
|
|
524
543
|
TrigClass = nullptr;
|
|
525
544
|
}
|
|
526
545
|
}
|
|
546
|
+
|
|
527
547
|
if (!TrigClass)
|
|
528
548
|
{
|
|
549
|
+
FailedTriggers.Add(MakeShared<FJsonValueString>(ClassPath.IsEmpty() ? TypeName : ClassPath));
|
|
529
550
|
continue;
|
|
530
551
|
}
|
|
531
552
|
|
|
532
553
|
UInputTrigger* Trigger = NewObject<UInputTrigger>(IMC, TrigClass);
|
|
533
|
-
|
|
534
|
-
// Set properties via reflection (same pattern as modifiers)
|
|
535
|
-
for (const auto& Pair : (*TrigObj)->Values)
|
|
554
|
+
if (!Trigger)
|
|
536
555
|
{
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
if (!Prop) continue;
|
|
541
|
-
|
|
542
|
-
void* PropAddr = Prop->ContainerPtrToValuePtr<void>(Trigger);
|
|
556
|
+
FailedTriggers.Add(MakeShared<FJsonValueString>(ClassPath.IsEmpty() ? TypeName : ClassPath));
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
543
559
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}
|
|
550
|
-
else if (FDoubleProperty* DoubleProp = CastField<FDoubleProperty>(Prop))
|
|
551
|
-
{
|
|
552
|
-
double Val = 0;
|
|
553
|
-
Pair.Value->TryGetNumber(Val);
|
|
554
|
-
DoubleProp->SetPropertyValue(PropAddr, Val);
|
|
555
|
-
}
|
|
556
|
-
else if (FBoolProperty* BoolProp = CastField<FBoolProperty>(Prop))
|
|
560
|
+
// Set properties via reflection. Accept both top-level fields and a
|
|
561
|
+
// nested "properties" object (same as modifiers).
|
|
562
|
+
auto ApplyTriggerProps = [&](const TSharedPtr<FJsonObject>& Obj)
|
|
563
|
+
{
|
|
564
|
+
for (const auto& Pair : Obj->Values)
|
|
557
565
|
{
|
|
558
|
-
|
|
559
|
-
Pair.
|
|
560
|
-
|
|
566
|
+
if (Pair.Key == TEXT("type") || Pair.Key == TEXT("class") || Pair.Key == TEXT("properties")) continue;
|
|
567
|
+
FProperty* Prop = TrigClass->FindPropertyByName(FName(*Pair.Key));
|
|
568
|
+
if (!Prop) continue;
|
|
569
|
+
void* PropAddr = Prop->ContainerPtrToValuePtr<void>(Trigger);
|
|
570
|
+
if (FFloatProperty* FloatProp = CastField<FFloatProperty>(Prop))
|
|
571
|
+
{
|
|
572
|
+
double Val = 0; Pair.Value->TryGetNumber(Val);
|
|
573
|
+
FloatProp->SetPropertyValue(PropAddr, (float)Val);
|
|
574
|
+
}
|
|
575
|
+
else if (FDoubleProperty* DoubleProp = CastField<FDoubleProperty>(Prop))
|
|
576
|
+
{
|
|
577
|
+
double Val = 0; Pair.Value->TryGetNumber(Val);
|
|
578
|
+
DoubleProp->SetPropertyValue(PropAddr, Val);
|
|
579
|
+
}
|
|
580
|
+
else if (FBoolProperty* BoolProp = CastField<FBoolProperty>(Prop))
|
|
581
|
+
{
|
|
582
|
+
bool Val = false; Pair.Value->TryGetBool(Val);
|
|
583
|
+
BoolProp->SetPropertyValue(PropAddr, Val);
|
|
584
|
+
}
|
|
585
|
+
else if (FIntProperty* IntProp = CastField<FIntProperty>(Prop))
|
|
586
|
+
{
|
|
587
|
+
double Val = 0; Pair.Value->TryGetNumber(Val);
|
|
588
|
+
IntProp->SetPropertyValue(PropAddr, (int32)Val);
|
|
589
|
+
}
|
|
561
590
|
}
|
|
591
|
+
};
|
|
592
|
+
ApplyTriggerProps(*TrigObj);
|
|
593
|
+
const TSharedPtr<FJsonObject>* PropsObj = nullptr;
|
|
594
|
+
if ((*TrigObj)->TryGetObjectField(TEXT("properties"), PropsObj) && PropsObj && (*PropsObj).IsValid())
|
|
595
|
+
{
|
|
596
|
+
ApplyTriggerProps(*PropsObj);
|
|
562
597
|
}
|
|
563
598
|
|
|
564
|
-
|
|
599
|
+
NewTriggers.Add(Trigger);
|
|
565
600
|
}
|
|
601
|
+
|
|
602
|
+
Mapping.Triggers = NewTriggers;
|
|
566
603
|
}
|
|
567
604
|
|
|
568
605
|
// Mark dirty — caller can use asset(save) to persist (#197 fix)
|
|
@@ -577,6 +614,12 @@ TSharedPtr<FJsonValue> FGameplayHandlers::SetMappingModifiers(const TSharedPtr<F
|
|
|
577
614
|
Result->SetNumberField(TEXT("mappingIndex"), MappingIndex);
|
|
578
615
|
Result->SetNumberField(TEXT("modifierCount"), Mapping.Modifiers.Num());
|
|
579
616
|
Result->SetNumberField(TEXT("triggerCount"), Mapping.Triggers.Num());
|
|
617
|
+
if (FailedTriggers.Num() > 0)
|
|
618
|
+
{
|
|
619
|
+
// #725: tell the caller which trigger specs did not resolve rather than
|
|
620
|
+
// silently dropping them (or, worse, leaving a null entry behind).
|
|
621
|
+
Result->SetArrayField(TEXT("failedTriggers"), FailedTriggers);
|
|
622
|
+
}
|
|
580
623
|
return MCPResult(Result);
|
|
581
624
|
}
|
|
582
625
|
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
#include "Landscape.h"
|
|
10
10
|
#include "LandscapeEditTypes.h"
|
|
11
11
|
#include "LandscapeProxy.h"
|
|
12
|
+
#include "LandscapeStreamingProxy.h"
|
|
12
13
|
#include "LandscapeInfo.h"
|
|
13
14
|
#include "LandscapeComponent.h"
|
|
14
15
|
#include "LandscapeSplineActor.h"
|
|
@@ -38,6 +39,9 @@ void FLandscapeHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
|
|
|
38
39
|
Registry.RegisterHandler(TEXT("create_landscape"), &CreateLandscape);
|
|
39
40
|
Registry.RegisterHandler(TEXT("create_landscape_layer_info"), &CreateLandscapeLayerInfo);
|
|
40
41
|
Registry.RegisterHandler(TEXT("get_landscape_material_usage_summary"), &GetMaterialUsageSummary);
|
|
42
|
+
// #733: World Partition landscape streaming-proxy enumeration + spatial lookup.
|
|
43
|
+
Registry.RegisterHandler(TEXT("list_landscape_proxies"), &ListLandscapeProxies);
|
|
44
|
+
Registry.RegisterHandler(TEXT("find_landscape_proxy_at"), &FindLandscapeProxyAt);
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
TSharedPtr<FJsonValue> FLandscapeHandlers::GetLandscapeInfo(const TSharedPtr<FJsonObject>& Params)
|
|
@@ -745,3 +749,99 @@ TSharedPtr<FJsonValue> FLandscapeHandlers::GetMaterialUsageSummary(const TShared
|
|
|
745
749
|
Result->SetNumberField(TEXT("totalNaniteComponents"), TotalNanite);
|
|
746
750
|
return MCPResult(Result);
|
|
747
751
|
}
|
|
752
|
+
|
|
753
|
+
// #733: enumerate LandscapeStreamingProxy actors currently loaded in the world,
|
|
754
|
+
// with per-proxy world bounds and the parent Landscape count. On a World
|
|
755
|
+
// Partition map, an unloaded proxy silently reads layer weights as 0, so a
|
|
756
|
+
// measurement is only trustworthy once the covering proxy is confirmed loaded.
|
|
757
|
+
// Unloaded proxies are not spawned as actors, so the actor iterator only yields
|
|
758
|
+
// loaded ones - hence loaded is always true for enumerated entries; callers use
|
|
759
|
+
// the count + bounds to reason about coverage.
|
|
760
|
+
TSharedPtr<FJsonValue> FLandscapeHandlers::ListLandscapeProxies(const TSharedPtr<FJsonObject>& Params)
|
|
761
|
+
{
|
|
762
|
+
REQUIRE_EDITOR_WORLD(World);
|
|
763
|
+
|
|
764
|
+
int32 ParentLandscapes = 0;
|
|
765
|
+
TArray<TSharedPtr<FJsonValue>> Proxies;
|
|
766
|
+
for (TActorIterator<AActor> It(World); It; ++It)
|
|
767
|
+
{
|
|
768
|
+
AActor* Actor = *It;
|
|
769
|
+
if (!Actor) continue;
|
|
770
|
+
if (Actor->IsA<ALandscape>())
|
|
771
|
+
{
|
|
772
|
+
ParentLandscapes++;
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
ALandscapeStreamingProxy* Proxy = Cast<ALandscapeStreamingProxy>(Actor);
|
|
776
|
+
if (!Proxy) continue;
|
|
777
|
+
|
|
778
|
+
FVector Origin, Extent;
|
|
779
|
+
Proxy->GetActorBounds(false, Origin, Extent);
|
|
780
|
+
|
|
781
|
+
TSharedPtr<FJsonObject> ProxyObj = MakeShared<FJsonObject>();
|
|
782
|
+
ProxyObj->SetStringField(TEXT("label"), Proxy->GetActorLabel());
|
|
783
|
+
ProxyObj->SetBoolField(TEXT("loaded"), true);
|
|
784
|
+
|
|
785
|
+
TSharedPtr<FJsonObject> Bounds = MakeShared<FJsonObject>();
|
|
786
|
+
TSharedPtr<FJsonObject> OriginObj = MakeShared<FJsonObject>();
|
|
787
|
+
OriginObj->SetNumberField(TEXT("x"), Origin.X);
|
|
788
|
+
OriginObj->SetNumberField(TEXT("y"), Origin.Y);
|
|
789
|
+
OriginObj->SetNumberField(TEXT("z"), Origin.Z);
|
|
790
|
+
TSharedPtr<FJsonObject> ExtentObj = MakeShared<FJsonObject>();
|
|
791
|
+
ExtentObj->SetNumberField(TEXT("x"), Extent.X);
|
|
792
|
+
ExtentObj->SetNumberField(TEXT("y"), Extent.Y);
|
|
793
|
+
ExtentObj->SetNumberField(TEXT("z"), Extent.Z);
|
|
794
|
+
Bounds->SetObjectField(TEXT("origin"), OriginObj);
|
|
795
|
+
Bounds->SetObjectField(TEXT("extent"), ExtentObj);
|
|
796
|
+
ProxyObj->SetObjectField(TEXT("worldBounds"), Bounds);
|
|
797
|
+
|
|
798
|
+
Proxies.Add(MakeShared<FJsonValueObject>(ProxyObj));
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
auto Result = MCPSuccess();
|
|
802
|
+
Result->SetNumberField(TEXT("loadedProxies"), Proxies.Num());
|
|
803
|
+
Result->SetNumberField(TEXT("parentLandscapes"), ParentLandscapes);
|
|
804
|
+
Result->SetArrayField(TEXT("proxies"), Proxies);
|
|
805
|
+
Result->SetStringField(TEXT("note"), TEXT("World Partition unloaded proxies are not spawned as actors, so only loaded proxies are listed."));
|
|
806
|
+
return MCPResult(Result);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// #733: resolve which loaded LandscapeStreamingProxy's world bounds contain a
|
|
810
|
+
// world X/Y. Returns the covering proxy (loaded:true) or loaded:false when no
|
|
811
|
+
// loaded proxy covers the position - which usually means the covering proxy is
|
|
812
|
+
// streamed out, making any 0-weight readback there ambiguous rather than real.
|
|
813
|
+
TSharedPtr<FJsonValue> FLandscapeHandlers::FindLandscapeProxyAt(const TSharedPtr<FJsonObject>& Params)
|
|
814
|
+
{
|
|
815
|
+
REQUIRE_EDITOR_WORLD(World);
|
|
816
|
+
|
|
817
|
+
if (!Params->HasField(TEXT("worldX")) || !Params->HasField(TEXT("worldY")))
|
|
818
|
+
{
|
|
819
|
+
return MCPError(TEXT("Missing 'worldX'/'worldY' world position"));
|
|
820
|
+
}
|
|
821
|
+
const double TargetX = OptionalNumber(Params, TEXT("worldX"), 0.0);
|
|
822
|
+
const double TargetY = OptionalNumber(Params, TEXT("worldY"), 0.0);
|
|
823
|
+
|
|
824
|
+
for (TActorIterator<AActor> It(World); It; ++It)
|
|
825
|
+
{
|
|
826
|
+
ALandscapeStreamingProxy* Proxy = Cast<ALandscapeStreamingProxy>(*It);
|
|
827
|
+
if (!Proxy) continue;
|
|
828
|
+
|
|
829
|
+
FVector Origin, Extent;
|
|
830
|
+
Proxy->GetActorBounds(false, Origin, Extent);
|
|
831
|
+
if (TargetX >= Origin.X - Extent.X && TargetX <= Origin.X + Extent.X &&
|
|
832
|
+
TargetY >= Origin.Y - Extent.Y && TargetY <= Origin.Y + Extent.Y)
|
|
833
|
+
{
|
|
834
|
+
auto Result = MCPSuccess();
|
|
835
|
+
Result->SetBoolField(TEXT("found"), true);
|
|
836
|
+
Result->SetBoolField(TEXT("loaded"), true);
|
|
837
|
+
Result->SetStringField(TEXT("label"), Proxy->GetActorLabel());
|
|
838
|
+
return MCPResult(Result);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
auto Result = MCPSuccess();
|
|
843
|
+
Result->SetBoolField(TEXT("found"), false);
|
|
844
|
+
Result->SetBoolField(TEXT("loaded"), false);
|
|
845
|
+
Result->SetStringField(TEXT("note"), TEXT("No loaded proxy covers this position; the covering proxy is likely streamed out, so weight/height readbacks here are ambiguous."));
|
|
846
|
+
return MCPResult(Result);
|
|
847
|
+
}
|
|
@@ -26,4 +26,8 @@ private:
|
|
|
26
26
|
static TSharedPtr<FJsonValue> CreateLandscapeLayerInfo(const TSharedPtr<FJsonObject>& Params);
|
|
27
27
|
// v0.7.19 issue #150 — concise material + component count summary per proxy
|
|
28
28
|
static TSharedPtr<FJsonValue> GetMaterialUsageSummary(const TSharedPtr<FJsonObject>& Params);
|
|
29
|
+
// #733: enumerate loaded World Partition landscape streaming proxies with
|
|
30
|
+
// per-proxy world bounds, and resolve which proxy covers a world position.
|
|
31
|
+
static TSharedPtr<FJsonValue> ListLandscapeProxies(const TSharedPtr<FJsonObject>& Params);
|
|
32
|
+
static TSharedPtr<FJsonValue> FindLandscapeProxyAt(const TSharedPtr<FJsonObject>& Params);
|
|
29
33
|
};
|
|
@@ -89,6 +89,8 @@
|
|
|
89
89
|
void FLevelHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
|
|
90
90
|
{
|
|
91
91
|
Registry.RegisterHandler(TEXT("get_world_outliner"), &GetOutliner);
|
|
92
|
+
// #717: query/set per-actor editor-only visibility (temporarily hidden).
|
|
93
|
+
Registry.RegisterHandler(TEXT("set_editor_visibility"), &SetEditorVisibility);
|
|
92
94
|
Registry.RegisterHandler(TEXT("place_actor"), &PlaceActor);
|
|
93
95
|
Registry.RegisterHandler(TEXT("delete_actor"), &DeleteActor);
|
|
94
96
|
Registry.RegisterHandler(TEXT("get_actor_details"), &GetActorDetails);
|
|
@@ -180,6 +182,13 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetOutliner(const TSharedPtr<FJsonObject>
|
|
|
180
182
|
int32 Limit = OptionalInt(Params, TEXT("limit"), 50);
|
|
181
183
|
bool bIncludeStreaming = OptionalBool(Params, TEXT("includeStreaming"), false);
|
|
182
184
|
|
|
185
|
+
// #717: optional tri-state filter on editor-only visibility. When present,
|
|
186
|
+
// only actors whose IsTemporarilyHiddenInEditor() matches are returned. The
|
|
187
|
+
// per-actor editorHidden flag is always reported so callers can find lights
|
|
188
|
+
// that are hidden in the viewport but still render in game.
|
|
189
|
+
bool bEditorHiddenFilterValue = false;
|
|
190
|
+
const bool bHasEditorHiddenFilter = Params->TryGetBoolField(TEXT("editorHidden"), bEditorHiddenFilterValue);
|
|
191
|
+
|
|
183
192
|
TArray<TSharedPtr<FJsonValue>> ActorsArray;
|
|
184
193
|
int32 TotalCount = 0;
|
|
185
194
|
int32 StreamingSkipped = 0;
|
|
@@ -213,6 +222,16 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetOutliner(const TSharedPtr<FJsonObject>
|
|
|
213
222
|
{
|
|
214
223
|
continue;
|
|
215
224
|
}
|
|
225
|
+
|
|
226
|
+
#if WITH_EDITOR
|
|
227
|
+
const bool bEditorHidden = Actor->IsTemporarilyHiddenInEditor();
|
|
228
|
+
#else
|
|
229
|
+
const bool bEditorHidden = false;
|
|
230
|
+
#endif
|
|
231
|
+
if (bHasEditorHiddenFilter && bEditorHidden != bEditorHiddenFilterValue)
|
|
232
|
+
{
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
216
235
|
if (ActorsArray.Num() >= Limit) break;
|
|
217
236
|
|
|
218
237
|
TSharedPtr<FJsonObject> ActorObj = MakeShared<FJsonObject>();
|
|
@@ -220,6 +239,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetOutliner(const TSharedPtr<FJsonObject>
|
|
|
220
239
|
ActorObj->SetStringField(TEXT("label"), ActorLabel);
|
|
221
240
|
ActorObj->SetStringField(TEXT("class"), ActorClass);
|
|
222
241
|
ActorObj->SetStringField(TEXT("path"), Actor->GetPathName());
|
|
242
|
+
ActorObj->SetBoolField(TEXT("editorHidden"), bEditorHidden);
|
|
223
243
|
|
|
224
244
|
FVector Location = Actor->GetActorLocation();
|
|
225
245
|
TSharedPtr<FJsonObject> LocationObj = MakeShared<FJsonObject>();
|
|
@@ -262,6 +282,65 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetOutliner(const TSharedPtr<FJsonObject>
|
|
|
262
282
|
return MCPResult(Result);
|
|
263
283
|
}
|
|
264
284
|
|
|
285
|
+
// #717: bulk set editor-only visibility (temporarily hidden in editor). Targets
|
|
286
|
+
// either an explicit actorLabels list or every actor (all=true). Editor-hidden
|
|
287
|
+
// actors still render in game, so unhiding them is a common cleanup step.
|
|
288
|
+
TSharedPtr<FJsonValue> FLevelHandlers::SetEditorVisibility(const TSharedPtr<FJsonObject>& Params)
|
|
289
|
+
{
|
|
290
|
+
REQUIRE_EDITOR_WORLD(World);
|
|
291
|
+
|
|
292
|
+
bool bHidden = false;
|
|
293
|
+
if (!Params->TryGetBoolField(TEXT("hidden"), bHidden))
|
|
294
|
+
{
|
|
295
|
+
return MCPError(TEXT("Missing 'hidden' parameter (true = hide in editor, false = show)"));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const bool bAll = OptionalBool(Params, TEXT("all"), false);
|
|
299
|
+
|
|
300
|
+
TSet<FString> TargetLabels;
|
|
301
|
+
const TArray<TSharedPtr<FJsonValue>>* LabelsArr = nullptr;
|
|
302
|
+
if (Params->TryGetArrayField(TEXT("actorLabels"), LabelsArr) && LabelsArr)
|
|
303
|
+
{
|
|
304
|
+
for (const TSharedPtr<FJsonValue>& V : *LabelsArr)
|
|
305
|
+
{
|
|
306
|
+
FString S;
|
|
307
|
+
if (V.IsValid() && V->TryGetString(S)) TargetLabels.Add(S);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!bAll && TargetLabels.Num() == 0)
|
|
312
|
+
{
|
|
313
|
+
return MCPError(TEXT("Provide 'actorLabels' (array) or 'all'=true"));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
int32 Changed = 0;
|
|
317
|
+
int32 Matched = 0;
|
|
318
|
+
TArray<TSharedPtr<FJsonValue>> Affected;
|
|
319
|
+
for (TActorIterator<AActor> ActorIt(World); ActorIt; ++ActorIt)
|
|
320
|
+
{
|
|
321
|
+
AActor* Actor = *ActorIt;
|
|
322
|
+
if (!Actor) continue;
|
|
323
|
+
const FString Label = Actor->GetActorLabel();
|
|
324
|
+
if (!bAll && !TargetLabels.Contains(Label)) continue;
|
|
325
|
+
Matched++;
|
|
326
|
+
#if WITH_EDITOR
|
|
327
|
+
if (Actor->IsTemporarilyHiddenInEditor() != bHidden)
|
|
328
|
+
{
|
|
329
|
+
Actor->SetIsTemporarilyHiddenInEditor(bHidden);
|
|
330
|
+
Changed++;
|
|
331
|
+
Affected.Add(MakeShared<FJsonValueString>(Label));
|
|
332
|
+
}
|
|
333
|
+
#endif
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
auto Result = MCPSuccess();
|
|
337
|
+
Result->SetBoolField(TEXT("hidden"), bHidden);
|
|
338
|
+
Result->SetNumberField(TEXT("matched"), Matched);
|
|
339
|
+
Result->SetNumberField(TEXT("changed"), Changed);
|
|
340
|
+
Result->SetArrayField(TEXT("affected"), Affected);
|
|
341
|
+
return MCPResult(Result);
|
|
342
|
+
}
|
|
343
|
+
|
|
265
344
|
TSharedPtr<FJsonValue> FLevelHandlers::PlaceActor(const TSharedPtr<FJsonObject>& Params)
|
|
266
345
|
{
|
|
267
346
|
FString ActorClass;
|
|
@@ -13,6 +13,8 @@ public:
|
|
|
13
13
|
private:
|
|
14
14
|
// Handler implementations
|
|
15
15
|
static TSharedPtr<FJsonValue> GetOutliner(const TSharedPtr<FJsonObject>& Params);
|
|
16
|
+
// #717: bulk set editor-only visibility (temporarily hidden in editor)
|
|
17
|
+
static TSharedPtr<FJsonValue> SetEditorVisibility(const TSharedPtr<FJsonObject>& Params);
|
|
16
18
|
static TSharedPtr<FJsonValue> PlaceActor(const TSharedPtr<FJsonObject>& Params);
|
|
17
19
|
static TSharedPtr<FJsonValue> DeleteActor(const TSharedPtr<FJsonObject>& Params);
|
|
18
20
|
static TSharedPtr<FJsonValue> GetActorDetails(const TSharedPtr<FJsonObject>& Params);
|
|
@@ -131,6 +131,21 @@ TSharedPtr<FJsonValue> FLevelHandlers::SpawnLight(const TSharedPtr<FJsonObject>&
|
|
|
131
131
|
{
|
|
132
132
|
LightComponent->SetLightColor(LightColor);
|
|
133
133
|
}
|
|
134
|
+
// #723: attenuationRadius was accepted by the schema but never applied.
|
|
135
|
+
// It lives on the local-light components (point/spot/rect); directional
|
|
136
|
+
// and sky lights have no attenuation radius, so they ignore it.
|
|
137
|
+
double AttenuationRadius = 0.0;
|
|
138
|
+
if (Params->TryGetNumberField(TEXT("attenuationRadius"), AttenuationRadius) && AttenuationRadius > 0.0)
|
|
139
|
+
{
|
|
140
|
+
if (UPointLightComponent* PointComp = Cast<UPointLightComponent>(LightComponent))
|
|
141
|
+
{
|
|
142
|
+
PointComp->SetAttenuationRadius(static_cast<float>(AttenuationRadius));
|
|
143
|
+
}
|
|
144
|
+
else if (URectLightComponent* RectComp = Cast<URectLightComponent>(LightComponent))
|
|
145
|
+
{
|
|
146
|
+
RectComp->SetAttenuationRadius(static_cast<float>(AttenuationRadius));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
134
149
|
LightComponent->SetVisibility(true);
|
|
135
150
|
LightComponent->MarkRenderStateDirty();
|
|
136
151
|
}
|
|
@@ -137,10 +137,15 @@ TSharedPtr<FJsonValue> FPhysicsHandlers::SetPhysicsEnabled(const TSharedPtr<FJso
|
|
|
137
137
|
FString ActorLabel;
|
|
138
138
|
if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
|
|
139
139
|
|
|
140
|
+
// #721: the published TS schema names this parameter "simulate" while the
|
|
141
|
+
// handler historically read only "enabled", so a schema-conformant call
|
|
142
|
+
// silently no-opped. Accept either spelling (simulate | enabled) and error
|
|
143
|
+
// explicitly when neither is present rather than succeeding silently.
|
|
140
144
|
bool bEnabled = true;
|
|
141
|
-
if (!Params->TryGetBoolField(TEXT("
|
|
145
|
+
if (!Params->TryGetBoolField(TEXT("simulate"), bEnabled) &&
|
|
146
|
+
!Params->TryGetBoolField(TEXT("enabled"), bEnabled))
|
|
142
147
|
{
|
|
143
|
-
return MCPError(TEXT("Missing 'enabled' parameter (true/false)"));
|
|
148
|
+
return MCPError(TEXT("Missing 'simulate' (aka 'enabled') parameter (true/false)"));
|
|
144
149
|
}
|
|
145
150
|
|
|
146
151
|
REQUIRE_EDITOR_WORLD(World);
|
|
@@ -571,8 +571,12 @@ TSharedPtr<FJsonValue> FWidgetHandlers::AddWidget(const TSharedPtr<FJsonObject>&
|
|
|
571
571
|
}
|
|
572
572
|
}
|
|
573
573
|
|
|
574
|
-
//
|
|
575
|
-
|
|
574
|
+
// #728: the WidgetBlueprintCompiler ensures every added widget has an entry in
|
|
575
|
+
// WidgetVariableNameToGuidMap (UMGEditor WidgetBlueprintCompiler.cpp: "Widget
|
|
576
|
+
// [X] was added but did not get a GUID"). The map was present in 5.4, absent
|
|
577
|
+
// in the 5.5-5.7 window, and present again in 5.8, so register the GUID on
|
|
578
|
+
// 5.4 and on 5.8+ (skipping the versions where the member does not exist).
|
|
579
|
+
#if (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 4) || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 8) || ENGINE_MAJOR_VERSION > 5
|
|
576
580
|
if (!WidgetBP->WidgetVariableNameToGuidMap.Contains(NewWidget->GetFName()))
|
|
577
581
|
{
|
|
578
582
|
WidgetBP->WidgetVariableNameToGuidMap.Add(NewWidget->GetFName(), FGuid::NewGuid());
|
|
@@ -906,6 +910,15 @@ TSharedPtr<FJsonValue> FWidgetHandlers::WrapRoot(const TSharedPtr<FJsonObject>&
|
|
|
906
910
|
WidgetBP->WidgetTree->RootWidget = Wrapper;
|
|
907
911
|
Wrapper->AddChild(OldRoot);
|
|
908
912
|
|
|
913
|
+
// #728: register the new wrapper's GUID so the WidgetBlueprintCompiler ensure
|
|
914
|
+
// does not fire (see add_widget). Same version window as there.
|
|
915
|
+
#if (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 4) || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 8) || ENGINE_MAJOR_VERSION > 5
|
|
916
|
+
if (!WidgetBP->WidgetVariableNameToGuidMap.Contains(Wrapper->GetFName()))
|
|
917
|
+
{
|
|
918
|
+
WidgetBP->WidgetVariableNameToGuidMap.Add(Wrapper->GetFName(), FGuid::NewGuid());
|
|
919
|
+
}
|
|
920
|
+
#endif
|
|
921
|
+
|
|
909
922
|
WidgetBP->MarkPackageDirty();
|
|
910
923
|
FKismetEditorUtilities::CompileBlueprint(WidgetBP);
|
|
911
924
|
UEditorAssetLibrary::SaveAsset(AssetPath);
|
|
@@ -62,6 +62,7 @@ public class UE_MCP_Bridge : ModuleRules
|
|
|
62
62
|
"Landscape",
|
|
63
63
|
"LevelEditor",
|
|
64
64
|
"LevelSequence",
|
|
65
|
+
"LevelSequenceEditor",
|
|
65
66
|
"MaterialEditor",
|
|
66
67
|
"MovieScene",
|
|
67
68
|
"MovieSceneTracks",
|
|
@@ -77,6 +78,7 @@ public class UE_MCP_Bridge : ModuleRules
|
|
|
77
78
|
"PropertyEditor",
|
|
78
79
|
"PythonScriptPlugin",
|
|
79
80
|
"Sequencer",
|
|
81
|
+
"Settings",
|
|
80
82
|
"Slate",
|
|
81
83
|
"SlateCore",
|
|
82
84
|
"StateTreeModule",
|
|
@@ -7,6 +7,19 @@ description: Use when authoring or modifying Unreal Blueprint assets through ue-
|
|
|
7
7
|
|
|
8
8
|
The `blueprint` tool covers reading, authoring, and compiling Blueprints. The default workflow is **read → mutate → compile**, never fire-and-forget.
|
|
9
9
|
|
|
10
|
+
## Authoring a graph body: prefer the Epic DSL (#711)
|
|
11
|
+
|
|
12
|
+
When you are authoring the **contents of a graph** (event graph, function body, macro) - a set of nodes plus their wiring - do **not** default to node-by-node `add_node`/`connect_pins`. Reach for Epic's graph DSL first:
|
|
13
|
+
|
|
14
|
+
1. `blueprint(action="epic_get_graph_dsl_docs")` - read the S-expression grammar once.
|
|
15
|
+
2. `blueprint(action="epic_write_graph_dsl", ...)` - author + compile the whole graph body in one call.
|
|
16
|
+
|
|
17
|
+
The DSL authors and compiles an entire graph in a single round-trip, which is materially faster and more reliable than stitching individual K2Nodes together (one correct pass vs several failed iterations). Use it for graph bodies whenever it is available.
|
|
18
|
+
|
|
19
|
+
**Availability / fallback.** The `epic_*` actions come from Epic's ToolsetRegistry and require **UE 5.8+ with the Epic toolset plugins enabled**. Check with `epic(action="status")`. When they are unavailable (pre-5.8, or the registry is off), fall back to the native node path below.
|
|
20
|
+
|
|
21
|
+
**Keep using the native actions for** read/discovery, SCS components, CDO/class defaults, interfaces + event dispatchers, structured `compile`/`validate`, and anything with no Epic equivalent - the native path adds idempotency and rollback the raw tools lack. See the `ue-mcp-epic-routing` skill for the full epic-vs-native decision.
|
|
22
|
+
|
|
10
23
|
## Discovery before authoring
|
|
11
24
|
|
|
12
25
|
For any existing Blueprint:
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ue-mcp-epic-routing
|
|
3
|
+
description: Use when deciding between ue-mcp's native category actions and Epic's wrapped ToolsetRegistry tools (the `epic_*` actions, incl. the Blueprint graph DSL) for a task in Unreal. Pulls in when authoring Blueprint graph bodies, or any time both a native action and an epic_* action could do the job and you need to pick.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Epic-vs-native tool routing (ue-mcp)
|
|
7
|
+
|
|
8
|
+
ue-mcp exposes two overlapping surfaces. Picking the right one per task avoids slow, failure-prone paths.
|
|
9
|
+
|
|
10
|
+
- **Native category actions** - `blueprint(...)`, `level(...)`, `material(...)`, etc. Hand-written, idempotent, with rollback and structured errors.
|
|
11
|
+
- **Epic `epic_*` actions** - Epic's own MCP toolsets, surfaced in-category when the ToolsetRegistry is available (UE 5.8+ with the Epic toolset plugins enabled). Discover them with `epic(action="status")` and `epic(action="list_toolsets")`.
|
|
12
|
+
|
|
13
|
+
This is **not** a blanket "always use epic" rule. Epic needs 5.8 + plugins, and much of ue-mcp has no epic equivalent or adds idempotency/rollback the raw tools lack.
|
|
14
|
+
|
|
15
|
+
## The one that matters most: Blueprint graph bodies -> the DSL
|
|
16
|
+
|
|
17
|
+
Authoring the **contents of a graph** (event graph, function body, macro) is the headline case.
|
|
18
|
+
|
|
19
|
+
- Prefer `blueprint(action="epic_write_graph_dsl", ...)`. Call `blueprint(action="epic_get_graph_dsl_docs")` first to get the S-expression grammar.
|
|
20
|
+
- It authors **and compiles the whole graph in one call** - materially faster and more reliable than node-by-node `add_node` + `connect_pins` (typically one correct pass instead of several failed iterations).
|
|
21
|
+
|
|
22
|
+
## Decision table
|
|
23
|
+
|
|
24
|
+
| Task | Route |
|
|
25
|
+
|------|-------|
|
|
26
|
+
| Author/replace a graph body (nodes + wiring) | `blueprint(epic_write_graph_dsl)` (docs first) |
|
|
27
|
+
| Read / inspect a graph, list graphs/variables/functions | native `blueprint(read*, list_*, get_execution_flow)` |
|
|
28
|
+
| SCS components, reparenting, component properties | native `blueprint(add_component, ...)` |
|
|
29
|
+
| CDO / class defaults, tick settings | native `blueprint(set_class_default, set_actor_tick_settings)` |
|
|
30
|
+
| Interfaces, event dispatchers | native `blueprint(create_interface, add_interface, add_event_dispatcher)` |
|
|
31
|
+
| Structured compile / validate with diagnostics | native `blueprint(compile, validate)` |
|
|
32
|
+
| A task with no native action but an `epic_*` one exists | the `epic_*` action |
|
|
33
|
+
| A task with no epic equivalent | the native action |
|
|
34
|
+
|
|
35
|
+
## Fallback
|
|
36
|
+
|
|
37
|
+
If `epic(action="status")` reports the registry unavailable (pre-5.8, plugins off, or `available=false`), route **everything** through the native path - the DSL actions will not exist. The native node-by-node path (`add_node` -> `set_node_property` -> `connect_pins` -> `compile`) always works; see the `ue-mcp-blueprint` skill.
|