ultimate-unreal-engine-mcp 0.1.0

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.
Files changed (124) hide show
  1. package/README.md +729 -0
  2. package/dist/build/error-parser.js +51 -0
  3. package/dist/build/fix-suggester.js +84 -0
  4. package/dist/build/ubt-runner.js +146 -0
  5. package/dist/cli.js +13 -0
  6. package/dist/config.js +8 -0
  7. package/dist/docs/data/ue57-api.js +228 -0
  8. package/dist/docs/doc-index.js +110 -0
  9. package/dist/docs/types.js +4 -0
  10. package/dist/generators/class-generator.js +363 -0
  11. package/dist/generators/file-modifier.js +276 -0
  12. package/dist/generators/uht-validator.js +177 -0
  13. package/dist/index.js +89 -0
  14. package/dist/parsers/cpp-class-index.js +230 -0
  15. package/dist/parsers/cpp-parser.js +369 -0
  16. package/dist/parsers/ini-parser.js +216 -0
  17. package/dist/parsers/uproject-parser.js +130 -0
  18. package/dist/plugin-bridge/client.js +217 -0
  19. package/dist/plugin-bridge/protocol.js +6 -0
  20. package/dist/plugin-bridge/retry.js +23 -0
  21. package/dist/setup.js +209 -0
  22. package/dist/tools/ai-systems/index.js +247 -0
  23. package/dist/tools/ai-systems/types.js +4 -0
  24. package/dist/tools/animation/index.js +241 -0
  25. package/dist/tools/animation/types.js +4 -0
  26. package/dist/tools/audio/index.js +204 -0
  27. package/dist/tools/audio/types.js +4 -0
  28. package/dist/tools/blueprint/index.js +495 -0
  29. package/dist/tools/blueprint/types.js +4 -0
  30. package/dist/tools/build/index.js +163 -0
  31. package/dist/tools/chaos/index.js +230 -0
  32. package/dist/tools/chaos/types.js +4 -0
  33. package/dist/tools/collision-physics/index.js +211 -0
  34. package/dist/tools/config/index.js +288 -0
  35. package/dist/tools/cpp/index.js +305 -0
  36. package/dist/tools/docs/index.js +251 -0
  37. package/dist/tools/editor/index.js +242 -0
  38. package/dist/tools/gas/index.js +222 -0
  39. package/dist/tools/gas/types.js +5 -0
  40. package/dist/tools/import-export/index.js +218 -0
  41. package/dist/tools/input/index.js +146 -0
  42. package/dist/tools/known-issues/index.js +88 -0
  43. package/dist/tools/known-issues/middleware.js +55 -0
  44. package/dist/tools/known-issues/store.js +125 -0
  45. package/dist/tools/livelink/index.js +203 -0
  46. package/dist/tools/livelink/types.js +4 -0
  47. package/dist/tools/material/index.js +190 -0
  48. package/dist/tools/motion-design/index.js +251 -0
  49. package/dist/tools/motion-design/types.js +6 -0
  50. package/dist/tools/movie-render/index.js +220 -0
  51. package/dist/tools/networking/index.js +149 -0
  52. package/dist/tools/pcg/index.js +164 -0
  53. package/dist/tools/selection/index.js +180 -0
  54. package/dist/tools/sequencer/index.js +218 -0
  55. package/dist/tools/validation/index.js +183 -0
  56. package/dist/tools/validation/types.js +4 -0
  57. package/dist/tools/viewport/index.js +310 -0
  58. package/dist/tools/worldpartition/index.js +226 -0
  59. package/dist/tools/worldpartition/types.js +4 -0
  60. package/dist/utils/execFileNoThrow.js +40 -0
  61. package/dist/utils/logger.js +27 -0
  62. package/dist/utils/path-guard.js +26 -0
  63. package/package.json +40 -0
  64. package/unreal-plugin/MCPBridge/MCPBridge.uplugin +29 -0
  65. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/MCPBridgeEditor.Build.cs +68 -0
  66. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.cpp +919 -0
  67. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.h +23 -0
  68. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.cpp +415 -0
  69. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.h +16 -0
  70. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.cpp +653 -0
  71. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.h +24 -0
  72. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.cpp +290 -0
  73. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.h +17 -0
  74. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.cpp +624 -0
  75. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.h +22 -0
  76. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.cpp +616 -0
  77. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.h +25 -0
  78. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.cpp +744 -0
  79. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.h +24 -0
  80. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeEditor.cpp +23 -0
  81. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.cpp +149 -0
  82. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.h +38 -0
  83. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.cpp +771 -0
  84. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.h +22 -0
  85. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.cpp +749 -0
  86. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.h +22 -0
  87. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.cpp +172 -0
  88. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.h +16 -0
  89. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.cpp +715 -0
  90. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.h +22 -0
  91. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.cpp +679 -0
  92. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.h +22 -0
  93. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.cpp +381 -0
  94. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.h +24 -0
  95. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.cpp +504 -0
  96. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.h +22 -0
  97. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.cpp +511 -0
  98. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.h +22 -0
  99. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.cpp +1110 -0
  100. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.h +28 -0
  101. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.cpp +590 -0
  102. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.h +16 -0
  103. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.cpp +482 -0
  104. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.h +16 -0
  105. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.cpp +338 -0
  106. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.h +16 -0
  107. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.cpp +677 -0
  108. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.h +22 -0
  109. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.cpp +721 -0
  110. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.h +16 -0
  111. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.cpp +368 -0
  112. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.h +22 -0
  113. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.cpp +1208 -0
  114. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.h +29 -0
  115. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.cpp +822 -0
  116. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.h +23 -0
  117. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Public/MCPBridgeEditor.h +14 -0
  118. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/MCPBridgeRuntime.Build.cs +28 -0
  119. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPBridgeRuntime.cpp +22 -0
  120. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPCommandRouter.cpp +118 -0
  121. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPTcpServer.cpp +196 -0
  122. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPBridgeRuntime.h +15 -0
  123. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPCommandRouter.h +55 -0
  124. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPTcpServer.h +59 -0
