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.
- package/README.md +60 -60
- package/bin/flockbay-mcp.mjs +62 -62
- package/bin/flockbay.mjs +95 -95
- package/dist/{index-r3VTdgFI.cjs → index-IntvnrmZ.cjs} +71 -14
- package/dist/{index-D8AeVbvv.mjs → index-wkeSi0iC.mjs} +70 -13
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/lib.cjs +1 -1
- package/dist/lib.mjs +1 -1
- package/dist/{migratePlugin-CEvGPul8.mjs → migratePlugin-C71hAoc5.mjs} +12 -2
- package/dist/{migratePlugin-wFK-k3Wk.cjs → migratePlugin-D1XjjWFq.cjs} +12 -2
- package/dist/{runCodex-DuyuYqoB.cjs → runCodex-B7i_ICoQ.cjs} +34 -23
- package/dist/{runCodex-ZfUyhHF6.mjs → runCodex-BSx-hCu6.mjs} +34 -23
- package/dist/{runGemini-DBU1mMdp.mjs → runGemini-Dwyh7_WI.mjs} +2 -2
- package/dist/{runGemini-D7j5Y5TF.cjs → runGemini-gBKR3rdL.cjs} +2 -2
- package/dist/{types-DNr0xwSy.cjs → types-5z8UADAi.cjs} +224 -15
- package/dist/{types-BRJuZQj_.mjs → types-Bgzap7qR.mjs} +224 -16
- package/package.json +131 -131
- package/scripts/claude_local_launcher.cjs +72 -72
- package/scripts/claude_remote_launcher.cjs +15 -15
- package/scripts/claude_version_utils.cjs +445 -445
- package/scripts/ripgrep_launcher.cjs +32 -32
- package/scripts/run-with-root-env.mjs +37 -32
- package/scripts/session_hook_forwarder.cjs +49 -49
- package/scripts/test-codex-abort-history.mjs +77 -77
- package/scripts/unpack-tools.cjs +244 -244
- package/tools/licenses/difftastic-LICENSE +21 -21
- package/tools/licenses/ripgrep-LICENSE +3 -3
- package/tools/unreal-mcp/UPSTREAM_VERSION.md +8 -8
- package/tools/unreal-mcp/upstream/Docs/README.md +8 -8
- package/tools/unreal-mcp/upstream/Docs/Tools/README.md +6 -6
- package/tools/unreal-mcp/upstream/Docs/Tools/actor_tools.md +183 -183
- package/tools/unreal-mcp/upstream/Docs/Tools/blueprint_tools.md +268 -268
- package/tools/unreal-mcp/upstream/Docs/Tools/editor_tools.md +104 -104
- package/tools/unreal-mcp/upstream/Docs/Tools/node_tools.md +274 -274
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Config/FilterPlugin.ini +8 -8
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/FlockbayMCP.uplugin +36 -36
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/FlockbayMCP.Build.cs +82 -82
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPBlueprintCommands.cpp +1364 -1364
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPBlueprintNodeCommands.cpp +1310 -1310
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPCommandSchema.cpp +756 -756
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPCommonUtils.cpp +1177 -1177
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPEditorCommands.cpp +2618 -2618
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPProjectCommands.cpp +71 -71
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPUMGCommands.cpp +543 -543
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/MCPServerRunnable.cpp +372 -372
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/UnrealMCPBridge.cpp +457 -457
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/UnrealMCPModule.cpp +21 -21
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPBlueprintCommands.h +35 -35
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPBlueprintNodeCommands.h +30 -30
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPCommandSchema.h +41 -41
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPCommonUtils.h +59 -59
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPEditorCommands.h +61 -61
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPProjectCommands.h +20 -20
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPUMGCommands.h +82 -82
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/MCPServerRunnable.h +33 -33
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/UnrealMCPBridge.h +64 -64
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/UnrealMCPModule.h +22 -22
- package/tools/unreal-mcp/upstream/Python/README.md +39 -39
- package/tools/unreal-mcp/upstream/Python/pyproject.toml +22 -22
- package/tools/unreal-mcp/upstream/Python/scripts/actors/test_cube.py +202 -202
- package/tools/unreal-mcp/upstream/Python/scripts/blueprints/test_create_and_spawn_blueprints_with_different_components.py +496 -496
- package/tools/unreal-mcp/upstream/Python/scripts/blueprints/test_create_and_spawn_cube_blueprint.py +193 -193
- package/tools/unreal-mcp/upstream/Python/scripts/node/test_component_reference.py +266 -266
- package/tools/unreal-mcp/upstream/Python/scripts/node/test_create_bird_blueprint_with_input_and_camera.py +617 -617
- package/tools/unreal-mcp/upstream/Python/scripts/node/test_input_mapping.py +365 -365
- package/tools/unreal-mcp/upstream/Python/scripts/node/test_physics_variables.py +389 -389
- package/tools/unreal-mcp/upstream/Python/tools/blueprint_tools.py +419 -419
- package/tools/unreal-mcp/upstream/Python/tools/editor_tools.py +369 -369
- package/tools/unreal-mcp/upstream/Python/tools/node_tools.py +429 -429
- package/tools/unreal-mcp/upstream/Python/tools/project_tools.py +63 -63
- package/tools/unreal-mcp/upstream/Python/tools/umg_tools.py +332 -332
- package/tools/unreal-mcp/upstream/Python/unreal_mcp_server.py +398 -398
- 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
|
+
}
|