flockbay 0.10.49 → 0.10.52

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 (74) hide show
  1. package/README.md +60 -60
  2. package/bin/flockbay-mcp.mjs +62 -62
  3. package/bin/flockbay.mjs +95 -95
  4. package/dist/{index-r3VTdgFI.cjs → index-IntvnrmZ.cjs} +71 -14
  5. package/dist/{index-D8AeVbvv.mjs → index-wkeSi0iC.mjs} +70 -13
  6. package/dist/index.cjs +2 -2
  7. package/dist/index.mjs +2 -2
  8. package/dist/lib.cjs +1 -1
  9. package/dist/lib.mjs +1 -1
  10. package/dist/{migratePlugin-CEvGPul8.mjs → migratePlugin-C71hAoc5.mjs} +12 -2
  11. package/dist/{migratePlugin-wFK-k3Wk.cjs → migratePlugin-D1XjjWFq.cjs} +12 -2
  12. package/dist/{runCodex-DuyuYqoB.cjs → runCodex-B7i_ICoQ.cjs} +34 -23
  13. package/dist/{runCodex-ZfUyhHF6.mjs → runCodex-BSx-hCu6.mjs} +34 -23
  14. package/dist/{runGemini-DBU1mMdp.mjs → runGemini-Dwyh7_WI.mjs} +2 -2
  15. package/dist/{runGemini-D7j5Y5TF.cjs → runGemini-gBKR3rdL.cjs} +2 -2
  16. package/dist/{types-DNr0xwSy.cjs → types-5z8UADAi.cjs} +224 -15
  17. package/dist/{types-BRJuZQj_.mjs → types-Bgzap7qR.mjs} +224 -16
  18. package/package.json +131 -131
  19. package/scripts/claude_local_launcher.cjs +72 -72
  20. package/scripts/claude_remote_launcher.cjs +15 -15
  21. package/scripts/claude_version_utils.cjs +445 -445
  22. package/scripts/ripgrep_launcher.cjs +32 -32
  23. package/scripts/run-with-root-env.mjs +37 -32
  24. package/scripts/session_hook_forwarder.cjs +49 -49
  25. package/scripts/test-codex-abort-history.mjs +77 -77
  26. package/scripts/unpack-tools.cjs +244 -244
  27. package/tools/licenses/difftastic-LICENSE +21 -21
  28. package/tools/licenses/ripgrep-LICENSE +3 -3
  29. package/tools/unreal-mcp/UPSTREAM_VERSION.md +8 -8
  30. package/tools/unreal-mcp/upstream/Docs/README.md +8 -8
  31. package/tools/unreal-mcp/upstream/Docs/Tools/README.md +6 -6
  32. package/tools/unreal-mcp/upstream/Docs/Tools/actor_tools.md +183 -183
  33. package/tools/unreal-mcp/upstream/Docs/Tools/blueprint_tools.md +268 -268
  34. package/tools/unreal-mcp/upstream/Docs/Tools/editor_tools.md +104 -104
  35. package/tools/unreal-mcp/upstream/Docs/Tools/node_tools.md +274 -274
  36. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Config/FilterPlugin.ini +8 -8
  37. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/FlockbayMCP.uplugin +36 -36
  38. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/FlockbayMCP.Build.cs +82 -82
  39. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPBlueprintCommands.cpp +1364 -1364
  40. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPBlueprintNodeCommands.cpp +1310 -1310
  41. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPCommandSchema.cpp +756 -756
  42. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPCommonUtils.cpp +1177 -1177
  43. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPEditorCommands.cpp +2618 -2618
  44. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPProjectCommands.cpp +71 -71
  45. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPUMGCommands.cpp +543 -543
  46. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/MCPServerRunnable.cpp +372 -372
  47. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/UnrealMCPBridge.cpp +457 -457
  48. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/UnrealMCPModule.cpp +21 -21
  49. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPBlueprintCommands.h +35 -35
  50. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPBlueprintNodeCommands.h +30 -30
  51. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPCommandSchema.h +41 -41
  52. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPCommonUtils.h +59 -59
  53. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPEditorCommands.h +61 -61
  54. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPProjectCommands.h +20 -20
  55. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPUMGCommands.h +82 -82
  56. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/MCPServerRunnable.h +33 -33
  57. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/UnrealMCPBridge.h +64 -64
  58. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/UnrealMCPModule.h +22 -22
  59. package/tools/unreal-mcp/upstream/Python/README.md +39 -39
  60. package/tools/unreal-mcp/upstream/Python/pyproject.toml +22 -22
  61. package/tools/unreal-mcp/upstream/Python/scripts/actors/test_cube.py +202 -202
  62. package/tools/unreal-mcp/upstream/Python/scripts/blueprints/test_create_and_spawn_blueprints_with_different_components.py +496 -496
  63. package/tools/unreal-mcp/upstream/Python/scripts/blueprints/test_create_and_spawn_cube_blueprint.py +193 -193
  64. package/tools/unreal-mcp/upstream/Python/scripts/node/test_component_reference.py +266 -266
  65. package/tools/unreal-mcp/upstream/Python/scripts/node/test_create_bird_blueprint_with_input_and_camera.py +617 -617
  66. package/tools/unreal-mcp/upstream/Python/scripts/node/test_input_mapping.py +365 -365
  67. package/tools/unreal-mcp/upstream/Python/scripts/node/test_physics_variables.py +389 -389
  68. package/tools/unreal-mcp/upstream/Python/tools/blueprint_tools.py +419 -419
  69. package/tools/unreal-mcp/upstream/Python/tools/editor_tools.py +369 -369
  70. package/tools/unreal-mcp/upstream/Python/tools/node_tools.py +429 -429
  71. package/tools/unreal-mcp/upstream/Python/tools/project_tools.py +63 -63
  72. package/tools/unreal-mcp/upstream/Python/tools/umg_tools.py +332 -332
  73. package/tools/unreal-mcp/upstream/Python/unreal_mcp_server.py +398 -398
  74. package/tools/unreal-mcp/upstream/Python/uv.lock +521 -521
