ue-mcp 1.2.1 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/context-cli.js +16 -16
- package/dist/engine-observer.js +68 -68
- package/dist/flow/guard.d.ts +26 -37
- package/dist/flow/guard.js +40 -25
- package/dist/flow/guard.js.map +1 -1
- package/dist/flow/guarded-bridge.d.ts +0 -10
- package/dist/flow/guarded-bridge.js +18 -23
- package/dist/flow/guarded-bridge.js.map +1 -1
- package/dist/flow/schema.d.ts +276 -16
- package/dist/flow/task-guards.d.ts +1 -1
- package/dist/flow/task-guards.js +44 -68
- package/dist/flow/task-guards.js.map +1 -1
- package/dist/instructions.js +130 -130
- package/dist/plugin-cli.js +299 -299
- package/dist/tool-counts.json +2 -2
- package/package.json +2 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/EngineStatusHooks.cpp +76 -76
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/EngineStatusHooks.h +27 -27
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerRegistry.cpp +128 -128
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerRegistry.h +63 -63
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRig.cpp +230 -230
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Struct.cpp +323 -323
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Internal.h +20 -20
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/ChooserHandlers_Nested.cpp +537 -537
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DemoHandlers.h +77 -77
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_Build.cpp +353 -353
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.h +18 -18
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_InputRuntime.cpp +246 -246
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.cpp +597 -597
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.h +51 -51
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +878 -878
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers_Sculpt.cpp +449 -449
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Function.cpp +275 -275
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Graph.cpp +551 -551
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NetworkingHandlers.cpp +531 -531
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NetworkingHandlers.h +28 -28
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +677 -677
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.cpp +822 -822
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.h +22 -22
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.cpp +218 -218
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.h +17 -17
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/VolumeHelpers_Internal.cpp +46 -46
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/VolumeHelpers_Internal.h +23 -23
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/MCPExternalRegistry.cpp +83 -83
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/UE_MCP_BridgeModule.h +14 -14
- package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/UE_MCP_BridgeStatusModule.cpp +37 -37
- package/dist/manifest-signature.d.ts +0 -1
- package/dist/manifest-signature.js +0 -53
- package/dist/manifest-signature.js.map +0 -1
- package/dist/pie/schema.d.ts +0 -791
- package/dist/pie/schema.js +0 -151
- package/dist/pie/schema.js.map +0 -1
- package/dist/watch-cli.js.map +0 -1
- /package/plugin/ue_mcp_bridge/{ue_mcp_bridge.uplugin → UE_MCP_Bridge.uplugin} +0 -0
|
@@ -1,677 +1,677 @@
|
|
|
1
|
-
#include "PhysicsHandlers.h"
|
|
2
|
-
#include "HandlerRegistry.h"
|
|
3
|
-
#include "HandlerUtils.h"
|
|
4
|
-
#include "Handlers/BlueprintHandlers_Internal.h"
|
|
5
|
-
#include "Components/PrimitiveComponent.h"
|
|
6
|
-
#include "PhysicsEngine/BodyInstance.h"
|
|
7
|
-
#include "Engine/Blueprint.h"
|
|
8
|
-
#include "Engine/World.h"
|
|
9
|
-
#include "Editor.h"
|
|
10
|
-
#include "Editor/EditorEngine.h"
|
|
11
|
-
#include "EditorAssetLibrary.h"
|
|
12
|
-
#include "Kismet2/KismetEditorUtilities.h"
|
|
13
|
-
#include "GameFramework/Actor.h"
|
|
14
|
-
#include "EngineUtils.h"
|
|
15
|
-
#include "Dom/JsonObject.h"
|
|
16
|
-
#include "Dom/JsonValue.h"
|
|
17
|
-
|
|
18
|
-
void FPhysicsHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
|
|
19
|
-
{
|
|
20
|
-
Registry.RegisterHandler(TEXT("set_collision_profile"), &SetCollisionProfile);
|
|
21
|
-
Registry.RegisterHandler(TEXT("set_collision_enabled"), &SetCollisionEnabled);
|
|
22
|
-
Registry.RegisterHandler(TEXT("set_collision"), &SetCollision);
|
|
23
|
-
Registry.RegisterHandler(TEXT("set_simulate_physics"), &SetPhysicsEnabled);
|
|
24
|
-
Registry.RegisterHandler(TEXT("set_physics_properties"), &SetBodyProperties);
|
|
25
|
-
// #676: perturb a physics body (impulse or force) for over-time observation.
|
|
26
|
-
Registry.RegisterHandler(TEXT("add_impulse"), &AddImpulse);
|
|
27
|
-
Registry.RegisterHandler(TEXT("add_force"), &AddImpulse);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
namespace
|
|
31
|
-
{
|
|
32
|
-
// Resolve a collision channel by canonical enum name ("ECC_Visibility",
|
|
33
|
-
// "Visibility", "WorldStatic", "Pawn", "GameTraceChannel1"). Bare names are
|
|
34
|
-
// retried with the ECC_ prefix the enum uses.
|
|
35
|
-
bool ResolveCollisionChannel(const FString& Name, ECollisionChannel& Out)
|
|
36
|
-
{
|
|
37
|
-
if (UEnum* E = StaticEnum<ECollisionChannel>())
|
|
38
|
-
{
|
|
39
|
-
int64 V = E->GetValueByNameString(Name);
|
|
40
|
-
if (V == INDEX_NONE && !Name.StartsWith(TEXT("ECC_")))
|
|
41
|
-
{
|
|
42
|
-
V = E->GetValueByNameString(FString(TEXT("ECC_")) + Name);
|
|
43
|
-
}
|
|
44
|
-
if (V != INDEX_NONE) { Out = static_cast<ECollisionChannel>(V); return true; }
|
|
45
|
-
}
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
bool ResolveCollisionResponse(const FString& Name, ECollisionResponse& Out)
|
|
50
|
-
{
|
|
51
|
-
if (Name.Equals(TEXT("Block"), ESearchCase::IgnoreCase) || Name.Equals(TEXT("ECR_Block"), ESearchCase::IgnoreCase)) { Out = ECR_Block; return true; }
|
|
52
|
-
if (Name.Equals(TEXT("Overlap"), ESearchCase::IgnoreCase) || Name.Equals(TEXT("ECR_Overlap"), ESearchCase::IgnoreCase)) { Out = ECR_Overlap; return true; }
|
|
53
|
-
if (Name.Equals(TEXT("Ignore"), ESearchCase::IgnoreCase) || Name.Equals(TEXT("ECR_Ignore"), ESearchCase::IgnoreCase)) { Out = ECR_Ignore; return true; }
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
bool ResolveCollisionEnabled(const FString& Name, ECollisionEnabled::Type& Out)
|
|
58
|
-
{
|
|
59
|
-
if (Name.Equals(TEXT("NoCollision"), ESearchCase::IgnoreCase)) { Out = ECollisionEnabled::NoCollision; return true; }
|
|
60
|
-
if (Name.Equals(TEXT("QueryOnly"), ESearchCase::IgnoreCase)) { Out = ECollisionEnabled::QueryOnly; return true; }
|
|
61
|
-
if (Name.Equals(TEXT("PhysicsOnly"), ESearchCase::IgnoreCase)) { Out = ECollisionEnabled::PhysicsOnly; return true; }
|
|
62
|
-
if (Name.Equals(TEXT("QueryAndPhysics"), ESearchCase::IgnoreCase)) { Out = ECollisionEnabled::QueryAndPhysics; return true; }
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
TSharedPtr<FJsonValue> FPhysicsHandlers::SetCollisionProfile(const TSharedPtr<FJsonObject>& Params)
|
|
68
|
-
{
|
|
69
|
-
FString ActorLabel;
|
|
70
|
-
if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
|
|
71
|
-
|
|
72
|
-
FString ProfileName;
|
|
73
|
-
if (auto Err = RequireString(Params, TEXT("profileName"), ProfileName)) return Err;
|
|
74
|
-
|
|
75
|
-
REQUIRE_EDITOR_WORLD(World);
|
|
76
|
-
|
|
77
|
-
AActor* Actor = FindActorByLabel(World, ActorLabel);
|
|
78
|
-
if (!Actor)
|
|
79
|
-
{
|
|
80
|
-
return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Capture previous profile from first component (for rollback)
|
|
84
|
-
TArray<UPrimitiveComponent*> PrimitiveComponents;
|
|
85
|
-
Actor->GetComponents<UPrimitiveComponent>(PrimitiveComponents);
|
|
86
|
-
FString PrevProfile;
|
|
87
|
-
bool bAllAlreadyMatch = !PrimitiveComponents.IsEmpty();
|
|
88
|
-
for (UPrimitiveComponent* PrimComp : PrimitiveComponents)
|
|
89
|
-
{
|
|
90
|
-
if (!PrimComp) continue;
|
|
91
|
-
const FString CompProfile = PrimComp->GetCollisionProfileName().ToString();
|
|
92
|
-
if (PrevProfile.IsEmpty()) PrevProfile = CompProfile;
|
|
93
|
-
if (CompProfile != ProfileName) bAllAlreadyMatch = false;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
auto Result = MCPSuccess();
|
|
97
|
-
Result->SetStringField(TEXT("actorLabel"), ActorLabel);
|
|
98
|
-
Result->SetStringField(TEXT("profileName"), ProfileName);
|
|
99
|
-
|
|
100
|
-
if (bAllAlreadyMatch)
|
|
101
|
-
{
|
|
102
|
-
MCPSetExisted(Result);
|
|
103
|
-
Result->SetBoolField(TEXT("updated"), false);
|
|
104
|
-
Result->SetNumberField(TEXT("componentsModified"), 0);
|
|
105
|
-
return MCPResult(Result);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
int32 ComponentsModified = 0;
|
|
109
|
-
for (UPrimitiveComponent* PrimComp : PrimitiveComponents)
|
|
110
|
-
{
|
|
111
|
-
if (!PrimComp) continue;
|
|
112
|
-
PrimComp->SetCollisionProfileName(FName(*ProfileName));
|
|
113
|
-
ComponentsModified++;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
Result->SetNumberField(TEXT("componentsModified"), ComponentsModified);
|
|
117
|
-
Result->SetBoolField(TEXT("success"), ComponentsModified > 0);
|
|
118
|
-
|
|
119
|
-
if (ComponentsModified == 0)
|
|
120
|
-
{
|
|
121
|
-
Result->SetStringField(TEXT("warning"), TEXT("No PrimitiveComponents found on actor"));
|
|
122
|
-
}
|
|
123
|
-
else
|
|
124
|
-
{
|
|
125
|
-
MCPSetUpdated(Result);
|
|
126
|
-
TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
|
|
127
|
-
Payload->SetStringField(TEXT("actorLabel"), ActorLabel);
|
|
128
|
-
Payload->SetStringField(TEXT("profileName"), PrevProfile);
|
|
129
|
-
MCPSetRollback(Result, TEXT("set_collision_profile"), Payload);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return MCPResult(Result);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
TSharedPtr<FJsonValue> FPhysicsHandlers::SetPhysicsEnabled(const TSharedPtr<FJsonObject>& Params)
|
|
136
|
-
{
|
|
137
|
-
FString ActorLabel;
|
|
138
|
-
if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
|
|
139
|
-
|
|
140
|
-
// #721: the published TS schema names this parameter "simulate" while the
|
|
141
|
-
// handler historically read only "enabled", so a schema-conformant call
|
|
142
|
-
// silently no-opped. Accept either spelling (simulate | enabled) and error
|
|
143
|
-
// explicitly when neither is present rather than succeeding silently.
|
|
144
|
-
bool bEnabled = true;
|
|
145
|
-
if (!Params->TryGetBoolField(TEXT("simulate"), bEnabled) &&
|
|
146
|
-
!Params->TryGetBoolField(TEXT("enabled"), bEnabled))
|
|
147
|
-
{
|
|
148
|
-
return MCPError(TEXT("Missing 'simulate' (aka 'enabled') parameter (true/false)"));
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
REQUIRE_EDITOR_WORLD(World);
|
|
152
|
-
|
|
153
|
-
AActor* Actor = FindActorByLabel(World, ActorLabel);
|
|
154
|
-
if (!Actor)
|
|
155
|
-
{
|
|
156
|
-
return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Capture previous state for rollback / idempotency check
|
|
160
|
-
TArray<UPrimitiveComponent*> PrimitiveComponents;
|
|
161
|
-
Actor->GetComponents<UPrimitiveComponent>(PrimitiveComponents);
|
|
162
|
-
bool bPrev = false;
|
|
163
|
-
bool bAnySim = false;
|
|
164
|
-
bool bAllAlready = !PrimitiveComponents.IsEmpty();
|
|
165
|
-
for (UPrimitiveComponent* PrimComp : PrimitiveComponents)
|
|
166
|
-
{
|
|
167
|
-
if (!PrimComp) continue;
|
|
168
|
-
const bool bCompSim = PrimComp->IsSimulatingPhysics();
|
|
169
|
-
if (!bAnySim) { bPrev = bCompSim; bAnySim = true; }
|
|
170
|
-
if (bCompSim != bEnabled) bAllAlready = false;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
auto Result = MCPSuccess();
|
|
174
|
-
Result->SetStringField(TEXT("actorLabel"), ActorLabel);
|
|
175
|
-
Result->SetBoolField(TEXT("enabled"), bEnabled);
|
|
176
|
-
|
|
177
|
-
if (bAllAlready)
|
|
178
|
-
{
|
|
179
|
-
MCPSetExisted(Result);
|
|
180
|
-
Result->SetBoolField(TEXT("updated"), false);
|
|
181
|
-
Result->SetNumberField(TEXT("componentsModified"), 0);
|
|
182
|
-
return MCPResult(Result);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
int32 ComponentsModified = 0;
|
|
186
|
-
for (UPrimitiveComponent* PrimComp : PrimitiveComponents)
|
|
187
|
-
{
|
|
188
|
-
if (!PrimComp) continue;
|
|
189
|
-
PrimComp->SetSimulatePhysics(bEnabled);
|
|
190
|
-
ComponentsModified++;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
Result->SetNumberField(TEXT("componentsModified"), ComponentsModified);
|
|
194
|
-
Result->SetBoolField(TEXT("success"), ComponentsModified > 0);
|
|
195
|
-
|
|
196
|
-
if (ComponentsModified == 0)
|
|
197
|
-
{
|
|
198
|
-
Result->SetStringField(TEXT("warning"), TEXT("No PrimitiveComponents found on actor"));
|
|
199
|
-
}
|
|
200
|
-
else
|
|
201
|
-
{
|
|
202
|
-
MCPSetUpdated(Result);
|
|
203
|
-
TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
|
|
204
|
-
Payload->SetStringField(TEXT("actorLabel"), ActorLabel);
|
|
205
|
-
Payload->SetBoolField(TEXT("enabled"), bPrev);
|
|
206
|
-
MCPSetRollback(Result, TEXT("set_physics_enabled"), Payload);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return MCPResult(Result);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
TSharedPtr<FJsonValue> FPhysicsHandlers::SetCollisionEnabled(const TSharedPtr<FJsonObject>& Params)
|
|
213
|
-
{
|
|
214
|
-
FString ActorLabel;
|
|
215
|
-
if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
|
|
216
|
-
|
|
217
|
-
FString CollisionType;
|
|
218
|
-
if (auto Err = RequireString(Params, TEXT("collisionType"), CollisionType)) return Err;
|
|
219
|
-
|
|
220
|
-
// Map string to ECollisionEnabled
|
|
221
|
-
ECollisionEnabled::Type CollisionEnabled;
|
|
222
|
-
if (CollisionType.Equals(TEXT("NoCollision"), ESearchCase::IgnoreCase))
|
|
223
|
-
{
|
|
224
|
-
CollisionEnabled = ECollisionEnabled::NoCollision;
|
|
225
|
-
}
|
|
226
|
-
else if (CollisionType.Equals(TEXT("QueryOnly"), ESearchCase::IgnoreCase))
|
|
227
|
-
{
|
|
228
|
-
CollisionEnabled = ECollisionEnabled::QueryOnly;
|
|
229
|
-
}
|
|
230
|
-
else if (CollisionType.Equals(TEXT("PhysicsOnly"), ESearchCase::IgnoreCase))
|
|
231
|
-
{
|
|
232
|
-
CollisionEnabled = ECollisionEnabled::PhysicsOnly;
|
|
233
|
-
}
|
|
234
|
-
else if (CollisionType.Equals(TEXT("QueryAndPhysics"), ESearchCase::IgnoreCase))
|
|
235
|
-
{
|
|
236
|
-
CollisionEnabled = ECollisionEnabled::QueryAndPhysics;
|
|
237
|
-
}
|
|
238
|
-
else
|
|
239
|
-
{
|
|
240
|
-
return MCPError(FString::Printf(TEXT("Unknown collision type: '%s'. Use NoCollision, QueryOnly, PhysicsOnly, or QueryAndPhysics."), *CollisionType));
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
REQUIRE_EDITOR_WORLD(World);
|
|
244
|
-
|
|
245
|
-
AActor* Actor = FindActorByLabel(World, ActorLabel);
|
|
246
|
-
if (!Actor)
|
|
247
|
-
{
|
|
248
|
-
return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Capture previous state
|
|
252
|
-
TArray<UPrimitiveComponent*> PrimitiveComponents;
|
|
253
|
-
Actor->GetComponents<UPrimitiveComponent>(PrimitiveComponents);
|
|
254
|
-
ECollisionEnabled::Type PrevType = ECollisionEnabled::NoCollision;
|
|
255
|
-
bool bAnyFound = false;
|
|
256
|
-
bool bAllAlready = !PrimitiveComponents.IsEmpty();
|
|
257
|
-
for (UPrimitiveComponent* PrimComp : PrimitiveComponents)
|
|
258
|
-
{
|
|
259
|
-
if (!PrimComp) continue;
|
|
260
|
-
const ECollisionEnabled::Type CompCol = PrimComp->GetCollisionEnabled();
|
|
261
|
-
if (!bAnyFound) { PrevType = CompCol; bAnyFound = true; }
|
|
262
|
-
if (CompCol != CollisionEnabled) bAllAlready = false;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
auto Result = MCPSuccess();
|
|
266
|
-
Result->SetStringField(TEXT("actorLabel"), ActorLabel);
|
|
267
|
-
Result->SetStringField(TEXT("collisionType"), CollisionType);
|
|
268
|
-
|
|
269
|
-
if (bAllAlready)
|
|
270
|
-
{
|
|
271
|
-
MCPSetExisted(Result);
|
|
272
|
-
Result->SetBoolField(TEXT("updated"), false);
|
|
273
|
-
Result->SetNumberField(TEXT("componentsModified"), 0);
|
|
274
|
-
return MCPResult(Result);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
int32 ComponentsModified = 0;
|
|
278
|
-
for (UPrimitiveComponent* PrimComp : PrimitiveComponents)
|
|
279
|
-
{
|
|
280
|
-
if (!PrimComp) continue;
|
|
281
|
-
PrimComp->SetCollisionEnabled(CollisionEnabled);
|
|
282
|
-
ComponentsModified++;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
Result->SetNumberField(TEXT("componentsModified"), ComponentsModified);
|
|
286
|
-
Result->SetBoolField(TEXT("success"), ComponentsModified > 0);
|
|
287
|
-
|
|
288
|
-
if (ComponentsModified == 0)
|
|
289
|
-
{
|
|
290
|
-
Result->SetStringField(TEXT("warning"), TEXT("No PrimitiveComponents found on actor"));
|
|
291
|
-
}
|
|
292
|
-
else
|
|
293
|
-
{
|
|
294
|
-
MCPSetUpdated(Result);
|
|
295
|
-
FString PrevTypeStr;
|
|
296
|
-
switch (PrevType)
|
|
297
|
-
{
|
|
298
|
-
case ECollisionEnabled::NoCollision: PrevTypeStr = TEXT("NoCollision"); break;
|
|
299
|
-
case ECollisionEnabled::QueryOnly: PrevTypeStr = TEXT("QueryOnly"); break;
|
|
300
|
-
case ECollisionEnabled::PhysicsOnly: PrevTypeStr = TEXT("PhysicsOnly"); break;
|
|
301
|
-
case ECollisionEnabled::QueryAndPhysics: PrevTypeStr = TEXT("QueryAndPhysics"); break;
|
|
302
|
-
default: PrevTypeStr = TEXT("NoCollision"); break;
|
|
303
|
-
}
|
|
304
|
-
TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
|
|
305
|
-
Payload->SetStringField(TEXT("actorLabel"), ActorLabel);
|
|
306
|
-
Payload->SetStringField(TEXT("collisionType"), PrevTypeStr);
|
|
307
|
-
MCPSetRollback(Result, TEXT("set_collision_enabled"), Payload);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return MCPResult(Result);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// set_collision -- unified collision authoring for placed level instances AND
|
|
314
|
-
// Blueprint component templates. Applies any of: collisionProfile,
|
|
315
|
-
// collisionEnabled, objectType, responseToAllChannels, and a per-channel
|
|
316
|
-
// responses map {channel: Block|Overlap|Ignore}. Targets a specific component
|
|
317
|
-
// by name, or every primitive component when omitted. (#545)
|
|
318
|
-
TSharedPtr<FJsonValue> FPhysicsHandlers::SetCollision(const TSharedPtr<FJsonObject>& Params)
|
|
319
|
-
{
|
|
320
|
-
const FString ComponentName = OptionalString(Params, TEXT("componentName"));
|
|
321
|
-
|
|
322
|
-
// Gather the target primitive components from either a placed actor
|
|
323
|
-
// (actorLabel) or a Blueprint component template (assetPath).
|
|
324
|
-
TArray<UPrimitiveComponent*> Targets;
|
|
325
|
-
UBlueprint* Blueprint = nullptr; // set when editing a BP template
|
|
326
|
-
AActor* Actor = nullptr; // set when editing a placed actor
|
|
327
|
-
FString TargetDesc;
|
|
328
|
-
|
|
329
|
-
FString AssetPath;
|
|
330
|
-
FString ActorLabel;
|
|
331
|
-
if (Params->TryGetStringField(TEXT("assetPath"), AssetPath) && !AssetPath.IsEmpty())
|
|
332
|
-
{
|
|
333
|
-
Blueprint = Cast<UBlueprint>(UEditorAssetLibrary::LoadAsset(AssetPath));
|
|
334
|
-
if (!Blueprint)
|
|
335
|
-
{
|
|
336
|
-
return MCPError(FString::Printf(TEXT("Blueprint not found: %s"), *AssetPath));
|
|
337
|
-
}
|
|
338
|
-
if (ComponentName.IsEmpty())
|
|
339
|
-
{
|
|
340
|
-
return MCPError(TEXT("componentName is required when targeting a Blueprint component template"));
|
|
341
|
-
}
|
|
342
|
-
bool bInherited = false;
|
|
343
|
-
TArray<FString> Available;
|
|
344
|
-
UActorComponent* Template = ResolveComponentTemplate(Blueprint, ComponentName, /*bForWrite=*/true, bInherited, Available);
|
|
345
|
-
UPrimitiveComponent* Prim = Cast<UPrimitiveComponent>(Template);
|
|
346
|
-
if (!Prim)
|
|
347
|
-
{
|
|
348
|
-
return MCPError(FString::Printf(TEXT("Component '%s' is not a PrimitiveComponent (or not found). Available: [%s]"), *ComponentName, *FString::Join(Available, TEXT(", "))));
|
|
349
|
-
}
|
|
350
|
-
Targets.Add(Prim);
|
|
351
|
-
TargetDesc = FString::Printf(TEXT("%s:%s"), *AssetPath, *ComponentName);
|
|
352
|
-
}
|
|
353
|
-
else if (Params->TryGetStringField(TEXT("actorLabel"), ActorLabel) && !ActorLabel.IsEmpty())
|
|
354
|
-
{
|
|
355
|
-
UWorld* World = GetEditorWorld();
|
|
356
|
-
if (!World) return MCPError(TEXT("No editor world available"));
|
|
357
|
-
Actor = FindActorByLabel(World, ActorLabel);
|
|
358
|
-
if (!Actor) return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
|
|
359
|
-
|
|
360
|
-
TArray<UPrimitiveComponent*> AllPrims;
|
|
361
|
-
Actor->GetComponents<UPrimitiveComponent>(AllPrims);
|
|
362
|
-
for (UPrimitiveComponent* Prim : AllPrims)
|
|
363
|
-
{
|
|
364
|
-
if (!Prim) continue;
|
|
365
|
-
if (ComponentName.IsEmpty() ||
|
|
366
|
-
Prim->GetName().Equals(ComponentName, ESearchCase::IgnoreCase) ||
|
|
367
|
-
Prim->GetName().StartsWith(ComponentName, ESearchCase::IgnoreCase))
|
|
368
|
-
{
|
|
369
|
-
Targets.Add(Prim);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
if (Targets.IsEmpty())
|
|
373
|
-
{
|
|
374
|
-
return MCPError(FString::Printf(TEXT("No matching PrimitiveComponent on actor '%s'"), *ActorLabel));
|
|
375
|
-
}
|
|
376
|
-
TargetDesc = ActorLabel;
|
|
377
|
-
}
|
|
378
|
-
else
|
|
379
|
-
{
|
|
380
|
-
return MCPError(TEXT("Provide either actorLabel (placed instance) or assetPath (Blueprint component template)"));
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Decode the requested settings once.
|
|
384
|
-
const FString Profile = OptionalString(Params, TEXT("collisionProfile"));
|
|
385
|
-
const FString EnabledStr = OptionalString(Params, TEXT("collisionEnabled"));
|
|
386
|
-
const FString ObjectTypeStr = OptionalString(Params, TEXT("objectType"));
|
|
387
|
-
const FString AllResponseStr = OptionalString(Params, TEXT("responseToAllChannels"));
|
|
388
|
-
|
|
389
|
-
ECollisionEnabled::Type EnabledVal = ECollisionEnabled::QueryAndPhysics;
|
|
390
|
-
const bool bSetEnabled = !EnabledStr.IsEmpty();
|
|
391
|
-
if (bSetEnabled && !ResolveCollisionEnabled(EnabledStr, EnabledVal))
|
|
392
|
-
{
|
|
393
|
-
return MCPError(FString::Printf(TEXT("Unknown collisionEnabled '%s'. Use NoCollision, QueryOnly, PhysicsOnly, QueryAndPhysics."), *EnabledStr));
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
ECollisionChannel ObjectType = ECC_WorldStatic;
|
|
397
|
-
const bool bSetObjectType = !ObjectTypeStr.IsEmpty();
|
|
398
|
-
if (bSetObjectType && !ResolveCollisionChannel(ObjectTypeStr, ObjectType))
|
|
399
|
-
{
|
|
400
|
-
return MCPError(FString::Printf(TEXT("Unknown objectType channel '%s'"), *ObjectTypeStr));
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
ECollisionResponse AllResponse = ECR_Block;
|
|
404
|
-
const bool bSetAllResponse = !AllResponseStr.IsEmpty();
|
|
405
|
-
if (bSetAllResponse && !ResolveCollisionResponse(AllResponseStr, AllResponse))
|
|
406
|
-
{
|
|
407
|
-
return MCPError(FString::Printf(TEXT("Unknown responseToAllChannels '%s'. Use Block, Overlap, Ignore."), *AllResponseStr));
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Per-channel responses: pre-resolve the map so a bad channel/response name
|
|
411
|
-
// fails before any mutation.
|
|
412
|
-
TArray<TPair<ECollisionChannel, ECollisionResponse>> ChannelResponses;
|
|
413
|
-
const TSharedPtr<FJsonObject>* ResponsesObj = nullptr;
|
|
414
|
-
if (Params->TryGetObjectField(TEXT("responses"), ResponsesObj) && ResponsesObj && (*ResponsesObj).IsValid())
|
|
415
|
-
{
|
|
416
|
-
for (const auto& Pair : (*ResponsesObj)->Values)
|
|
417
|
-
{
|
|
418
|
-
const FString ChannelName(*Pair.Key);
|
|
419
|
-
ECollisionChannel Ch;
|
|
420
|
-
if (!ResolveCollisionChannel(ChannelName, Ch))
|
|
421
|
-
{
|
|
422
|
-
return MCPError(FString::Printf(TEXT("Unknown response channel '%s'"), *ChannelName));
|
|
423
|
-
}
|
|
424
|
-
FString RespStr;
|
|
425
|
-
Pair.Value->TryGetString(RespStr);
|
|
426
|
-
ECollisionResponse Resp;
|
|
427
|
-
if (!ResolveCollisionResponse(RespStr, Resp))
|
|
428
|
-
{
|
|
429
|
-
return MCPError(FString::Printf(TEXT("Unknown response '%s' for channel '%s'. Use Block, Overlap, Ignore."), *RespStr, *ChannelName));
|
|
430
|
-
}
|
|
431
|
-
ChannelResponses.Emplace(Ch, Resp);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
if (!bSetEnabled && !bSetObjectType && !bSetAllResponse && Profile.IsEmpty() && ChannelResponses.Num() == 0)
|
|
436
|
-
{
|
|
437
|
-
return MCPError(TEXT("Nothing to set. Provide collisionProfile, collisionEnabled, objectType, responseToAllChannels, and/or responses."));
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
TArray<FString> Applied;
|
|
441
|
-
for (UPrimitiveComponent* Prim : Targets)
|
|
442
|
-
{
|
|
443
|
-
Prim->Modify();
|
|
444
|
-
// Apply profile first (it resets channel responses), then overrides.
|
|
445
|
-
if (!Profile.IsEmpty()) { Prim->SetCollisionProfileName(FName(*Profile)); }
|
|
446
|
-
if (bSetEnabled) { Prim->SetCollisionEnabled(EnabledVal); }
|
|
447
|
-
if (bSetObjectType) { Prim->SetCollisionObjectType(ObjectType); }
|
|
448
|
-
if (bSetAllResponse) { Prim->SetCollisionResponseToAllChannels(AllResponse); }
|
|
449
|
-
for (const TPair<ECollisionChannel, ECollisionResponse>& CR : ChannelResponses)
|
|
450
|
-
{
|
|
451
|
-
Prim->SetCollisionResponseToChannel(CR.Key, CR.Value);
|
|
452
|
-
}
|
|
453
|
-
Prim->MarkPackageDirty();
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (!Profile.IsEmpty()) Applied.Add(TEXT("collisionProfile"));
|
|
457
|
-
if (bSetEnabled) Applied.Add(TEXT("collisionEnabled"));
|
|
458
|
-
if (bSetObjectType) Applied.Add(TEXT("objectType"));
|
|
459
|
-
if (bSetAllResponse) Applied.Add(TEXT("responseToAllChannels"));
|
|
460
|
-
if (ChannelResponses.Num() > 0) Applied.Add(TEXT("responses"));
|
|
461
|
-
|
|
462
|
-
// Persist Blueprint template edits with a recompile + save.
|
|
463
|
-
if (Blueprint)
|
|
464
|
-
{
|
|
465
|
-
FKismetEditorUtilities::CompileBlueprint(Blueprint);
|
|
466
|
-
SaveAssetPackage(Blueprint);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
auto Result = MCPSuccess();
|
|
470
|
-
MCPSetUpdated(Result);
|
|
471
|
-
Result->SetStringField(TEXT("target"), TargetDesc);
|
|
472
|
-
Result->SetNumberField(TEXT("componentsModified"), Targets.Num());
|
|
473
|
-
TArray<TSharedPtr<FJsonValue>> AppliedArr;
|
|
474
|
-
for (const FString& A : Applied) AppliedArr.Add(MakeShared<FJsonValueString>(A));
|
|
475
|
-
Result->SetArrayField(TEXT("applied"), AppliedArr);
|
|
476
|
-
return MCPResult(Result);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
TSharedPtr<FJsonValue> FPhysicsHandlers::SetBodyProperties(const TSharedPtr<FJsonObject>& Params)
|
|
480
|
-
{
|
|
481
|
-
FString ActorLabel;
|
|
482
|
-
if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
|
|
483
|
-
|
|
484
|
-
REQUIRE_EDITOR_WORLD(World);
|
|
485
|
-
|
|
486
|
-
AActor* Actor = FindActorByLabel(World, ActorLabel);
|
|
487
|
-
if (!Actor)
|
|
488
|
-
{
|
|
489
|
-
return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// Get all PrimitiveComponents
|
|
493
|
-
int32 ComponentsModified = 0;
|
|
494
|
-
TArray<UPrimitiveComponent*> PrimitiveComponents;
|
|
495
|
-
Actor->GetComponents<UPrimitiveComponent>(PrimitiveComponents);
|
|
496
|
-
|
|
497
|
-
// Track which properties were set
|
|
498
|
-
TArray<FString> PropertiesSet;
|
|
499
|
-
|
|
500
|
-
// Capture previous values from first component for rollback payload
|
|
501
|
-
TSharedPtr<FJsonObject> PrevPayload = MakeShared<FJsonObject>();
|
|
502
|
-
PrevPayload->SetStringField(TEXT("actorLabel"), ActorLabel);
|
|
503
|
-
bool bCapturedPrev = false;
|
|
504
|
-
|
|
505
|
-
for (UPrimitiveComponent* PrimComp : PrimitiveComponents)
|
|
506
|
-
{
|
|
507
|
-
if (!PrimComp) continue;
|
|
508
|
-
|
|
509
|
-
FBodyInstance* BodyInstance = PrimComp->GetBodyInstance();
|
|
510
|
-
if (!BodyInstance) continue;
|
|
511
|
-
|
|
512
|
-
if (!bCapturedPrev)
|
|
513
|
-
{
|
|
514
|
-
double Mass = 0.0;
|
|
515
|
-
if (Params->TryGetNumberField(TEXT("mass"), Mass))
|
|
516
|
-
{
|
|
517
|
-
PrevPayload->SetNumberField(TEXT("mass"), BodyInstance->GetMassOverride());
|
|
518
|
-
}
|
|
519
|
-
double LinearDamping = 0.0;
|
|
520
|
-
if (Params->TryGetNumberField(TEXT("linearDamping"), LinearDamping))
|
|
521
|
-
{
|
|
522
|
-
PrevPayload->SetNumberField(TEXT("linearDamping"), BodyInstance->LinearDamping);
|
|
523
|
-
}
|
|
524
|
-
double AngularDamping = 0.0;
|
|
525
|
-
if (Params->TryGetNumberField(TEXT("angularDamping"), AngularDamping))
|
|
526
|
-
{
|
|
527
|
-
PrevPayload->SetNumberField(TEXT("angularDamping"), BodyInstance->AngularDamping);
|
|
528
|
-
}
|
|
529
|
-
bool bEnableGravity = true;
|
|
530
|
-
if (Params->TryGetBoolField(TEXT("enableGravity"), bEnableGravity))
|
|
531
|
-
{
|
|
532
|
-
PrevPayload->SetBoolField(TEXT("enableGravity"), BodyInstance->bEnableGravity);
|
|
533
|
-
}
|
|
534
|
-
bCapturedPrev = true;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// Set mass override if provided
|
|
538
|
-
double Mass = 0.0;
|
|
539
|
-
if (Params->TryGetNumberField(TEXT("mass"), Mass))
|
|
540
|
-
{
|
|
541
|
-
BodyInstance->SetMassOverride(Mass);
|
|
542
|
-
if (ComponentsModified == 0) PropertiesSet.Add(TEXT("mass"));
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Set linear damping if provided
|
|
546
|
-
double LinearDamping = 0.0;
|
|
547
|
-
if (Params->TryGetNumberField(TEXT("linearDamping"), LinearDamping))
|
|
548
|
-
{
|
|
549
|
-
BodyInstance->LinearDamping = LinearDamping;
|
|
550
|
-
PrimComp->SetLinearDamping(LinearDamping);
|
|
551
|
-
if (ComponentsModified == 0) PropertiesSet.Add(TEXT("linearDamping"));
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Set angular damping if provided
|
|
555
|
-
double AngularDamping = 0.0;
|
|
556
|
-
if (Params->TryGetNumberField(TEXT("angularDamping"), AngularDamping))
|
|
557
|
-
{
|
|
558
|
-
BodyInstance->AngularDamping = AngularDamping;
|
|
559
|
-
PrimComp->SetAngularDamping(AngularDamping);
|
|
560
|
-
if (ComponentsModified == 0) PropertiesSet.Add(TEXT("angularDamping"));
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// Set gravity enabled if provided
|
|
564
|
-
bool bEnableGravity = true;
|
|
565
|
-
if (Params->TryGetBoolField(TEXT("enableGravity"), bEnableGravity))
|
|
566
|
-
{
|
|
567
|
-
PrimComp->SetEnableGravity(bEnableGravity);
|
|
568
|
-
if (ComponentsModified == 0) PropertiesSet.Add(TEXT("enableGravity"));
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
ComponentsModified++;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Build properties set list
|
|
575
|
-
TArray<TSharedPtr<FJsonValue>> PropsArray;
|
|
576
|
-
for (const FString& PropName : PropertiesSet)
|
|
577
|
-
{
|
|
578
|
-
PropsArray.Add(MakeShared<FJsonValueString>(PropName));
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
auto Result = MCPSuccess();
|
|
582
|
-
Result->SetStringField(TEXT("actorLabel"), ActorLabel);
|
|
583
|
-
Result->SetArrayField(TEXT("propertiesSet"), PropsArray);
|
|
584
|
-
Result->SetNumberField(TEXT("componentsModified"), ComponentsModified);
|
|
585
|
-
Result->SetBoolField(TEXT("success"), ComponentsModified > 0);
|
|
586
|
-
|
|
587
|
-
if (ComponentsModified == 0)
|
|
588
|
-
{
|
|
589
|
-
Result->SetStringField(TEXT("warning"), TEXT("No PrimitiveComponents with BodyInstance found on actor"));
|
|
590
|
-
}
|
|
591
|
-
else if (PropertiesSet.Num() > 0)
|
|
592
|
-
{
|
|
593
|
-
MCPSetUpdated(Result);
|
|
594
|
-
MCPSetRollback(Result, TEXT("set_body_properties"), PrevPayload);
|
|
595
|
-
}
|
|
596
|
-
else
|
|
597
|
-
{
|
|
598
|
-
// No properties were actually requested
|
|
599
|
-
MCPSetExisted(Result);
|
|
600
|
-
Result->SetBoolField(TEXT("updated"), false);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
return MCPResult(Result);
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// #676: apply an impulse or force to an actor's physics body so its motion can
|
|
607
|
-
// be observed over time (loop read_actor_motion to sample the response).
|
|
608
|
-
// Registered for both add_impulse and add_force; the mode param (or the
|
|
609
|
-
// registered name intent) selects. Defaults to impulse.
|
|
610
|
-
TSharedPtr<FJsonValue> FPhysicsHandlers::AddImpulse(const TSharedPtr<FJsonObject>& Params)
|
|
611
|
-
{
|
|
612
|
-
FString ActorLabel;
|
|
613
|
-
if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
|
|
614
|
-
|
|
615
|
-
const FString WorldScope = OptionalString(Params, TEXT("world"), TEXT("auto"));
|
|
616
|
-
UWorld* World = ResolveWorldFromParams(Params, *WorldScope);
|
|
617
|
-
if (!World) return MCPError(TEXT("World not available"));
|
|
618
|
-
|
|
619
|
-
AActor* Actor = FindActorByLabelNameOrPath(World, ActorLabel);
|
|
620
|
-
if (!Actor) return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
|
|
621
|
-
|
|
622
|
-
// Resolve the primitive component: named, or the root.
|
|
623
|
-
const FString ComponentName = OptionalString(Params, TEXT("componentName"));
|
|
624
|
-
UPrimitiveComponent* Prim = nullptr;
|
|
625
|
-
if (!ComponentName.IsEmpty())
|
|
626
|
-
{
|
|
627
|
-
for (UActorComponent* C : Actor->GetComponents())
|
|
628
|
-
{
|
|
629
|
-
if (C->GetName() == ComponentName) { Prim = Cast<UPrimitiveComponent>(C); break; }
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
else
|
|
633
|
-
{
|
|
634
|
-
Prim = Cast<UPrimitiveComponent>(Actor->GetRootComponent());
|
|
635
|
-
if (!Prim) Prim = Actor->FindComponentByClass<UPrimitiveComponent>();
|
|
636
|
-
}
|
|
637
|
-
if (!Prim) return MCPError(FString::Printf(TEXT("No PrimitiveComponent found on '%s'"), *ActorLabel));
|
|
638
|
-
if (!Prim->IsSimulatingPhysics())
|
|
639
|
-
{
|
|
640
|
-
return MCPError(FString::Printf(TEXT("Component '%s' is not simulating physics; call set_simulate_physics first"), *Prim->GetName()));
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
FVector Vec = FVector::ZeroVector;
|
|
644
|
-
if (auto Err = RequireVec3(Params, TEXT("impulse"), Vec))
|
|
645
|
-
{
|
|
646
|
-
// Accept 'force' or 'vector' as aliases.
|
|
647
|
-
Vec = OptionalVec3(Params, TEXT("force"), OptionalVec3(Params, TEXT("vector")));
|
|
648
|
-
if (Vec.IsNearlyZero()) return Err;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
const FString Mode = OptionalString(Params, TEXT("mode"), TEXT("impulse")).ToLower();
|
|
652
|
-
const FString BoneName = OptionalString(Params, TEXT("boneName"));
|
|
653
|
-
const FName Bone = BoneName.IsEmpty() ? NAME_None : FName(*BoneName);
|
|
654
|
-
|
|
655
|
-
const TSharedPtr<FJsonObject>* AtLoc = nullptr;
|
|
656
|
-
if (Params->TryGetObjectField(TEXT("location"), AtLoc) && AtLoc)
|
|
657
|
-
{
|
|
658
|
-
FVector Loc = FVector::ZeroVector; ReadVec3Fields(*AtLoc, Loc);
|
|
659
|
-
Prim->AddImpulseAtLocation(Vec, Loc, Bone);
|
|
660
|
-
}
|
|
661
|
-
else if (Mode == TEXT("force"))
|
|
662
|
-
{
|
|
663
|
-
Prim->AddForce(Vec, Bone, OptionalBool(Params, TEXT("accelChange"), false));
|
|
664
|
-
}
|
|
665
|
-
else
|
|
666
|
-
{
|
|
667
|
-
Prim->AddImpulse(Vec, Bone, OptionalBool(Params, TEXT("velChange"), false));
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
auto Result = MCPSuccess();
|
|
671
|
-
Result->SetStringField(TEXT("actorLabel"), Actor->GetActorLabel());
|
|
672
|
-
Result->SetStringField(TEXT("component"), Prim->GetName());
|
|
673
|
-
Result->SetStringField(TEXT("mode"), Mode);
|
|
674
|
-
Result->SetObjectField(TEXT("vector"), MCPVec3ToJsonObject(Vec));
|
|
675
|
-
Result->SetObjectField(TEXT("linearVelocity"), MCPVec3ToJsonObject(Prim->GetPhysicsLinearVelocity()));
|
|
676
|
-
return MCPResult(Result);
|
|
677
|
-
}
|
|
1
|
+
#include "PhysicsHandlers.h"
|
|
2
|
+
#include "HandlerRegistry.h"
|
|
3
|
+
#include "HandlerUtils.h"
|
|
4
|
+
#include "Handlers/BlueprintHandlers_Internal.h"
|
|
5
|
+
#include "Components/PrimitiveComponent.h"
|
|
6
|
+
#include "PhysicsEngine/BodyInstance.h"
|
|
7
|
+
#include "Engine/Blueprint.h"
|
|
8
|
+
#include "Engine/World.h"
|
|
9
|
+
#include "Editor.h"
|
|
10
|
+
#include "Editor/EditorEngine.h"
|
|
11
|
+
#include "EditorAssetLibrary.h"
|
|
12
|
+
#include "Kismet2/KismetEditorUtilities.h"
|
|
13
|
+
#include "GameFramework/Actor.h"
|
|
14
|
+
#include "EngineUtils.h"
|
|
15
|
+
#include "Dom/JsonObject.h"
|
|
16
|
+
#include "Dom/JsonValue.h"
|
|
17
|
+
|
|
18
|
+
void FPhysicsHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
|
|
19
|
+
{
|
|
20
|
+
Registry.RegisterHandler(TEXT("set_collision_profile"), &SetCollisionProfile);
|
|
21
|
+
Registry.RegisterHandler(TEXT("set_collision_enabled"), &SetCollisionEnabled);
|
|
22
|
+
Registry.RegisterHandler(TEXT("set_collision"), &SetCollision);
|
|
23
|
+
Registry.RegisterHandler(TEXT("set_simulate_physics"), &SetPhysicsEnabled);
|
|
24
|
+
Registry.RegisterHandler(TEXT("set_physics_properties"), &SetBodyProperties);
|
|
25
|
+
// #676: perturb a physics body (impulse or force) for over-time observation.
|
|
26
|
+
Registry.RegisterHandler(TEXT("add_impulse"), &AddImpulse);
|
|
27
|
+
Registry.RegisterHandler(TEXT("add_force"), &AddImpulse);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
namespace
|
|
31
|
+
{
|
|
32
|
+
// Resolve a collision channel by canonical enum name ("ECC_Visibility",
|
|
33
|
+
// "Visibility", "WorldStatic", "Pawn", "GameTraceChannel1"). Bare names are
|
|
34
|
+
// retried with the ECC_ prefix the enum uses.
|
|
35
|
+
bool ResolveCollisionChannel(const FString& Name, ECollisionChannel& Out)
|
|
36
|
+
{
|
|
37
|
+
if (UEnum* E = StaticEnum<ECollisionChannel>())
|
|
38
|
+
{
|
|
39
|
+
int64 V = E->GetValueByNameString(Name);
|
|
40
|
+
if (V == INDEX_NONE && !Name.StartsWith(TEXT("ECC_")))
|
|
41
|
+
{
|
|
42
|
+
V = E->GetValueByNameString(FString(TEXT("ECC_")) + Name);
|
|
43
|
+
}
|
|
44
|
+
if (V != INDEX_NONE) { Out = static_cast<ECollisionChannel>(V); return true; }
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
bool ResolveCollisionResponse(const FString& Name, ECollisionResponse& Out)
|
|
50
|
+
{
|
|
51
|
+
if (Name.Equals(TEXT("Block"), ESearchCase::IgnoreCase) || Name.Equals(TEXT("ECR_Block"), ESearchCase::IgnoreCase)) { Out = ECR_Block; return true; }
|
|
52
|
+
if (Name.Equals(TEXT("Overlap"), ESearchCase::IgnoreCase) || Name.Equals(TEXT("ECR_Overlap"), ESearchCase::IgnoreCase)) { Out = ECR_Overlap; return true; }
|
|
53
|
+
if (Name.Equals(TEXT("Ignore"), ESearchCase::IgnoreCase) || Name.Equals(TEXT("ECR_Ignore"), ESearchCase::IgnoreCase)) { Out = ECR_Ignore; return true; }
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
bool ResolveCollisionEnabled(const FString& Name, ECollisionEnabled::Type& Out)
|
|
58
|
+
{
|
|
59
|
+
if (Name.Equals(TEXT("NoCollision"), ESearchCase::IgnoreCase)) { Out = ECollisionEnabled::NoCollision; return true; }
|
|
60
|
+
if (Name.Equals(TEXT("QueryOnly"), ESearchCase::IgnoreCase)) { Out = ECollisionEnabled::QueryOnly; return true; }
|
|
61
|
+
if (Name.Equals(TEXT("PhysicsOnly"), ESearchCase::IgnoreCase)) { Out = ECollisionEnabled::PhysicsOnly; return true; }
|
|
62
|
+
if (Name.Equals(TEXT("QueryAndPhysics"), ESearchCase::IgnoreCase)) { Out = ECollisionEnabled::QueryAndPhysics; return true; }
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
TSharedPtr<FJsonValue> FPhysicsHandlers::SetCollisionProfile(const TSharedPtr<FJsonObject>& Params)
|
|
68
|
+
{
|
|
69
|
+
FString ActorLabel;
|
|
70
|
+
if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
|
|
71
|
+
|
|
72
|
+
FString ProfileName;
|
|
73
|
+
if (auto Err = RequireString(Params, TEXT("profileName"), ProfileName)) return Err;
|
|
74
|
+
|
|
75
|
+
REQUIRE_EDITOR_WORLD(World);
|
|
76
|
+
|
|
77
|
+
AActor* Actor = FindActorByLabel(World, ActorLabel);
|
|
78
|
+
if (!Actor)
|
|
79
|
+
{
|
|
80
|
+
return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Capture previous profile from first component (for rollback)
|
|
84
|
+
TArray<UPrimitiveComponent*> PrimitiveComponents;
|
|
85
|
+
Actor->GetComponents<UPrimitiveComponent>(PrimitiveComponents);
|
|
86
|
+
FString PrevProfile;
|
|
87
|
+
bool bAllAlreadyMatch = !PrimitiveComponents.IsEmpty();
|
|
88
|
+
for (UPrimitiveComponent* PrimComp : PrimitiveComponents)
|
|
89
|
+
{
|
|
90
|
+
if (!PrimComp) continue;
|
|
91
|
+
const FString CompProfile = PrimComp->GetCollisionProfileName().ToString();
|
|
92
|
+
if (PrevProfile.IsEmpty()) PrevProfile = CompProfile;
|
|
93
|
+
if (CompProfile != ProfileName) bAllAlreadyMatch = false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
auto Result = MCPSuccess();
|
|
97
|
+
Result->SetStringField(TEXT("actorLabel"), ActorLabel);
|
|
98
|
+
Result->SetStringField(TEXT("profileName"), ProfileName);
|
|
99
|
+
|
|
100
|
+
if (bAllAlreadyMatch)
|
|
101
|
+
{
|
|
102
|
+
MCPSetExisted(Result);
|
|
103
|
+
Result->SetBoolField(TEXT("updated"), false);
|
|
104
|
+
Result->SetNumberField(TEXT("componentsModified"), 0);
|
|
105
|
+
return MCPResult(Result);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
int32 ComponentsModified = 0;
|
|
109
|
+
for (UPrimitiveComponent* PrimComp : PrimitiveComponents)
|
|
110
|
+
{
|
|
111
|
+
if (!PrimComp) continue;
|
|
112
|
+
PrimComp->SetCollisionProfileName(FName(*ProfileName));
|
|
113
|
+
ComponentsModified++;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
Result->SetNumberField(TEXT("componentsModified"), ComponentsModified);
|
|
117
|
+
Result->SetBoolField(TEXT("success"), ComponentsModified > 0);
|
|
118
|
+
|
|
119
|
+
if (ComponentsModified == 0)
|
|
120
|
+
{
|
|
121
|
+
Result->SetStringField(TEXT("warning"), TEXT("No PrimitiveComponents found on actor"));
|
|
122
|
+
}
|
|
123
|
+
else
|
|
124
|
+
{
|
|
125
|
+
MCPSetUpdated(Result);
|
|
126
|
+
TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
|
|
127
|
+
Payload->SetStringField(TEXT("actorLabel"), ActorLabel);
|
|
128
|
+
Payload->SetStringField(TEXT("profileName"), PrevProfile);
|
|
129
|
+
MCPSetRollback(Result, TEXT("set_collision_profile"), Payload);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return MCPResult(Result);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
TSharedPtr<FJsonValue> FPhysicsHandlers::SetPhysicsEnabled(const TSharedPtr<FJsonObject>& Params)
|
|
136
|
+
{
|
|
137
|
+
FString ActorLabel;
|
|
138
|
+
if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
|
|
139
|
+
|
|
140
|
+
// #721: the published TS schema names this parameter "simulate" while the
|
|
141
|
+
// handler historically read only "enabled", so a schema-conformant call
|
|
142
|
+
// silently no-opped. Accept either spelling (simulate | enabled) and error
|
|
143
|
+
// explicitly when neither is present rather than succeeding silently.
|
|
144
|
+
bool bEnabled = true;
|
|
145
|
+
if (!Params->TryGetBoolField(TEXT("simulate"), bEnabled) &&
|
|
146
|
+
!Params->TryGetBoolField(TEXT("enabled"), bEnabled))
|
|
147
|
+
{
|
|
148
|
+
return MCPError(TEXT("Missing 'simulate' (aka 'enabled') parameter (true/false)"));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
REQUIRE_EDITOR_WORLD(World);
|
|
152
|
+
|
|
153
|
+
AActor* Actor = FindActorByLabel(World, ActorLabel);
|
|
154
|
+
if (!Actor)
|
|
155
|
+
{
|
|
156
|
+
return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Capture previous state for rollback / idempotency check
|
|
160
|
+
TArray<UPrimitiveComponent*> PrimitiveComponents;
|
|
161
|
+
Actor->GetComponents<UPrimitiveComponent>(PrimitiveComponents);
|
|
162
|
+
bool bPrev = false;
|
|
163
|
+
bool bAnySim = false;
|
|
164
|
+
bool bAllAlready = !PrimitiveComponents.IsEmpty();
|
|
165
|
+
for (UPrimitiveComponent* PrimComp : PrimitiveComponents)
|
|
166
|
+
{
|
|
167
|
+
if (!PrimComp) continue;
|
|
168
|
+
const bool bCompSim = PrimComp->IsSimulatingPhysics();
|
|
169
|
+
if (!bAnySim) { bPrev = bCompSim; bAnySim = true; }
|
|
170
|
+
if (bCompSim != bEnabled) bAllAlready = false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
auto Result = MCPSuccess();
|
|
174
|
+
Result->SetStringField(TEXT("actorLabel"), ActorLabel);
|
|
175
|
+
Result->SetBoolField(TEXT("enabled"), bEnabled);
|
|
176
|
+
|
|
177
|
+
if (bAllAlready)
|
|
178
|
+
{
|
|
179
|
+
MCPSetExisted(Result);
|
|
180
|
+
Result->SetBoolField(TEXT("updated"), false);
|
|
181
|
+
Result->SetNumberField(TEXT("componentsModified"), 0);
|
|
182
|
+
return MCPResult(Result);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
int32 ComponentsModified = 0;
|
|
186
|
+
for (UPrimitiveComponent* PrimComp : PrimitiveComponents)
|
|
187
|
+
{
|
|
188
|
+
if (!PrimComp) continue;
|
|
189
|
+
PrimComp->SetSimulatePhysics(bEnabled);
|
|
190
|
+
ComponentsModified++;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
Result->SetNumberField(TEXT("componentsModified"), ComponentsModified);
|
|
194
|
+
Result->SetBoolField(TEXT("success"), ComponentsModified > 0);
|
|
195
|
+
|
|
196
|
+
if (ComponentsModified == 0)
|
|
197
|
+
{
|
|
198
|
+
Result->SetStringField(TEXT("warning"), TEXT("No PrimitiveComponents found on actor"));
|
|
199
|
+
}
|
|
200
|
+
else
|
|
201
|
+
{
|
|
202
|
+
MCPSetUpdated(Result);
|
|
203
|
+
TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
|
|
204
|
+
Payload->SetStringField(TEXT("actorLabel"), ActorLabel);
|
|
205
|
+
Payload->SetBoolField(TEXT("enabled"), bPrev);
|
|
206
|
+
MCPSetRollback(Result, TEXT("set_physics_enabled"), Payload);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return MCPResult(Result);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
TSharedPtr<FJsonValue> FPhysicsHandlers::SetCollisionEnabled(const TSharedPtr<FJsonObject>& Params)
|
|
213
|
+
{
|
|
214
|
+
FString ActorLabel;
|
|
215
|
+
if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
|
|
216
|
+
|
|
217
|
+
FString CollisionType;
|
|
218
|
+
if (auto Err = RequireString(Params, TEXT("collisionType"), CollisionType)) return Err;
|
|
219
|
+
|
|
220
|
+
// Map string to ECollisionEnabled
|
|
221
|
+
ECollisionEnabled::Type CollisionEnabled;
|
|
222
|
+
if (CollisionType.Equals(TEXT("NoCollision"), ESearchCase::IgnoreCase))
|
|
223
|
+
{
|
|
224
|
+
CollisionEnabled = ECollisionEnabled::NoCollision;
|
|
225
|
+
}
|
|
226
|
+
else if (CollisionType.Equals(TEXT("QueryOnly"), ESearchCase::IgnoreCase))
|
|
227
|
+
{
|
|
228
|
+
CollisionEnabled = ECollisionEnabled::QueryOnly;
|
|
229
|
+
}
|
|
230
|
+
else if (CollisionType.Equals(TEXT("PhysicsOnly"), ESearchCase::IgnoreCase))
|
|
231
|
+
{
|
|
232
|
+
CollisionEnabled = ECollisionEnabled::PhysicsOnly;
|
|
233
|
+
}
|
|
234
|
+
else if (CollisionType.Equals(TEXT("QueryAndPhysics"), ESearchCase::IgnoreCase))
|
|
235
|
+
{
|
|
236
|
+
CollisionEnabled = ECollisionEnabled::QueryAndPhysics;
|
|
237
|
+
}
|
|
238
|
+
else
|
|
239
|
+
{
|
|
240
|
+
return MCPError(FString::Printf(TEXT("Unknown collision type: '%s'. Use NoCollision, QueryOnly, PhysicsOnly, or QueryAndPhysics."), *CollisionType));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
REQUIRE_EDITOR_WORLD(World);
|
|
244
|
+
|
|
245
|
+
AActor* Actor = FindActorByLabel(World, ActorLabel);
|
|
246
|
+
if (!Actor)
|
|
247
|
+
{
|
|
248
|
+
return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Capture previous state
|
|
252
|
+
TArray<UPrimitiveComponent*> PrimitiveComponents;
|
|
253
|
+
Actor->GetComponents<UPrimitiveComponent>(PrimitiveComponents);
|
|
254
|
+
ECollisionEnabled::Type PrevType = ECollisionEnabled::NoCollision;
|
|
255
|
+
bool bAnyFound = false;
|
|
256
|
+
bool bAllAlready = !PrimitiveComponents.IsEmpty();
|
|
257
|
+
for (UPrimitiveComponent* PrimComp : PrimitiveComponents)
|
|
258
|
+
{
|
|
259
|
+
if (!PrimComp) continue;
|
|
260
|
+
const ECollisionEnabled::Type CompCol = PrimComp->GetCollisionEnabled();
|
|
261
|
+
if (!bAnyFound) { PrevType = CompCol; bAnyFound = true; }
|
|
262
|
+
if (CompCol != CollisionEnabled) bAllAlready = false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
auto Result = MCPSuccess();
|
|
266
|
+
Result->SetStringField(TEXT("actorLabel"), ActorLabel);
|
|
267
|
+
Result->SetStringField(TEXT("collisionType"), CollisionType);
|
|
268
|
+
|
|
269
|
+
if (bAllAlready)
|
|
270
|
+
{
|
|
271
|
+
MCPSetExisted(Result);
|
|
272
|
+
Result->SetBoolField(TEXT("updated"), false);
|
|
273
|
+
Result->SetNumberField(TEXT("componentsModified"), 0);
|
|
274
|
+
return MCPResult(Result);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
int32 ComponentsModified = 0;
|
|
278
|
+
for (UPrimitiveComponent* PrimComp : PrimitiveComponents)
|
|
279
|
+
{
|
|
280
|
+
if (!PrimComp) continue;
|
|
281
|
+
PrimComp->SetCollisionEnabled(CollisionEnabled);
|
|
282
|
+
ComponentsModified++;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
Result->SetNumberField(TEXT("componentsModified"), ComponentsModified);
|
|
286
|
+
Result->SetBoolField(TEXT("success"), ComponentsModified > 0);
|
|
287
|
+
|
|
288
|
+
if (ComponentsModified == 0)
|
|
289
|
+
{
|
|
290
|
+
Result->SetStringField(TEXT("warning"), TEXT("No PrimitiveComponents found on actor"));
|
|
291
|
+
}
|
|
292
|
+
else
|
|
293
|
+
{
|
|
294
|
+
MCPSetUpdated(Result);
|
|
295
|
+
FString PrevTypeStr;
|
|
296
|
+
switch (PrevType)
|
|
297
|
+
{
|
|
298
|
+
case ECollisionEnabled::NoCollision: PrevTypeStr = TEXT("NoCollision"); break;
|
|
299
|
+
case ECollisionEnabled::QueryOnly: PrevTypeStr = TEXT("QueryOnly"); break;
|
|
300
|
+
case ECollisionEnabled::PhysicsOnly: PrevTypeStr = TEXT("PhysicsOnly"); break;
|
|
301
|
+
case ECollisionEnabled::QueryAndPhysics: PrevTypeStr = TEXT("QueryAndPhysics"); break;
|
|
302
|
+
default: PrevTypeStr = TEXT("NoCollision"); break;
|
|
303
|
+
}
|
|
304
|
+
TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
|
|
305
|
+
Payload->SetStringField(TEXT("actorLabel"), ActorLabel);
|
|
306
|
+
Payload->SetStringField(TEXT("collisionType"), PrevTypeStr);
|
|
307
|
+
MCPSetRollback(Result, TEXT("set_collision_enabled"), Payload);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return MCPResult(Result);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// set_collision -- unified collision authoring for placed level instances AND
|
|
314
|
+
// Blueprint component templates. Applies any of: collisionProfile,
|
|
315
|
+
// collisionEnabled, objectType, responseToAllChannels, and a per-channel
|
|
316
|
+
// responses map {channel: Block|Overlap|Ignore}. Targets a specific component
|
|
317
|
+
// by name, or every primitive component when omitted. (#545)
|
|
318
|
+
TSharedPtr<FJsonValue> FPhysicsHandlers::SetCollision(const TSharedPtr<FJsonObject>& Params)
|
|
319
|
+
{
|
|
320
|
+
const FString ComponentName = OptionalString(Params, TEXT("componentName"));
|
|
321
|
+
|
|
322
|
+
// Gather the target primitive components from either a placed actor
|
|
323
|
+
// (actorLabel) or a Blueprint component template (assetPath).
|
|
324
|
+
TArray<UPrimitiveComponent*> Targets;
|
|
325
|
+
UBlueprint* Blueprint = nullptr; // set when editing a BP template
|
|
326
|
+
AActor* Actor = nullptr; // set when editing a placed actor
|
|
327
|
+
FString TargetDesc;
|
|
328
|
+
|
|
329
|
+
FString AssetPath;
|
|
330
|
+
FString ActorLabel;
|
|
331
|
+
if (Params->TryGetStringField(TEXT("assetPath"), AssetPath) && !AssetPath.IsEmpty())
|
|
332
|
+
{
|
|
333
|
+
Blueprint = Cast<UBlueprint>(UEditorAssetLibrary::LoadAsset(AssetPath));
|
|
334
|
+
if (!Blueprint)
|
|
335
|
+
{
|
|
336
|
+
return MCPError(FString::Printf(TEXT("Blueprint not found: %s"), *AssetPath));
|
|
337
|
+
}
|
|
338
|
+
if (ComponentName.IsEmpty())
|
|
339
|
+
{
|
|
340
|
+
return MCPError(TEXT("componentName is required when targeting a Blueprint component template"));
|
|
341
|
+
}
|
|
342
|
+
bool bInherited = false;
|
|
343
|
+
TArray<FString> Available;
|
|
344
|
+
UActorComponent* Template = ResolveComponentTemplate(Blueprint, ComponentName, /*bForWrite=*/true, bInherited, Available);
|
|
345
|
+
UPrimitiveComponent* Prim = Cast<UPrimitiveComponent>(Template);
|
|
346
|
+
if (!Prim)
|
|
347
|
+
{
|
|
348
|
+
return MCPError(FString::Printf(TEXT("Component '%s' is not a PrimitiveComponent (or not found). Available: [%s]"), *ComponentName, *FString::Join(Available, TEXT(", "))));
|
|
349
|
+
}
|
|
350
|
+
Targets.Add(Prim);
|
|
351
|
+
TargetDesc = FString::Printf(TEXT("%s:%s"), *AssetPath, *ComponentName);
|
|
352
|
+
}
|
|
353
|
+
else if (Params->TryGetStringField(TEXT("actorLabel"), ActorLabel) && !ActorLabel.IsEmpty())
|
|
354
|
+
{
|
|
355
|
+
UWorld* World = GetEditorWorld();
|
|
356
|
+
if (!World) return MCPError(TEXT("No editor world available"));
|
|
357
|
+
Actor = FindActorByLabel(World, ActorLabel);
|
|
358
|
+
if (!Actor) return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
|
|
359
|
+
|
|
360
|
+
TArray<UPrimitiveComponent*> AllPrims;
|
|
361
|
+
Actor->GetComponents<UPrimitiveComponent>(AllPrims);
|
|
362
|
+
for (UPrimitiveComponent* Prim : AllPrims)
|
|
363
|
+
{
|
|
364
|
+
if (!Prim) continue;
|
|
365
|
+
if (ComponentName.IsEmpty() ||
|
|
366
|
+
Prim->GetName().Equals(ComponentName, ESearchCase::IgnoreCase) ||
|
|
367
|
+
Prim->GetName().StartsWith(ComponentName, ESearchCase::IgnoreCase))
|
|
368
|
+
{
|
|
369
|
+
Targets.Add(Prim);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (Targets.IsEmpty())
|
|
373
|
+
{
|
|
374
|
+
return MCPError(FString::Printf(TEXT("No matching PrimitiveComponent on actor '%s'"), *ActorLabel));
|
|
375
|
+
}
|
|
376
|
+
TargetDesc = ActorLabel;
|
|
377
|
+
}
|
|
378
|
+
else
|
|
379
|
+
{
|
|
380
|
+
return MCPError(TEXT("Provide either actorLabel (placed instance) or assetPath (Blueprint component template)"));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Decode the requested settings once.
|
|
384
|
+
const FString Profile = OptionalString(Params, TEXT("collisionProfile"));
|
|
385
|
+
const FString EnabledStr = OptionalString(Params, TEXT("collisionEnabled"));
|
|
386
|
+
const FString ObjectTypeStr = OptionalString(Params, TEXT("objectType"));
|
|
387
|
+
const FString AllResponseStr = OptionalString(Params, TEXT("responseToAllChannels"));
|
|
388
|
+
|
|
389
|
+
ECollisionEnabled::Type EnabledVal = ECollisionEnabled::QueryAndPhysics;
|
|
390
|
+
const bool bSetEnabled = !EnabledStr.IsEmpty();
|
|
391
|
+
if (bSetEnabled && !ResolveCollisionEnabled(EnabledStr, EnabledVal))
|
|
392
|
+
{
|
|
393
|
+
return MCPError(FString::Printf(TEXT("Unknown collisionEnabled '%s'. Use NoCollision, QueryOnly, PhysicsOnly, QueryAndPhysics."), *EnabledStr));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
ECollisionChannel ObjectType = ECC_WorldStatic;
|
|
397
|
+
const bool bSetObjectType = !ObjectTypeStr.IsEmpty();
|
|
398
|
+
if (bSetObjectType && !ResolveCollisionChannel(ObjectTypeStr, ObjectType))
|
|
399
|
+
{
|
|
400
|
+
return MCPError(FString::Printf(TEXT("Unknown objectType channel '%s'"), *ObjectTypeStr));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
ECollisionResponse AllResponse = ECR_Block;
|
|
404
|
+
const bool bSetAllResponse = !AllResponseStr.IsEmpty();
|
|
405
|
+
if (bSetAllResponse && !ResolveCollisionResponse(AllResponseStr, AllResponse))
|
|
406
|
+
{
|
|
407
|
+
return MCPError(FString::Printf(TEXT("Unknown responseToAllChannels '%s'. Use Block, Overlap, Ignore."), *AllResponseStr));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Per-channel responses: pre-resolve the map so a bad channel/response name
|
|
411
|
+
// fails before any mutation.
|
|
412
|
+
TArray<TPair<ECollisionChannel, ECollisionResponse>> ChannelResponses;
|
|
413
|
+
const TSharedPtr<FJsonObject>* ResponsesObj = nullptr;
|
|
414
|
+
if (Params->TryGetObjectField(TEXT("responses"), ResponsesObj) && ResponsesObj && (*ResponsesObj).IsValid())
|
|
415
|
+
{
|
|
416
|
+
for (const auto& Pair : (*ResponsesObj)->Values)
|
|
417
|
+
{
|
|
418
|
+
const FString ChannelName(*Pair.Key);
|
|
419
|
+
ECollisionChannel Ch;
|
|
420
|
+
if (!ResolveCollisionChannel(ChannelName, Ch))
|
|
421
|
+
{
|
|
422
|
+
return MCPError(FString::Printf(TEXT("Unknown response channel '%s'"), *ChannelName));
|
|
423
|
+
}
|
|
424
|
+
FString RespStr;
|
|
425
|
+
Pair.Value->TryGetString(RespStr);
|
|
426
|
+
ECollisionResponse Resp;
|
|
427
|
+
if (!ResolveCollisionResponse(RespStr, Resp))
|
|
428
|
+
{
|
|
429
|
+
return MCPError(FString::Printf(TEXT("Unknown response '%s' for channel '%s'. Use Block, Overlap, Ignore."), *RespStr, *ChannelName));
|
|
430
|
+
}
|
|
431
|
+
ChannelResponses.Emplace(Ch, Resp);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!bSetEnabled && !bSetObjectType && !bSetAllResponse && Profile.IsEmpty() && ChannelResponses.Num() == 0)
|
|
436
|
+
{
|
|
437
|
+
return MCPError(TEXT("Nothing to set. Provide collisionProfile, collisionEnabled, objectType, responseToAllChannels, and/or responses."));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
TArray<FString> Applied;
|
|
441
|
+
for (UPrimitiveComponent* Prim : Targets)
|
|
442
|
+
{
|
|
443
|
+
Prim->Modify();
|
|
444
|
+
// Apply profile first (it resets channel responses), then overrides.
|
|
445
|
+
if (!Profile.IsEmpty()) { Prim->SetCollisionProfileName(FName(*Profile)); }
|
|
446
|
+
if (bSetEnabled) { Prim->SetCollisionEnabled(EnabledVal); }
|
|
447
|
+
if (bSetObjectType) { Prim->SetCollisionObjectType(ObjectType); }
|
|
448
|
+
if (bSetAllResponse) { Prim->SetCollisionResponseToAllChannels(AllResponse); }
|
|
449
|
+
for (const TPair<ECollisionChannel, ECollisionResponse>& CR : ChannelResponses)
|
|
450
|
+
{
|
|
451
|
+
Prim->SetCollisionResponseToChannel(CR.Key, CR.Value);
|
|
452
|
+
}
|
|
453
|
+
Prim->MarkPackageDirty();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (!Profile.IsEmpty()) Applied.Add(TEXT("collisionProfile"));
|
|
457
|
+
if (bSetEnabled) Applied.Add(TEXT("collisionEnabled"));
|
|
458
|
+
if (bSetObjectType) Applied.Add(TEXT("objectType"));
|
|
459
|
+
if (bSetAllResponse) Applied.Add(TEXT("responseToAllChannels"));
|
|
460
|
+
if (ChannelResponses.Num() > 0) Applied.Add(TEXT("responses"));
|
|
461
|
+
|
|
462
|
+
// Persist Blueprint template edits with a recompile + save.
|
|
463
|
+
if (Blueprint)
|
|
464
|
+
{
|
|
465
|
+
FKismetEditorUtilities::CompileBlueprint(Blueprint);
|
|
466
|
+
SaveAssetPackage(Blueprint);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
auto Result = MCPSuccess();
|
|
470
|
+
MCPSetUpdated(Result);
|
|
471
|
+
Result->SetStringField(TEXT("target"), TargetDesc);
|
|
472
|
+
Result->SetNumberField(TEXT("componentsModified"), Targets.Num());
|
|
473
|
+
TArray<TSharedPtr<FJsonValue>> AppliedArr;
|
|
474
|
+
for (const FString& A : Applied) AppliedArr.Add(MakeShared<FJsonValueString>(A));
|
|
475
|
+
Result->SetArrayField(TEXT("applied"), AppliedArr);
|
|
476
|
+
return MCPResult(Result);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
TSharedPtr<FJsonValue> FPhysicsHandlers::SetBodyProperties(const TSharedPtr<FJsonObject>& Params)
|
|
480
|
+
{
|
|
481
|
+
FString ActorLabel;
|
|
482
|
+
if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
|
|
483
|
+
|
|
484
|
+
REQUIRE_EDITOR_WORLD(World);
|
|
485
|
+
|
|
486
|
+
AActor* Actor = FindActorByLabel(World, ActorLabel);
|
|
487
|
+
if (!Actor)
|
|
488
|
+
{
|
|
489
|
+
return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Get all PrimitiveComponents
|
|
493
|
+
int32 ComponentsModified = 0;
|
|
494
|
+
TArray<UPrimitiveComponent*> PrimitiveComponents;
|
|
495
|
+
Actor->GetComponents<UPrimitiveComponent>(PrimitiveComponents);
|
|
496
|
+
|
|
497
|
+
// Track which properties were set
|
|
498
|
+
TArray<FString> PropertiesSet;
|
|
499
|
+
|
|
500
|
+
// Capture previous values from first component for rollback payload
|
|
501
|
+
TSharedPtr<FJsonObject> PrevPayload = MakeShared<FJsonObject>();
|
|
502
|
+
PrevPayload->SetStringField(TEXT("actorLabel"), ActorLabel);
|
|
503
|
+
bool bCapturedPrev = false;
|
|
504
|
+
|
|
505
|
+
for (UPrimitiveComponent* PrimComp : PrimitiveComponents)
|
|
506
|
+
{
|
|
507
|
+
if (!PrimComp) continue;
|
|
508
|
+
|
|
509
|
+
FBodyInstance* BodyInstance = PrimComp->GetBodyInstance();
|
|
510
|
+
if (!BodyInstance) continue;
|
|
511
|
+
|
|
512
|
+
if (!bCapturedPrev)
|
|
513
|
+
{
|
|
514
|
+
double Mass = 0.0;
|
|
515
|
+
if (Params->TryGetNumberField(TEXT("mass"), Mass))
|
|
516
|
+
{
|
|
517
|
+
PrevPayload->SetNumberField(TEXT("mass"), BodyInstance->GetMassOverride());
|
|
518
|
+
}
|
|
519
|
+
double LinearDamping = 0.0;
|
|
520
|
+
if (Params->TryGetNumberField(TEXT("linearDamping"), LinearDamping))
|
|
521
|
+
{
|
|
522
|
+
PrevPayload->SetNumberField(TEXT("linearDamping"), BodyInstance->LinearDamping);
|
|
523
|
+
}
|
|
524
|
+
double AngularDamping = 0.0;
|
|
525
|
+
if (Params->TryGetNumberField(TEXT("angularDamping"), AngularDamping))
|
|
526
|
+
{
|
|
527
|
+
PrevPayload->SetNumberField(TEXT("angularDamping"), BodyInstance->AngularDamping);
|
|
528
|
+
}
|
|
529
|
+
bool bEnableGravity = true;
|
|
530
|
+
if (Params->TryGetBoolField(TEXT("enableGravity"), bEnableGravity))
|
|
531
|
+
{
|
|
532
|
+
PrevPayload->SetBoolField(TEXT("enableGravity"), BodyInstance->bEnableGravity);
|
|
533
|
+
}
|
|
534
|
+
bCapturedPrev = true;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Set mass override if provided
|
|
538
|
+
double Mass = 0.0;
|
|
539
|
+
if (Params->TryGetNumberField(TEXT("mass"), Mass))
|
|
540
|
+
{
|
|
541
|
+
BodyInstance->SetMassOverride(Mass);
|
|
542
|
+
if (ComponentsModified == 0) PropertiesSet.Add(TEXT("mass"));
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Set linear damping if provided
|
|
546
|
+
double LinearDamping = 0.0;
|
|
547
|
+
if (Params->TryGetNumberField(TEXT("linearDamping"), LinearDamping))
|
|
548
|
+
{
|
|
549
|
+
BodyInstance->LinearDamping = LinearDamping;
|
|
550
|
+
PrimComp->SetLinearDamping(LinearDamping);
|
|
551
|
+
if (ComponentsModified == 0) PropertiesSet.Add(TEXT("linearDamping"));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Set angular damping if provided
|
|
555
|
+
double AngularDamping = 0.0;
|
|
556
|
+
if (Params->TryGetNumberField(TEXT("angularDamping"), AngularDamping))
|
|
557
|
+
{
|
|
558
|
+
BodyInstance->AngularDamping = AngularDamping;
|
|
559
|
+
PrimComp->SetAngularDamping(AngularDamping);
|
|
560
|
+
if (ComponentsModified == 0) PropertiesSet.Add(TEXT("angularDamping"));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Set gravity enabled if provided
|
|
564
|
+
bool bEnableGravity = true;
|
|
565
|
+
if (Params->TryGetBoolField(TEXT("enableGravity"), bEnableGravity))
|
|
566
|
+
{
|
|
567
|
+
PrimComp->SetEnableGravity(bEnableGravity);
|
|
568
|
+
if (ComponentsModified == 0) PropertiesSet.Add(TEXT("enableGravity"));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
ComponentsModified++;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Build properties set list
|
|
575
|
+
TArray<TSharedPtr<FJsonValue>> PropsArray;
|
|
576
|
+
for (const FString& PropName : PropertiesSet)
|
|
577
|
+
{
|
|
578
|
+
PropsArray.Add(MakeShared<FJsonValueString>(PropName));
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
auto Result = MCPSuccess();
|
|
582
|
+
Result->SetStringField(TEXT("actorLabel"), ActorLabel);
|
|
583
|
+
Result->SetArrayField(TEXT("propertiesSet"), PropsArray);
|
|
584
|
+
Result->SetNumberField(TEXT("componentsModified"), ComponentsModified);
|
|
585
|
+
Result->SetBoolField(TEXT("success"), ComponentsModified > 0);
|
|
586
|
+
|
|
587
|
+
if (ComponentsModified == 0)
|
|
588
|
+
{
|
|
589
|
+
Result->SetStringField(TEXT("warning"), TEXT("No PrimitiveComponents with BodyInstance found on actor"));
|
|
590
|
+
}
|
|
591
|
+
else if (PropertiesSet.Num() > 0)
|
|
592
|
+
{
|
|
593
|
+
MCPSetUpdated(Result);
|
|
594
|
+
MCPSetRollback(Result, TEXT("set_body_properties"), PrevPayload);
|
|
595
|
+
}
|
|
596
|
+
else
|
|
597
|
+
{
|
|
598
|
+
// No properties were actually requested
|
|
599
|
+
MCPSetExisted(Result);
|
|
600
|
+
Result->SetBoolField(TEXT("updated"), false);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return MCPResult(Result);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// #676: apply an impulse or force to an actor's physics body so its motion can
|
|
607
|
+
// be observed over time (loop read_actor_motion to sample the response).
|
|
608
|
+
// Registered for both add_impulse and add_force; the mode param (or the
|
|
609
|
+
// registered name intent) selects. Defaults to impulse.
|
|
610
|
+
TSharedPtr<FJsonValue> FPhysicsHandlers::AddImpulse(const TSharedPtr<FJsonObject>& Params)
|
|
611
|
+
{
|
|
612
|
+
FString ActorLabel;
|
|
613
|
+
if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
|
|
614
|
+
|
|
615
|
+
const FString WorldScope = OptionalString(Params, TEXT("world"), TEXT("auto"));
|
|
616
|
+
UWorld* World = ResolveWorldFromParams(Params, *WorldScope);
|
|
617
|
+
if (!World) return MCPError(TEXT("World not available"));
|
|
618
|
+
|
|
619
|
+
AActor* Actor = FindActorByLabelNameOrPath(World, ActorLabel);
|
|
620
|
+
if (!Actor) return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
|
|
621
|
+
|
|
622
|
+
// Resolve the primitive component: named, or the root.
|
|
623
|
+
const FString ComponentName = OptionalString(Params, TEXT("componentName"));
|
|
624
|
+
UPrimitiveComponent* Prim = nullptr;
|
|
625
|
+
if (!ComponentName.IsEmpty())
|
|
626
|
+
{
|
|
627
|
+
for (UActorComponent* C : Actor->GetComponents())
|
|
628
|
+
{
|
|
629
|
+
if (C->GetName() == ComponentName) { Prim = Cast<UPrimitiveComponent>(C); break; }
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
else
|
|
633
|
+
{
|
|
634
|
+
Prim = Cast<UPrimitiveComponent>(Actor->GetRootComponent());
|
|
635
|
+
if (!Prim) Prim = Actor->FindComponentByClass<UPrimitiveComponent>();
|
|
636
|
+
}
|
|
637
|
+
if (!Prim) return MCPError(FString::Printf(TEXT("No PrimitiveComponent found on '%s'"), *ActorLabel));
|
|
638
|
+
if (!Prim->IsSimulatingPhysics())
|
|
639
|
+
{
|
|
640
|
+
return MCPError(FString::Printf(TEXT("Component '%s' is not simulating physics; call set_simulate_physics first"), *Prim->GetName()));
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
FVector Vec = FVector::ZeroVector;
|
|
644
|
+
if (auto Err = RequireVec3(Params, TEXT("impulse"), Vec))
|
|
645
|
+
{
|
|
646
|
+
// Accept 'force' or 'vector' as aliases.
|
|
647
|
+
Vec = OptionalVec3(Params, TEXT("force"), OptionalVec3(Params, TEXT("vector")));
|
|
648
|
+
if (Vec.IsNearlyZero()) return Err;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const FString Mode = OptionalString(Params, TEXT("mode"), TEXT("impulse")).ToLower();
|
|
652
|
+
const FString BoneName = OptionalString(Params, TEXT("boneName"));
|
|
653
|
+
const FName Bone = BoneName.IsEmpty() ? NAME_None : FName(*BoneName);
|
|
654
|
+
|
|
655
|
+
const TSharedPtr<FJsonObject>* AtLoc = nullptr;
|
|
656
|
+
if (Params->TryGetObjectField(TEXT("location"), AtLoc) && AtLoc)
|
|
657
|
+
{
|
|
658
|
+
FVector Loc = FVector::ZeroVector; ReadVec3Fields(*AtLoc, Loc);
|
|
659
|
+
Prim->AddImpulseAtLocation(Vec, Loc, Bone);
|
|
660
|
+
}
|
|
661
|
+
else if (Mode == TEXT("force"))
|
|
662
|
+
{
|
|
663
|
+
Prim->AddForce(Vec, Bone, OptionalBool(Params, TEXT("accelChange"), false));
|
|
664
|
+
}
|
|
665
|
+
else
|
|
666
|
+
{
|
|
667
|
+
Prim->AddImpulse(Vec, Bone, OptionalBool(Params, TEXT("velChange"), false));
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
auto Result = MCPSuccess();
|
|
671
|
+
Result->SetStringField(TEXT("actorLabel"), Actor->GetActorLabel());
|
|
672
|
+
Result->SetStringField(TEXT("component"), Prim->GetName());
|
|
673
|
+
Result->SetStringField(TEXT("mode"), Mode);
|
|
674
|
+
Result->SetObjectField(TEXT("vector"), MCPVec3ToJsonObject(Vec));
|
|
675
|
+
Result->SetObjectField(TEXT("linearVelocity"), MCPVec3ToJsonObject(Prim->GetPhysicsLinearVelocity()));
|
|
676
|
+
return MCPResult(Result);
|
|
677
|
+
}
|