ultimate-unreal-engine-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +729 -0
- package/dist/build/error-parser.js +51 -0
- package/dist/build/fix-suggester.js +84 -0
- package/dist/build/ubt-runner.js +146 -0
- package/dist/cli.js +13 -0
- package/dist/config.js +8 -0
- package/dist/docs/data/ue57-api.js +228 -0
- package/dist/docs/doc-index.js +110 -0
- package/dist/docs/types.js +4 -0
- package/dist/generators/class-generator.js +363 -0
- package/dist/generators/file-modifier.js +276 -0
- package/dist/generators/uht-validator.js +177 -0
- package/dist/index.js +89 -0
- package/dist/parsers/cpp-class-index.js +230 -0
- package/dist/parsers/cpp-parser.js +369 -0
- package/dist/parsers/ini-parser.js +216 -0
- package/dist/parsers/uproject-parser.js +130 -0
- package/dist/plugin-bridge/client.js +217 -0
- package/dist/plugin-bridge/protocol.js +6 -0
- package/dist/plugin-bridge/retry.js +23 -0
- package/dist/setup.js +209 -0
- package/dist/tools/ai-systems/index.js +247 -0
- package/dist/tools/ai-systems/types.js +4 -0
- package/dist/tools/animation/index.js +241 -0
- package/dist/tools/animation/types.js +4 -0
- package/dist/tools/audio/index.js +204 -0
- package/dist/tools/audio/types.js +4 -0
- package/dist/tools/blueprint/index.js +495 -0
- package/dist/tools/blueprint/types.js +4 -0
- package/dist/tools/build/index.js +163 -0
- package/dist/tools/chaos/index.js +230 -0
- package/dist/tools/chaos/types.js +4 -0
- package/dist/tools/collision-physics/index.js +211 -0
- package/dist/tools/config/index.js +288 -0
- package/dist/tools/cpp/index.js +305 -0
- package/dist/tools/docs/index.js +251 -0
- package/dist/tools/editor/index.js +242 -0
- package/dist/tools/gas/index.js +222 -0
- package/dist/tools/gas/types.js +5 -0
- package/dist/tools/import-export/index.js +218 -0
- package/dist/tools/input/index.js +146 -0
- package/dist/tools/known-issues/index.js +88 -0
- package/dist/tools/known-issues/middleware.js +55 -0
- package/dist/tools/known-issues/store.js +125 -0
- package/dist/tools/livelink/index.js +203 -0
- package/dist/tools/livelink/types.js +4 -0
- package/dist/tools/material/index.js +190 -0
- package/dist/tools/motion-design/index.js +251 -0
- package/dist/tools/motion-design/types.js +6 -0
- package/dist/tools/movie-render/index.js +220 -0
- package/dist/tools/networking/index.js +149 -0
- package/dist/tools/pcg/index.js +164 -0
- package/dist/tools/selection/index.js +180 -0
- package/dist/tools/sequencer/index.js +218 -0
- package/dist/tools/validation/index.js +183 -0
- package/dist/tools/validation/types.js +4 -0
- package/dist/tools/viewport/index.js +310 -0
- package/dist/tools/worldpartition/index.js +226 -0
- package/dist/tools/worldpartition/types.js +4 -0
- package/dist/utils/execFileNoThrow.js +40 -0
- package/dist/utils/logger.js +27 -0
- package/dist/utils/path-guard.js +26 -0
- package/package.json +40 -0
- package/unreal-plugin/MCPBridge/MCPBridge.uplugin +29 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/MCPBridgeEditor.Build.cs +68 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.cpp +919 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.h +23 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.cpp +415 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.cpp +653 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.h +24 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.cpp +290 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.h +17 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.cpp +624 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.cpp +616 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.h +25 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.cpp +744 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.h +24 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeEditor.cpp +23 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.cpp +149 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.h +38 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.cpp +771 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.cpp +749 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.cpp +172 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.cpp +715 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.cpp +679 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.cpp +381 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.h +24 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.cpp +504 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.cpp +511 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.cpp +1110 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.h +28 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.cpp +590 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.cpp +482 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.cpp +338 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.cpp +677 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.cpp +721 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.cpp +368 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.cpp +1208 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.h +29 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.cpp +822 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.h +23 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Public/MCPBridgeEditor.h +14 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/MCPBridgeRuntime.Build.cs +28 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPBridgeRuntime.cpp +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPCommandRouter.cpp +118 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPTcpServer.cpp +196 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPBridgeRuntime.h +15 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPCommandRouter.h +55 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPTcpServer.h +59 -0
package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.cpp
ADDED
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
// MCPCollisionPhysicsCommands.cpp (Plan 20-01)
|
|
2
|
+
// Implements four collision and physics command handlers for the MCP bridge:
|
|
3
|
+
// collision.read -- read collision preset, enabled state, object type, and per-channel response map (PHY-01)
|
|
4
|
+
// collision.set -- apply collision preset or individual channel overrides to a component (PHY-02)
|
|
5
|
+
// physics.material -- read and set friction, restitution, density, and surface type (PHY-03)
|
|
6
|
+
// physics.asset -- return per-bone body setup with primitive shapes and dimensions (PHY-04)
|
|
7
|
+
//
|
|
8
|
+
// All handlers run on the game thread (guaranteed by FMCPCommandRouter::Dispatch).
|
|
9
|
+
// Threat mitigations applied:
|
|
10
|
+
// T-20-01: collision.set response strings validated via explicit allowlist (Ignore/Overlap/Block only).
|
|
11
|
+
// Modify() before mutation, PostEditChange() after.
|
|
12
|
+
// T-20-02: physics.material write validates asset_path starts with "/Game/" or "/Engine/".
|
|
13
|
+
// Modify() before property writes.
|
|
14
|
+
// T-20-05: responses array iteration bounded to max 32 channels (ECollisionChannel enum range).
|
|
15
|
+
|
|
16
|
+
#include "MCPCollisionPhysicsCommands.h"
|
|
17
|
+
|
|
18
|
+
#include "Editor.h"
|
|
19
|
+
#include "Engine/World.h"
|
|
20
|
+
#include "GameFramework/Actor.h"
|
|
21
|
+
#include "EngineUtils.h"
|
|
22
|
+
#include "Components/PrimitiveComponent.h"
|
|
23
|
+
#include "Engine/EngineTypes.h"
|
|
24
|
+
#include "CollisionProfile.h"
|
|
25
|
+
#include "PhysicsEngine/BodyInstance.h"
|
|
26
|
+
#include "PhysicalMaterials/PhysicalMaterial.h"
|
|
27
|
+
#include "PhysicsEngine/PhysicsAsset.h"
|
|
28
|
+
#include "PhysicsEngine/SkeletalBodySetup.h"
|
|
29
|
+
#include "Serialization/JsonSerializer.h"
|
|
30
|
+
#include "Serialization/JsonWriter.h"
|
|
31
|
+
#include "Dom/JsonObject.h"
|
|
32
|
+
#include "Dom/JsonValue.h"
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Internal helpers
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/** Returns a JSON success response string (without trailing newline). */
|
|
39
|
+
static FString BuildPhySuccessResponse(const FString& CorrId, TSharedPtr<FJsonObject> Data)
|
|
40
|
+
{
|
|
41
|
+
TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
|
|
42
|
+
Obj->SetBoolField(TEXT("success"), true);
|
|
43
|
+
Obj->SetStringField(TEXT("correlationId"), CorrId);
|
|
44
|
+
if (Data.IsValid())
|
|
45
|
+
{
|
|
46
|
+
Obj->SetObjectField(TEXT("data"), Data);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
FString Output;
|
|
50
|
+
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
|
|
51
|
+
FJsonSerializer::Serialize(Obj.ToSharedRef(), Writer);
|
|
52
|
+
return Output;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Returns a JSON error response string (without trailing newline). */
|
|
56
|
+
static FString BuildPhyErrorResponse(const FString& CorrId, const FString& Error)
|
|
57
|
+
{
|
|
58
|
+
TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
|
|
59
|
+
Obj->SetBoolField(TEXT("success"), false);
|
|
60
|
+
Obj->SetStringField(TEXT("correlationId"), CorrId);
|
|
61
|
+
Obj->SetStringField(TEXT("error"), Error);
|
|
62
|
+
|
|
63
|
+
FString Output;
|
|
64
|
+
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
|
|
65
|
+
FJsonSerializer::Serialize(Obj.ToSharedRef(), Writer);
|
|
66
|
+
return Output;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Convert ECollisionEnabled::Type to string. */
|
|
70
|
+
static FString CollisionEnabledToString(ECollisionEnabled::Type Type)
|
|
71
|
+
{
|
|
72
|
+
switch (Type)
|
|
73
|
+
{
|
|
74
|
+
case ECollisionEnabled::NoCollision: return TEXT("NoCollision");
|
|
75
|
+
case ECollisionEnabled::QueryOnly: return TEXT("QueryOnly");
|
|
76
|
+
case ECollisionEnabled::PhysicsOnly: return TEXT("PhysicsOnly");
|
|
77
|
+
case ECollisionEnabled::QueryAndPhysics: return TEXT("QueryAndPhysics");
|
|
78
|
+
default: return TEXT("Unknown");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Convert ECollisionResponse to string. */
|
|
83
|
+
static FString CollisionResponseToString(ECollisionResponse Response)
|
|
84
|
+
{
|
|
85
|
+
switch (Response)
|
|
86
|
+
{
|
|
87
|
+
case ECR_Ignore: return TEXT("Ignore");
|
|
88
|
+
case ECR_Overlap: return TEXT("Overlap");
|
|
89
|
+
case ECR_Block: return TEXT("Block");
|
|
90
|
+
default: return TEXT("Ignore");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Convert string to ECollisionResponse (explicit allowlist per T-20-01). */
|
|
95
|
+
static bool StringToCollisionResponse(const FString& Str, ECollisionResponse& OutResponse)
|
|
96
|
+
{
|
|
97
|
+
if (Str == TEXT("Ignore")) { OutResponse = ECR_Ignore; return true; }
|
|
98
|
+
if (Str == TEXT("Overlap")) { OutResponse = ECR_Overlap; return true; }
|
|
99
|
+
if (Str == TEXT("Block")) { OutResponse = ECR_Block; return true; }
|
|
100
|
+
return false; // reject any other value
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Build the per-channel collision response array for a UPrimitiveComponent. */
|
|
104
|
+
static TArray<TSharedPtr<FJsonValue>> BuildCollisionResponseArray(UPrimitiveComponent* Comp)
|
|
105
|
+
{
|
|
106
|
+
TArray<TSharedPtr<FJsonValue>> ResponsesArray;
|
|
107
|
+
|
|
108
|
+
UCollisionProfile* Profile = UCollisionProfile::Get();
|
|
109
|
+
|
|
110
|
+
// Iterate all 32 possible ECollisionChannel values.
|
|
111
|
+
// Channels ECC_WorldStatic(0) through ECC_GameTraceChannel18(31).
|
|
112
|
+
for (int32 Ch = 0; Ch < 32; ++Ch)
|
|
113
|
+
{
|
|
114
|
+
const ECollisionChannel Channel = static_cast<ECollisionChannel>(Ch);
|
|
115
|
+
const ECollisionResponse Response = Comp->GetCollisionResponseToChannel(Channel);
|
|
116
|
+
|
|
117
|
+
FString ChannelName;
|
|
118
|
+
if (Profile)
|
|
119
|
+
{
|
|
120
|
+
// GetChannelName returns the display name for a given channel index.
|
|
121
|
+
ChannelName = Profile->GetChannelName(Channel).ToString();
|
|
122
|
+
}
|
|
123
|
+
if (ChannelName.IsEmpty())
|
|
124
|
+
{
|
|
125
|
+
ChannelName = FString::Printf(TEXT("Channel%d"), Ch);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
TSharedPtr<FJsonObject> Entry = MakeShared<FJsonObject>();
|
|
129
|
+
Entry->SetNumberField(TEXT("channel"), static_cast<double>(Ch));
|
|
130
|
+
Entry->SetStringField(TEXT("channel_name"), ChannelName);
|
|
131
|
+
Entry->SetStringField(TEXT("response"), CollisionResponseToString(Response));
|
|
132
|
+
ResponsesArray.Add(MakeShared<FJsonValueObject>(Entry));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return ResponsesArray;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Build the full collision data JSON object for a component. */
|
|
139
|
+
static TSharedPtr<FJsonObject> BuildCollisionData(UPrimitiveComponent* Comp)
|
|
140
|
+
{
|
|
141
|
+
UCollisionProfile* Profile = UCollisionProfile::Get();
|
|
142
|
+
|
|
143
|
+
const FString ProfileName = Comp->GetCollisionProfileName().ToString();
|
|
144
|
+
const FString EnabledStr = CollisionEnabledToString(Comp->GetCollisionEnabled());
|
|
145
|
+
const ECollisionChannel ObjCh = Comp->GetCollisionObjectType();
|
|
146
|
+
const int32 ObjectTypeInt = static_cast<int32>(ObjCh);
|
|
147
|
+
|
|
148
|
+
FString ObjectTypeName;
|
|
149
|
+
if (Profile)
|
|
150
|
+
{
|
|
151
|
+
ObjectTypeName = Profile->GetChannelName(ObjCh).ToString();
|
|
152
|
+
}
|
|
153
|
+
if (ObjectTypeName.IsEmpty())
|
|
154
|
+
{
|
|
155
|
+
ObjectTypeName = FString::Printf(TEXT("Channel%d"), ObjectTypeInt);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
TArray<TSharedPtr<FJsonValue>> ResponsesArray = BuildCollisionResponseArray(Comp);
|
|
159
|
+
|
|
160
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
161
|
+
Data->SetStringField(TEXT("component_name"), Comp->GetName());
|
|
162
|
+
Data->SetStringField(TEXT("profile_name"), ProfileName);
|
|
163
|
+
Data->SetStringField(TEXT("collision_enabled"), EnabledStr);
|
|
164
|
+
Data->SetNumberField(TEXT("object_type"), static_cast<double>(ObjectTypeInt));
|
|
165
|
+
Data->SetStringField(TEXT("object_type_name"), ObjectTypeName);
|
|
166
|
+
Data->SetArrayField(TEXT("responses"), ResponsesArray);
|
|
167
|
+
|
|
168
|
+
return Data;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// RegisterCollisionPhysicsCommands
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
void RegisterCollisionPhysicsCommands(FMCPCommandRouter& Router)
|
|
176
|
+
{
|
|
177
|
+
// -----------------------------------------------------------------------
|
|
178
|
+
// collision.read (PHY-01)
|
|
179
|
+
// Reads the full collision configuration from a UPrimitiveComponent on an actor.
|
|
180
|
+
//
|
|
181
|
+
// Payload fields:
|
|
182
|
+
// actor_label string (required) -- label of the actor in the editor world
|
|
183
|
+
// component_name string (optional) -- name of UPrimitiveComponent; defaults to first primitive
|
|
184
|
+
//
|
|
185
|
+
// Returns: profile_name, collision_enabled, object_type (int+name), responses array (all 32 channels).
|
|
186
|
+
// -----------------------------------------------------------------------
|
|
187
|
+
Router.RegisterHandler(TEXT("collision.read"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
|
|
188
|
+
{
|
|
189
|
+
const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
|
|
190
|
+
|
|
191
|
+
// Extract payload.
|
|
192
|
+
TSharedPtr<FJsonObject> Payload;
|
|
193
|
+
const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
|
|
194
|
+
if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
|
|
195
|
+
{
|
|
196
|
+
Payload = (*PayloadVal)->AsObject();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Require actor_label.
|
|
200
|
+
FString ActorLabel;
|
|
201
|
+
if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("actor_label"), ActorLabel) || ActorLabel.IsEmpty())
|
|
202
|
+
{
|
|
203
|
+
SendResponse(BuildPhyErrorResponse(CorrId, TEXT("missing_actor_label")) + TEXT("\n"));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Optional component_name.
|
|
208
|
+
FString ComponentName;
|
|
209
|
+
Payload->TryGetStringField(TEXT("component_name"), ComponentName);
|
|
210
|
+
|
|
211
|
+
// Get editor world.
|
|
212
|
+
if (!GEditor)
|
|
213
|
+
{
|
|
214
|
+
SendResponse(BuildPhyErrorResponse(CorrId, TEXT("no_editor")) + TEXT("\n"));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
UWorld* World = GEditor->GetEditorWorldContext().World();
|
|
218
|
+
if (!World)
|
|
219
|
+
{
|
|
220
|
+
SendResponse(BuildPhyErrorResponse(CorrId, TEXT("no_world_open")) + TEXT("\n"));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Find actor by label.
|
|
225
|
+
AActor* FoundActor = nullptr;
|
|
226
|
+
for (TActorIterator<AActor> It(World); It; ++It)
|
|
227
|
+
{
|
|
228
|
+
if (It->GetActorLabel() == ActorLabel)
|
|
229
|
+
{
|
|
230
|
+
FoundActor = *It;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (!FoundActor)
|
|
235
|
+
{
|
|
236
|
+
SendResponse(BuildPhyErrorResponse(CorrId, TEXT("actor_not_found")) + TEXT("\n"));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Locate target UPrimitiveComponent.
|
|
241
|
+
UPrimitiveComponent* TargetComp = nullptr;
|
|
242
|
+
|
|
243
|
+
TArray<UPrimitiveComponent*> PrimComps;
|
|
244
|
+
FoundActor->GetComponents<UPrimitiveComponent>(PrimComps);
|
|
245
|
+
|
|
246
|
+
if (!ComponentName.IsEmpty())
|
|
247
|
+
{
|
|
248
|
+
for (UPrimitiveComponent* Comp : PrimComps)
|
|
249
|
+
{
|
|
250
|
+
if (Comp && Comp->GetName() == ComponentName)
|
|
251
|
+
{
|
|
252
|
+
TargetComp = Comp;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (!TargetComp)
|
|
257
|
+
{
|
|
258
|
+
SendResponse(BuildPhyErrorResponse(CorrId, TEXT("component_not_found")) + TEXT("\n"));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
else
|
|
263
|
+
{
|
|
264
|
+
// Default to first primitive component.
|
|
265
|
+
if (PrimComps.Num() > 0)
|
|
266
|
+
{
|
|
267
|
+
TargetComp = PrimComps[0];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!TargetComp)
|
|
272
|
+
{
|
|
273
|
+
SendResponse(BuildPhyErrorResponse(CorrId, TEXT("no_primitive_component_found")) + TEXT("\n"));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
TSharedPtr<FJsonObject> Data = BuildCollisionData(TargetComp);
|
|
278
|
+
Data->SetStringField(TEXT("actor_label"), ActorLabel);
|
|
279
|
+
|
|
280
|
+
SendResponse(BuildPhySuccessResponse(CorrId, Data) + TEXT("\n"));
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// -----------------------------------------------------------------------
|
|
284
|
+
// collision.set (PHY-02)
|
|
285
|
+
// Applies a collision preset or per-channel overrides to a UPrimitiveComponent.
|
|
286
|
+
// Calls Modify() before changes, PostEditChange() after.
|
|
287
|
+
//
|
|
288
|
+
// Payload fields:
|
|
289
|
+
// actor_label string (required)
|
|
290
|
+
// component_name string (optional) -- defaults to first primitive component
|
|
291
|
+
// preset string (optional) -- collision profile name to apply
|
|
292
|
+
// responses array (optional) -- [{channel: int, response: string}, ...]
|
|
293
|
+
// response must be "Ignore", "Overlap", or "Block"
|
|
294
|
+
//
|
|
295
|
+
// Threat T-20-01: response strings validated via explicit allowlist.
|
|
296
|
+
// Threat T-20-05: responses array capped at 32 entries.
|
|
297
|
+
// -----------------------------------------------------------------------
|
|
298
|
+
Router.RegisterHandler(TEXT("collision.set"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
|
|
299
|
+
{
|
|
300
|
+
const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
|
|
301
|
+
|
|
302
|
+
// Extract payload.
|
|
303
|
+
TSharedPtr<FJsonObject> Payload;
|
|
304
|
+
const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
|
|
305
|
+
if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
|
|
306
|
+
{
|
|
307
|
+
Payload = (*PayloadVal)->AsObject();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Require actor_label.
|
|
311
|
+
FString ActorLabel;
|
|
312
|
+
if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("actor_label"), ActorLabel) || ActorLabel.IsEmpty())
|
|
313
|
+
{
|
|
314
|
+
SendResponse(BuildPhyErrorResponse(CorrId, TEXT("missing_actor_label")) + TEXT("\n"));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Optional component_name.
|
|
319
|
+
FString ComponentName;
|
|
320
|
+
Payload->TryGetStringField(TEXT("component_name"), ComponentName);
|
|
321
|
+
|
|
322
|
+
// Optional preset.
|
|
323
|
+
FString Preset;
|
|
324
|
+
const bool bHasPreset = Payload->TryGetStringField(TEXT("preset"), Preset) && !Preset.IsEmpty();
|
|
325
|
+
|
|
326
|
+
// Optional responses array.
|
|
327
|
+
const TArray<TSharedPtr<FJsonValue>>* ResponsesJsonArr = nullptr;
|
|
328
|
+
const bool bHasResponses = Payload->TryGetArrayField(TEXT("responses"), ResponsesJsonArr)
|
|
329
|
+
&& ResponsesJsonArr != nullptr
|
|
330
|
+
&& ResponsesJsonArr->Num() > 0;
|
|
331
|
+
|
|
332
|
+
if (!bHasPreset && !bHasResponses)
|
|
333
|
+
{
|
|
334
|
+
SendResponse(BuildPhyErrorResponse(CorrId, TEXT("missing_preset_or_responses")) + TEXT("\n"));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Get editor world.
|
|
339
|
+
if (!GEditor)
|
|
340
|
+
{
|
|
341
|
+
SendResponse(BuildPhyErrorResponse(CorrId, TEXT("no_editor")) + TEXT("\n"));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
UWorld* World = GEditor->GetEditorWorldContext().World();
|
|
345
|
+
if (!World)
|
|
346
|
+
{
|
|
347
|
+
SendResponse(BuildPhyErrorResponse(CorrId, TEXT("no_world_open")) + TEXT("\n"));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Find actor by label.
|
|
352
|
+
AActor* FoundActor = nullptr;
|
|
353
|
+
for (TActorIterator<AActor> It(World); It; ++It)
|
|
354
|
+
{
|
|
355
|
+
if (It->GetActorLabel() == ActorLabel)
|
|
356
|
+
{
|
|
357
|
+
FoundActor = *It;
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (!FoundActor)
|
|
362
|
+
{
|
|
363
|
+
SendResponse(BuildPhyErrorResponse(CorrId, TEXT("actor_not_found")) + TEXT("\n"));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Locate target UPrimitiveComponent.
|
|
368
|
+
UPrimitiveComponent* TargetComp = nullptr;
|
|
369
|
+
|
|
370
|
+
TArray<UPrimitiveComponent*> PrimComps;
|
|
371
|
+
FoundActor->GetComponents<UPrimitiveComponent>(PrimComps);
|
|
372
|
+
|
|
373
|
+
if (!ComponentName.IsEmpty())
|
|
374
|
+
{
|
|
375
|
+
for (UPrimitiveComponent* Comp : PrimComps)
|
|
376
|
+
{
|
|
377
|
+
if (Comp && Comp->GetName() == ComponentName)
|
|
378
|
+
{
|
|
379
|
+
TargetComp = Comp;
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (!TargetComp)
|
|
384
|
+
{
|
|
385
|
+
SendResponse(BuildPhyErrorResponse(CorrId, TEXT("component_not_found")) + TEXT("\n"));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
else
|
|
390
|
+
{
|
|
391
|
+
if (PrimComps.Num() > 0)
|
|
392
|
+
{
|
|
393
|
+
TargetComp = PrimComps[0];
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!TargetComp)
|
|
398
|
+
{
|
|
399
|
+
SendResponse(BuildPhyErrorResponse(CorrId, TEXT("no_primitive_component_found")) + TEXT("\n"));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Modify() before any state change -- required for UE undo/redo history (T-20-01).
|
|
404
|
+
TargetComp->Modify();
|
|
405
|
+
|
|
406
|
+
// Apply collision profile preset if provided.
|
|
407
|
+
if (bHasPreset)
|
|
408
|
+
{
|
|
409
|
+
TargetComp->SetCollisionProfileName(FName(*Preset));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Apply per-channel overrides if provided.
|
|
413
|
+
if (bHasResponses && ResponsesJsonArr != nullptr)
|
|
414
|
+
{
|
|
415
|
+
// Threat T-20-05: cap at 32 entries (ECollisionChannel enum range).
|
|
416
|
+
const int32 MaxEntries = FMath::Min(ResponsesJsonArr->Num(), 32);
|
|
417
|
+
for (int32 Idx = 0; Idx < MaxEntries; ++Idx)
|
|
418
|
+
{
|
|
419
|
+
const TSharedPtr<FJsonValue>& Entry = (*ResponsesJsonArr)[Idx];
|
|
420
|
+
if (!Entry.IsValid() || Entry->Type != EJson::Object)
|
|
421
|
+
{
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const TSharedPtr<FJsonObject> EntryObj = Entry->AsObject();
|
|
425
|
+
|
|
426
|
+
double ChannelNum = 0.0;
|
|
427
|
+
FString ResponseStr;
|
|
428
|
+
if (!EntryObj->TryGetNumberField(TEXT("channel"), ChannelNum) ||
|
|
429
|
+
!EntryObj->TryGetStringField(TEXT("response"), ResponseStr))
|
|
430
|
+
{
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const int32 ChInt = static_cast<int32>(ChannelNum);
|
|
435
|
+
if (ChInt < 0 || ChInt >= 32)
|
|
436
|
+
{
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Threat T-20-01: explicit allowlist for response strings.
|
|
441
|
+
ECollisionResponse NewResponse = ECR_Ignore;
|
|
442
|
+
if (!StringToCollisionResponse(ResponseStr, NewResponse))
|
|
443
|
+
{
|
|
444
|
+
// Invalid response string -- skip this entry.
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const ECollisionChannel Channel = static_cast<ECollisionChannel>(ChInt);
|
|
449
|
+
TargetComp->SetCollisionResponseToChannel(Channel, NewResponse);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Notify editor of the change.
|
|
454
|
+
TargetComp->PostEditChange();
|
|
455
|
+
|
|
456
|
+
// Return updated collision state.
|
|
457
|
+
TSharedPtr<FJsonObject> Data = BuildCollisionData(TargetComp);
|
|
458
|
+
Data->SetStringField(TEXT("actor_label"), ActorLabel);
|
|
459
|
+
|
|
460
|
+
SendResponse(BuildPhySuccessResponse(CorrId, Data) + TEXT("\n"));
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// -----------------------------------------------------------------------
|
|
464
|
+
// physics.material (PHY-03)
|
|
465
|
+
// Reads or writes properties of a UPhysicalMaterial asset: friction, static_friction,
|
|
466
|
+
// restitution, density, and surface_type.
|
|
467
|
+
//
|
|
468
|
+
// Payload fields:
|
|
469
|
+
// asset_path string (required) -- path to UPhysicalMaterial (e.g., "/Game/PM_Rock")
|
|
470
|
+
// action string (required) -- "read" or "write"
|
|
471
|
+
// friction float (optional, write only)
|
|
472
|
+
// static_friction float (optional, write only)
|
|
473
|
+
// restitution float (optional, write only)
|
|
474
|
+
// density float (optional, write only)
|
|
475
|
+
// surface_type int (optional, write only)
|
|
476
|
+
//
|
|
477
|
+
// Threat T-20-02: asset_path validated to start with "/Game/" or "/Engine/" on write.
|
|
478
|
+
// -----------------------------------------------------------------------
|
|
479
|
+
Router.RegisterHandler(TEXT("physics.material"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
|
|
480
|
+
{
|
|
481
|
+
const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
|
|
482
|
+
|
|
483
|
+
// Extract payload.
|
|
484
|
+
TSharedPtr<FJsonObject> Payload;
|
|
485
|
+
const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
|
|
486
|
+
if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
|
|
487
|
+
{
|
|
488
|
+
Payload = (*PayloadVal)->AsObject();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Require asset_path.
|
|
492
|
+
FString AssetPath;
|
|
493
|
+
if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty())
|
|
494
|
+
{
|
|
495
|
+
SendResponse(BuildPhyErrorResponse(CorrId, TEXT("missing_asset_path")) + TEXT("\n"));
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Require action.
|
|
500
|
+
FString Action;
|
|
501
|
+
if (!Payload->TryGetStringField(TEXT("action"), Action) || Action.IsEmpty())
|
|
502
|
+
{
|
|
503
|
+
SendResponse(BuildPhyErrorResponse(CorrId, TEXT("missing_action")) + TEXT("\n"));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const bool bIsRead = (Action == TEXT("read"));
|
|
508
|
+
const bool bIsWrite = (Action == TEXT("write"));
|
|
509
|
+
|
|
510
|
+
if (!bIsRead && !bIsWrite)
|
|
511
|
+
{
|
|
512
|
+
SendResponse(BuildPhyErrorResponse(CorrId, TEXT("invalid_action: must be read or write")) + TEXT("\n"));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Threat T-20-02: validate asset path prefix on write.
|
|
517
|
+
if (bIsWrite)
|
|
518
|
+
{
|
|
519
|
+
const bool bValidPath = AssetPath.StartsWith(TEXT("/Game/")) || AssetPath.StartsWith(TEXT("/Engine/"));
|
|
520
|
+
if (!bValidPath)
|
|
521
|
+
{
|
|
522
|
+
SendResponse(BuildPhyErrorResponse(CorrId, TEXT("invalid_asset_path: must start with /Game/ or /Engine/")) + TEXT("\n"));
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Load the physical material.
|
|
528
|
+
UPhysicalMaterial* PhysMat = Cast<UPhysicalMaterial>(
|
|
529
|
+
StaticLoadObject(UPhysicalMaterial::StaticClass(), nullptr, *AssetPath));
|
|
530
|
+
if (!PhysMat)
|
|
531
|
+
{
|
|
532
|
+
SendResponse(BuildPhyErrorResponse(CorrId, TEXT("physical_material_not_found")) + TEXT("\n"));
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (bIsWrite)
|
|
537
|
+
{
|
|
538
|
+
// Modify() before any property change (T-20-02).
|
|
539
|
+
PhysMat->Modify();
|
|
540
|
+
|
|
541
|
+
double Val = 0.0;
|
|
542
|
+
if (Payload->TryGetNumberField(TEXT("friction"), Val))
|
|
543
|
+
{
|
|
544
|
+
PhysMat->Friction = static_cast<float>(Val);
|
|
545
|
+
}
|
|
546
|
+
if (Payload->TryGetNumberField(TEXT("static_friction"), Val))
|
|
547
|
+
{
|
|
548
|
+
PhysMat->StaticFriction = static_cast<float>(Val);
|
|
549
|
+
}
|
|
550
|
+
if (Payload->TryGetNumberField(TEXT("restitution"), Val))
|
|
551
|
+
{
|
|
552
|
+
PhysMat->Restitution = static_cast<float>(Val);
|
|
553
|
+
}
|
|
554
|
+
if (Payload->TryGetNumberField(TEXT("density"), Val))
|
|
555
|
+
{
|
|
556
|
+
PhysMat->Density = static_cast<float>(Val);
|
|
557
|
+
}
|
|
558
|
+
if (Payload->TryGetNumberField(TEXT("surface_type"), Val))
|
|
559
|
+
{
|
|
560
|
+
PhysMat->SurfaceType = static_cast<EPhysicalSurface>(static_cast<int32>(Val));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
PhysMat->PostEditChange();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Build response data (same for both read and write, returns current values).
|
|
567
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
568
|
+
Data->SetStringField(TEXT("asset_path"), AssetPath);
|
|
569
|
+
Data->SetNumberField(TEXT("friction"), static_cast<double>(PhysMat->Friction));
|
|
570
|
+
Data->SetNumberField(TEXT("static_friction"), static_cast<double>(PhysMat->StaticFriction));
|
|
571
|
+
Data->SetNumberField(TEXT("restitution"), static_cast<double>(PhysMat->Restitution));
|
|
572
|
+
Data->SetNumberField(TEXT("density"), static_cast<double>(PhysMat->Density));
|
|
573
|
+
Data->SetNumberField(TEXT("surface_type"), static_cast<double>(static_cast<int32>(PhysMat->SurfaceType.GetValue())));
|
|
574
|
+
|
|
575
|
+
// Surface type name via UPhysicsSettings surface names.
|
|
576
|
+
// We use a numeric representation here since the name lookup requires project-specific surface config.
|
|
577
|
+
Data->SetStringField(TEXT("action"), Action);
|
|
578
|
+
|
|
579
|
+
SendResponse(BuildPhySuccessResponse(CorrId, Data) + TEXT("\n"));
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// -----------------------------------------------------------------------
|
|
583
|
+
// physics.asset (PHY-04)
|
|
584
|
+
// Returns per-bone body setup data from a UPhysicsAsset, including primitive
|
|
585
|
+
// shapes (capsules, spheres, boxes) and their dimensions.
|
|
586
|
+
//
|
|
587
|
+
// Payload fields:
|
|
588
|
+
// asset_path string (required) -- path to UPhysicsAsset
|
|
589
|
+
//
|
|
590
|
+
// Returns: array of body setups, each with bone_name, physics_type, and primitives.
|
|
591
|
+
// -----------------------------------------------------------------------
|
|
592
|
+
Router.RegisterHandler(TEXT("physics.asset"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
|
|
593
|
+
{
|
|
594
|
+
const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
|
|
595
|
+
|
|
596
|
+
// Extract payload.
|
|
597
|
+
TSharedPtr<FJsonObject> Payload;
|
|
598
|
+
const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
|
|
599
|
+
if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
|
|
600
|
+
{
|
|
601
|
+
Payload = (*PayloadVal)->AsObject();
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Require asset_path.
|
|
605
|
+
FString AssetPath;
|
|
606
|
+
if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty())
|
|
607
|
+
{
|
|
608
|
+
SendResponse(BuildPhyErrorResponse(CorrId, TEXT("missing_asset_path")) + TEXT("\n"));
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Load the physics asset.
|
|
613
|
+
UPhysicsAsset* PhysAsset = Cast<UPhysicsAsset>(
|
|
614
|
+
StaticLoadObject(UPhysicsAsset::StaticClass(), nullptr, *AssetPath));
|
|
615
|
+
if (!PhysAsset)
|
|
616
|
+
{
|
|
617
|
+
SendResponse(BuildPhyErrorResponse(CorrId, TEXT("physics_asset_not_found")) + TEXT("\n"));
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Helper: convert physics type enum to string.
|
|
622
|
+
auto PhysicsTypeToString = [](EPhysicsType Type) -> FString
|
|
623
|
+
{
|
|
624
|
+
switch (Type)
|
|
625
|
+
{
|
|
626
|
+
case EPhysicsType::PhysType_Simulated: return TEXT("Simulated");
|
|
627
|
+
case EPhysicsType::PhysType_Kinematic: return TEXT("Kinematic");
|
|
628
|
+
case EPhysicsType::PhysType_Default: return TEXT("Default");
|
|
629
|
+
default: return TEXT("Unknown");
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
// Iterate all body setups.
|
|
634
|
+
TArray<TSharedPtr<FJsonValue>> BodySetupsArray;
|
|
635
|
+
|
|
636
|
+
for (USkeletalBodySetup* BodySetup : PhysAsset->SkeletalBodySetups)
|
|
637
|
+
{
|
|
638
|
+
if (!BodySetup)
|
|
639
|
+
{
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
TArray<TSharedPtr<FJsonValue>> PrimitivesArray;
|
|
644
|
+
|
|
645
|
+
// Capsules (SphylElems).
|
|
646
|
+
for (const FKSphylElem& Capsule : BodySetup->AggGeom.SphylElems)
|
|
647
|
+
{
|
|
648
|
+
TSharedPtr<FJsonObject> PrimObj = MakeShared<FJsonObject>();
|
|
649
|
+
PrimObj->SetStringField(TEXT("type"), TEXT("capsule"));
|
|
650
|
+
|
|
651
|
+
TSharedPtr<FJsonObject> CenterObj = MakeShared<FJsonObject>();
|
|
652
|
+
CenterObj->SetNumberField(TEXT("x"), static_cast<double>(Capsule.Center.X));
|
|
653
|
+
CenterObj->SetNumberField(TEXT("y"), static_cast<double>(Capsule.Center.Y));
|
|
654
|
+
CenterObj->SetNumberField(TEXT("z"), static_cast<double>(Capsule.Center.Z));
|
|
655
|
+
PrimObj->SetObjectField(TEXT("center"), CenterObj);
|
|
656
|
+
|
|
657
|
+
TSharedPtr<FJsonObject> RotObj = MakeShared<FJsonObject>();
|
|
658
|
+
RotObj->SetNumberField(TEXT("pitch"), static_cast<double>(Capsule.Rotation.Pitch));
|
|
659
|
+
RotObj->SetNumberField(TEXT("yaw"), static_cast<double>(Capsule.Rotation.Yaw));
|
|
660
|
+
RotObj->SetNumberField(TEXT("roll"), static_cast<double>(Capsule.Rotation.Roll));
|
|
661
|
+
PrimObj->SetObjectField(TEXT("rotation"), RotObj);
|
|
662
|
+
|
|
663
|
+
PrimObj->SetNumberField(TEXT("radius"), static_cast<double>(Capsule.Radius));
|
|
664
|
+
PrimObj->SetNumberField(TEXT("length"), static_cast<double>(Capsule.Length));
|
|
665
|
+
|
|
666
|
+
PrimitivesArray.Add(MakeShared<FJsonValueObject>(PrimObj));
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Spheres (SphereElems).
|
|
670
|
+
for (const FKSphereElem& Sphere : BodySetup->AggGeom.SphereElems)
|
|
671
|
+
{
|
|
672
|
+
TSharedPtr<FJsonObject> PrimObj = MakeShared<FJsonObject>();
|
|
673
|
+
PrimObj->SetStringField(TEXT("type"), TEXT("sphere"));
|
|
674
|
+
|
|
675
|
+
TSharedPtr<FJsonObject> CenterObj = MakeShared<FJsonObject>();
|
|
676
|
+
CenterObj->SetNumberField(TEXT("x"), static_cast<double>(Sphere.Center.X));
|
|
677
|
+
CenterObj->SetNumberField(TEXT("y"), static_cast<double>(Sphere.Center.Y));
|
|
678
|
+
CenterObj->SetNumberField(TEXT("z"), static_cast<double>(Sphere.Center.Z));
|
|
679
|
+
PrimObj->SetObjectField(TEXT("center"), CenterObj);
|
|
680
|
+
|
|
681
|
+
PrimObj->SetNumberField(TEXT("radius"), static_cast<double>(Sphere.Radius));
|
|
682
|
+
|
|
683
|
+
PrimitivesArray.Add(MakeShared<FJsonValueObject>(PrimObj));
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Boxes (BoxElems).
|
|
687
|
+
for (const FKBoxElem& Box : BodySetup->AggGeom.BoxElems)
|
|
688
|
+
{
|
|
689
|
+
TSharedPtr<FJsonObject> PrimObj = MakeShared<FJsonObject>();
|
|
690
|
+
PrimObj->SetStringField(TEXT("type"), TEXT("box"));
|
|
691
|
+
|
|
692
|
+
TSharedPtr<FJsonObject> CenterObj = MakeShared<FJsonObject>();
|
|
693
|
+
CenterObj->SetNumberField(TEXT("x"), static_cast<double>(Box.Center.X));
|
|
694
|
+
CenterObj->SetNumberField(TEXT("y"), static_cast<double>(Box.Center.Y));
|
|
695
|
+
CenterObj->SetNumberField(TEXT("z"), static_cast<double>(Box.Center.Z));
|
|
696
|
+
PrimObj->SetObjectField(TEXT("center"), CenterObj);
|
|
697
|
+
|
|
698
|
+
TSharedPtr<FJsonObject> RotObj = MakeShared<FJsonObject>();
|
|
699
|
+
RotObj->SetNumberField(TEXT("pitch"), static_cast<double>(Box.Rotation.Pitch));
|
|
700
|
+
RotObj->SetNumberField(TEXT("yaw"), static_cast<double>(Box.Rotation.Yaw));
|
|
701
|
+
RotObj->SetNumberField(TEXT("roll"), static_cast<double>(Box.Rotation.Roll));
|
|
702
|
+
PrimObj->SetObjectField(TEXT("rotation"), RotObj);
|
|
703
|
+
|
|
704
|
+
PrimObj->SetNumberField(TEXT("extent_x"), static_cast<double>(Box.X));
|
|
705
|
+
PrimObj->SetNumberField(TEXT("extent_y"), static_cast<double>(Box.Y));
|
|
706
|
+
PrimObj->SetNumberField(TEXT("extent_z"), static_cast<double>(Box.Z));
|
|
707
|
+
|
|
708
|
+
PrimitivesArray.Add(MakeShared<FJsonValueObject>(PrimObj));
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Convex (ConvexElems) -- vertex count only (geometry is complex).
|
|
712
|
+
for (const FKConvexElem& Convex : BodySetup->AggGeom.ConvexElems)
|
|
713
|
+
{
|
|
714
|
+
TSharedPtr<FJsonObject> PrimObj = MakeShared<FJsonObject>();
|
|
715
|
+
PrimObj->SetStringField(TEXT("type"), TEXT("convex"));
|
|
716
|
+
PrimObj->SetNumberField(TEXT("vertex_count"), static_cast<double>(Convex.VertexData.Num()));
|
|
717
|
+
|
|
718
|
+
PrimitivesArray.Add(MakeShared<FJsonValueObject>(PrimObj));
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Build body setup entry.
|
|
722
|
+
TSharedPtr<FJsonObject> BodyObj = MakeShared<FJsonObject>();
|
|
723
|
+
BodyObj->SetStringField(TEXT("bone_name"), BodySetup->BoneName.ToString());
|
|
724
|
+
BodyObj->SetStringField(TEXT("physics_type"), PhysicsTypeToString(BodySetup->PhysicsType));
|
|
725
|
+
BodyObj->SetArrayField(TEXT("primitives"), PrimitivesArray);
|
|
726
|
+
|
|
727
|
+
// Mass override if applicable.
|
|
728
|
+
if (BodySetup->DefaultInstance.bOverrideMass)
|
|
729
|
+
{
|
|
730
|
+
BodyObj->SetBoolField(TEXT("mass_override_enabled"), true);
|
|
731
|
+
BodyObj->SetNumberField(TEXT("mass_override"),
|
|
732
|
+
static_cast<double>(BodySetup->DefaultInstance.GetMassOverride()));
|
|
733
|
+
}
|
|
734
|
+
else
|
|
735
|
+
{
|
|
736
|
+
BodyObj->SetBoolField(TEXT("mass_override_enabled"), false);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
BodySetupsArray.Add(MakeShared<FJsonValueObject>(BodyObj));
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
743
|
+
Data->SetStringField(TEXT("asset_path"), AssetPath);
|
|
744
|
+
Data->SetNumberField(TEXT("body_count"), static_cast<double>(BodySetupsArray.Num()));
|
|
745
|
+
Data->SetArrayField(TEXT("body_setups"), BodySetupsArray);
|
|
746
|
+
|
|
747
|
+
SendResponse(BuildPhySuccessResponse(CorrId, Data) + TEXT("\n"));
|
|
748
|
+
});
|
|
749
|
+
}
|