@@ -1,1177 +1,1177 @@
1
- #include "Commands/UnrealMCPCommonUtils.h"
2
- #include "GameFramework/Actor.h"
3
- #include "Engine/Blueprint.h"
4
- #include "EdGraph/EdGraph.h"
5
- #include "EdGraph/EdGraphNode.h"
6
- #include "EdGraph/EdGraphPin.h"
7
- #include "K2Node_Event.h"
8
- #include "K2Node_CallFunction.h"
9
- #include "K2Node_VariableGet.h"
10
- #include "K2Node_VariableSet.h"
11
- #include "K2Node_InputAction.h"
12
- #include "K2Node_Self.h"
13
- #include "EdGraphSchema_K2.h"
14
- #include "Kismet2/BlueprintEditorUtils.h"
15
- #include "Components/StaticMeshComponent.h"
16
- #include "Components/LightComponent.h"
17
- #include "Components/PrimitiveComponent.h"
18
- #include "Components/SceneComponent.h"
19
- #include "UObject/UnrealType.h"
20
- #include "UObject/UObjectIterator.h"
21
- #include "Engine/Selection.h"
22
- #include "EditorAssetLibrary.h"
23
- #include "AssetRegistry/AssetRegistryModule.h"
24
- #include "Engine/BlueprintGeneratedClass.h"
25
- #include "BlueprintNodeSpawner.h"
26
- #include "BlueprintActionDatabase.h"
27
- #include "Dom/JsonObject.h"
28
- #include "Dom/JsonValue.h"
29
-
30
- // JSON Utilities
31
- TSharedPtr<FJsonObject> FUnrealMCPCommonUtils::CreateErrorResponse(const FString& Message)
32
- {
33
- TSharedPtr<FJsonObject> ResponseObject = MakeShared<FJsonObject>();
34
- ResponseObject->SetBoolField(TEXT("success"), false);
35
- ResponseObject->SetStringField(TEXT("error"), Message);
36
- return ResponseObject;
37
- }
38
-
39
- TSharedPtr<FJsonObject> FUnrealMCPCommonUtils::CreateSuccessResponse(const TSharedPtr<FJsonObject>& Data)
40
- {
41
- TSharedPtr<FJsonObject> ResponseObject = MakeShared<FJsonObject>();
42
- ResponseObject->SetBoolField(TEXT("success"), true);
43
-
44
- if (Data.IsValid())
45
- {
46
- ResponseObject->SetObjectField(TEXT("data"), Data);
47
- }
48
-
49
- return ResponseObject;
50
- }
51
-
52
- void FUnrealMCPCommonUtils::GetIntArrayFromJson(const TSharedPtr<FJsonObject>& JsonObject, const FString& FieldName, TArray<int32>& OutArray)
53
- {
54
- OutArray.Reset();
55
-
56
- if (!JsonObject->HasField(FieldName))
57
- {
58
- return;
59
- }
60
-
61
- const TArray<TSharedPtr<FJsonValue>>* JsonArray;
62
- if (JsonObject->TryGetArrayField(FieldName, JsonArray))
63
- {
64
- for (const TSharedPtr<FJsonValue>& Value : *JsonArray)
65
- {
66
- OutArray.Add((int32)Value->AsNumber());
67
- }
68
- }
69
- }
70
-
71
- void FUnrealMCPCommonUtils::GetFloatArrayFromJson(const TSharedPtr<FJsonObject>& JsonObject, const FString& FieldName, TArray<float>& OutArray)
72
- {
73
- OutArray.Reset();
74
-
75
- if (!JsonObject->HasField(FieldName))
76
- {
77
- return;
78
- }
79
-
80
- const TArray<TSharedPtr<FJsonValue>>* JsonArray;
81
- if (JsonObject->TryGetArrayField(FieldName, JsonArray))
82
- {
83
- for (const TSharedPtr<FJsonValue>& Value : *JsonArray)
84
- {
85
- OutArray.Add((float)Value->AsNumber());
86
- }
87
- }
88
- }
89
-
90
- FVector2D FUnrealMCPCommonUtils::GetVector2DFromJson(const TSharedPtr<FJsonObject>& JsonObject, const FString& FieldName)
91
- {
92
- FVector2D Result(0.0f, 0.0f);
93
-
94
- if (!JsonObject->HasField(FieldName))
95
- {
96
- return Result;
97
- }
98
-
99
- // Accept either [x,y] or {x,y}.
100
- const TSharedPtr<FJsonObject>* ObjValue = nullptr;
101
- if (JsonObject->TryGetObjectField(FieldName, ObjValue) && ObjValue && ObjValue->IsValid())
102
- {
103
- double X = 0.0;
104
- double Y = 0.0;
105
- (*ObjValue)->TryGetNumberField(TEXT("x"), X) || (*ObjValue)->TryGetNumberField(TEXT("X"), X);
106
- (*ObjValue)->TryGetNumberField(TEXT("y"), Y) || (*ObjValue)->TryGetNumberField(TEXT("Y"), Y);
107
- Result.X = (float)X;
108
- Result.Y = (float)Y;
109
- return Result;
110
- }
111
-
112
- const TArray<TSharedPtr<FJsonValue>>* JsonArray;
113
- if (JsonObject->TryGetArrayField(FieldName, JsonArray) && JsonArray->Num() >= 2)
114
- {
115
- Result.X = (float)(*JsonArray)[0]->AsNumber();
116
- Result.Y = (float)(*JsonArray)[1]->AsNumber();
117
- }
118
-
119
- return Result;
120
- }
121
-
122
- FVector FUnrealMCPCommonUtils::GetVectorFromJson(const TSharedPtr<FJsonObject>& JsonObject, const FString& FieldName)
123
- {
124
- FVector Result(0.0f, 0.0f, 0.0f);
125
-
126
- if (!JsonObject->HasField(FieldName))
127
- {
128
- return Result;
129
- }
130
-
131
- // Accept either [x,y,z] or {x,y,z}.
132
- const TSharedPtr<FJsonObject>* ObjValue = nullptr;
133
- if (JsonObject->TryGetObjectField(FieldName, ObjValue) && ObjValue && ObjValue->IsValid())
134
- {
135
- double X = 0.0;
136
- double Y = 0.0;
137
- double Z = 0.0;
138
- ((*ObjValue)->TryGetNumberField(TEXT("x"), X) || (*ObjValue)->TryGetNumberField(TEXT("X"), X));
139
- ((*ObjValue)->TryGetNumberField(TEXT("y"), Y) || (*ObjValue)->TryGetNumberField(TEXT("Y"), Y));
140
- ((*ObjValue)->TryGetNumberField(TEXT("z"), Z) || (*ObjValue)->TryGetNumberField(TEXT("Z"), Z));
141
- Result.X = (float)X;
142
- Result.Y = (float)Y;
143
- Result.Z = (float)Z;
144
- return Result;
145
- }
146
-
147
- const TArray<TSharedPtr<FJsonValue>>* JsonArray;
148
- if (JsonObject->TryGetArrayField(FieldName, JsonArray) && JsonArray->Num() >= 3)
149
- {
150
- Result.X = (float)(*JsonArray)[0]->AsNumber();
151
- Result.Y = (float)(*JsonArray)[1]->AsNumber();
152
- Result.Z = (float)(*JsonArray)[2]->AsNumber();
153
- }
154
-
155
- return Result;
156
- }
157
-
158
- FRotator FUnrealMCPCommonUtils::GetRotatorFromJson(const TSharedPtr<FJsonObject>& JsonObject, const FString& FieldName)
159
- {
160
- FRotator Result(0.0f, 0.0f, 0.0f);
161
-
162
- if (!JsonObject->HasField(FieldName))
163
- {
164
- return Result;
165
- }
166
-
167
- // Accept either [pitch,yaw,roll] or {pitch,yaw,roll}.
168
- const TSharedPtr<FJsonObject>* ObjValue = nullptr;
169
- if (JsonObject->TryGetObjectField(FieldName, ObjValue) && ObjValue && ObjValue->IsValid())
170
- {
171
- double Pitch = 0.0;
172
- double Yaw = 0.0;
173
- double Roll = 0.0;
174
- ((*ObjValue)->TryGetNumberField(TEXT("pitch"), Pitch) || (*ObjValue)->TryGetNumberField(TEXT("Pitch"), Pitch));
175
- ((*ObjValue)->TryGetNumberField(TEXT("yaw"), Yaw) || (*ObjValue)->TryGetNumberField(TEXT("Yaw"), Yaw));
176
- ((*ObjValue)->TryGetNumberField(TEXT("roll"), Roll) || (*ObjValue)->TryGetNumberField(TEXT("Roll"), Roll));
177
- Result.Pitch = (float)Pitch;
178
- Result.Yaw = (float)Yaw;
179
- Result.Roll = (float)Roll;
180
- return Result;
181
- }
182
-
183
- const TArray<TSharedPtr<FJsonValue>>* JsonArray;
184
- if (JsonObject->TryGetArrayField(FieldName, JsonArray) && JsonArray->Num() >= 3)
185
- {
186
- Result.Pitch = (float)(*JsonArray)[0]->AsNumber();
187
- Result.Yaw = (float)(*JsonArray)[1]->AsNumber();
188
- Result.Roll = (float)(*JsonArray)[2]->AsNumber();
189
- }
190
-
191
- return Result;
192
- }
193
-
194
- // Blueprint Utilities
195
- UBlueprint* FUnrealMCPCommonUtils::FindBlueprint(const FString& BlueprintName)
196
- {
197
- return FindBlueprintByName(BlueprintName);
198
- }
199
-
200
- UBlueprint* FUnrealMCPCommonUtils::FindBlueprintByName(const FString& BlueprintName)
201
- {
202
- FString Trimmed = BlueprintName.TrimStartAndEnd();
203
- if (Trimmed.IsEmpty())
204
- {
205
- return nullptr;
206
- }
207
-
208
- // Allow callers to pass either a simple asset name (BP_MyAsset),
209
- // or a full object path (/Game/Path/BP_MyAsset.BP_MyAsset),
210
- // or a Blueprint'...' soft path.
211
- if (Trimmed.StartsWith(TEXT("/")) || Trimmed.StartsWith(TEXT("Blueprint'")))
212
- {
213
- UObject* Obj = LoadObject<UObject>(nullptr, *Trimmed);
214
- if (!Obj && Trimmed.StartsWith(TEXT("/")) && !Trimmed.Contains(TEXT("'")))
215
- {
216
- Obj = LoadObject<UObject>(nullptr, *FString::Printf(TEXT("Blueprint'%s'"), *Trimmed));
217
- }
218
- if (UBlueprint* BP = Cast<UBlueprint>(Obj)) return BP;
219
- if (UBlueprintGeneratedClass* BPGC = Cast<UBlueprintGeneratedClass>(Obj))
220
- {
221
- return Cast<UBlueprint>(BPGC->ClassGeneratedBy);
222
- }
223
- return nullptr;
224
- }
225
-
226
- FString AssetPath = TEXT("/Game/Blueprints/") + Trimmed;
227
- if (UBlueprint* BP = LoadObject<UBlueprint>(nullptr, *AssetPath)) return BP;
228
- if (UBlueprint* BP = LoadObject<UBlueprint>(nullptr, *FString::Printf(TEXT("Blueprint'%s'"), *AssetPath))) return BP;
229
- return nullptr;
230
- }
231
-
232
- UEdGraph* FUnrealMCPCommonUtils::FindOrCreateEventGraph(UBlueprint* Blueprint)
233
- {
234
- if (!Blueprint)
235
- {
236
- return nullptr;
237
- }
238
-
239
- // Try to find the event graph
240
- for (UEdGraph* Graph : Blueprint->UbergraphPages)
241
- {
242
- if (Graph->GetName().Contains(TEXT("EventGraph")))
243
- {
244
- return Graph;
245
- }
246
- }
247
-
248
- // Create a new event graph if none exists
249
- UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(Blueprint, FName(TEXT("EventGraph")), UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
250
- FBlueprintEditorUtils::AddUbergraphPage(Blueprint, NewGraph);
251
- return NewGraph;
252
- }
253
-
254
- // Blueprint node utilities
255
- UK2Node_Event* FUnrealMCPCommonUtils::CreateEventNode(UEdGraph* Graph, const FString& EventName, const FVector2D& Position)
256
- {
257
- if (!Graph)
258
- {
259
- return nullptr;
260
- }
261
-
262
- UBlueprint* Blueprint = FBlueprintEditorUtils::FindBlueprintForGraph(Graph);
263
- if (!Blueprint)
264
- {
265
- return nullptr;
266
- }
267
-
268
- // Check for existing event node with this exact name
269
- for (UEdGraphNode* Node : Graph->Nodes)
270
- {
271
- UK2Node_Event* EventNode = Cast<UK2Node_Event>(Node);
272
- if (EventNode && EventNode->EventReference.GetMemberName() == FName(*EventName))
273
- {
274
- UE_LOG(LogTemp, Display, TEXT("Using existing event node with name %s (ID: %s)"),
275
- *EventName, *EventNode->NodeGuid.ToString());
276
- return EventNode;
277
- }
278
- }
279
-
280
- // No existing node found, create a new one
281
- UK2Node_Event* EventNode = nullptr;
282
-
283
- // Find the function to create the event
284
- UClass* BlueprintClass = Blueprint->GeneratedClass;
285
- UFunction* EventFunction = BlueprintClass->FindFunctionByName(FName(*EventName));
286
-
287
- if (EventFunction)
288
- {
289
- EventNode = NewObject<UK2Node_Event>(Graph);
290
- EventNode->EventReference.SetExternalMember(FName(*EventName), BlueprintClass);
291
- EventNode->NodePosX = Position.X;
292
- EventNode->NodePosY = Position.Y;
293
- Graph->AddNode(EventNode, true);
294
- EventNode->PostPlacedNewNode();
295
- EventNode->AllocateDefaultPins();
296
- UE_LOG(LogTemp, Display, TEXT("Created new event node with name %s (ID: %s)"),
297
- *EventName, *EventNode->NodeGuid.ToString());
298
- }
299
- else
300
- {
301
- UE_LOG(LogTemp, Error, TEXT("Failed to find function for event name: %s"), *EventName);
302
- }
303
-
304
- return EventNode;
305
- }
306
-
307
- UK2Node_CallFunction* FUnrealMCPCommonUtils::CreateFunctionCallNode(UEdGraph* Graph, UFunction* Function, const FVector2D& Position)
308
- {
309
- if (!Graph || !Function)
310
- {
311
- return nullptr;
312
- }
313
-
314
- UK2Node_CallFunction* FunctionNode = NewObject<UK2Node_CallFunction>(Graph);
315
- FunctionNode->SetFromFunction(Function);
316
- FunctionNode->NodePosX = Position.X;
317
- FunctionNode->NodePosY = Position.Y;
318
- Graph->AddNode(FunctionNode, true);
319
- FunctionNode->CreateNewGuid();
320
- FunctionNode->PostPlacedNewNode();
321
- FunctionNode->AllocateDefaultPins();
322
-
323
- return FunctionNode;
324
- }
325
-
326
- UK2Node_VariableGet* FUnrealMCPCommonUtils::CreateVariableGetNode(UEdGraph* Graph, UBlueprint* Blueprint, const FString& VariableName, const FVector2D& Position)
327
- {
328
- if (!Graph || !Blueprint)
329
- {
330
- return nullptr;
331
- }
332
-
333
- UK2Node_VariableGet* VariableGetNode = NewObject<UK2Node_VariableGet>(Graph);
334
-
335
- FName VarName(*VariableName);
336
- FProperty* Property = FindFProperty<FProperty>(Blueprint->GeneratedClass, VarName);
337
-
338
- if (Property)
339
- {
340
- VariableGetNode->VariableReference.SetFromField<FProperty>(Property, false);
341
- VariableGetNode->NodePosX = Position.X;
342
- VariableGetNode->NodePosY = Position.Y;
343
- Graph->AddNode(VariableGetNode, true);
344
- VariableGetNode->PostPlacedNewNode();
345
- VariableGetNode->AllocateDefaultPins();
346
-
347
- return VariableGetNode;
348
- }
349
-
350
- return nullptr;
351
- }
352
-
353
- UK2Node_VariableSet* FUnrealMCPCommonUtils::CreateVariableSetNode(UEdGraph* Graph, UBlueprint* Blueprint, const FString& VariableName, const FVector2D& Position)
354
- {
355
- if (!Graph || !Blueprint)
356
- {
357
- return nullptr;
358
- }
359
-
360
- UK2Node_VariableSet* VariableSetNode = NewObject<UK2Node_VariableSet>(Graph);
361
-
362
- FName VarName(*VariableName);
363
- FProperty* Property = FindFProperty<FProperty>(Blueprint->GeneratedClass, VarName);
364
-
365
- if (Property)
366
- {
367
- VariableSetNode->VariableReference.SetFromField<FProperty>(Property, false);
368
- VariableSetNode->NodePosX = Position.X;
369
- VariableSetNode->NodePosY = Position.Y;
370
- Graph->AddNode(VariableSetNode, true);
371
- VariableSetNode->PostPlacedNewNode();
372
- VariableSetNode->AllocateDefaultPins();
373
-
374
- return VariableSetNode;
375
- }
376
-
377
- return nullptr;
378
- }
379
-
380
- UK2Node_InputAction* FUnrealMCPCommonUtils::CreateInputActionNode(UEdGraph* Graph, const FString& ActionName, const FVector2D& Position)
381
- {
382
- if (!Graph)
383
- {
384
- return nullptr;
385
- }
386
-
387
- UK2Node_InputAction* InputActionNode = NewObject<UK2Node_InputAction>(Graph);
388
- InputActionNode->InputActionName = FName(*ActionName);
389
- InputActionNode->NodePosX = Position.X;
390
- InputActionNode->NodePosY = Position.Y;
391
- Graph->AddNode(InputActionNode, true);
392
- InputActionNode->CreateNewGuid();
393
- InputActionNode->PostPlacedNewNode();
394
- InputActionNode->AllocateDefaultPins();
395
-
396
- return InputActionNode;
397
- }
398
-
399
- UK2Node_Self* FUnrealMCPCommonUtils::CreateSelfReferenceNode(UEdGraph* Graph, const FVector2D& Position)
400
- {
401
- if (!Graph)
402
- {
403
- return nullptr;
404
- }
405
-
406
- UK2Node_Self* SelfNode = NewObject<UK2Node_Self>(Graph);
407
- SelfNode->NodePosX = Position.X;
408
- SelfNode->NodePosY = Position.Y;
409
- Graph->AddNode(SelfNode, true);
410
- SelfNode->CreateNewGuid();
411
- SelfNode->PostPlacedNewNode();
412
- SelfNode->AllocateDefaultPins();
413
-
414
- return SelfNode;
415
- }
416
-
417
- bool FUnrealMCPCommonUtils::ConnectGraphNodes(UEdGraph* Graph, UEdGraphNode* SourceNode, const FString& SourcePinName,
418
- UEdGraphNode* TargetNode, const FString& TargetPinName)
419
- {
420
- if (!Graph || !SourceNode || !TargetNode)
421
- {
422
- return false;
423
- }
424
-
425
- UEdGraphPin* SourcePin = FindPin(SourceNode, SourcePinName, EGPD_Output);
426
- UEdGraphPin* TargetPin = FindPin(TargetNode, TargetPinName, EGPD_Input);
427
-
428
- if (SourcePin && TargetPin)
429
- {
430
- SourcePin->MakeLinkTo(TargetPin);
431
- return true;
432
- }
433
-
434
- return false;
435
- }
436
-
437
- UEdGraphPin* FUnrealMCPCommonUtils::FindPin(UEdGraphNode* Node, const FString& PinName, EEdGraphPinDirection Direction)
438
- {
439
- if (!Node)
440
- {
441
- return nullptr;
442
- }
443
-
444
- // Log all pins for debugging
445
- UE_LOG(LogTemp, Display, TEXT("FindPin: Looking for pin '%s' (Direction: %d) in node '%s'"),
446
- *PinName, (int32)Direction, *Node->GetName());
447
-
448
- for (UEdGraphPin* Pin : Node->Pins)
449
- {
450
- UE_LOG(LogTemp, Display, TEXT(" - Available pin: '%s', Direction: %d, Category: %s"),
451
- *Pin->PinName.ToString(), (int32)Pin->Direction, *Pin->PinType.PinCategory.ToString());
452
- }
453
-
454
- // First try exact match
455
- for (UEdGraphPin* Pin : Node->Pins)
456
- {
457
- if (Pin->PinName.ToString() == PinName && (Direction == EGPD_MAX || Pin->Direction == Direction))
458
- {
459
- UE_LOG(LogTemp, Display, TEXT(" - Found exact matching pin: '%s'"), *Pin->PinName.ToString());
460
- return Pin;
461
- }
462
- }
463
-
464
- // If no exact match and we're looking for a component reference, try case-insensitive match
465
- for (UEdGraphPin* Pin : Node->Pins)
466
- {
467
- if (Pin->PinName.ToString().Equals(PinName, ESearchCase::IgnoreCase) &&
468
- (Direction == EGPD_MAX || Pin->Direction == Direction))
469
- {
470
- UE_LOG(LogTemp, Display, TEXT(" - Found case-insensitive matching pin: '%s'"), *Pin->PinName.ToString());
471
- return Pin;
472
- }
473
- }
474
-
475
- // If we're looking for a component output and didn't find it by name, try to find the first data output pin
476
- if (Direction == EGPD_Output && Cast<UK2Node_VariableGet>(Node) != nullptr)
477
- {
478
- for (UEdGraphPin* Pin : Node->Pins)
479
- {
480
- if (Pin->Direction == EGPD_Output && Pin->PinType.PinCategory != UEdGraphSchema_K2::PC_Exec)
481
- {
482
- UE_LOG(LogTemp, Display, TEXT(" - Found fallback data output pin: '%s'"), *Pin->PinName.ToString());
483
- return Pin;
484
- }
485
- }
486
- }
487
-
488
- UE_LOG(LogTemp, Warning, TEXT(" - No matching pin found for '%s'"), *PinName);
489
- return nullptr;
490
- }
491
-
492
- // Actor utilities
493
- TSharedPtr<FJsonValue> FUnrealMCPCommonUtils::ActorToJson(AActor* Actor)
494
- {
495
- if (!Actor)
496
- {
497
- return MakeShared<FJsonValueNull>();
498
- }
499
-
500
- TSharedPtr<FJsonObject> ActorObject = MakeShared<FJsonObject>();
501
- ActorObject->SetStringField(TEXT("name"), Actor->GetName());
502
- ActorObject->SetStringField(TEXT("class"), Actor->GetClass()->GetName());
503
-
504
- FVector Location = Actor->GetActorLocation();
505
- TArray<TSharedPtr<FJsonValue>> LocationArray;
506
- LocationArray.Add(MakeShared<FJsonValueNumber>(Location.X));
507
- LocationArray.Add(MakeShared<FJsonValueNumber>(Location.Y));
508
- LocationArray.Add(MakeShared<FJsonValueNumber>(Location.Z));
509
- ActorObject->SetArrayField(TEXT("location"), LocationArray);
510
-
511
- FRotator Rotation = Actor->GetActorRotation();
512
- TArray<TSharedPtr<FJsonValue>> RotationArray;
513
- RotationArray.Add(MakeShared<FJsonValueNumber>(Rotation.Pitch));
514
- RotationArray.Add(MakeShared<FJsonValueNumber>(Rotation.Yaw));
515
- RotationArray.Add(MakeShared<FJsonValueNumber>(Rotation.Roll));
516
- ActorObject->SetArrayField(TEXT("rotation"), RotationArray);
517
-
518
- FVector Scale = Actor->GetActorScale3D();
519
- TArray<TSharedPtr<FJsonValue>> ScaleArray;
520
- ScaleArray.Add(MakeShared<FJsonValueNumber>(Scale.X));
521
- ScaleArray.Add(MakeShared<FJsonValueNumber>(Scale.Y));
522
- ScaleArray.Add(MakeShared<FJsonValueNumber>(Scale.Z));
523
- ActorObject->SetArrayField(TEXT("scale"), ScaleArray);
524
-
525
- return MakeShared<FJsonValueObject>(ActorObject);
526
- }
527
-
528
- TSharedPtr<FJsonObject> FUnrealMCPCommonUtils::ActorToJsonObject(AActor* Actor, bool bDetailed)
529
- {
530
- if (!Actor)
531
- {
532
- return nullptr;
533
- }
534
-
535
- TSharedPtr<FJsonObject> ActorObject = MakeShared<FJsonObject>();
536
- ActorObject->SetStringField(TEXT("name"), Actor->GetName());
537
- ActorObject->SetStringField(TEXT("class"), Actor->GetClass()->GetName());
538
-
539
- #if WITH_EDITOR
540
- ActorObject->SetStringField(TEXT("label"), Actor->GetActorLabel());
541
- #endif
542
-
543
- FVector Location = Actor->GetActorLocation();
544
- TArray<TSharedPtr<FJsonValue>> LocationArray;
545
- LocationArray.Add(MakeShared<FJsonValueNumber>(Location.X));
546
- LocationArray.Add(MakeShared<FJsonValueNumber>(Location.Y));
547
- LocationArray.Add(MakeShared<FJsonValueNumber>(Location.Z));
548
- ActorObject->SetArrayField(TEXT("location"), LocationArray);
549
-
550
- FRotator Rotation = Actor->GetActorRotation();
551
- TArray<TSharedPtr<FJsonValue>> RotationArray;
552
- RotationArray.Add(MakeShared<FJsonValueNumber>(Rotation.Pitch));
553
- RotationArray.Add(MakeShared<FJsonValueNumber>(Rotation.Yaw));
554
- RotationArray.Add(MakeShared<FJsonValueNumber>(Rotation.Roll));
555
- ActorObject->SetArrayField(TEXT("rotation"), RotationArray);
556
-
557
- FVector Scale = Actor->GetActorScale3D();
558
- TArray<TSharedPtr<FJsonValue>> ScaleArray;
559
- ScaleArray.Add(MakeShared<FJsonValueNumber>(Scale.X));
560
- ScaleArray.Add(MakeShared<FJsonValueNumber>(Scale.Y));
561
- ScaleArray.Add(MakeShared<FJsonValueNumber>(Scale.Z));
562
- ActorObject->SetArrayField(TEXT("scale"), ScaleArray);
563
-
564
- if (!bDetailed)
565
- {
566
- return ActorObject;
567
- }
568
-
569
- const auto isEditableProperty = [](const FProperty* Prop) -> bool {
570
- if (!Prop) return false;
571
- if (!Prop->HasAnyPropertyFlags(CPF_Edit)) return false;
572
- if (Prop->HasAnyPropertyFlags(CPF_Transient | CPF_Deprecated | CPF_DisableEditOnInstance)) return false;
573
- return true;
574
- };
575
-
576
- const auto addPropertyDescriptors = [&](UObject* Obj, int32 Limit, TArray<TSharedPtr<FJsonValue>>& OutProps) {
577
- if (!Obj) return;
578
- int32 Count = 0;
579
- for (TFieldIterator<FProperty> It(Obj->GetClass()); It; ++It)
580
- {
581
- FProperty* Prop = *It;
582
- if (!isEditableProperty(Prop)) continue;
583
- if (Limit > 0 && Count >= Limit) break;
584
-
585
- TSharedPtr<FJsonObject> P = MakeShared<FJsonObject>();
586
- P->SetStringField(TEXT("name"), Prop->GetName());
587
- P->SetStringField(TEXT("type"), Prop->GetCPPType());
588
- P->SetBoolField(TEXT("isArray"), Prop->IsA<FArrayProperty>());
589
- OutProps.Add(MakeShared<FJsonValueObject>(P));
590
- Count++;
591
- }
592
- };
593
-
594
- // Actor editable properties
595
- {
596
- TArray<TSharedPtr<FJsonValue>> Props;
597
- addPropertyDescriptors(Actor, 250, Props);
598
- ActorObject->SetArrayField(TEXT("properties"), Props);
599
- }
600
-
601
- // Actor component pointer fields (useful for nested property paths like PointLightComponent.LightColor)
602
- {
603
- TArray<TSharedPtr<FJsonValue>> Fields;
604
- int32 Count = 0;
605
- for (TFieldIterator<FProperty> It(Actor->GetClass()); It; ++It)
606
- {
607
- FProperty* Prop = *It;
608
- FObjectPropertyBase* ObjProp = CastField<FObjectPropertyBase>(Prop);
609
- if (!ObjProp) continue;
610
- if (!ObjProp->PropertyClass || !ObjProp->PropertyClass->IsChildOf(UActorComponent::StaticClass())) continue;
611
- if (Count >= 50) break;
612
-
613
- UObject* Ref = ObjProp->GetObjectPropertyValue_InContainer(Actor);
614
- UActorComponent* Comp = Cast<UActorComponent>(Ref);
615
-
616
- TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
617
- Obj->SetStringField(TEXT("property"), Prop->GetName());
618
- Obj->SetStringField(TEXT("class"), ObjProp->PropertyClass->GetName());
619
- if (Comp)
620
- {
621
- Obj->SetStringField(TEXT("componentName"), Comp->GetName());
622
- }
623
- Fields.Add(MakeShared<FJsonValueObject>(Obj));
624
- Count++;
625
- }
626
- ActorObject->SetArrayField(TEXT("componentFields"), Fields);
627
- }
628
-
629
- // Components + their editable properties (best-effort, bounded)
630
- {
631
- TArray<TSharedPtr<FJsonValue>> Components;
632
- TArray<UActorComponent*> Comps;
633
- Actor->GetComponents(Comps);
634
- Comps.Sort([](const UActorComponent& A, const UActorComponent& B) {
635
- return A.GetFName().LexicalLess(B.GetFName());
636
- });
637
-
638
- int32 Added = 0;
639
- for (UActorComponent* C : Comps)
640
- {
641
- if (!C) continue;
642
- if (Added >= 50) break;
643
- TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
644
- Obj->SetStringField(TEXT("name"), C->GetName());
645
- Obj->SetStringField(TEXT("class"), C->GetClass()->GetName());
646
-
647
- TArray<TSharedPtr<FJsonValue>> Props;
648
- addPropertyDescriptors(C, 200, Props);
649
- Obj->SetArrayField(TEXT("properties"), Props);
650
-
651
- Components.Add(MakeShared<FJsonValueObject>(Obj));
652
- Added++;
653
- }
654
- ActorObject->SetArrayField(TEXT("components"), Components);
655
- }
656
-
657
- return ActorObject;
658
- }
659
-
660
- UK2Node_Event* FUnrealMCPCommonUtils::FindExistingEventNode(UEdGraph* Graph, const FString& EventName)
661
- {
662
- if (!Graph)
663
- {
664
- return nullptr;
665
- }
666
-
667
- // Look for existing event nodes
668
- for (UEdGraphNode* Node : Graph->Nodes)
669
- {
670
- UK2Node_Event* EventNode = Cast<UK2Node_Event>(Node);
671
- if (EventNode && EventNode->EventReference.GetMemberName() == FName(*EventName))
672
- {
673
- UE_LOG(LogTemp, Display, TEXT("Found existing event node with name: %s"), *EventName);
674
- return EventNode;
675
- }
676
- }
677
-
678
- return nullptr;
679
- }
680
-
681
- bool FUnrealMCPCommonUtils::SetObjectProperty(UObject* Object, const FString& PropertyName,
682
- const TSharedPtr<FJsonValue>& Value, FString& OutErrorMessage)
683
- {
684
- if (!Object)
685
- {
686
- OutErrorMessage = TEXT("Invalid object");
687
- return false;
688
- }
689
-
690
- const auto splitPath = [](const FString& Raw) -> TArray<FString> {
691
- FString P = Raw;
692
- P.ReplaceInline(TEXT("/"), TEXT("."), ESearchCase::IgnoreCase);
693
- P = P.TrimStartAndEnd();
694
- TArray<FString> Segs;
695
- P.ParseIntoArray(Segs, TEXT("."), true);
696
- Segs.RemoveAll([](const FString& S) { return S.TrimStartAndEnd().IsEmpty(); });
697
- for (FString& S : Segs) S = S.TrimStartAndEnd();
698
- return Segs;
699
- };
700
-
701
- const auto resolvePropertyPath = [&](UObject* Root, const FString& RawPath, UObject*& OutContainerObj, FProperty*& OutProp, void*& OutPropAddr) -> bool {
702
- OutContainerObj = Root;
703
- OutProp = nullptr;
704
- OutPropAddr = nullptr;
705
-
706
- const TArray<FString> Segs = splitPath(RawPath);
707
- if (Segs.Num() == 0)
708
- {
709
- OutErrorMessage = TEXT("Property path is empty");
710
- return false;
711
- }
712
-
713
- UObject* Current = Root;
714
- for (int32 i = 0; i < Segs.Num(); i++)
715
- {
716
- const FString& Seg = Segs[i];
717
- const bool bIsLeaf = (i == Segs.Num() - 1);
718
-
719
- FProperty* Prop = Current->GetClass()->FindPropertyByName(*Seg);
720
- if (!Prop)
721
- {
722
- OutErrorMessage = FString::Printf(TEXT("Property not found: %s"), *Seg);
723
- return false;
724
- }
725
-
726
- if (bIsLeaf)
727
- {
728
- OutContainerObj = Current;
729
- OutProp = Prop;
730
- OutPropAddr = Prop->ContainerPtrToValuePtr<void>(Current);
731
- return true;
732
- }
733
-
734
- FObjectPropertyBase* ObjProp = CastField<FObjectPropertyBase>(Prop);
735
- if (!ObjProp)
736
- {
737
- OutErrorMessage = FString::Printf(TEXT("Unsupported property path segment (not an object): %s"), *Seg);
738
- return false;
739
- }
740
-
741
- UObject* Next = ObjProp->GetObjectPropertyValue_InContainer(Current);
742
- if (!Next)
743
- {
744
- OutErrorMessage = FString::Printf(TEXT("Null object at path segment: %s"), *Seg);
745
- return false;
746
- }
747
- Current = Next;
748
- }
749
-
750
- OutErrorMessage = TEXT("Failed to resolve property path");
751
- return false;
752
- };
753
-
754
- UObject* ContainerObj = nullptr;
755
- FProperty* Property = nullptr;
756
- void* PropertyAddr = nullptr;
757
- if (!resolvePropertyPath(Object, PropertyName, ContainerObj, Property, PropertyAddr))
758
- {
759
- return false;
760
- }
761
-
762
- if (!Property || !ContainerObj || !PropertyAddr)
763
- {
764
- OutErrorMessage = TEXT("Property resolution failed");
765
- return false;
766
- }
767
-
768
- // Mark dirty for undo/redo and details panel refresh (editor only).
769
- #if WITH_EDITOR
770
- ContainerObj->Modify();
771
- #endif
772
-
773
- // Handle different property types
774
- if (Property->IsA<FBoolProperty>())
775
- {
776
- ((FBoolProperty*)Property)->SetPropertyValue(PropertyAddr, Value->AsBool());
777
- return true;
778
- }
779
- else if (Property->IsA<FIntProperty>())
780
- {
781
- int32 IntValue = static_cast<int32>(Value->AsNumber());
782
- FIntProperty* IntProperty = CastField<FIntProperty>(Property);
783
- if (IntProperty)
784
- {
785
- IntProperty->SetPropertyValue_InContainer(Object, IntValue);
786
- return true;
787
- }
788
- }
789
- else if (Property->IsA<FFloatProperty>())
790
- {
791
- ((FFloatProperty*)Property)->SetPropertyValue(PropertyAddr, Value->AsNumber());
792
- return true;
793
- }
794
- else if (Property->IsA<FStrProperty>())
795
- {
796
- ((FStrProperty*)Property)->SetPropertyValue(PropertyAddr, Value->AsString());
797
- return true;
798
- }
799
- else if (Property->IsA<FNameProperty>())
800
- {
801
- const FString S = Value->Type == EJson::String ? Value->AsString() : FString::Printf(TEXT("%g"), Value->AsNumber());
802
- CastFieldChecked<FNameProperty>(Property)->SetPropertyValue(PropertyAddr, FName(*S));
803
- return true;
804
- }
805
- else if (Property->IsA<FTextProperty>())
806
- {
807
- const FString S = Value->Type == EJson::String ? Value->AsString() : FString::Printf(TEXT("%g"), Value->AsNumber());
808
- CastFieldChecked<FTextProperty>(Property)->SetPropertyValue(PropertyAddr, FText::FromString(S));
809
- return true;
810
- }
811
- else if (Property->IsA<FStructProperty>())
812
- {
813
- FStructProperty* StructProp = CastField<FStructProperty>(Property);
814
- if (!StructProp || !StructProp->Struct)
815
- {
816
- OutErrorMessage = FString::Printf(TEXT("Invalid struct property: %s"), *PropertyName);
817
- return false;
818
- }
819
-
820
- const UScriptStruct* S = StructProp->Struct;
821
- if (S == TBaseStructure<FVector>::Get())
822
- {
823
- if (Value->Type != EJson::Array || Value->AsArray().Num() != 3)
824
- {
825
- OutErrorMessage = TEXT("FVector requires an array of 3 numbers");
826
- return false;
827
- }
828
- const auto& Arr = Value->AsArray();
829
- const FVector V((float)Arr[0]->AsNumber(), (float)Arr[1]->AsNumber(), (float)Arr[2]->AsNumber());
830
- StructProp->CopySingleValue(PropertyAddr, &V);
831
- return true;
832
- }
833
- if (S == TBaseStructure<FVector2D>::Get())
834
- {
835
- if (Value->Type != EJson::Array || Value->AsArray().Num() != 2)
836
- {
837
- OutErrorMessage = TEXT("FVector2D requires an array of 2 numbers");
838
- return false;
839
- }
840
- const auto& Arr = Value->AsArray();
841
- const FVector2D V((float)Arr[0]->AsNumber(), (float)Arr[1]->AsNumber());
842
- StructProp->CopySingleValue(PropertyAddr, &V);
843
- return true;
844
- }
845
- if (S == TBaseStructure<FRotator>::Get())
846
- {
847
- if (Value->Type != EJson::Array || Value->AsArray().Num() != 3)
848
- {
849
- OutErrorMessage = TEXT("FRotator requires an array of 3 numbers [Pitch,Yaw,Roll]");
850
- return false;
851
- }
852
- const auto& Arr = Value->AsArray();
853
- const FRotator R((float)Arr[0]->AsNumber(), (float)Arr[1]->AsNumber(), (float)Arr[2]->AsNumber());
854
- StructProp->CopySingleValue(PropertyAddr, &R);
855
- return true;
856
- }
857
- if (S == TBaseStructure<FLinearColor>::Get())
858
- {
859
- if (Value->Type != EJson::Array || (Value->AsArray().Num() != 3 && Value->AsArray().Num() != 4))
860
- {
861
- OutErrorMessage = TEXT("FLinearColor requires an array of 3 or 4 numbers [R,G,B,(A)]");
862
- return false;
863
- }
864
- const auto& Arr = Value->AsArray();
865
- const float A = Arr.Num() >= 4 ? (float)Arr[3]->AsNumber() : 1.0f;
866
- const FLinearColor C((float)Arr[0]->AsNumber(), (float)Arr[1]->AsNumber(), (float)Arr[2]->AsNumber(), A);
867
- StructProp->CopySingleValue(PropertyAddr, &C);
868
- return true;
869
- }
870
- if (S == TBaseStructure<FColor>::Get())
871
- {
872
- if (Value->Type != EJson::Array || (Value->AsArray().Num() != 3 && Value->AsArray().Num() != 4))
873
- {
874
- OutErrorMessage = TEXT("FColor requires an array of 3 or 4 numbers [R,G,B,(A)]");
875
- return false;
876
- }
877
- const auto& Arr = Value->AsArray();
878
- const double r = Arr[0]->AsNumber();
879
- const double g = Arr[1]->AsNumber();
880
- const double b = Arr[2]->AsNumber();
881
- const double a = Arr.Num() >= 4 ? Arr[3]->AsNumber() : 255.0;
882
- const bool bLooksNormalized = r <= 1.0 && g <= 1.0 && b <= 1.0 && a <= 1.0;
883
- const auto toByte = [&](double v) -> uint8 {
884
- const double x = bLooksNormalized ? v * 255.0 : v;
885
- return (uint8)FMath::Clamp((int32)FMath::RoundHalfToZero(x), 0, 255);
886
- };
887
- const FColor C(toByte(r), toByte(g), toByte(b), toByte(a));
888
- StructProp->CopySingleValue(PropertyAddr, &C);
889
- return true;
890
- }
891
-
892
- OutErrorMessage = FString::Printf(TEXT("Unsupported struct type: %s"), *S->GetName());
893
- return false;
894
- }
895
- else if (Property->IsA<FObjectPropertyBase>())
896
- {
897
- FObjectPropertyBase* ObjProp = CastField<FObjectPropertyBase>(Property);
898
- if (!ObjProp)
899
- {
900
- OutErrorMessage = FString::Printf(TEXT("Invalid ObjectProperty: %s"), *PropertyName);
901
- return false;
902
- }
903
-
904
- if (Value->Type == EJson::Null)
905
- {
906
- ObjProp->SetObjectPropertyValue(PropertyAddr, nullptr);
907
- return true;
908
- }
909
-
910
- if (Value->Type != EJson::String)
911
- {
912
- OutErrorMessage = FString::Printf(TEXT("ObjectProperty requires a string object path (or null): %s"), *PropertyName);
913
- return false;
914
- }
915
-
916
- FString Path = Value->AsString().TrimStartAndEnd();
917
- if (Path.IsEmpty())
918
- {
919
- ObjProp->SetObjectPropertyValue(PropertyAddr, nullptr);
920
- return true;
921
- }
922
-
923
- UObject* Loaded = StaticLoadObject(ObjProp->PropertyClass, nullptr, *Path);
924
- if (!Loaded && !Path.Contains(TEXT("'")))
925
- {
926
- Loaded = StaticLoadObject(
927
- ObjProp->PropertyClass,
928
- nullptr,
929
- *FString::Printf(TEXT("%s'%s'"), *ObjProp->PropertyClass->GetName(), *Path));
930
- }
931
-
932
- if (!Loaded)
933
- {
934
- OutErrorMessage = FString::Printf(TEXT("Failed to load object for %s: %s"), *PropertyName, *Path);
935
- return false;
936
- }
937
-
938
- ObjProp->SetObjectPropertyValue(PropertyAddr, Loaded);
939
- return true;
940
- }
941
- else if (Property->IsA<FArrayProperty>())
942
- {
943
- if (Value->Type != EJson::Array)
944
- {
945
- OutErrorMessage = FString::Printf(TEXT("ArrayProperty requires an array value: %s"), *PropertyName);
946
- return false;
947
- }
948
-
949
- FArrayProperty* ArrProp = CastField<FArrayProperty>(Property);
950
- if (!ArrProp || !ArrProp->Inner)
951
- {
952
- OutErrorMessage = FString::Printf(TEXT("Invalid ArrayProperty: %s"), *PropertyName);
953
- return false;
954
- }
955
-
956
- const TArray<TSharedPtr<FJsonValue>>& Arr = Value->AsArray();
957
- FScriptArrayHelper Helper(ArrProp, PropertyAddr);
958
- Helper.Resize(Arr.Num());
959
-
960
- for (int32 i = 0; i < Arr.Num(); i++)
961
- {
962
- void* ElemPtr = Helper.GetRawPtr(i);
963
- const TSharedPtr<FJsonValue>& Elem = Arr[i];
964
-
965
- if (FObjectPropertyBase* InnerObj = CastField<FObjectPropertyBase>(ArrProp->Inner))
966
- {
967
- if (!Elem.IsValid() || Elem->Type == EJson::Null)
968
- {
969
- InnerObj->SetObjectPropertyValue(ElemPtr, nullptr);
970
- continue;
971
- }
972
- if (Elem->Type != EJson::String)
973
- {
974
- OutErrorMessage = FString::Printf(TEXT("Array element %d must be a string object path (or null) for %s"), i, *PropertyName);
975
- return false;
976
- }
977
- FString Path = Elem->AsString().TrimStartAndEnd();
978
- UObject* Loaded = Path.IsEmpty() ? nullptr : StaticLoadObject(InnerObj->PropertyClass, nullptr, *Path);
979
- if (!Loaded && !Path.IsEmpty() && !Path.Contains(TEXT("'")))
980
- {
981
- Loaded = StaticLoadObject(
982
- InnerObj->PropertyClass,
983
- nullptr,
984
- *FString::Printf(TEXT("%s'%s'"), *InnerObj->PropertyClass->GetName(), *Path));
985
- }
986
- if (!Path.IsEmpty() && !Loaded)
987
- {
988
- OutErrorMessage = FString::Printf(TEXT("Failed to load object for %s[%d]: %s"), *PropertyName, i, *Path);
989
- return false;
990
- }
991
- InnerObj->SetObjectPropertyValue(ElemPtr, Loaded);
992
- continue;
993
- }
994
-
995
- if (FStrProperty* InnerStr = CastField<FStrProperty>(ArrProp->Inner))
996
- {
997
- if (!Elem.IsValid() || Elem->Type == EJson::Null)
998
- {
999
- InnerStr->SetPropertyValue(ElemPtr, TEXT(""));
1000
- continue;
1001
- }
1002
- InnerStr->SetPropertyValue(ElemPtr, Elem->Type == EJson::String ? Elem->AsString() : FString::Printf(TEXT("%g"), Elem->AsNumber()));
1003
- continue;
1004
- }
1005
-
1006
- if (FNumericProperty* InnerNum = CastField<FNumericProperty>(ArrProp->Inner))
1007
- {
1008
- if (!Elem.IsValid() || Elem->Type != EJson::Number)
1009
- {
1010
- OutErrorMessage = FString::Printf(TEXT("Array element %d must be a number for %s"), i, *PropertyName);
1011
- return false;
1012
- }
1013
- if (InnerNum->IsInteger()) InnerNum->SetIntPropertyValue(ElemPtr, (int64)Elem->AsNumber());
1014
- else InnerNum->SetFloatingPointPropertyValue(ElemPtr, Elem->AsNumber());
1015
- continue;
1016
- }
1017
-
1018
- OutErrorMessage = FString::Printf(TEXT("Unsupported ArrayProperty element type: %s for %s"), *ArrProp->Inner->GetClass()->GetName(), *PropertyName);
1019
- return false;
1020
- }
1021
-
1022
- return true;
1023
- }
1024
- else if (Property->IsA<FByteProperty>())
1025
- {
1026
- FByteProperty* ByteProp = CastField<FByteProperty>(Property);
1027
- UEnum* EnumDef = ByteProp ? ByteProp->GetIntPropertyEnum() : nullptr;
1028
-
1029
- // If this is a TEnumAsByte property (has associated enum)
1030
- if (EnumDef)
1031
- {
1032
- // Handle numeric value
1033
- if (Value->Type == EJson::Number)
1034
- {
1035
- uint8 ByteValue = static_cast<uint8>(Value->AsNumber());
1036
- ByteProp->SetPropertyValue(PropertyAddr, ByteValue);
1037
-
1038
- UE_LOG(LogTemp, Display, TEXT("Setting enum property %s to numeric value: %d"),
1039
- *PropertyName, ByteValue);
1040
- return true;
1041
- }
1042
- // Handle string enum value
1043
- else if (Value->Type == EJson::String)
1044
- {
1045
- FString EnumValueName = Value->AsString();
1046
-
1047
- // Try to convert numeric string to number first
1048
- if (EnumValueName.IsNumeric())
1049
- {
1050
- uint8 ByteValue = FCString::Atoi(*EnumValueName);
1051
- ByteProp->SetPropertyValue(PropertyAddr, ByteValue);
1052
-
1053
- UE_LOG(LogTemp, Display, TEXT("Setting enum property %s to numeric string value: %s -> %d"),
1054
- *PropertyName, *EnumValueName, ByteValue);
1055
- return true;
1056
- }
1057
-
1058
- // Handle qualified enum names (e.g., "Player0" or "EAutoReceiveInput::Player0")
1059
- if (EnumValueName.Contains(TEXT("::")))
1060
- {
1061
- EnumValueName.Split(TEXT("::"), nullptr, &EnumValueName);
1062
- }
1063
-
1064
- int64 EnumValue = EnumDef->GetValueByNameString(EnumValueName);
1065
- if (EnumValue == INDEX_NONE)
1066
- {
1067
- // Try with full name as fallback
1068
- EnumValue = EnumDef->GetValueByNameString(Value->AsString());
1069
- }
1070
-
1071
- if (EnumValue != INDEX_NONE)
1072
- {
1073
- ByteProp->SetPropertyValue(PropertyAddr, static_cast<uint8>(EnumValue));
1074
-
1075
- UE_LOG(LogTemp, Display, TEXT("Setting enum property %s to name value: %s -> %lld"),
1076
- *PropertyName, *EnumValueName, EnumValue);
1077
- return true;
1078
- }
1079
- else
1080
- {
1081
- // Log all possible enum values for debugging
1082
- UE_LOG(LogTemp, Warning, TEXT("Could not find enum value for '%s'. Available options:"), *EnumValueName);
1083
- for (int32 i = 0; i < EnumDef->NumEnums(); i++)
1084
- {
1085
- UE_LOG(LogTemp, Warning, TEXT(" - %s (value: %d)"),
1086
- *EnumDef->GetNameStringByIndex(i), EnumDef->GetValueByIndex(i));
1087
- }
1088
-
1089
- OutErrorMessage = FString::Printf(TEXT("Could not find enum value for '%s'"), *EnumValueName);
1090
- return false;
1091
- }
1092
- }
1093
- }
1094
- else
1095
- {
1096
- // Regular byte property
1097
- uint8 ByteValue = static_cast<uint8>(Value->AsNumber());
1098
- ByteProp->SetPropertyValue(PropertyAddr, ByteValue);
1099
- return true;
1100
- }
1101
- }
1102
- else if (Property->IsA<FEnumProperty>())
1103
- {
1104
- FEnumProperty* EnumProp = CastField<FEnumProperty>(Property);
1105
- UEnum* EnumDef = EnumProp ? EnumProp->GetEnum() : nullptr;
1106
- FNumericProperty* UnderlyingNumericProp = EnumProp ? EnumProp->GetUnderlyingProperty() : nullptr;
1107
-
1108
- if (EnumDef && UnderlyingNumericProp)
1109
- {
1110
- // Handle numeric value
1111
- if (Value->Type == EJson::Number)
1112
- {
1113
- int64 EnumValue = static_cast<int64>(Value->AsNumber());
1114
- UnderlyingNumericProp->SetIntPropertyValue(PropertyAddr, EnumValue);
1115
-
1116
- UE_LOG(LogTemp, Display, TEXT("Setting enum property %s to numeric value: %lld"),
1117
- *PropertyName, EnumValue);
1118
- return true;
1119
- }
1120
- // Handle string enum value
1121
- else if (Value->Type == EJson::String)
1122
- {
1123
- FString EnumValueName = Value->AsString();
1124
-
1125
- // Try to convert numeric string to number first
1126
- if (EnumValueName.IsNumeric())
1127
- {
1128
- int64 EnumValue = FCString::Atoi64(*EnumValueName);
1129
- UnderlyingNumericProp->SetIntPropertyValue(PropertyAddr, EnumValue);
1130
-
1131
- UE_LOG(LogTemp, Display, TEXT("Setting enum property %s to numeric string value: %s -> %lld"),
1132
- *PropertyName, *EnumValueName, EnumValue);
1133
- return true;
1134
- }
1135
-
1136
- // Handle qualified enum names
1137
- if (EnumValueName.Contains(TEXT("::")))
1138
- {
1139
- EnumValueName.Split(TEXT("::"), nullptr, &EnumValueName);
1140
- }
1141
-
1142
- int64 EnumValue = EnumDef->GetValueByNameString(EnumValueName);
1143
- if (EnumValue == INDEX_NONE)
1144
- {
1145
- // Try with full name as fallback
1146
- EnumValue = EnumDef->GetValueByNameString(Value->AsString());
1147
- }
1148
-
1149
- if (EnumValue != INDEX_NONE)
1150
- {
1151
- UnderlyingNumericProp->SetIntPropertyValue(PropertyAddr, EnumValue);
1152
-
1153
- UE_LOG(LogTemp, Display, TEXT("Setting enum property %s to name value: %s -> %lld"),
1154
- *PropertyName, *EnumValueName, EnumValue);
1155
- return true;
1156
- }
1157
- else
1158
- {
1159
- // Log all possible enum values for debugging
1160
- UE_LOG(LogTemp, Warning, TEXT("Could not find enum value for '%s'. Available options:"), *EnumValueName);
1161
- for (int32 i = 0; i < EnumDef->NumEnums(); i++)
1162
- {
1163
- UE_LOG(LogTemp, Warning, TEXT(" - %s (value: %d)"),
1164
- *EnumDef->GetNameStringByIndex(i), EnumDef->GetValueByIndex(i));
1165
- }
1166
-
1167
- OutErrorMessage = FString::Printf(TEXT("Could not find enum value for '%s'"), *EnumValueName);
1168
- return false;
1169
- }
1170
- }
1171
- }
1172
- }
1173
-
1174
- OutErrorMessage = FString::Printf(TEXT("Unsupported property type: %s for property %s"),
1175
- *Property->GetClass()->GetName(), *PropertyName);
1176
- return false;
1177
- }
1
+ #include "Commands/UnrealMCPCommonUtils.h"
2
+ #include "GameFramework/Actor.h"
3
+ #include "Engine/Blueprint.h"
4
+ #include "EdGraph/EdGraph.h"
5
+ #include "EdGraph/EdGraphNode.h"
6
+ #include "EdGraph/EdGraphPin.h"
7
+ #include "K2Node_Event.h"
8
+ #include "K2Node_CallFunction.h"
9
+ #include "K2Node_VariableGet.h"
10
+ #include "K2Node_VariableSet.h"
11
+ #include "K2Node_InputAction.h"
12
+ #include "K2Node_Self.h"
13
+ #include "EdGraphSchema_K2.h"
14
+ #include "Kismet2/BlueprintEditorUtils.h"
15
+ #include "Components/StaticMeshComponent.h"
16
+ #include "Components/LightComponent.h"
17
+ #include "Components/PrimitiveComponent.h"
18
+ #include "Components/SceneComponent.h"
19
+ #include "UObject/UnrealType.h"
20
+ #include "UObject/UObjectIterator.h"
21
+ #include "Engine/Selection.h"
22
+ #include "EditorAssetLibrary.h"
23
+ #include "AssetRegistry/AssetRegistryModule.h"
24
+ #include "Engine/BlueprintGeneratedClass.h"
25
+ #include "BlueprintNodeSpawner.h"
26
+ #include "BlueprintActionDatabase.h"
27
+ #include "Dom/JsonObject.h"
28
+ #include "Dom/JsonValue.h"
29
+
30
+ // JSON Utilities
31
+ TSharedPtr<FJsonObject> FUnrealMCPCommonUtils::CreateErrorResponse(const FString& Message)
32
+ {
33
+ TSharedPtr<FJsonObject> ResponseObject = MakeShared<FJsonObject>();
34
+ ResponseObject->SetBoolField(TEXT("success"), false);
35
+ ResponseObject->SetStringField(TEXT("error"), Message);
36
+ return ResponseObject;
37
+ }
38
+
39
+ TSharedPtr<FJsonObject> FUnrealMCPCommonUtils::CreateSuccessResponse(const TSharedPtr<FJsonObject>& Data)
40
+ {
41
+ TSharedPtr<FJsonObject> ResponseObject = MakeShared<FJsonObject>();
42
+ ResponseObject->SetBoolField(TEXT("success"), true);
43
+
44
+ if (Data.IsValid())
45
+ {
46
+ ResponseObject->SetObjectField(TEXT("data"), Data);
47
+ }
48
+
49
+ return ResponseObject;
50
+ }
51
+
52
+ void FUnrealMCPCommonUtils::GetIntArrayFromJson(const TSharedPtr<FJsonObject>& JsonObject, const FString& FieldName, TArray<int32>& OutArray)
53
+ {
54
+ OutArray.Reset();
55
+
56
+ if (!JsonObject->HasField(FieldName))
57
+ {
58
+ return;
59
+ }
60
+
61
+ const TArray<TSharedPtr<FJsonValue>>* JsonArray;
62
+ if (JsonObject->TryGetArrayField(FieldName, JsonArray))
63
+ {
64
+ for (const TSharedPtr<FJsonValue>& Value : *JsonArray)
65
+ {
66
+ OutArray.Add((int32)Value->AsNumber());
67
+ }
68
+ }
69
+ }
70
+
71
+ void FUnrealMCPCommonUtils::GetFloatArrayFromJson(const TSharedPtr<FJsonObject>& JsonObject, const FString& FieldName, TArray<float>& OutArray)
72
+ {
73
+ OutArray.Reset();
74
+
75
+ if (!JsonObject->HasField(FieldName))
76
+ {
77
+ return;
78
+ }
79
+
80
+ const TArray<TSharedPtr<FJsonValue>>* JsonArray;
81
+ if (JsonObject->TryGetArrayField(FieldName, JsonArray))
82
+ {
83
+ for (const TSharedPtr<FJsonValue>& Value : *JsonArray)
84
+ {
85
+ OutArray.Add((float)Value->AsNumber());
86
+ }
87
+ }
88
+ }
89
+
90
+ FVector2D FUnrealMCPCommonUtils::GetVector2DFromJson(const TSharedPtr<FJsonObject>& JsonObject, const FString& FieldName)
91
+ {
92
+ FVector2D Result(0.0f, 0.0f);
93
+
94
+ if (!JsonObject->HasField(FieldName))
95
+ {
96
+ return Result;
97
+ }
98
+
99
+ // Accept either [x,y] or {x,y}.
100
+ const TSharedPtr<FJsonObject>* ObjValue = nullptr;
101
+ if (JsonObject->TryGetObjectField(FieldName, ObjValue) && ObjValue && ObjValue->IsValid())
102
+ {
103
+ double X = 0.0;
104
+ double Y = 0.0;
105
+ (*ObjValue)->TryGetNumberField(TEXT("x"), X) || (*ObjValue)->TryGetNumberField(TEXT("X"), X);
106
+ (*ObjValue)->TryGetNumberField(TEXT("y"), Y) || (*ObjValue)->TryGetNumberField(TEXT("Y"), Y);
107
+ Result.X = (float)X;
108
+ Result.Y = (float)Y;
109
+ return Result;
110
+ }
111
+
112
+ const TArray<TSharedPtr<FJsonValue>>* JsonArray;
113
+ if (JsonObject->TryGetArrayField(FieldName, JsonArray) && JsonArray->Num() >= 2)
114
+ {
115
+ Result.X = (float)(*JsonArray)[0]->AsNumber();
116
+ Result.Y = (float)(*JsonArray)[1]->AsNumber();
117
+ }
118
+
119
+ return Result;
120
+ }
121
+
122
+ FVector FUnrealMCPCommonUtils::GetVectorFromJson(const TSharedPtr<FJsonObject>& JsonObject, const FString& FieldName)
123
+ {
124
+ FVector Result(0.0f, 0.0f, 0.0f);
125
+
126
+ if (!JsonObject->HasField(FieldName))
127
+ {
128
+ return Result;
129
+ }
130
+
131
+ // Accept either [x,y,z] or {x,y,z}.
132
+ const TSharedPtr<FJsonObject>* ObjValue = nullptr;
133
+ if (JsonObject->TryGetObjectField(FieldName, ObjValue) && ObjValue && ObjValue->IsValid())
134
+ {
135
+ double X = 0.0;
136
+ double Y = 0.0;
137
+ double Z = 0.0;
138
+ ((*ObjValue)->TryGetNumberField(TEXT("x"), X) || (*ObjValue)->TryGetNumberField(TEXT("X"), X));
139
+ ((*ObjValue)->TryGetNumberField(TEXT("y"), Y) || (*ObjValue)->TryGetNumberField(TEXT("Y"), Y));
140
+ ((*ObjValue)->TryGetNumberField(TEXT("z"), Z) || (*ObjValue)->TryGetNumberField(TEXT("Z"), Z));
141
+ Result.X = (float)X;
142
+ Result.Y = (float)Y;
143
+ Result.Z = (float)Z;
144
+ return Result;
145
+ }
146
+
147
+ const TArray<TSharedPtr<FJsonValue>>* JsonArray;
148
+ if (JsonObject->TryGetArrayField(FieldName, JsonArray) && JsonArray->Num() >= 3)
149
+ {
150
+ Result.X = (float)(*JsonArray)[0]->AsNumber();
151
+ Result.Y = (float)(*JsonArray)[1]->AsNumber();
152
+ Result.Z = (float)(*JsonArray)[2]->AsNumber();
153
+ }
154
+
155
+ return Result;
156
+ }
157
+
158
+ FRotator FUnrealMCPCommonUtils::GetRotatorFromJson(const TSharedPtr<FJsonObject>& JsonObject, const FString& FieldName)
159
+ {
160
+ FRotator Result(0.0f, 0.0f, 0.0f);
161
+
162
+ if (!JsonObject->HasField(FieldName))
163
+ {
164
+ return Result;
165
+ }
166
+
167
+ // Accept either [pitch,yaw,roll] or {pitch,yaw,roll}.
168
+ const TSharedPtr<FJsonObject>* ObjValue = nullptr;
169
+ if (JsonObject->TryGetObjectField(FieldName, ObjValue) && ObjValue && ObjValue->IsValid())
170
+ {
171
+ double Pitch = 0.0;
172
+ double Yaw = 0.0;
173
+ double Roll = 0.0;
174
+ ((*ObjValue)->TryGetNumberField(TEXT("pitch"), Pitch) || (*ObjValue)->TryGetNumberField(TEXT("Pitch"), Pitch));
175
+ ((*ObjValue)->TryGetNumberField(TEXT("yaw"), Yaw) || (*ObjValue)->TryGetNumberField(TEXT("Yaw"), Yaw));
176
+ ((*ObjValue)->TryGetNumberField(TEXT("roll"), Roll) || (*ObjValue)->TryGetNumberField(TEXT("Roll"), Roll));
177
+ Result.Pitch = (float)Pitch;
178
+ Result.Yaw = (float)Yaw;
179
+ Result.Roll = (float)Roll;
180
+ return Result;
181
+ }
182
+
183
+ const TArray<TSharedPtr<FJsonValue>>* JsonArray;
184
+ if (JsonObject->TryGetArrayField(FieldName, JsonArray) && JsonArray->Num() >= 3)
185
+ {
186
+ Result.Pitch = (float)(*JsonArray)[0]->AsNumber();
187
+ Result.Yaw = (float)(*JsonArray)[1]->AsNumber();
188
+ Result.Roll = (float)(*JsonArray)[2]->AsNumber();
189
+ }
190
+
191
+ return Result;
192
+ }
193
+
194
+ // Blueprint Utilities
195
+ UBlueprint* FUnrealMCPCommonUtils::FindBlueprint(const FString& BlueprintName)
196
+ {
197
+ return FindBlueprintByName(BlueprintName);
198
+ }
199
+
200
+ UBlueprint* FUnrealMCPCommonUtils::FindBlueprintByName(const FString& BlueprintName)
201
+ {
202
+ FString Trimmed = BlueprintName.TrimStartAndEnd();
203
+ if (Trimmed.IsEmpty())
204
+ {
205
+ return nullptr;
206
+ }
207
+
208
+ // Allow callers to pass either a simple asset name (BP_MyAsset),
209
+ // or a full object path (/Game/Path/BP_MyAsset.BP_MyAsset),
210
+ // or a Blueprint'...' soft path.
211
+ if (Trimmed.StartsWith(TEXT("/")) || Trimmed.StartsWith(TEXT("Blueprint'")))
212
+ {
213
+ UObject* Obj = LoadObject<UObject>(nullptr, *Trimmed);
214
+ if (!Obj && Trimmed.StartsWith(TEXT("/")) && !Trimmed.Contains(TEXT("'")))
215
+ {
216
+ Obj = LoadObject<UObject>(nullptr, *FString::Printf(TEXT("Blueprint'%s'"), *Trimmed));
217
+ }
218
+ if (UBlueprint* BP = Cast<UBlueprint>(Obj)) return BP;
219
+ if (UBlueprintGeneratedClass* BPGC = Cast<UBlueprintGeneratedClass>(Obj))
220
+ {
221
+ return Cast<UBlueprint>(BPGC->ClassGeneratedBy);
222
+ }
223
+ return nullptr;
224
+ }
225
+
226
+ FString AssetPath = TEXT("/Game/Blueprints/") + Trimmed;
227
+ if (UBlueprint* BP = LoadObject<UBlueprint>(nullptr, *AssetPath)) return BP;
228
+ if (UBlueprint* BP = LoadObject<UBlueprint>(nullptr, *FString::Printf(TEXT("Blueprint'%s'"), *AssetPath))) return BP;
229
+ return nullptr;
230
+ }
231
+
232
+ UEdGraph* FUnrealMCPCommonUtils::FindOrCreateEventGraph(UBlueprint* Blueprint)
233
+ {
234
+ if (!Blueprint)
235
+ {
236
+ return nullptr;
237
+ }
238
+
239
+ // Try to find the event graph
240
+ for (UEdGraph* Graph : Blueprint->UbergraphPages)
241
+ {
242
+ if (Graph->GetName().Contains(TEXT("EventGraph")))
243
+ {
244
+ return Graph;
245
+ }
246
+ }
247
+
248
+ // Create a new event graph if none exists
249
+ UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(Blueprint, FName(TEXT("EventGraph")), UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
250
+ FBlueprintEditorUtils::AddUbergraphPage(Blueprint, NewGraph);
251
+ return NewGraph;
252
+ }
253
+
254
+ // Blueprint node utilities
255
+ UK2Node_Event* FUnrealMCPCommonUtils::CreateEventNode(UEdGraph* Graph, const FString& EventName, const FVector2D& Position)
256
+ {
257
+ if (!Graph)
258
+ {
259
+ return nullptr;
260
+ }
261
+
262
+ UBlueprint* Blueprint = FBlueprintEditorUtils::FindBlueprintForGraph(Graph);
263
+ if (!Blueprint)
264
+ {
265
+ return nullptr;
266
+ }
267
+
268
+ // Check for existing event node with this exact name
269
+ for (UEdGraphNode* Node : Graph->Nodes)
270
+ {
271
+ UK2Node_Event* EventNode = Cast<UK2Node_Event>(Node);
272
+ if (EventNode && EventNode->EventReference.GetMemberName() == FName(*EventName))
273
+ {
274
+ UE_LOG(LogTemp, Display, TEXT("Using existing event node with name %s (ID: %s)"),
275
+ *EventName, *EventNode->NodeGuid.ToString());
276
+ return EventNode;
277
+ }
278
+ }
279
+
280
+ // No existing node found, create a new one
281
+ UK2Node_Event* EventNode = nullptr;
282
+
283
+ // Find the function to create the event
284
+ UClass* BlueprintClass = Blueprint->GeneratedClass;
285
+ UFunction* EventFunction = BlueprintClass->FindFunctionByName(FName(*EventName));
286
+
287
+ if (EventFunction)
288
+ {
289
+ EventNode = NewObject<UK2Node_Event>(Graph);
290
+ EventNode->EventReference.SetExternalMember(FName(*EventName), BlueprintClass);
291
+ EventNode->NodePosX = Position.X;
292
+ EventNode->NodePosY = Position.Y;
293
+ Graph->AddNode(EventNode, true);
294
+ EventNode->PostPlacedNewNode();
295
+ EventNode->AllocateDefaultPins();
296
+ UE_LOG(LogTemp, Display, TEXT("Created new event node with name %s (ID: %s)"),
297
+ *EventName, *EventNode->NodeGuid.ToString());
298
+ }
299
+ else
300
+ {
301
+ UE_LOG(LogTemp, Error, TEXT("Failed to find function for event name: %s"), *EventName);
302
+ }
303
+
304
+ return EventNode;
305
+ }
306
+
307
+ UK2Node_CallFunction* FUnrealMCPCommonUtils::CreateFunctionCallNode(UEdGraph* Graph, UFunction* Function, const FVector2D& Position)
308
+ {
309
+ if (!Graph || !Function)
310
+ {
311
+ return nullptr;
312
+ }
313
+
314
+ UK2Node_CallFunction* FunctionNode = NewObject<UK2Node_CallFunction>(Graph);
315
+ FunctionNode->SetFromFunction(Function);
316
+ FunctionNode->NodePosX = Position.X;
317
+ FunctionNode->NodePosY = Position.Y;
318
+ Graph->AddNode(FunctionNode, true);
319
+ FunctionNode->CreateNewGuid();
320
+ FunctionNode->PostPlacedNewNode();
321
+ FunctionNode->AllocateDefaultPins();
322
+
323
+ return FunctionNode;
324
+ }
325
+
326
+ UK2Node_VariableGet* FUnrealMCPCommonUtils::CreateVariableGetNode(UEdGraph* Graph, UBlueprint* Blueprint, const FString& VariableName, const FVector2D& Position)
327
+ {
328
+ if (!Graph || !Blueprint)
329
+ {
330
+ return nullptr;
331
+ }
332
+
333
+ UK2Node_VariableGet* VariableGetNode = NewObject<UK2Node_VariableGet>(Graph);
334
+
335
+ FName VarName(*VariableName);
336
+ FProperty* Property = FindFProperty<FProperty>(Blueprint->GeneratedClass, VarName);
337
+
338
+ if (Property)
339
+ {
340
+ VariableGetNode->VariableReference.SetFromField<FProperty>(Property, false);
341
+ VariableGetNode->NodePosX = Position.X;
342
+ VariableGetNode->NodePosY = Position.Y;
343
+ Graph->AddNode(VariableGetNode, true);
344
+ VariableGetNode->PostPlacedNewNode();
345
+ VariableGetNode->AllocateDefaultPins();
346
+
347
+ return VariableGetNode;
348
+ }
349
+
350
+ return nullptr;
351
+ }
352
+
353
+ UK2Node_VariableSet* FUnrealMCPCommonUtils::CreateVariableSetNode(UEdGraph* Graph, UBlueprint* Blueprint, const FString& VariableName, const FVector2D& Position)
354
+ {
355
+ if (!Graph || !Blueprint)
356
+ {
357
+ return nullptr;
358
+ }
359
+
360
+ UK2Node_VariableSet* VariableSetNode = NewObject<UK2Node_VariableSet>(Graph);
361
+
362
+ FName VarName(*VariableName);
363
+ FProperty* Property = FindFProperty<FProperty>(Blueprint->GeneratedClass, VarName);
364
+
365
+ if (Property)
366
+ {
367
+ VariableSetNode->VariableReference.SetFromField<FProperty>(Property, false);
368
+ VariableSetNode->NodePosX = Position.X;
369
+ VariableSetNode->NodePosY = Position.Y;
370
+ Graph->AddNode(VariableSetNode, true);
371
+ VariableSetNode->PostPlacedNewNode();
372
+ VariableSetNode->AllocateDefaultPins();
373
+
374
+ return VariableSetNode;
375
+ }
376
+
377
+ return nullptr;
378
+ }
379
+
380
+ UK2Node_InputAction* FUnrealMCPCommonUtils::CreateInputActionNode(UEdGraph* Graph, const FString& ActionName, const FVector2D& Position)
381
+ {
382
+ if (!Graph)
383
+ {
384
+ return nullptr;
385
+ }
386
+
387
+ UK2Node_InputAction* InputActionNode = NewObject<UK2Node_InputAction>(Graph);
388
+ InputActionNode->InputActionName = FName(*ActionName);
389
+ InputActionNode->NodePosX = Position.X;
390
+ InputActionNode->NodePosY = Position.Y;
391
+ Graph->AddNode(InputActionNode, true);
392
+ InputActionNode->CreateNewGuid();
393
+ InputActionNode->PostPlacedNewNode();
394
+ InputActionNode->AllocateDefaultPins();
395
+
396
+ return InputActionNode;
397
+ }
398
+
399
+ UK2Node_Self* FUnrealMCPCommonUtils::CreateSelfReferenceNode(UEdGraph* Graph, const FVector2D& Position)
400
+ {
401
+ if (!Graph)
402
+ {
403
+ return nullptr;
404
+ }
405
+
406
+ UK2Node_Self* SelfNode = NewObject<UK2Node_Self>(Graph);
407
+ SelfNode->NodePosX = Position.X;
408
+ SelfNode->NodePosY = Position.Y;
409
+ Graph->AddNode(SelfNode, true);
410
+ SelfNode->CreateNewGuid();
411
+ SelfNode->PostPlacedNewNode();
412
+ SelfNode->AllocateDefaultPins();
413
+
414
+ return SelfNode;
415
+ }
416
+
417
+ bool FUnrealMCPCommonUtils::ConnectGraphNodes(UEdGraph* Graph, UEdGraphNode* SourceNode, const FString& SourcePinName,
418
+ UEdGraphNode* TargetNode, const FString& TargetPinName)
419
+ {
420
+ if (!Graph || !SourceNode || !TargetNode)
421
+ {
422
+ return false;
423
+ }
424
+
425
+ UEdGraphPin* SourcePin = FindPin(SourceNode, SourcePinName, EGPD_Output);
426
+ UEdGraphPin* TargetPin = FindPin(TargetNode, TargetPinName, EGPD_Input);
427
+
428
+ if (SourcePin && TargetPin)
429
+ {
430
+ SourcePin->MakeLinkTo(TargetPin);
431
+ return true;
432
+ }
433
+
434
+ return false;
435
+ }
436
+
437
+ UEdGraphPin* FUnrealMCPCommonUtils::FindPin(UEdGraphNode* Node, const FString& PinName, EEdGraphPinDirection Direction)
438
+ {
439
+ if (!Node)
440
+ {
441
+ return nullptr;
442
+ }
443
+
444
+ // Log all pins for debugging
445
+ UE_LOG(LogTemp, Display, TEXT("FindPin: Looking for pin '%s' (Direction: %d) in node '%s'"),
446
+ *PinName, (int32)Direction, *Node->GetName());
447
+
448
+ for (UEdGraphPin* Pin : Node->Pins)
449
+ {
450
+ UE_LOG(LogTemp, Display, TEXT(" - Available pin: '%s', Direction: %d, Category: %s"),
451
+ *Pin->PinName.ToString(), (int32)Pin->Direction, *Pin->PinType.PinCategory.ToString());
452
+ }
453
+
454
+ // First try exact match
455
+ for (UEdGraphPin* Pin : Node->Pins)
456
+ {
457
+ if (Pin->PinName.ToString() == PinName && (Direction == EGPD_MAX || Pin->Direction == Direction))
458
+ {
459
+ UE_LOG(LogTemp, Display, TEXT(" - Found exact matching pin: '%s'"), *Pin->PinName.ToString());
460
+ return Pin;
461
+ }
462
+ }
463
+
464
+ // If no exact match and we're looking for a component reference, try case-insensitive match
465
+ for (UEdGraphPin* Pin : Node->Pins)
466
+ {
467
+ if (Pin->PinName.ToString().Equals(PinName, ESearchCase::IgnoreCase) &&
468
+ (Direction == EGPD_MAX || Pin->Direction == Direction))
469
+ {
470
+ UE_LOG(LogTemp, Display, TEXT(" - Found case-insensitive matching pin: '%s'"), *Pin->PinName.ToString());
471
+ return Pin;
472
+ }
473
+ }
474
+
475
+ // If we're looking for a component output and didn't find it by name, try to find the first data output pin
476
+ if (Direction == EGPD_Output && Cast<UK2Node_VariableGet>(Node) != nullptr)
477
+ {
478
+ for (UEdGraphPin* Pin : Node->Pins)
479
+ {
480
+ if (Pin->Direction == EGPD_Output && Pin->PinType.PinCategory != UEdGraphSchema_K2::PC_Exec)
481
+ {
482
+ UE_LOG(LogTemp, Display, TEXT(" - Found fallback data output pin: '%s'"), *Pin->PinName.ToString());
483
+ return Pin;
484
+ }
485
+ }
486
+ }
487
+
488
+ UE_LOG(LogTemp, Warning, TEXT(" - No matching pin found for '%s'"), *PinName);
489
+ return nullptr;
490
+ }
491
+
492
+ // Actor utilities
493
+ TSharedPtr<FJsonValue> FUnrealMCPCommonUtils::ActorToJson(AActor* Actor)
494
+ {
495
+ if (!Actor)
496
+ {
497
+ return MakeShared<FJsonValueNull>();
498
+ }
499
+
500
+ TSharedPtr<FJsonObject> ActorObject = MakeShared<FJsonObject>();
501
+ ActorObject->SetStringField(TEXT("name"), Actor->GetName());
502
+ ActorObject->SetStringField(TEXT("class"), Actor->GetClass()->GetName());
503
+
504
+ FVector Location = Actor->GetActorLocation();
505
+ TArray<TSharedPtr<FJsonValue>> LocationArray;
506
+ LocationArray.Add(MakeShared<FJsonValueNumber>(Location.X));
507
+ LocationArray.Add(MakeShared<FJsonValueNumber>(Location.Y));
508
+ LocationArray.Add(MakeShared<FJsonValueNumber>(Location.Z));
509
+ ActorObject->SetArrayField(TEXT("location"), LocationArray);
510
+
511
+ FRotator Rotation = Actor->GetActorRotation();
512
+ TArray<TSharedPtr<FJsonValue>> RotationArray;
513
+ RotationArray.Add(MakeShared<FJsonValueNumber>(Rotation.Pitch));
514
+ RotationArray.Add(MakeShared<FJsonValueNumber>(Rotation.Yaw));
515
+ RotationArray.Add(MakeShared<FJsonValueNumber>(Rotation.Roll));
516
+ ActorObject->SetArrayField(TEXT("rotation"), RotationArray);
517
+
518
+ FVector Scale = Actor->GetActorScale3D();
519
+ TArray<TSharedPtr<FJsonValue>> ScaleArray;
520
+ ScaleArray.Add(MakeShared<FJsonValueNumber>(Scale.X));
521
+ ScaleArray.Add(MakeShared<FJsonValueNumber>(Scale.Y));
522
+ ScaleArray.Add(MakeShared<FJsonValueNumber>(Scale.Z));
523
+ ActorObject->SetArrayField(TEXT("scale"), ScaleArray);
524
+
525
+ return MakeShared<FJsonValueObject>(ActorObject);
526
+ }
527
+
528
+ TSharedPtr<FJsonObject> FUnrealMCPCommonUtils::ActorToJsonObject(AActor* Actor, bool bDetailed)
529
+ {
530
+ if (!Actor)
531
+ {
532
+ return nullptr;
533
+ }
534
+
535
+ TSharedPtr<FJsonObject> ActorObject = MakeShared<FJsonObject>();
536
+ ActorObject->SetStringField(TEXT("name"), Actor->GetName());
537
+ ActorObject->SetStringField(TEXT("class"), Actor->GetClass()->GetName());
538
+
539
+ #if WITH_EDITOR
540
+ ActorObject->SetStringField(TEXT("label"), Actor->GetActorLabel());
541
+ #endif
542
+
543
+ FVector Location = Actor->GetActorLocation();
544
+ TArray<TSharedPtr<FJsonValue>> LocationArray;
545
+ LocationArray.Add(MakeShared<FJsonValueNumber>(Location.X));
546
+ LocationArray.Add(MakeShared<FJsonValueNumber>(Location.Y));
547
+ LocationArray.Add(MakeShared<FJsonValueNumber>(Location.Z));
548
+ ActorObject->SetArrayField(TEXT("location"), LocationArray);
549
+
550
+ FRotator Rotation = Actor->GetActorRotation();
551
+ TArray<TSharedPtr<FJsonValue>> RotationArray;
552
+ RotationArray.Add(MakeShared<FJsonValueNumber>(Rotation.Pitch));
553
+ RotationArray.Add(MakeShared<FJsonValueNumber>(Rotation.Yaw));
554
+ RotationArray.Add(MakeShared<FJsonValueNumber>(Rotation.Roll));
555
+ ActorObject->SetArrayField(TEXT("rotation"), RotationArray);
556
+
557
+ FVector Scale = Actor->GetActorScale3D();
558
+ TArray<TSharedPtr<FJsonValue>> ScaleArray;
559
+ ScaleArray.Add(MakeShared<FJsonValueNumber>(Scale.X));
560
+ ScaleArray.Add(MakeShared<FJsonValueNumber>(Scale.Y));
561
+ ScaleArray.Add(MakeShared<FJsonValueNumber>(Scale.Z));
562
+ ActorObject->SetArrayField(TEXT("scale"), ScaleArray);
563
+
564
+ if (!bDetailed)
565
+ {
566
+ return ActorObject;
567
+ }
568
+
569
+ const auto isEditableProperty = [](const FProperty* Prop) -> bool {
570
+ if (!Prop) return false;
571
+ if (!Prop->HasAnyPropertyFlags(CPF_Edit)) return false;
572
+ if (Prop->HasAnyPropertyFlags(CPF_Transient | CPF_Deprecated | CPF_DisableEditOnInstance)) return false;
573
+ return true;
574
+ };
575
+
576
+ const auto addPropertyDescriptors = [&](UObject* Obj, int32 Limit, TArray<TSharedPtr<FJsonValue>>& OutProps) {
577
+ if (!Obj) return;
578
+ int32 Count = 0;
579
+ for (TFieldIterator<FProperty> It(Obj->GetClass()); It; ++It)
580
+ {
581
+ FProperty* Prop = *It;
582
+ if (!isEditableProperty(Prop)) continue;
583
+ if (Limit > 0 && Count >= Limit) break;
584
+
585
+ TSharedPtr<FJsonObject> P = MakeShared<FJsonObject>();
586
+ P->SetStringField(TEXT("name"), Prop->GetName());
587
+ P->SetStringField(TEXT("type"), Prop->GetCPPType());
588
+ P->SetBoolField(TEXT("isArray"), Prop->IsA<FArrayProperty>());
589
+ OutProps.Add(MakeShared<FJsonValueObject>(P));
590
+ Count++;
591
+ }
592
+ };
593
+
594
+ // Actor editable properties
595
+ {
596
+ TArray<TSharedPtr<FJsonValue>> Props;
597
+ addPropertyDescriptors(Actor, 250, Props);
598
+ ActorObject->SetArrayField(TEXT("properties"), Props);
599
+ }
600
+
601
+ // Actor component pointer fields (useful for nested property paths like PointLightComponent.LightColor)
602
+ {
603
+ TArray<TSharedPtr<FJsonValue>> Fields;
604
+ int32 Count = 0;
605
+ for (TFieldIterator<FProperty> It(Actor->GetClass()); It; ++It)
606
+ {
607
+ FProperty* Prop = *It;
608
+ FObjectPropertyBase* ObjProp = CastField<FObjectPropertyBase>(Prop);
609
+ if (!ObjProp) continue;
610
+ if (!ObjProp->PropertyClass || !ObjProp->PropertyClass->IsChildOf(UActorComponent::StaticClass())) continue;
611
+ if (Count >= 50) break;
612
+
613
+ UObject* Ref = ObjProp->GetObjectPropertyValue_InContainer(Actor);
614
+ UActorComponent* Comp = Cast<UActorComponent>(Ref);
615
+
616
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
617
+ Obj->SetStringField(TEXT("property"), Prop->GetName());
618
+ Obj->SetStringField(TEXT("class"), ObjProp->PropertyClass->GetName());
619
+ if (Comp)
620
+ {
621
+ Obj->SetStringField(TEXT("componentName"), Comp->GetName());
622
+ }
623
+ Fields.Add(MakeShared<FJsonValueObject>(Obj));
624
+ Count++;
625
+ }
626
+ ActorObject->SetArrayField(TEXT("componentFields"), Fields);
627
+ }
628
+
629
+ // Components + their editable properties (best-effort, bounded)
630
+ {
631
+ TArray<TSharedPtr<FJsonValue>> Components;
632
+ TArray<UActorComponent*> Comps;
633
+ Actor->GetComponents(Comps);
634
+ Comps.Sort([](const UActorComponent& A, const UActorComponent& B) {
635
+ return A.GetFName().LexicalLess(B.GetFName());
636
+ });
637
+
638
+ int32 Added = 0;
639
+ for (UActorComponent* C : Comps)
640
+ {
641
+ if (!C) continue;
642
+ if (Added >= 50) break;
643
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
644
+ Obj->SetStringField(TEXT("name"), C->GetName());
645
+ Obj->SetStringField(TEXT("class"), C->GetClass()->GetName());
646
+
647
+ TArray<TSharedPtr<FJsonValue>> Props;
648
+ addPropertyDescriptors(C, 200, Props);
649
+ Obj->SetArrayField(TEXT("properties"), Props);
650
+
651
+ Components.Add(MakeShared<FJsonValueObject>(Obj));
652
+ Added++;
653
+ }
654
+ ActorObject->SetArrayField(TEXT("components"), Components);
655
+ }
656
+
657
+ return ActorObject;
658
+ }
659
+
660
+ UK2Node_Event* FUnrealMCPCommonUtils::FindExistingEventNode(UEdGraph* Graph, const FString& EventName)
661
+ {
662
+ if (!Graph)
663
+ {
664
+ return nullptr;
665
+ }
666
+
667
+ // Look for existing event nodes
668
+ for (UEdGraphNode* Node : Graph->Nodes)
669
+ {
670
+ UK2Node_Event* EventNode = Cast<UK2Node_Event>(Node);
671
+ if (EventNode && EventNode->EventReference.GetMemberName() == FName(*EventName))
672
+ {
673
+ UE_LOG(LogTemp, Display, TEXT("Found existing event node with name: %s"), *EventName);
674
+ return EventNode;
675
+ }
676
+ }
677
+
678
+ return nullptr;
679
+ }
680
+
681
+ bool FUnrealMCPCommonUtils::SetObjectProperty(UObject* Object, const FString& PropertyName,
682
+ const TSharedPtr<FJsonValue>& Value, FString& OutErrorMessage)
683
+ {
684
+ if (!Object)
685
+ {
686
+ OutErrorMessage = TEXT("Invalid object");
687
+ return false;
688
+ }
689
+
690
+ const auto splitPath = [](const FString& Raw) -> TArray<FString> {
691
+ FString P = Raw;
692
+ P.ReplaceInline(TEXT("/"), TEXT("."), ESearchCase::IgnoreCase);
693
+ P = P.TrimStartAndEnd();
694
+ TArray<FString> Segs;
695
+ P.ParseIntoArray(Segs, TEXT("."), true);
696
+ Segs.RemoveAll([](const FString& S) { return S.TrimStartAndEnd().IsEmpty(); });
697
+ for (FString& S : Segs) S = S.TrimStartAndEnd();
698
+ return Segs;
699
+ };
700
+
701
+ const auto resolvePropertyPath = [&](UObject* Root, const FString& RawPath, UObject*& OutContainerObj, FProperty*& OutProp, void*& OutPropAddr) -> bool {
702
+ OutContainerObj = Root;
703
+ OutProp = nullptr;
704
+ OutPropAddr = nullptr;
705
+
706
+ const TArray<FString> Segs = splitPath(RawPath);
707
+ if (Segs.Num() == 0)
708
+ {
709
+ OutErrorMessage = TEXT("Property path is empty");
710
+ return false;
711
+ }
712
+
713
+ UObject* Current = Root;
714
+ for (int32 i = 0; i < Segs.Num(); i++)
715
+ {
716
+ const FString& Seg = Segs[i];
717
+ const bool bIsLeaf = (i == Segs.Num() - 1);
718
+
719
+ FProperty* Prop = Current->GetClass()->FindPropertyByName(*Seg);
720
+ if (!Prop)
721
+ {
722
+ OutErrorMessage = FString::Printf(TEXT("Property not found: %s"), *Seg);
723
+ return false;
724
+ }
725
+
726
+ if (bIsLeaf)
727
+ {
728
+ OutContainerObj = Current;
729
+ OutProp = Prop;
730
+ OutPropAddr = Prop->ContainerPtrToValuePtr<void>(Current);
731
+ return true;
732
+ }
733
+
734
+ FObjectPropertyBase* ObjProp = CastField<FObjectPropertyBase>(Prop);
735
+ if (!ObjProp)
736
+ {
737
+ OutErrorMessage = FString::Printf(TEXT("Unsupported property path segment (not an object): %s"), *Seg);
738
+ return false;
739
+ }
740
+
741
+ UObject* Next = ObjProp->GetObjectPropertyValue_InContainer(Current);
742
+ if (!Next)
743
+ {
744
+ OutErrorMessage = FString::Printf(TEXT("Null object at path segment: %s"), *Seg);
745
+ return false;
746
+ }
747
+ Current = Next;
748
+ }
749
+
750
+ OutErrorMessage = TEXT("Failed to resolve property path");
751
+ return false;
752
+ };
753
+
754
+ UObject* ContainerObj = nullptr;
755
+ FProperty* Property = nullptr;
756
+ void* PropertyAddr = nullptr;
757
+ if (!resolvePropertyPath(Object, PropertyName, ContainerObj, Property, PropertyAddr))
758
+ {
759
+ return false;
760
+ }
761
+
762
+ if (!Property || !ContainerObj || !PropertyAddr)
763
+ {
764
+ OutErrorMessage = TEXT("Property resolution failed");
765
+ return false;
766
+ }
767
+
768
+ // Mark dirty for undo/redo and details panel refresh (editor only).
769
+ #if WITH_EDITOR
770
+ ContainerObj->Modify();
771
+ #endif
772
+
773
+ // Handle different property types
774
+ if (Property->IsA<FBoolProperty>())
775
+ {
776
+ ((FBoolProperty*)Property)->SetPropertyValue(PropertyAddr, Value->AsBool());
777
+ return true;
778
+ }
779
+ else if (Property->IsA<FIntProperty>())
780
+ {
781
+ int32 IntValue = static_cast<int32>(Value->AsNumber());
782
+ FIntProperty* IntProperty = CastField<FIntProperty>(Property);
783
+ if (IntProperty)
784
+ {
785
+ IntProperty->SetPropertyValue_InContainer(Object, IntValue);
786
+ return true;
787
+ }
788
+ }
789
+ else if (Property->IsA<FFloatProperty>())
790
+ {
791
+ ((FFloatProperty*)Property)->SetPropertyValue(PropertyAddr, Value->AsNumber());
792
+ return true;
793
+ }
794
+ else if (Property->IsA<FStrProperty>())
795
+ {
796
+ ((FStrProperty*)Property)->SetPropertyValue(PropertyAddr, Value->AsString());
797
+ return true;
798
+ }
799
+ else if (Property->IsA<FNameProperty>())
800
+ {
801
+ const FString S = Value->Type == EJson::String ? Value->AsString() : FString::Printf(TEXT("%g"), Value->AsNumber());
802
+ CastFieldChecked<FNameProperty>(Property)->SetPropertyValue(PropertyAddr, FName(*S));
803
+ return true;
804
+ }
805
+ else if (Property->IsA<FTextProperty>())
806
+ {
807
+ const FString S = Value->Type == EJson::String ? Value->AsString() : FString::Printf(TEXT("%g"), Value->AsNumber());
808
+ CastFieldChecked<FTextProperty>(Property)->SetPropertyValue(PropertyAddr, FText::FromString(S));
809
+ return true;
810
+ }
811
+ else if (Property->IsA<FStructProperty>())
812
+ {
813
+ FStructProperty* StructProp = CastField<FStructProperty>(Property);
814
+ if (!StructProp || !StructProp->Struct)
815
+ {
816
+ OutErrorMessage = FString::Printf(TEXT("Invalid struct property: %s"), *PropertyName);
817
+ return false;
818
+ }
819
+
820
+ const UScriptStruct* S = StructProp->Struct;
821
+ if (S == TBaseStructure<FVector>::Get())
822
+ {
823
+ if (Value->Type != EJson::Array || Value->AsArray().Num() != 3)
824
+ {
825
+ OutErrorMessage = TEXT("FVector requires an array of 3 numbers");
826
+ return false;
827
+ }
828
+ const auto& Arr = Value->AsArray();
829
+ const FVector V((float)Arr[0]->AsNumber(), (float)Arr[1]->AsNumber(), (float)Arr[2]->AsNumber());
830
+ StructProp->CopySingleValue(PropertyAddr, &V);
831
+ return true;
832
+ }
833
+ if (S == TBaseStructure<FVector2D>::Get())
834
+ {
835
+ if (Value->Type != EJson::Array || Value->AsArray().Num() != 2)
836
+ {
837
+ OutErrorMessage = TEXT("FVector2D requires an array of 2 numbers");
838
+ return false;
839
+ }
840
+ const auto& Arr = Value->AsArray();
841
+ const FVector2D V((float)Arr[0]->AsNumber(), (float)Arr[1]->AsNumber());
842
+ StructProp->CopySingleValue(PropertyAddr, &V);
843
+ return true;
844
+ }
845
+ if (S == TBaseStructure<FRotator>::Get())
846
+ {
847
+ if (Value->Type != EJson::Array || Value->AsArray().Num() != 3)
848
+ {
849
+ OutErrorMessage = TEXT("FRotator requires an array of 3 numbers [Pitch,Yaw,Roll]");
850
+ return false;
851
+ }
852
+ const auto& Arr = Value->AsArray();
853
+ const FRotator R((float)Arr[0]->AsNumber(), (float)Arr[1]->AsNumber(), (float)Arr[2]->AsNumber());
854
+ StructProp->CopySingleValue(PropertyAddr, &R);
855
+ return true;
856
+ }
857
+ if (S == TBaseStructure<FLinearColor>::Get())
858
+ {
859
+ if (Value->Type != EJson::Array || (Value->AsArray().Num() != 3 && Value->AsArray().Num() != 4))
860
+ {
861
+ OutErrorMessage = TEXT("FLinearColor requires an array of 3 or 4 numbers [R,G,B,(A)]");
862
+ return false;
863
+ }
864
+ const auto& Arr = Value->AsArray();
865
+ const float A = Arr.Num() >= 4 ? (float)Arr[3]->AsNumber() : 1.0f;
866
+ const FLinearColor C((float)Arr[0]->AsNumber(), (float)Arr[1]->AsNumber(), (float)Arr[2]->AsNumber(), A);
867
+ StructProp->CopySingleValue(PropertyAddr, &C);
868
+ return true;
869
+ }
870
+ if (S == TBaseStructure<FColor>::Get())
871
+ {
872
+ if (Value->Type != EJson::Array || (Value->AsArray().Num() != 3 && Value->AsArray().Num() != 4))
873
+ {
874
+ OutErrorMessage = TEXT("FColor requires an array of 3 or 4 numbers [R,G,B,(A)]");
875
+ return false;
876
+ }
877
+ const auto& Arr = Value->AsArray();
878
+ const double r = Arr[0]->AsNumber();
879
+ const double g = Arr[1]->AsNumber();
880
+ const double b = Arr[2]->AsNumber();
881
+ const double a = Arr.Num() >= 4 ? Arr[3]->AsNumber() : 255.0;
882
+ const bool bLooksNormalized = r <= 1.0 && g <= 1.0 && b <= 1.0 && a <= 1.0;
883
+ const auto toByte = [&](double v) -> uint8 {
884
+ const double x = bLooksNormalized ? v * 255.0 : v;
885
+ return (uint8)FMath::Clamp((int32)FMath::RoundHalfToZero(x), 0, 255);
886
+ };
887
+ const FColor C(toByte(r), toByte(g), toByte(b), toByte(a));
888
+ StructProp->CopySingleValue(PropertyAddr, &C);
889
+ return true;
890
+ }
891
+
892
+ OutErrorMessage = FString::Printf(TEXT("Unsupported struct type: %s"), *S->GetName());
893
+ return false;
894
+ }
895
+ else if (Property->IsA<FObjectPropertyBase>())
896
+ {
897
+ FObjectPropertyBase* ObjProp = CastField<FObjectPropertyBase>(Property);
898
+ if (!ObjProp)
899
+ {
900
+ OutErrorMessage = FString::Printf(TEXT("Invalid ObjectProperty: %s"), *PropertyName);
901
+ return false;
902
+ }
903
+
904
+ if (Value->Type == EJson::Null)
905
+ {
906
+ ObjProp->SetObjectPropertyValue(PropertyAddr, nullptr);
907
+ return true;
908
+ }
909
+
910
+ if (Value->Type != EJson::String)
911
+ {
912
+ OutErrorMessage = FString::Printf(TEXT("ObjectProperty requires a string object path (or null): %s"), *PropertyName);
913
+ return false;
914
+ }
915
+
916
+ FString Path = Value->AsString().TrimStartAndEnd();
917
+ if (Path.IsEmpty())
918
+ {
919
+ ObjProp->SetObjectPropertyValue(PropertyAddr, nullptr);
920
+ return true;
921
+ }
922
+
923
+ UObject* Loaded = StaticLoadObject(ObjProp->PropertyClass, nullptr, *Path);
924
+ if (!Loaded && !Path.Contains(TEXT("'")))
925
+ {
926
+ Loaded = StaticLoadObject(
927
+ ObjProp->PropertyClass,
928
+ nullptr,
929
+ *FString::Printf(TEXT("%s'%s'"), *ObjProp->PropertyClass->GetName(), *Path));
930
+ }
931
+
932
+ if (!Loaded)
933
+ {
934
+ OutErrorMessage = FString::Printf(TEXT("Failed to load object for %s: %s"), *PropertyName, *Path);
935
+ return false;
936
+ }
937
+
938
+ ObjProp->SetObjectPropertyValue(PropertyAddr, Loaded);
939
+ return true;
940
+ }
941
+ else if (Property->IsA<FArrayProperty>())
942
+ {
943
+ if (Value->Type != EJson::Array)
944
+ {
945
+ OutErrorMessage = FString::Printf(TEXT("ArrayProperty requires an array value: %s"), *PropertyName);
946
+ return false;
947
+ }
948
+
949
+ FArrayProperty* ArrProp = CastField<FArrayProperty>(Property);
950
+ if (!ArrProp || !ArrProp->Inner)
951
+ {
952
+ OutErrorMessage = FString::Printf(TEXT("Invalid ArrayProperty: %s"), *PropertyName);
953
+ return false;
954
+ }
955
+
956
+ const TArray<TSharedPtr<FJsonValue>>& Arr = Value->AsArray();
957
+ FScriptArrayHelper Helper(ArrProp, PropertyAddr);
958
+ Helper.Resize(Arr.Num());
959
+
960
+ for (int32 i = 0; i < Arr.Num(); i++)
961
+ {
962
+ void* ElemPtr = Helper.GetRawPtr(i);
963
+ const TSharedPtr<FJsonValue>& Elem = Arr[i];
964
+
965
+ if (FObjectPropertyBase* InnerObj = CastField<FObjectPropertyBase>(ArrProp->Inner))
966
+ {
967
+ if (!Elem.IsValid() || Elem->Type == EJson::Null)
968
+ {
969
+ InnerObj->SetObjectPropertyValue(ElemPtr, nullptr);
970
+ continue;
971
+ }
972
+ if (Elem->Type != EJson::String)
973
+ {
974
+ OutErrorMessage = FString::Printf(TEXT("Array element %d must be a string object path (or null) for %s"), i, *PropertyName);
975
+ return false;
976
+ }
977
+ FString Path = Elem->AsString().TrimStartAndEnd();
978
+ UObject* Loaded = Path.IsEmpty() ? nullptr : StaticLoadObject(InnerObj->PropertyClass, nullptr, *Path);
979
+ if (!Loaded && !Path.IsEmpty() && !Path.Contains(TEXT("'")))
980
+ {
981
+ Loaded = StaticLoadObject(
982
+ InnerObj->PropertyClass,
983
+ nullptr,
984
+ *FString::Printf(TEXT("%s'%s'"), *InnerObj->PropertyClass->GetName(), *Path));
985
+ }
986
+ if (!Path.IsEmpty() && !Loaded)
987
+ {
988
+ OutErrorMessage = FString::Printf(TEXT("Failed to load object for %s[%d]: %s"), *PropertyName, i, *Path);
989
+ return false;
990
+ }
991
+ InnerObj->SetObjectPropertyValue(ElemPtr, Loaded);
992
+ continue;
993
+ }
994
+
995
+ if (FStrProperty* InnerStr = CastField<FStrProperty>(ArrProp->Inner))
996
+ {
997
+ if (!Elem.IsValid() || Elem->Type == EJson::Null)
998
+ {
999
+ InnerStr->SetPropertyValue(ElemPtr, TEXT(""));
1000
+ continue;
1001
+ }
1002
+ InnerStr->SetPropertyValue(ElemPtr, Elem->Type == EJson::String ? Elem->AsString() : FString::Printf(TEXT("%g"), Elem->AsNumber()));
1003
+ continue;
1004
+ }
1005
+
1006
+ if (FNumericProperty* InnerNum = CastField<FNumericProperty>(ArrProp->Inner))
1007
+ {
1008
+ if (!Elem.IsValid() || Elem->Type != EJson::Number)
1009
+ {
1010
+ OutErrorMessage = FString::Printf(TEXT("Array element %d must be a number for %s"), i, *PropertyName);
1011
+ return false;
1012
+ }
1013
+ if (InnerNum->IsInteger()) InnerNum->SetIntPropertyValue(ElemPtr, (int64)Elem->AsNumber());
1014
+ else InnerNum->SetFloatingPointPropertyValue(ElemPtr, Elem->AsNumber());
1015
+ continue;
1016
+ }
1017
+
1018
+ OutErrorMessage = FString::Printf(TEXT("Unsupported ArrayProperty element type: %s for %s"), *ArrProp->Inner->GetClass()->GetName(), *PropertyName);
1019
+ return false;
1020
+ }
1021
+
1022
+ return true;
1023
+ }
1024
+ else if (Property->IsA<FByteProperty>())
1025
+ {
1026
+ FByteProperty* ByteProp = CastField<FByteProperty>(Property);
1027
+ UEnum* EnumDef = ByteProp ? ByteProp->GetIntPropertyEnum() : nullptr;
1028
+
1029
+ // If this is a TEnumAsByte property (has associated enum)
1030
+ if (EnumDef)
1031
+ {
1032
+ // Handle numeric value
1033
+ if (Value->Type == EJson::Number)
1034
+ {
1035
+ uint8 ByteValue = static_cast<uint8>(Value->AsNumber());
1036
+ ByteProp->SetPropertyValue(PropertyAddr, ByteValue);
1037
+
1038
+ UE_LOG(LogTemp, Display, TEXT("Setting enum property %s to numeric value: %d"),
1039
+ *PropertyName, ByteValue);
1040
+ return true;
1041
+ }
1042
+ // Handle string enum value
1043
+ else if (Value->Type == EJson::String)
1044
+ {
1045
+ FString EnumValueName = Value->AsString();
1046
+
1047
+ // Try to convert numeric string to number first
1048
+ if (EnumValueName.IsNumeric())
1049
+ {
1050
+ uint8 ByteValue = FCString::Atoi(*EnumValueName);
1051
+ ByteProp->SetPropertyValue(PropertyAddr, ByteValue);
1052
+
1053
+ UE_LOG(LogTemp, Display, TEXT("Setting enum property %s to numeric string value: %s -> %d"),
1054
+ *PropertyName, *EnumValueName, ByteValue);
1055
+ return true;
1056
+ }
1057
+
1058
+ // Handle qualified enum names (e.g., "Player0" or "EAutoReceiveInput::Player0")
1059
+ if (EnumValueName.Contains(TEXT("::")))
1060
+ {
1061
+ EnumValueName.Split(TEXT("::"), nullptr, &EnumValueName);
1062
+ }
1063
+
1064
+ int64 EnumValue = EnumDef->GetValueByNameString(EnumValueName);
1065
+ if (EnumValue == INDEX_NONE)
1066
+ {
1067
+ // Try with full name as fallback
1068
+ EnumValue = EnumDef->GetValueByNameString(Value->AsString());
1069
+ }
1070
+
1071
+ if (EnumValue != INDEX_NONE)
1072
+ {
1073
+ ByteProp->SetPropertyValue(PropertyAddr, static_cast<uint8>(EnumValue));
1074
+
1075
+ UE_LOG(LogTemp, Display, TEXT("Setting enum property %s to name value: %s -> %lld"),
1076
+ *PropertyName, *EnumValueName, EnumValue);
1077
+ return true;
1078
+ }
1079
+ else
1080
+ {
1081
+ // Log all possible enum values for debugging
1082
+ UE_LOG(LogTemp, Warning, TEXT("Could not find enum value for '%s'. Available options:"), *EnumValueName);
1083
+ for (int32 i = 0; i < EnumDef->NumEnums(); i++)
1084
+ {
1085
+ UE_LOG(LogTemp, Warning, TEXT(" - %s (value: %d)"),
1086
+ *EnumDef->GetNameStringByIndex(i), EnumDef->GetValueByIndex(i));
1087
+ }
1088
+
1089
+ OutErrorMessage = FString::Printf(TEXT("Could not find enum value for '%s'"), *EnumValueName);
1090
+ return false;
1091
+ }
1092
+ }
1093
+ }
1094
+ else
1095
+ {
1096
+ // Regular byte property
1097
+ uint8 ByteValue = static_cast<uint8>(Value->AsNumber());
1098
+ ByteProp->SetPropertyValue(PropertyAddr, ByteValue);
1099
+ return true;
1100
+ }
1101
+ }
1102
+ else if (Property->IsA<FEnumProperty>())
1103
+ {
1104
+ FEnumProperty* EnumProp = CastField<FEnumProperty>(Property);
1105
+ UEnum* EnumDef = EnumProp ? EnumProp->GetEnum() : nullptr;
1106
+ FNumericProperty* UnderlyingNumericProp = EnumProp ? EnumProp->GetUnderlyingProperty() : nullptr;
1107
+
1108
+ if (EnumDef && UnderlyingNumericProp)
1109
+ {
1110
+ // Handle numeric value
1111
+ if (Value->Type == EJson::Number)
1112
+ {
1113
+ int64 EnumValue = static_cast<int64>(Value->AsNumber());
1114
+ UnderlyingNumericProp->SetIntPropertyValue(PropertyAddr, EnumValue);
1115
+
1116
+ UE_LOG(LogTemp, Display, TEXT("Setting enum property %s to numeric value: %lld"),
1117
+ *PropertyName, EnumValue);
1118
+ return true;
1119
+ }
1120
+ // Handle string enum value
1121
+ else if (Value->Type == EJson::String)
1122
+ {
1123
+ FString EnumValueName = Value->AsString();
1124
+
1125
+ // Try to convert numeric string to number first
1126
+ if (EnumValueName.IsNumeric())
1127
+ {
1128
+ int64 EnumValue = FCString::Atoi64(*EnumValueName);
1129
+ UnderlyingNumericProp->SetIntPropertyValue(PropertyAddr, EnumValue);
1130
+
1131
+ UE_LOG(LogTemp, Display, TEXT("Setting enum property %s to numeric string value: %s -> %lld"),
1132
+ *PropertyName, *EnumValueName, EnumValue);
1133
+ return true;
1134
+ }
1135
+
1136
+ // Handle qualified enum names
1137
+ if (EnumValueName.Contains(TEXT("::")))
1138
+ {
1139
+ EnumValueName.Split(TEXT("::"), nullptr, &EnumValueName);
1140
+ }
1141
+
1142
+ int64 EnumValue = EnumDef->GetValueByNameString(EnumValueName);
1143
+ if (EnumValue == INDEX_NONE)
1144
+ {
1145
+ // Try with full name as fallback
1146
+ EnumValue = EnumDef->GetValueByNameString(Value->AsString());
1147
+ }
1148
+
1149
+ if (EnumValue != INDEX_NONE)
1150
+ {
1151
+ UnderlyingNumericProp->SetIntPropertyValue(PropertyAddr, EnumValue);
1152
+
1153
+ UE_LOG(LogTemp, Display, TEXT("Setting enum property %s to name value: %s -> %lld"),
1154
+ *PropertyName, *EnumValueName, EnumValue);
1155
+ return true;
1156
+ }
1157
+ else
1158
+ {
1159
+ // Log all possible enum values for debugging
1160
+ UE_LOG(LogTemp, Warning, TEXT("Could not find enum value for '%s'. Available options:"), *EnumValueName);
1161
+ for (int32 i = 0; i < EnumDef->NumEnums(); i++)
1162
+ {
1163
+ UE_LOG(LogTemp, Warning, TEXT(" - %s (value: %d)"),
1164
+ *EnumDef->GetNameStringByIndex(i), EnumDef->GetValueByIndex(i));
1165
+ }
1166
+
1167
+ OutErrorMessage = FString::Printf(TEXT("Could not find enum value for '%s'"), *EnumValueName);
1168
+ return false;
1169
+ }
1170
+ }
1171
+ }
1172
+ }
1173
+
1174
+ OutErrorMessage = FString::Printf(TEXT("Unsupported property type: %s for property %s"),
1175
+ *Property->GetClass()->GetName(), *PropertyName);
1176
+ return false;
1177
+ }