@@ -0,0 +1,919 @@
1
+ // MCPAICommands.cpp (Plan 22-01)
2
+ // Implements five AI system inspection command handlers for the MCP bridge:
3
+ // ai.behaviorTree -- inspect Behavior Tree node hierarchy, decorators, services (AI-01)
4
+ // ai.stateTree -- read State Tree states, transitions, and tasks (AI-02)
5
+ // ai.blackboard -- list Blackboard keys with types and default values (AI-03)
6
+ // ai.eqs -- inspect EQS query templates: generators, tests, scoring (AI-04)
7
+ // ai.navmesh -- query NavMesh build status, bounds, point reachability (AI-05)
8
+ //
9
+ // All handlers run on the game thread via FMCPCommandRouter::Dispatch.
10
+ // All operations are read-only -- no Modify() calls needed.
11
+ // asset_path is validated to start with "/Game/" or "/Engine/" before any
12
+ // StaticLoadObject call to prevent path traversal (T-22-01).
13
+ // Behavior Tree recursion is capped at 100 levels to prevent DoS (T-22-02).
14
+
15
+ #include "MCPAICommands.h"
16
+
17
+ // Behavior Tree headers
18
+ #include "BehaviorTree/BehaviorTree.h"
19
+ #include "BehaviorTree/BTCompositeNode.h"
20
+ #include "BehaviorTree/BTDecorator.h"
21
+ #include "BehaviorTree/BTService.h"
22
+ #include "BehaviorTree/BTTaskNode.h"
23
+
24
+ // Blackboard headers
25
+ #include "BehaviorTree/BlackboardData.h"
26
+ #include "BehaviorTree/Blackboard/BlackboardKeyType.h"
27
+
28
+ // State Tree headers
29
+ #include "StateTree.h"
30
+ #include "StateTreeTypes.h"
31
+
32
+ // EQS headers
33
+ #include "EnvironmentQuery/EnvQuery.h"
34
+ #include "EnvironmentQuery/EnvQueryOption.h"
35
+ #include "EnvironmentQuery/EnvQueryGenerator.h"
36
+ #include "EnvironmentQuery/EnvQueryTest.h"
37
+
38
+ // Navigation System headers
39
+ #include "NavigationSystem.h"
40
+ #include "NavigationData.h"
41
+ #include "NavMesh/RecastNavMesh.h"
42
+ #include "NavigationPath.h"
43
+ #include "NavFilters/NavigationQueryFilter.h"
44
+
45
+ // Editor world context
46
+ #include "Editor.h"
47
+
48
+ // JSON
49
+ #include "Serialization/JsonSerializer.h"
50
+ #include "Serialization/JsonWriter.h"
51
+ #include "Dom/JsonObject.h"
52
+ #include "Dom/JsonValue.h"
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Internal helpers
56
+ // ---------------------------------------------------------------------------
57
+
58
+ /** Returns a JSON success response string (without trailing newline). */
59
+ static FString BuildAISuccessResponse(const FString& CorrId, TSharedPtr<FJsonObject> Data)
60
+ {
61
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
62
+ Obj->SetBoolField(TEXT("success"), true);
63
+ Obj->SetStringField(TEXT("correlationId"), CorrId);
64
+ if (Data.IsValid())
65
+ {
66
+ Obj->SetObjectField(TEXT("data"), Data);
67
+ }
68
+
69
+ FString Output;
70
+ TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
71
+ FJsonSerializer::Serialize(Obj.ToSharedRef(), Writer);
72
+ return Output;
73
+ }
74
+
75
+ /** Returns a JSON error response string (without trailing newline). */
76
+ static FString BuildAIErrorResponse(const FString& CorrId, const FString& Error)
77
+ {
78
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
79
+ Obj->SetBoolField(TEXT("success"), false);
80
+ Obj->SetStringField(TEXT("correlationId"), CorrId);
81
+ Obj->SetStringField(TEXT("error"), Error);
82
+
83
+ FString Output;
84
+ TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
85
+ FJsonSerializer::Serialize(Obj.ToSharedRef(), Writer);
86
+ return Output;
87
+ }
88
+
89
+ /**
90
+ * Validate that asset_path starts with "/Game/" or "/Engine/" to prevent
91
+ * path traversal attacks (T-22-01).
92
+ */
93
+ static bool IsValidAssetPath(const FString& AssetPath)
94
+ {
95
+ return AssetPath.StartsWith(TEXT("/Game/")) || AssetPath.StartsWith(TEXT("/Engine/"));
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Behavior Tree recursive walk helper (T-22-02: cap depth at 100)
100
+ // ---------------------------------------------------------------------------
101
+
102
+ static TSharedPtr<FJsonObject> BuildBTNodeJson(UBTNode* Node, int32 Depth);
103
+
104
+ /**
105
+ * Build a JSON object representing a BT composite node and its subtree.
106
+ * Depth is tracked to enforce the T-22-02 recursion limit of 100 levels.
107
+ */
108
+ static TSharedPtr<FJsonObject> BuildBTCompositeNodeJson(UBTCompositeNode* Composite, int32 Depth)
109
+ {
110
+ TSharedPtr<FJsonObject> NodeObj = MakeShared<FJsonObject>();
111
+ NodeObj->SetStringField(TEXT("node_name"), Composite->GetNodeName());
112
+ NodeObj->SetStringField(TEXT("node_class"), Composite->GetClass()->GetName());
113
+ NodeObj->SetStringField(TEXT("node_type"), TEXT("Composite"));
114
+
115
+ // Build decorators array.
116
+ TArray<TSharedPtr<FJsonValue>> DecoratorsArray;
117
+ for (UBTDecorator* Decorator : Composite->Decorators)
118
+ {
119
+ if (!Decorator)
120
+ {
121
+ continue;
122
+ }
123
+ TSharedPtr<FJsonObject> DecObj = MakeShared<FJsonObject>();
124
+ DecObj->SetStringField(TEXT("node_name"), Decorator->GetNodeName());
125
+ DecObj->SetStringField(TEXT("node_class"), Decorator->GetClass()->GetName());
126
+ DecObj->SetStringField(TEXT("node_type"), TEXT("Decorator"));
127
+ DecoratorsArray.Add(MakeShared<FJsonValueObject>(DecObj));
128
+ }
129
+ NodeObj->SetArrayField(TEXT("decorators"), DecoratorsArray);
130
+
131
+ // Build services array.
132
+ TArray<TSharedPtr<FJsonValue>> ServicesArray;
133
+ for (UBTService* Service : Composite->Services)
134
+ {
135
+ if (!Service)
136
+ {
137
+ continue;
138
+ }
139
+ TSharedPtr<FJsonObject> SvcObj = MakeShared<FJsonObject>();
140
+ SvcObj->SetStringField(TEXT("node_name"), Service->GetNodeName());
141
+ SvcObj->SetStringField(TEXT("node_class"), Service->GetClass()->GetName());
142
+ SvcObj->SetStringField(TEXT("node_type"), TEXT("Service"));
143
+ ServicesArray.Add(MakeShared<FJsonValueObject>(SvcObj));
144
+ }
145
+ NodeObj->SetArrayField(TEXT("services"), ServicesArray);
146
+
147
+ // Build children array -- recurse if depth allows (T-22-02).
148
+ TArray<TSharedPtr<FJsonValue>> ChildrenArray;
149
+ if (Depth < 100)
150
+ {
151
+ const int32 NumChildren = Composite->GetChildrenNum();
152
+ for (int32 i = 0; i < NumChildren; ++i)
153
+ {
154
+ FBTCompositeChild& ChildInfo = Composite->Children[i];
155
+ UBTNode* ChildNode = ChildInfo.ChildComposite ? Cast<UBTNode>(ChildInfo.ChildComposite) : Cast<UBTNode>(ChildInfo.ChildTask);
156
+ if (ChildNode)
157
+ {
158
+ TSharedPtr<FJsonObject> ChildJson = BuildBTNodeJson(ChildNode, Depth + 1);
159
+ if (ChildJson.IsValid())
160
+ {
161
+ ChildrenArray.Add(MakeShared<FJsonValueObject>(ChildJson));
162
+ }
163
+ }
164
+
165
+ // Each child slot may also have decorators attached to the child edge.
166
+ for (UBTDecorator* ChildDecorator : ChildInfo.Decorators)
167
+ {
168
+ if (!ChildDecorator)
169
+ {
170
+ continue;
171
+ }
172
+ TSharedPtr<FJsonObject> DecObj = MakeShared<FJsonObject>();
173
+ DecObj->SetStringField(TEXT("node_name"), ChildDecorator->GetNodeName());
174
+ DecObj->SetStringField(TEXT("node_class"), ChildDecorator->GetClass()->GetName());
175
+ DecObj->SetStringField(TEXT("node_type"), TEXT("Decorator"));
176
+ // Edge decorators are reported as children of the composite for context.
177
+ }
178
+ }
179
+ }
180
+ else
181
+ {
182
+ // T-22-02: Maximum recursion depth reached. Add a truncation notice.
183
+ TSharedPtr<FJsonObject> TruncObj = MakeShared<FJsonObject>();
184
+ TruncObj->SetStringField(TEXT("node_name"), TEXT("[TRUNCATED: max_depth_100_reached]"));
185
+ TruncObj->SetStringField(TEXT("node_class"), TEXT("TruncationMarker"));
186
+ TruncObj->SetStringField(TEXT("node_type"), TEXT("Truncated"));
187
+ ChildrenArray.Add(MakeShared<FJsonValueObject>(TruncObj));
188
+ }
189
+ NodeObj->SetArrayField(TEXT("children"), ChildrenArray);
190
+
191
+ return NodeObj;
192
+ }
193
+
194
+ /** Dispatch node to the correct JSON builder based on node type. */
195
+ static TSharedPtr<FJsonObject> BuildBTNodeJson(UBTNode* Node, int32 Depth)
196
+ {
197
+ if (!Node)
198
+ {
199
+ return nullptr;
200
+ }
201
+
202
+ UBTCompositeNode* Composite = Cast<UBTCompositeNode>(Node);
203
+ if (Composite)
204
+ {
205
+ return BuildBTCompositeNodeJson(Composite, Depth);
206
+ }
207
+
208
+ // Leaf nodes: Task, Decorator, Service -- build basic JSON.
209
+ TSharedPtr<FJsonObject> NodeObj = MakeShared<FJsonObject>();
210
+ NodeObj->SetStringField(TEXT("node_name"), Node->GetNodeName());
211
+ NodeObj->SetStringField(TEXT("node_class"), Node->GetClass()->GetName());
212
+
213
+ if (Cast<UBTTaskNode>(Node))
214
+ {
215
+ NodeObj->SetStringField(TEXT("node_type"), TEXT("Task"));
216
+ }
217
+ else if (Cast<UBTDecorator>(Node))
218
+ {
219
+ NodeObj->SetStringField(TEXT("node_type"), TEXT("Decorator"));
220
+ }
221
+ else if (Cast<UBTService>(Node))
222
+ {
223
+ NodeObj->SetStringField(TEXT("node_type"), TEXT("Service"));
224
+ }
225
+ else
226
+ {
227
+ NodeObj->SetStringField(TEXT("node_type"), TEXT("Unknown"));
228
+ }
229
+
230
+ NodeObj->SetArrayField(TEXT("decorators"), TArray<TSharedPtr<FJsonValue>>());
231
+ NodeObj->SetArrayField(TEXT("services"), TArray<TSharedPtr<FJsonValue>>());
232
+ NodeObj->SetArrayField(TEXT("children"), TArray<TSharedPtr<FJsonValue>>());
233
+
234
+ return NodeObj;
235
+ }
236
+
237
+ // ---------------------------------------------------------------------------
238
+ // RegisterAICommands
239
+ // ---------------------------------------------------------------------------
240
+
241
+ void RegisterAICommands(FMCPCommandRouter& Router)
242
+ {
243
+ // -----------------------------------------------------------------------
244
+ // ai.behaviorTree (AI-01)
245
+ // Inspects a Behavior Tree asset's node hierarchy, decorators, and services.
246
+ // Required payload field: asset_path (must start with /Game/ or /Engine/).
247
+ // Returns: asset_path, root_node (recursive tree with node_name, node_class,
248
+ // node_type, decorators[], services[], children[]).
249
+ // Recursion limited to 100 levels (T-22-02).
250
+ // -----------------------------------------------------------------------
251
+ Router.RegisterHandler(TEXT("ai.behaviorTree"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
252
+ {
253
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
254
+
255
+ // Extract payload.
256
+ TSharedPtr<FJsonObject> Payload;
257
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
258
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
259
+ {
260
+ Payload = (*PayloadVal)->AsObject();
261
+ }
262
+
263
+ FString AssetPath;
264
+ if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty())
265
+ {
266
+ SendResponse(BuildAIErrorResponse(CorrId, TEXT("missing_asset_path")) + TEXT("\n"));
267
+ return;
268
+ }
269
+
270
+ // Validate path prefix (T-22-01).
271
+ if (!IsValidAssetPath(AssetPath))
272
+ {
273
+ SendResponse(BuildAIErrorResponse(CorrId, TEXT("invalid_asset_path")) + TEXT("\n"));
274
+ return;
275
+ }
276
+
277
+ // Load the BehaviorTree asset.
278
+ UBehaviorTree* BT = Cast<UBehaviorTree>(StaticLoadObject(UBehaviorTree::StaticClass(), nullptr, *AssetPath));
279
+ if (!BT)
280
+ {
281
+ SendResponse(BuildAIErrorResponse(CorrId, TEXT("behavior_tree_not_found")) + TEXT("\n"));
282
+ return;
283
+ }
284
+
285
+ // Walk the tree starting from RootNode (a UBTCompositeNode).
286
+ TSharedPtr<FJsonObject> RootNodeJson;
287
+ if (BT->RootNode)
288
+ {
289
+ RootNodeJson = BuildBTCompositeNodeJson(BT->RootNode, 0);
290
+ }
291
+ else
292
+ {
293
+ // No root node -- return an empty node marker.
294
+ RootNodeJson = MakeShared<FJsonObject>();
295
+ RootNodeJson->SetStringField(TEXT("node_name"), TEXT("[empty]"));
296
+ RootNodeJson->SetStringField(TEXT("node_class"), TEXT("None"));
297
+ RootNodeJson->SetStringField(TEXT("node_type"), TEXT("None"));
298
+ RootNodeJson->SetArrayField(TEXT("decorators"), TArray<TSharedPtr<FJsonValue>>());
299
+ RootNodeJson->SetArrayField(TEXT("services"), TArray<TSharedPtr<FJsonValue>>());
300
+ RootNodeJson->SetArrayField(TEXT("children"), TArray<TSharedPtr<FJsonValue>>());
301
+ }
302
+
303
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
304
+ Data->SetStringField(TEXT("asset_path"), AssetPath);
305
+ Data->SetObjectField(TEXT("root_node"), RootNodeJson);
306
+
307
+ SendResponse(BuildAISuccessResponse(CorrId, Data) + TEXT("\n"));
308
+ });
309
+
310
+ // -----------------------------------------------------------------------
311
+ // ai.stateTree (AI-02)
312
+ // Reads a State Tree asset's states, transitions, and tasks.
313
+ // Required payload field: asset_path (must start with /Game/ or /Engine/).
314
+ // Returns: asset_path, states[] (name, type, parent_state, tasks[], transitions[]).
315
+ //
316
+ // UE 5.7 API Note: UStateTree stores its states in FStateTreeEditorData
317
+ // (accessible via StateTree->EditorData when WITH_EDITORONLY_DATA is defined).
318
+ // In compiled/cooked form the states are baked into FStateTreeStateHandle arrays.
319
+ // We use EditorData here since this handler runs in the editor context only.
320
+ // -----------------------------------------------------------------------
321
+ Router.RegisterHandler(TEXT("ai.stateTree"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
322
+ {
323
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
324
+
325
+ // Extract payload.
326
+ TSharedPtr<FJsonObject> Payload;
327
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
328
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
329
+ {
330
+ Payload = (*PayloadVal)->AsObject();
331
+ }
332
+
333
+ FString AssetPath;
334
+ if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty())
335
+ {
336
+ SendResponse(BuildAIErrorResponse(CorrId, TEXT("missing_asset_path")) + TEXT("\n"));
337
+ return;
338
+ }
339
+
340
+ // Validate path prefix (T-22-01).
341
+ if (!IsValidAssetPath(AssetPath))
342
+ {
343
+ SendResponse(BuildAIErrorResponse(CorrId, TEXT("invalid_asset_path")) + TEXT("\n"));
344
+ return;
345
+ }
346
+
347
+ // Load the StateTree asset.
348
+ UStateTree* StateTree = Cast<UStateTree>(StaticLoadObject(UStateTree::StaticClass(), nullptr, *AssetPath));
349
+ if (!StateTree)
350
+ {
351
+ SendResponse(BuildAIErrorResponse(CorrId, TEXT("state_tree_not_found")) + TEXT("\n"));
352
+ return;
353
+ }
354
+
355
+ // Access states via EditorData (editor-only property, available in editor context).
356
+ // UStateTree exposes EditorData as a UObject* property; cast to UStateTreeEditorData
357
+ // to get the States array. If EditorData is not available (cooked build), fall back
358
+ // to reporting only the asset name and state count from the baked structure.
359
+ TArray<TSharedPtr<FJsonValue>> StatesArray;
360
+
361
+ #if WITH_EDITORONLY_DATA
362
+ if (StateTree->EditorData)
363
+ {
364
+ // UStateTreeEditorData stores states in a TArray<FStateTreeStateBase*> or
365
+ // TArray<FStateTreeEditorState>. Access via reflection to remain resilient
366
+ // to minor API changes across UE 5.x point releases.
367
+ UObject* EditorDataObj = StateTree->EditorData;
368
+
369
+ // Walk the states using the property system to remain compatible with
370
+ // UE 5.7 API changes. Look for an array property named "States" on EditorData.
371
+ FArrayProperty* StatesProp = nullptr;
372
+ for (TFieldIterator<FArrayProperty> PropIt(EditorDataObj->GetClass()); PropIt; ++PropIt)
373
+ {
374
+ if (PropIt->GetName() == TEXT("States"))
375
+ {
376
+ StatesProp = *PropIt;
377
+ break;
378
+ }
379
+ }
380
+
381
+ if (StatesProp)
382
+ {
383
+ FScriptArrayHelper ArrayHelper(StatesProp, StatesProp->ContainerPtrToValuePtr<void>(EditorDataObj));
384
+ FStructProperty* ElemProp = CastField<FStructProperty>(StatesProp->Inner);
385
+
386
+ for (int32 i = 0; i < ArrayHelper.Num(); ++i)
387
+ {
388
+ void* StatePtr = ArrayHelper.GetRawPtr(i);
389
+ TSharedPtr<FJsonObject> StateObj = MakeShared<FJsonObject>();
390
+
391
+ // Extract 'Name' field from the state struct.
392
+ FString StateName = FString::Printf(TEXT("State_%d"), i);
393
+ if (ElemProp)
394
+ {
395
+ FNameProperty* NameProp = CastField<FNameProperty>(ElemProp->Struct->FindPropertyByName(TEXT("Name")));
396
+ if (NameProp)
397
+ {
398
+ FName NameVal = NameProp->GetPropertyValue_InContainer(StatePtr);
399
+ StateName = NameVal.ToString();
400
+ }
401
+ }
402
+ StateObj->SetStringField(TEXT("name"), StateName);
403
+
404
+ // Extract 'Type' field (EStateTreeStateType enum).
405
+ FString StateType = TEXT("Unknown");
406
+ if (ElemProp)
407
+ {
408
+ FByteProperty* TypeByteProp = CastField<FByteProperty>(ElemProp->Struct->FindPropertyByName(TEXT("Type")));
409
+ FEnumProperty* TypeEnumProp = CastField<FEnumProperty>(ElemProp->Struct->FindPropertyByName(TEXT("Type")));
410
+ if (TypeEnumProp)
411
+ {
412
+ UEnum* Enum = TypeEnumProp->GetEnum();
413
+ if (Enum)
414
+ {
415
+ int64 EnumVal = TypeEnumProp->GetUnderlyingProperty()->GetSignedIntPropertyValue(TypeEnumProp->ContainerPtrToValuePtr<void>(StatePtr));
416
+ StateType = Enum->GetNameStringByValue(EnumVal);
417
+ }
418
+ }
419
+ else if (TypeByteProp && TypeByteProp->Enum)
420
+ {
421
+ uint8 ByteVal = TypeByteProp->GetPropertyValue_InContainer(StatePtr);
422
+ StateType = TypeByteProp->Enum->GetNameStringByValue(static_cast<int64>(ByteVal));
423
+ }
424
+ }
425
+ StateObj->SetStringField(TEXT("type"), StateType);
426
+
427
+ // Parent state: reflect on ParentState or Parent field.
428
+ FString ParentState = TEXT("");
429
+ if (ElemProp)
430
+ {
431
+ FNameProperty* ParentProp = CastField<FNameProperty>(ElemProp->Struct->FindPropertyByName(TEXT("Parent")));
432
+ if (!ParentProp)
433
+ {
434
+ ParentProp = CastField<FNameProperty>(ElemProp->Struct->FindPropertyByName(TEXT("ParentState")));
435
+ }
436
+ if (ParentProp)
437
+ {
438
+ FName ParentVal = ParentProp->GetPropertyValue_InContainer(StatePtr);
439
+ ParentState = ParentVal.ToString();
440
+ }
441
+ }
442
+ StateObj->SetStringField(TEXT("parent_state"), ParentState);
443
+
444
+ // Tasks array: reflect on Tasks property.
445
+ TArray<TSharedPtr<FJsonValue>> TasksArray;
446
+ if (ElemProp)
447
+ {
448
+ FArrayProperty* TasksProp = CastField<FArrayProperty>(ElemProp->Struct->FindPropertyByName(TEXT("Tasks")));
449
+ if (TasksProp)
450
+ {
451
+ FScriptArrayHelper TasksHelper(TasksProp, TasksProp->ContainerPtrToValuePtr<void>(StatePtr));
452
+ FStructProperty* TaskElemProp = CastField<FStructProperty>(TasksProp->Inner);
453
+ for (int32 j = 0; j < TasksHelper.Num(); ++j)
454
+ {
455
+ void* TaskPtr = TasksHelper.GetRawPtr(j);
456
+ FString TaskClass = TEXT("UnknownTask");
457
+ if (TaskElemProp)
458
+ {
459
+ // Try to get the task class name from the Instance property.
460
+ FStructProperty* InstanceProp = CastField<FStructProperty>(TaskElemProp->Struct->FindPropertyByName(TEXT("Node")));
461
+ if (InstanceProp)
462
+ {
463
+ TaskClass = InstanceProp->Struct->GetName();
464
+ }
465
+ else
466
+ {
467
+ TaskClass = TaskElemProp->Struct->GetName();
468
+ }
469
+ }
470
+ TasksArray.Add(MakeShared<FJsonValueString>(TaskClass));
471
+ }
472
+ }
473
+ }
474
+ StateObj->SetArrayField(TEXT("tasks"), TasksArray);
475
+
476
+ // Transitions array: reflect on Transitions property.
477
+ TArray<TSharedPtr<FJsonValue>> TransitionsArray;
478
+ if (ElemProp)
479
+ {
480
+ FArrayProperty* TransProp = CastField<FArrayProperty>(ElemProp->Struct->FindPropertyByName(TEXT("Transitions")));
481
+ if (TransProp)
482
+ {
483
+ FScriptArrayHelper TransHelper(TransProp, TransProp->ContainerPtrToValuePtr<void>(StatePtr));
484
+ FStructProperty* TransElemProp = CastField<FStructProperty>(TransProp->Inner);
485
+ for (int32 j = 0; j < TransHelper.Num(); ++j)
486
+ {
487
+ void* TransPtr = TransHelper.GetRawPtr(j);
488
+ TSharedPtr<FJsonObject> TransObj = MakeShared<FJsonObject>();
489
+
490
+ FString TargetState = TEXT("");
491
+ FString Trigger = TEXT("");
492
+
493
+ if (TransElemProp)
494
+ {
495
+ FNameProperty* TargetProp = CastField<FNameProperty>(TransElemProp->Struct->FindPropertyByName(TEXT("State")));
496
+ if (!TargetProp)
497
+ {
498
+ TargetProp = CastField<FNameProperty>(TransElemProp->Struct->FindPropertyByName(TEXT("NextState")));
499
+ }
500
+ if (TargetProp)
501
+ {
502
+ TargetState = TargetProp->GetPropertyValue_InContainer(TransPtr).ToString();
503
+ }
504
+
505
+ FByteProperty* TriggerByteProp = CastField<FByteProperty>(TransElemProp->Struct->FindPropertyByName(TEXT("Trigger")));
506
+ FEnumProperty* TriggerEnumProp = CastField<FEnumProperty>(TransElemProp->Struct->FindPropertyByName(TEXT("Trigger")));
507
+ if (TriggerEnumProp && TriggerEnumProp->GetEnum())
508
+ {
509
+ int64 EnumVal = TriggerEnumProp->GetUnderlyingProperty()->GetSignedIntPropertyValue(TriggerEnumProp->ContainerPtrToValuePtr<void>(TransPtr));
510
+ Trigger = TriggerEnumProp->GetEnum()->GetNameStringByValue(EnumVal);
511
+ }
512
+ else if (TriggerByteProp && TriggerByteProp->Enum)
513
+ {
514
+ uint8 ByteVal = TriggerByteProp->GetPropertyValue_InContainer(TransPtr);
515
+ Trigger = TriggerByteProp->Enum->GetNameStringByValue(static_cast<int64>(ByteVal));
516
+ }
517
+ }
518
+
519
+ TransObj->SetStringField(TEXT("target_state"), TargetState);
520
+ TransObj->SetStringField(TEXT("trigger"), Trigger);
521
+ TransitionsArray.Add(MakeShared<FJsonValueObject>(TransObj));
522
+ }
523
+ }
524
+ }
525
+ StateObj->SetArrayField(TEXT("transitions"), TransitionsArray);
526
+
527
+ StatesArray.Add(MakeShared<FJsonValueObject>(StateObj));
528
+ }
529
+ }
530
+ else
531
+ {
532
+ // EditorData exists but States property not found -- report count only.
533
+ UE_LOG(LogTemp, Warning, TEXT("[MCPBridge] ai.stateTree: EditorData has no 'States' array property -- using fallback."));
534
+ }
535
+ }
536
+ #endif // WITH_EDITORONLY_DATA
537
+
538
+ // If we couldn't extract states via EditorData, report what we can.
539
+ if (StatesArray.IsEmpty())
540
+ {
541
+ // UStateTree baked data: StateTree->NumStates (available at runtime).
542
+ // Report state count as the only available info without editor data.
543
+ int32 NumStates = 0;
544
+ #if WITH_EDITORONLY_DATA
545
+ // Already attempted above; fall through.
546
+ #endif
547
+ UE_LOG(LogTemp, Warning, TEXT("[MCPBridge] ai.stateTree: No editor state data available for '%s'."), *AssetPath);
548
+ }
549
+
550
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
551
+ Data->SetStringField(TEXT("asset_path"), AssetPath);
552
+ Data->SetArrayField(TEXT("states"), StatesArray);
553
+
554
+ SendResponse(BuildAISuccessResponse(CorrId, Data) + TEXT("\n"));
555
+ });
556
+
557
+ // -----------------------------------------------------------------------
558
+ // ai.blackboard (AI-03)
559
+ // Lists Blackboard keys with their types and instance-sync flags.
560
+ // Required payload field: asset_path (must start with /Game/ or /Engine/).
561
+ // Returns: asset_path, keys[] (key_name, key_type, instance_synced), parent_asset.
562
+ // -----------------------------------------------------------------------
563
+ Router.RegisterHandler(TEXT("ai.blackboard"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
564
+ {
565
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
566
+
567
+ // Extract payload.
568
+ TSharedPtr<FJsonObject> Payload;
569
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
570
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
571
+ {
572
+ Payload = (*PayloadVal)->AsObject();
573
+ }
574
+
575
+ FString AssetPath;
576
+ if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty())
577
+ {
578
+ SendResponse(BuildAIErrorResponse(CorrId, TEXT("missing_asset_path")) + TEXT("\n"));
579
+ return;
580
+ }
581
+
582
+ // Validate path prefix (T-22-01).
583
+ if (!IsValidAssetPath(AssetPath))
584
+ {
585
+ SendResponse(BuildAIErrorResponse(CorrId, TEXT("invalid_asset_path")) + TEXT("\n"));
586
+ return;
587
+ }
588
+
589
+ // Load the BlackboardData asset.
590
+ UBlackboardData* Blackboard = Cast<UBlackboardData>(StaticLoadObject(UBlackboardData::StaticClass(), nullptr, *AssetPath));
591
+ if (!Blackboard)
592
+ {
593
+ SendResponse(BuildAIErrorResponse(CorrId, TEXT("blackboard_not_found")) + TEXT("\n"));
594
+ return;
595
+ }
596
+
597
+ // Helper lambda to convert FBlackboardEntry to JSON.
598
+ auto EntryToJson = [](const FBlackboardEntry& Entry) -> TSharedPtr<FJsonObject>
599
+ {
600
+ TSharedPtr<FJsonObject> KeyObj = MakeShared<FJsonObject>();
601
+ KeyObj->SetStringField(TEXT("key_name"), Entry.EntryName.ToString());
602
+
603
+ FString KeyType = TEXT("Unknown");
604
+ if (Entry.KeyType)
605
+ {
606
+ KeyType = Entry.KeyType->GetClass()->GetName();
607
+ }
608
+ KeyObj->SetStringField(TEXT("key_type"), KeyType);
609
+ KeyObj->SetBoolField(TEXT("instance_synced"), Entry.bInstanceSynced);
610
+ return KeyObj;
611
+ };
612
+
613
+ // Collect own keys.
614
+ TArray<TSharedPtr<FJsonValue>> KeysArray;
615
+ for (const FBlackboardEntry& Entry : Blackboard->Keys)
616
+ {
617
+ KeysArray.Add(MakeShared<FJsonValueObject>(EntryToJson(Entry)));
618
+ }
619
+
620
+ // Collect parent keys if a parent blackboard is set.
621
+ FString ParentAssetPath;
622
+ if (Blackboard->Parent)
623
+ {
624
+ ParentAssetPath = Blackboard->Parent->GetPathName();
625
+ for (const FBlackboardEntry& Entry : Blackboard->ParentKeys)
626
+ {
627
+ TSharedPtr<FJsonObject> KeyObj = EntryToJson(Entry);
628
+ KeyObj->SetBoolField(TEXT("from_parent"), true);
629
+ KeysArray.Add(MakeShared<FJsonValueObject>(KeyObj));
630
+ }
631
+ }
632
+
633
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
634
+ Data->SetStringField(TEXT("asset_path"), AssetPath);
635
+ Data->SetArrayField(TEXT("keys"), KeysArray);
636
+ Data->SetStringField(TEXT("parent_asset"), ParentAssetPath);
637
+
638
+ SendResponse(BuildAISuccessResponse(CorrId, Data) + TEXT("\n"));
639
+ });
640
+
641
+ // -----------------------------------------------------------------------
642
+ // ai.eqs (AI-04)
643
+ // Inspects an EQS query template: options, generators, tests, scoring.
644
+ // Required payload field: asset_path (must start with /Game/ or /Engine/).
645
+ // Returns: asset_path, options[] (generator_class, generator_name, tests[]).
646
+ // -----------------------------------------------------------------------
647
+ Router.RegisterHandler(TEXT("ai.eqs"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
648
+ {
649
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
650
+
651
+ // Extract payload.
652
+ TSharedPtr<FJsonObject> Payload;
653
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
654
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
655
+ {
656
+ Payload = (*PayloadVal)->AsObject();
657
+ }
658
+
659
+ FString AssetPath;
660
+ if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty())
661
+ {
662
+ SendResponse(BuildAIErrorResponse(CorrId, TEXT("missing_asset_path")) + TEXT("\n"));
663
+ return;
664
+ }
665
+
666
+ // Validate path prefix (T-22-01).
667
+ if (!IsValidAssetPath(AssetPath))
668
+ {
669
+ SendResponse(BuildAIErrorResponse(CorrId, TEXT("invalid_asset_path")) + TEXT("\n"));
670
+ return;
671
+ }
672
+
673
+ // Load the EnvQuery asset.
674
+ UEnvQuery* Query = Cast<UEnvQuery>(StaticLoadObject(UEnvQuery::StaticClass(), nullptr, *AssetPath));
675
+ if (!Query)
676
+ {
677
+ SendResponse(BuildAIErrorResponse(CorrId, TEXT("eqs_query_not_found")) + TEXT("\n"));
678
+ return;
679
+ }
680
+
681
+ // Iterate the Options array.
682
+ TArray<TSharedPtr<FJsonValue>> OptionsArray;
683
+ for (UEnvQueryOption* Option : Query->Options)
684
+ {
685
+ if (!Option)
686
+ {
687
+ continue;
688
+ }
689
+
690
+ TSharedPtr<FJsonObject> OptionObj = MakeShared<FJsonObject>();
691
+
692
+ // Generator info.
693
+ FString GeneratorClass = TEXT("None");
694
+ FString GeneratorName = TEXT("None");
695
+ if (Option->Generator)
696
+ {
697
+ GeneratorClass = Option->Generator->GetClass()->GetName();
698
+ GeneratorName = Option->Generator->GetName();
699
+ }
700
+ OptionObj->SetStringField(TEXT("generator_class"), GeneratorClass);
701
+ OptionObj->SetStringField(TEXT("generator_name"), GeneratorName);
702
+
703
+ // Tests array.
704
+ TArray<TSharedPtr<FJsonValue>> TestsArray;
705
+ for (UEnvQueryTest* Test : Option->Tests)
706
+ {
707
+ if (!Test)
708
+ {
709
+ continue;
710
+ }
711
+
712
+ TSharedPtr<FJsonObject> TestObj = MakeShared<FJsonObject>();
713
+ TestObj->SetStringField(TEXT("test_class"), Test->GetClass()->GetName());
714
+ TestObj->SetStringField(TEXT("test_purpose"), Test->GetClass()->GetName());
715
+
716
+ // Scoring equation: reflect on ScoringEquation property (EEnvTestScoreEquation enum).
717
+ FString ScoringEquation = TEXT("Unknown");
718
+ FEnumProperty* ScoringEnumProp = CastField<FEnumProperty>(Test->GetClass()->FindPropertyByName(TEXT("ScoringEquation")));
719
+ FByteProperty* ScoringByteProp = CastField<FByteProperty>(Test->GetClass()->FindPropertyByName(TEXT("ScoringEquation")));
720
+ if (ScoringEnumProp && ScoringEnumProp->GetEnum())
721
+ {
722
+ int64 EnumVal = ScoringEnumProp->GetUnderlyingProperty()->GetSignedIntPropertyValue(ScoringEnumProp->ContainerPtrToValuePtr<void>(Test));
723
+ ScoringEquation = ScoringEnumProp->GetEnum()->GetNameStringByValue(EnumVal);
724
+ }
725
+ else if (ScoringByteProp && ScoringByteProp->Enum)
726
+ {
727
+ uint8 ByteVal = ScoringByteProp->GetPropertyValue_InContainer(Test);
728
+ ScoringEquation = ScoringByteProp->Enum->GetNameStringByValue(static_cast<int64>(ByteVal));
729
+ }
730
+ TestObj->SetStringField(TEXT("scoring_equation"), ScoringEquation);
731
+
732
+ TestsArray.Add(MakeShared<FJsonValueObject>(TestObj));
733
+ }
734
+ OptionObj->SetArrayField(TEXT("tests"), TestsArray);
735
+
736
+ OptionsArray.Add(MakeShared<FJsonValueObject>(OptionObj));
737
+ }
738
+
739
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
740
+ Data->SetStringField(TEXT("asset_path"), AssetPath);
741
+ Data->SetArrayField(TEXT("options"), OptionsArray);
742
+
743
+ SendResponse(BuildAISuccessResponse(CorrId, Data) + TEXT("\n"));
744
+ });
745
+
746
+ // -----------------------------------------------------------------------
747
+ // ai.navmesh (AI-05)
748
+ // Queries NavMesh build status, bounds, agent config, and point reachability.
749
+ // Optional payload fields: start_point {x,y,z}, end_point {x,y,z} for
750
+ // reachability check. If neither provided, returns build status and bounds.
751
+ // Returns: build_status, bounds {min, max}, nav_data_class, agent_radius,
752
+ // agent_height, and optionally reachability {is_reachable, path_length, path_cost}.
753
+ // Single synchronous path query only (T-22-03); no batch queries.
754
+ // NavMesh data is editor-only and not sensitive (T-22-04, accepted).
755
+ // -----------------------------------------------------------------------
756
+ Router.RegisterHandler(TEXT("ai.navmesh"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
757
+ {
758
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
759
+
760
+ // Extract optional payload.
761
+ TSharedPtr<FJsonObject> Payload;
762
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
763
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
764
+ {
765
+ Payload = (*PayloadVal)->AsObject();
766
+ }
767
+
768
+ // Check for optional start_point and end_point.
769
+ bool bHasStartPoint = false;
770
+ bool bHasEndPoint = false;
771
+ FVector StartPoint = FVector::ZeroVector;
772
+ FVector EndPoint = FVector::ZeroVector;
773
+
774
+ if (Payload.IsValid())
775
+ {
776
+ const TSharedPtr<FJsonValue>* StartVal = Payload->Values.Find(TEXT("start_point"));
777
+ const TSharedPtr<FJsonValue>* EndVal = Payload->Values.Find(TEXT("end_point"));
778
+
779
+ if (StartVal && (*StartVal)->Type == EJson::Object)
780
+ {
781
+ TSharedPtr<FJsonObject> StartObj = (*StartVal)->AsObject();
782
+ double X = 0.0, Y = 0.0, Z = 0.0;
783
+ StartObj->TryGetNumberField(TEXT("x"), X);
784
+ StartObj->TryGetNumberField(TEXT("y"), Y);
785
+ StartObj->TryGetNumberField(TEXT("z"), Z);
786
+ StartPoint = FVector(static_cast<float>(X), static_cast<float>(Y), static_cast<float>(Z));
787
+ bHasStartPoint = true;
788
+ }
789
+
790
+ if (EndVal && (*EndVal)->Type == EJson::Object)
791
+ {
792
+ TSharedPtr<FJsonObject> EndObj = (*EndVal)->AsObject();
793
+ double X = 0.0, Y = 0.0, Z = 0.0;
794
+ EndObj->TryGetNumberField(TEXT("x"), X);
795
+ EndObj->TryGetNumberField(TEXT("y"), Y);
796
+ EndObj->TryGetNumberField(TEXT("z"), Z);
797
+ EndPoint = FVector(static_cast<float>(X), static_cast<float>(Y), static_cast<float>(Z));
798
+ bHasEndPoint = true;
799
+ }
800
+ }
801
+
802
+ // Get the world from the editor context.
803
+ UWorld* World = nullptr;
804
+ if (GEditor)
805
+ {
806
+ World = GEditor->GetEditorWorldContext().World();
807
+ }
808
+
809
+ if (!World)
810
+ {
811
+ SendResponse(BuildAIErrorResponse(CorrId, TEXT("no_editor_world")) + TEXT("\n"));
812
+ return;
813
+ }
814
+
815
+ // Get the navigation system.
816
+ UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(World);
817
+ if (!NavSys)
818
+ {
819
+ SendResponse(BuildAIErrorResponse(CorrId, TEXT("navigation_system_not_found")) + TEXT("\n"));
820
+ return;
821
+ }
822
+
823
+ // Get the main navigation data (e.g., RecastNavMesh).
824
+ ANavigationData* NavData = NavSys->GetMainNavData();
825
+
826
+ FString BuildStatus = TEXT("not_built");
827
+ FString NavDataClass = TEXT("None");
828
+ FVector BoundsMin = FVector::ZeroVector;
829
+ FVector BoundsMax = FVector::ZeroVector;
830
+ float AgentRadius = 0.0f;
831
+ float AgentHeight = 0.0f;
832
+
833
+ if (NavData)
834
+ {
835
+ NavDataClass = NavData->GetClass()->GetName();
836
+
837
+ // Determine build status from the nav data.
838
+ // UNavigationSystemV1 tracks build status per data actor.
839
+ BuildStatus = NavData->IsRegistered() ? TEXT("built") : TEXT("not_built");
840
+
841
+ // Get bounds from the nav data's runtime bounding box.
842
+ FBox NavBounds = NavData->GetBounds();
843
+ if (NavBounds.IsValid)
844
+ {
845
+ BoundsMin = NavBounds.Min;
846
+ BoundsMax = NavBounds.Max;
847
+ }
848
+
849
+ // Cast to ARecastNavMesh for agent properties.
850
+ ARecastNavMesh* RecastMesh = Cast<ARecastNavMesh>(NavData);
851
+ if (RecastMesh)
852
+ {
853
+ AgentRadius = RecastMesh->AgentRadius;
854
+ AgentHeight = RecastMesh->AgentHeight;
855
+ }
856
+ }
857
+
858
+ // Build the response data object.
859
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
860
+ Data->SetStringField(TEXT("build_status"), BuildStatus);
861
+ Data->SetStringField(TEXT("nav_data_class"), NavDataClass);
862
+
863
+ TSharedPtr<FJsonObject> BoundsObj = MakeShared<FJsonObject>();
864
+ TSharedPtr<FJsonObject> BoundsMinObj = MakeShared<FJsonObject>();
865
+ BoundsMinObj->SetNumberField(TEXT("x"), static_cast<double>(BoundsMin.X));
866
+ BoundsMinObj->SetNumberField(TEXT("y"), static_cast<double>(BoundsMin.Y));
867
+ BoundsMinObj->SetNumberField(TEXT("z"), static_cast<double>(BoundsMin.Z));
868
+ TSharedPtr<FJsonObject> BoundsMaxObj = MakeShared<FJsonObject>();
869
+ BoundsMaxObj->SetNumberField(TEXT("x"), static_cast<double>(BoundsMax.X));
870
+ BoundsMaxObj->SetNumberField(TEXT("y"), static_cast<double>(BoundsMax.Y));
871
+ BoundsMaxObj->SetNumberField(TEXT("z"), static_cast<double>(BoundsMax.Z));
872
+ BoundsObj->SetObjectField(TEXT("min"), BoundsMinObj);
873
+ BoundsObj->SetObjectField(TEXT("max"), BoundsMaxObj);
874
+ Data->SetObjectField(TEXT("bounds"), BoundsObj);
875
+
876
+ Data->SetNumberField(TEXT("agent_radius"), static_cast<double>(AgentRadius));
877
+ Data->SetNumberField(TEXT("agent_height"), static_cast<double>(AgentHeight));
878
+
879
+ // Reachability check if both points provided (T-22-03: single sync query only).
880
+ if (bHasStartPoint && bHasEndPoint && NavData)
881
+ {
882
+ // Build a synchronous path-finding query.
883
+ FPathFindingQuery Query(nullptr, *NavData, StartPoint, EndPoint);
884
+ FPathFindingResult Result = NavSys->FindPathSync(Query);
885
+
886
+ bool bIsReachable = Result.IsSuccessful() && Result.Path.IsValid();
887
+ float PathLength = 0.0f;
888
+ float PathCost = 0.0f;
889
+
890
+ if (bIsReachable)
891
+ {
892
+ PathLength = static_cast<float>(Result.Path->GetLength());
893
+ PathCost = Result.Path->GetCost();
894
+ }
895
+
896
+ TSharedPtr<FJsonObject> ReachabilityObj = MakeShared<FJsonObject>();
897
+ ReachabilityObj->SetBoolField(TEXT("is_reachable"), bIsReachable);
898
+ ReachabilityObj->SetNumberField(TEXT("path_length"), static_cast<double>(PathLength));
899
+ ReachabilityObj->SetNumberField(TEXT("path_cost"), static_cast<double>(PathCost));
900
+
901
+ TSharedPtr<FJsonObject> StartPointObj = MakeShared<FJsonObject>();
902
+ StartPointObj->SetNumberField(TEXT("x"), static_cast<double>(StartPoint.X));
903
+ StartPointObj->SetNumberField(TEXT("y"), static_cast<double>(StartPoint.Y));
904
+ StartPointObj->SetNumberField(TEXT("z"), static_cast<double>(StartPoint.Z));
905
+
906
+ TSharedPtr<FJsonObject> EndPointObj = MakeShared<FJsonObject>();
907
+ EndPointObj->SetNumberField(TEXT("x"), static_cast<double>(EndPoint.X));
908
+ EndPointObj->SetNumberField(TEXT("y"), static_cast<double>(EndPoint.Y));
909
+ EndPointObj->SetNumberField(TEXT("z"), static_cast<double>(EndPoint.Z));
910
+
911
+ ReachabilityObj->SetObjectField(TEXT("start_point"), StartPointObj);
912
+ ReachabilityObj->SetObjectField(TEXT("end_point"), EndPointObj);
913
+
914
+ Data->SetObjectField(TEXT("reachability"), ReachabilityObj);
915
+ }
916
+
917
+ SendResponse(BuildAISuccessResponse(CorrId, Data) + TEXT("\n"));
918
+ });
919
+ }