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
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
// MCPChaosCommands.cpp (Plan 26-01)
|
|
2
|
+
// Implements four Chaos physics command handlers for the MCP bridge:
|
|
3
|
+
// chaos.geometryCollection -- inspect geometry collection fracture hierarchy and cluster config (CHAOS-01)
|
|
4
|
+
// chaos.resetDestruction -- reset destruction state on a geometry collection actor (CHAOS-02)
|
|
5
|
+
// chaos.cloth -- read cloth simulation parameters (CHAOS-03)
|
|
6
|
+
// chaos.physicsCache -- manage physics cache recording: start, stop, query (CHAOS-04)
|
|
7
|
+
//
|
|
8
|
+
// All handlers run on the game thread (guaranteed by FMCPCommandRouter::Dispatch).
|
|
9
|
+
// Threat mitigations applied:
|
|
10
|
+
// T-26-01: asset_path validated to start with "/Game/" or "/Engine/" before StaticLoadObject.
|
|
11
|
+
// T-26-02: geometry collection hierarchy walk capped at 10000 entries.
|
|
12
|
+
// T-26-03: Modify() called before destruction reset, PostEditChange() after.
|
|
13
|
+
// T-26-04: chaos.physicsCache action validated via explicit allowlist (start/stop/query only).
|
|
14
|
+
|
|
15
|
+
#include "MCPChaosCommands.h"
|
|
16
|
+
|
|
17
|
+
#include "Editor.h"
|
|
18
|
+
#include "Engine/World.h"
|
|
19
|
+
#include "GameFramework/Actor.h"
|
|
20
|
+
#include "EngineUtils.h"
|
|
21
|
+
#include "Serialization/JsonSerializer.h"
|
|
22
|
+
#include "Serialization/JsonWriter.h"
|
|
23
|
+
#include "Dom/JsonObject.h"
|
|
24
|
+
#include "Dom/JsonValue.h"
|
|
25
|
+
|
|
26
|
+
// GeometryCollectionEngine module headers (Phase 26)
|
|
27
|
+
#include "GeometryCollection/GeometryCollectionComponent.h"
|
|
28
|
+
#include "GeometryCollection/GeometryCollectionObject.h"
|
|
29
|
+
|
|
30
|
+
// Skeletal mesh headers for cloth inspection
|
|
31
|
+
#include "Components/SkeletalMeshComponent.h"
|
|
32
|
+
#include "ClothingAsset.h"
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Internal helpers
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/** Returns a JSON success response string (without trailing newline). */
|
|
39
|
+
static FString BuildChaosSuccessResponse(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 BuildChaosErrorResponse(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
|
+
/** Find actor by label in the editor world. Returns nullptr if not found. */
|
|
70
|
+
static AActor* FindActorByLabel(UWorld* World, const FString& ActorLabel)
|
|
71
|
+
{
|
|
72
|
+
for (TActorIterator<AActor> It(World); It; ++It)
|
|
73
|
+
{
|
|
74
|
+
if (It->GetActorLabel() == ActorLabel)
|
|
75
|
+
{
|
|
76
|
+
return *It;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return nullptr;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// RegisterChaosCommands
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
void RegisterChaosCommands(FMCPCommandRouter& Router)
|
|
87
|
+
{
|
|
88
|
+
// -----------------------------------------------------------------------
|
|
89
|
+
// chaos.geometryCollection (CHAOS-01)
|
|
90
|
+
// Inspects fracture hierarchy, bone count per level, cluster grouping,
|
|
91
|
+
// and damage thresholds for a UGeometryCollection asset or actor.
|
|
92
|
+
//
|
|
93
|
+
// Payload fields:
|
|
94
|
+
// asset_path string (optional) -- path to UGeometryCollection asset
|
|
95
|
+
// actor_label string (optional) -- label of actor with UGeometryCollectionComponent
|
|
96
|
+
// (one of asset_path or actor_label required)
|
|
97
|
+
//
|
|
98
|
+
// Threat T-26-01: asset_path validated to start with "/Game/" or "/Engine/".
|
|
99
|
+
// Threat T-26-02: hierarchy iteration capped at 10000 entries.
|
|
100
|
+
// -----------------------------------------------------------------------
|
|
101
|
+
Router.RegisterHandler(TEXT("chaos.geometryCollection"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
|
|
102
|
+
{
|
|
103
|
+
const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
|
|
104
|
+
|
|
105
|
+
// Extract payload.
|
|
106
|
+
TSharedPtr<FJsonObject> Payload;
|
|
107
|
+
const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
|
|
108
|
+
if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
|
|
109
|
+
{
|
|
110
|
+
Payload = (*PayloadVal)->AsObject();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!Payload.IsValid())
|
|
114
|
+
{
|
|
115
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("missing_payload")) + TEXT("\n"));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
FString AssetPath;
|
|
120
|
+
FString ActorLabel;
|
|
121
|
+
const bool bHasAssetPath = Payload->TryGetStringField(TEXT("asset_path"), AssetPath) && !AssetPath.IsEmpty();
|
|
122
|
+
const bool bHasActorLabel = Payload->TryGetStringField(TEXT("actor_label"), ActorLabel) && !ActorLabel.IsEmpty();
|
|
123
|
+
|
|
124
|
+
if (!bHasAssetPath && !bHasActorLabel)
|
|
125
|
+
{
|
|
126
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("missing_asset_path_or_actor_label")) + TEXT("\n"));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Threat T-26-01: validate asset_path prefix.
|
|
131
|
+
if (bHasAssetPath)
|
|
132
|
+
{
|
|
133
|
+
const bool bValidPath = AssetPath.StartsWith(TEXT("/Game/")) || AssetPath.StartsWith(TEXT("/Engine/"));
|
|
134
|
+
if (!bValidPath)
|
|
135
|
+
{
|
|
136
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("invalid_asset_path: must start with /Game/ or /Engine/")) + TEXT("\n"));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
UGeometryCollection* GeoCollection = nullptr;
|
|
142
|
+
FString ResolvedLabel;
|
|
143
|
+
FString ResolvedAssetPath;
|
|
144
|
+
|
|
145
|
+
if (bHasAssetPath)
|
|
146
|
+
{
|
|
147
|
+
// Load asset by path.
|
|
148
|
+
GeoCollection = Cast<UGeometryCollection>(
|
|
149
|
+
StaticLoadObject(UGeometryCollection::StaticClass(), nullptr, *AssetPath));
|
|
150
|
+
if (!GeoCollection)
|
|
151
|
+
{
|
|
152
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("geometry_collection_asset_not_found")) + TEXT("\n"));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
ResolvedAssetPath = AssetPath;
|
|
156
|
+
}
|
|
157
|
+
else
|
|
158
|
+
{
|
|
159
|
+
// Find actor in editor world.
|
|
160
|
+
if (!GEditor)
|
|
161
|
+
{
|
|
162
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("no_editor")) + TEXT("\n"));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
UWorld* World = GEditor->GetEditorWorldContext().World();
|
|
166
|
+
if (!World)
|
|
167
|
+
{
|
|
168
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("no_world_open")) + TEXT("\n"));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
AActor* FoundActor = FindActorByLabel(World, ActorLabel);
|
|
173
|
+
if (!FoundActor)
|
|
174
|
+
{
|
|
175
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("actor_not_found")) + TEXT("\n"));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
UGeometryCollectionComponent* GeoComp = FoundActor->FindComponentByClass<UGeometryCollectionComponent>();
|
|
180
|
+
if (!GeoComp)
|
|
181
|
+
{
|
|
182
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("no_geometry_collection_component")) + TEXT("\n"));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// RestCollection is the UGeometryCollection asset on the component.
|
|
187
|
+
GeoCollection = const_cast<UGeometryCollection*>(GeoComp->GetRestCollection());
|
|
188
|
+
if (!GeoCollection)
|
|
189
|
+
{
|
|
190
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("geometry_collection_asset_null")) + TEXT("\n"));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
ResolvedLabel = ActorLabel;
|
|
194
|
+
ResolvedAssetPath = GeoCollection->GetPathName();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Access the internal FGeometryCollection data.
|
|
198
|
+
const TSharedPtr<FGeometryCollection, ESPMode::ThreadSafe> GeomColl = GeoCollection->GetGeometryCollection();
|
|
199
|
+
if (!GeomColl.IsValid())
|
|
200
|
+
{
|
|
201
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("geometry_collection_data_null")) + TEXT("\n"));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Build hierarchy from transform group.
|
|
206
|
+
// FGeometryCollection stores transforms in GeometryCollection::TransformGroup.
|
|
207
|
+
// Key arrays: Transform, BoneName, Parent, Children, SimulationType, TransformToGeometryIndex.
|
|
208
|
+
const int32 BoneCount = GeomColl->NumElements(FGeometryCollection::TransformGroup);
|
|
209
|
+
|
|
210
|
+
TArray<TSharedPtr<FJsonValue>> HierarchyArray;
|
|
211
|
+
TMap<int32, int32> LevelMap; // bone index -> level
|
|
212
|
+
|
|
213
|
+
// Compute levels via parent walk.
|
|
214
|
+
// Threat T-26-02: cap at 10000 entries.
|
|
215
|
+
const int32 MaxEntries = FMath::Min(BoneCount, 10000);
|
|
216
|
+
bool bTruncated = (BoneCount > 10000);
|
|
217
|
+
|
|
218
|
+
// Get parent array if available.
|
|
219
|
+
const TManagedArray<int32>* ParentArray = nullptr;
|
|
220
|
+
if (GeomColl->HasAttribute(TEXT("Parent"), FGeometryCollection::TransformGroup))
|
|
221
|
+
{
|
|
222
|
+
ParentArray = &GeomColl->GetAttribute<int32>(TEXT("Parent"), FGeometryCollection::TransformGroup);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Compute levels.
|
|
226
|
+
for (int32 i = 0; i < MaxEntries; ++i)
|
|
227
|
+
{
|
|
228
|
+
int32 Level = 0;
|
|
229
|
+
if (ParentArray)
|
|
230
|
+
{
|
|
231
|
+
int32 Current = i;
|
|
232
|
+
// Walk up parent chain, limit to BoneCount hops to avoid infinite loops.
|
|
233
|
+
for (int32 Hop = 0; Hop < BoneCount; ++Hop)
|
|
234
|
+
{
|
|
235
|
+
const int32 Parent = (*ParentArray)[Current];
|
|
236
|
+
if (Parent < 0)
|
|
237
|
+
{
|
|
238
|
+
break; // reached root
|
|
239
|
+
}
|
|
240
|
+
++Level;
|
|
241
|
+
Current = Parent;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
LevelMap.Add(i, Level);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Get bone name array if available.
|
|
248
|
+
const TManagedArray<FString>* BoneNameArray = nullptr;
|
|
249
|
+
if (GeomColl->HasAttribute(TEXT("BoneName"), FGeometryCollection::TransformGroup))
|
|
250
|
+
{
|
|
251
|
+
BoneNameArray = &GeomColl->GetAttribute<FString>(TEXT("BoneName"), FGeometryCollection::TransformGroup);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Get children array if available.
|
|
255
|
+
const TManagedArray<TSet<int32>>* ChildrenArray = nullptr;
|
|
256
|
+
if (GeomColl->HasAttribute(TEXT("Children"), FGeometryCollection::TransformGroup))
|
|
257
|
+
{
|
|
258
|
+
ChildrenArray = &GeomColl->GetAttribute<TSet<int32>>(TEXT("Children"), FGeometryCollection::TransformGroup);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Compute level-to-bone-count map.
|
|
262
|
+
TMap<int32, int32> LevelBoneCount;
|
|
263
|
+
for (auto& Pair : LevelMap)
|
|
264
|
+
{
|
|
265
|
+
LevelBoneCount.FindOrAdd(Pair.Value)++;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Build hierarchy array.
|
|
269
|
+
for (int32 i = 0; i < MaxEntries; ++i)
|
|
270
|
+
{
|
|
271
|
+
TSharedPtr<FJsonObject> BoneObj = MakeShared<FJsonObject>();
|
|
272
|
+
BoneObj->SetNumberField(TEXT("index"), static_cast<double>(i));
|
|
273
|
+
|
|
274
|
+
const int32 ParentIdx = (ParentArray) ? (*ParentArray)[i] : -1;
|
|
275
|
+
BoneObj->SetNumberField(TEXT("parent"), static_cast<double>(ParentIdx));
|
|
276
|
+
BoneObj->SetNumberField(TEXT("level"), static_cast<double>(LevelMap.FindRef(i)));
|
|
277
|
+
|
|
278
|
+
FString BoneName = FString::Printf(TEXT("Bone_%d"), i);
|
|
279
|
+
if (BoneNameArray && (*BoneNameArray)[i].Len() > 0)
|
|
280
|
+
{
|
|
281
|
+
BoneName = (*BoneNameArray)[i];
|
|
282
|
+
}
|
|
283
|
+
BoneObj->SetStringField(TEXT("name"), BoneName);
|
|
284
|
+
|
|
285
|
+
// Build children array.
|
|
286
|
+
TArray<TSharedPtr<FJsonValue>> ChildrenJsonArr;
|
|
287
|
+
if (ChildrenArray)
|
|
288
|
+
{
|
|
289
|
+
for (int32 ChildIdx : (*ChildrenArray)[i])
|
|
290
|
+
{
|
|
291
|
+
ChildrenJsonArr.Add(MakeShared<FJsonValueNumber>(static_cast<double>(ChildIdx)));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
BoneObj->SetArrayField(TEXT("children"), ChildrenJsonArr);
|
|
295
|
+
|
|
296
|
+
HierarchyArray.Add(MakeShared<FJsonValueObject>(BoneObj));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Build levels summary array.
|
|
300
|
+
TArray<TSharedPtr<FJsonValue>> LevelsArray;
|
|
301
|
+
for (auto& Pair : LevelBoneCount)
|
|
302
|
+
{
|
|
303
|
+
TSharedPtr<FJsonObject> LvlObj = MakeShared<FJsonObject>();
|
|
304
|
+
LvlObj->SetNumberField(TEXT("level"), static_cast<double>(Pair.Key));
|
|
305
|
+
LvlObj->SetNumberField(TEXT("bone_count"), static_cast<double>(Pair.Value));
|
|
306
|
+
LevelsArray.Add(MakeShared<FJsonValueObject>(LvlObj));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Damage thresholds from geometry collection settings.
|
|
310
|
+
TArray<TSharedPtr<FJsonValue>> DamageThresholdsArr;
|
|
311
|
+
for (const float Threshold : GeoCollection->DamageThreshold)
|
|
312
|
+
{
|
|
313
|
+
DamageThresholdsArr.Add(MakeShared<FJsonValueNumber>(static_cast<double>(Threshold)));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Build response data.
|
|
317
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
318
|
+
if (!ResolvedAssetPath.IsEmpty())
|
|
319
|
+
{
|
|
320
|
+
Data->SetStringField(TEXT("asset_path"), ResolvedAssetPath);
|
|
321
|
+
}
|
|
322
|
+
if (!ResolvedLabel.IsEmpty())
|
|
323
|
+
{
|
|
324
|
+
Data->SetStringField(TEXT("actor_label"), ResolvedLabel);
|
|
325
|
+
}
|
|
326
|
+
Data->SetNumberField(TEXT("bone_count"), static_cast<double>(BoneCount));
|
|
327
|
+
Data->SetArrayField(TEXT("levels"), LevelsArray);
|
|
328
|
+
Data->SetArrayField(TEXT("hierarchy"), HierarchyArray);
|
|
329
|
+
Data->SetArrayField(TEXT("damage_thresholds"), DamageThresholdsArr);
|
|
330
|
+
|
|
331
|
+
if (bTruncated)
|
|
332
|
+
{
|
|
333
|
+
Data->SetBoolField(TEXT("truncated"), true);
|
|
334
|
+
Data->SetStringField(TEXT("truncation_note"), TEXT("Hierarchy capped at 10000 entries (T-26-02)"));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
SendResponse(BuildChaosSuccessResponse(CorrId, Data) + TEXT("\n"));
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// -----------------------------------------------------------------------
|
|
341
|
+
// chaos.resetDestruction (CHAOS-02)
|
|
342
|
+
// Resets the fracture/destruction state of a geometry collection actor back
|
|
343
|
+
// to its unfractured initial configuration.
|
|
344
|
+
//
|
|
345
|
+
// Payload fields:
|
|
346
|
+
// actor_label string (required) -- label of the geometry collection actor
|
|
347
|
+
//
|
|
348
|
+
// Threat T-26-03: Modify() before state change, PostEditChange() after.
|
|
349
|
+
// Validates actor exists before any mutation.
|
|
350
|
+
// -----------------------------------------------------------------------
|
|
351
|
+
Router.RegisterHandler(TEXT("chaos.resetDestruction"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
|
|
352
|
+
{
|
|
353
|
+
const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
|
|
354
|
+
|
|
355
|
+
// Extract payload.
|
|
356
|
+
TSharedPtr<FJsonObject> Payload;
|
|
357
|
+
const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
|
|
358
|
+
if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
|
|
359
|
+
{
|
|
360
|
+
Payload = (*PayloadVal)->AsObject();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Require actor_label.
|
|
364
|
+
FString ActorLabel;
|
|
365
|
+
if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("actor_label"), ActorLabel) || ActorLabel.IsEmpty())
|
|
366
|
+
{
|
|
367
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("missing_actor_label")) + TEXT("\n"));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Get editor world.
|
|
372
|
+
if (!GEditor)
|
|
373
|
+
{
|
|
374
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("no_editor")) + TEXT("\n"));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
UWorld* World = GEditor->GetEditorWorldContext().World();
|
|
378
|
+
if (!World)
|
|
379
|
+
{
|
|
380
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("no_world_open")) + TEXT("\n"));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
AActor* FoundActor = FindActorByLabel(World, ActorLabel);
|
|
385
|
+
if (!FoundActor)
|
|
386
|
+
{
|
|
387
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("actor_not_found")) + TEXT("\n"));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
UGeometryCollectionComponent* GeoComp = FoundActor->FindComponentByClass<UGeometryCollectionComponent>();
|
|
392
|
+
if (!GeoComp)
|
|
393
|
+
{
|
|
394
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("no_geometry_collection_component")) + TEXT("\n"));
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Threat T-26-03: Modify() before mutation for undo/redo history.
|
|
399
|
+
GeoComp->Modify();
|
|
400
|
+
|
|
401
|
+
// Reset to initial unfractured state.
|
|
402
|
+
// UGeometryCollectionComponent::SetSimulatePhysics(false) + SetSimulatePhysics(true)
|
|
403
|
+
// is the reliable fallback approach that works across UE 5.x versions.
|
|
404
|
+
GeoComp->SetSimulatePhysics(false);
|
|
405
|
+
GeoComp->ResetDynamicCollection();
|
|
406
|
+
|
|
407
|
+
// Notify editor of the change.
|
|
408
|
+
GeoComp->PostEditChange();
|
|
409
|
+
|
|
410
|
+
// Get bone count for response.
|
|
411
|
+
int32 BoneCount = 0;
|
|
412
|
+
const UGeometryCollection* RestColl = GeoComp->GetRestCollection();
|
|
413
|
+
if (RestColl)
|
|
414
|
+
{
|
|
415
|
+
const TSharedPtr<FGeometryCollection, ESPMode::ThreadSafe> GeomColl = RestColl->GetGeometryCollection();
|
|
416
|
+
if (GeomColl.IsValid())
|
|
417
|
+
{
|
|
418
|
+
BoneCount = GeomColl->NumElements(FGeometryCollection::TransformGroup);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
423
|
+
Data->SetStringField(TEXT("actor_label"), ActorLabel);
|
|
424
|
+
Data->SetBoolField(TEXT("success"), true);
|
|
425
|
+
Data->SetNumberField(TEXT("bone_count"), static_cast<double>(BoneCount));
|
|
426
|
+
|
|
427
|
+
SendResponse(BuildChaosSuccessResponse(CorrId, Data) + TEXT("\n"));
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// -----------------------------------------------------------------------
|
|
431
|
+
// chaos.cloth (CHAOS-03)
|
|
432
|
+
// Reads cloth simulation parameters from a skeletal mesh actor's clothing assets.
|
|
433
|
+
//
|
|
434
|
+
// Payload fields:
|
|
435
|
+
// actor_label string (required) -- label of actor with USkeletalMeshComponent
|
|
436
|
+
//
|
|
437
|
+
// Returns cloth_assets array with simulation parameters per clothing asset.
|
|
438
|
+
// Gracefully handles missing ChaosCloth plugin or unavailable cloth data.
|
|
439
|
+
// -----------------------------------------------------------------------
|
|
440
|
+
Router.RegisterHandler(TEXT("chaos.cloth"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
|
|
441
|
+
{
|
|
442
|
+
const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
|
|
443
|
+
|
|
444
|
+
// Extract payload.
|
|
445
|
+
TSharedPtr<FJsonObject> Payload;
|
|
446
|
+
const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
|
|
447
|
+
if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
|
|
448
|
+
{
|
|
449
|
+
Payload = (*PayloadVal)->AsObject();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Require actor_label.
|
|
453
|
+
FString ActorLabel;
|
|
454
|
+
if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("actor_label"), ActorLabel) || ActorLabel.IsEmpty())
|
|
455
|
+
{
|
|
456
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("missing_actor_label")) + TEXT("\n"));
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Get editor world.
|
|
461
|
+
if (!GEditor)
|
|
462
|
+
{
|
|
463
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("no_editor")) + TEXT("\n"));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
UWorld* World = GEditor->GetEditorWorldContext().World();
|
|
467
|
+
if (!World)
|
|
468
|
+
{
|
|
469
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("no_world_open")) + TEXT("\n"));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
AActor* FoundActor = FindActorByLabel(World, ActorLabel);
|
|
474
|
+
if (!FoundActor)
|
|
475
|
+
{
|
|
476
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("actor_not_found")) + TEXT("\n"));
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Find skeletal mesh component with cloth assets.
|
|
481
|
+
USkeletalMeshComponent* SkelComp = FoundActor->FindComponentByClass<USkeletalMeshComponent>();
|
|
482
|
+
if (!SkelComp)
|
|
483
|
+
{
|
|
484
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("no_skeletal_mesh_component")) + TEXT("\n"));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Access clothing simulation interactor to check if Chaos cloth is available.
|
|
489
|
+
UClothingSimulationInteractor* ClothInteractor = SkelComp->GetClothingSimulationInteractor();
|
|
490
|
+
if (!ClothInteractor)
|
|
491
|
+
{
|
|
492
|
+
// Chaos cloth plugin may not be enabled or no cloth assets on this mesh.
|
|
493
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
494
|
+
Data->SetStringField(TEXT("actor_label"), ActorLabel);
|
|
495
|
+
Data->SetStringField(TEXT("error_code"), TEXT("chaos_cloth_not_available"));
|
|
496
|
+
Data->SetStringField(TEXT("message"), TEXT("No cloth simulation interactor found. Ensure the ChaosCloth plugin is enabled in project settings and the skeletal mesh has clothing assets assigned."));
|
|
497
|
+
Data->SetArrayField(TEXT("cloth_assets"), TArray<TSharedPtr<FJsonValue>>());
|
|
498
|
+
|
|
499
|
+
SendResponse(BuildChaosSuccessResponse(CorrId, Data) + TEXT("\n"));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Access the skeletal mesh's clothing assets.
|
|
504
|
+
USkeletalMesh* SkelMesh = SkelComp->GetSkeletalMeshAsset();
|
|
505
|
+
if (!SkelMesh)
|
|
506
|
+
{
|
|
507
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("no_skeletal_mesh_asset")) + TEXT("\n"));
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
TArray<TSharedPtr<FJsonValue>> ClothAssetsArray;
|
|
512
|
+
|
|
513
|
+
// Iterate clothing assets on the skeletal mesh.
|
|
514
|
+
for (UClothingAssetBase* ClothAssetBase : SkelMesh->GetMeshClothingAssets())
|
|
515
|
+
{
|
|
516
|
+
if (!ClothAssetBase)
|
|
517
|
+
{
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
TSharedPtr<FJsonObject> ClothObj = MakeShared<FJsonObject>();
|
|
522
|
+
ClothObj->SetStringField(TEXT("asset_name"), ClothAssetBase->GetName());
|
|
523
|
+
|
|
524
|
+
// UClothingAssetCommon is the standard concrete type -- attempt cast.
|
|
525
|
+
UClothingAssetCommon* ClothAsset = Cast<UClothingAssetCommon>(ClothAssetBase);
|
|
526
|
+
if (!ClothAsset || ClothAsset->ClothConfigs.Num() == 0)
|
|
527
|
+
{
|
|
528
|
+
// No config data available -- report zeros with a note.
|
|
529
|
+
ClothObj->SetNumberField(TEXT("self_collision_thickness"), 0.0);
|
|
530
|
+
ClothObj->SetNumberField(TEXT("friction"), 0.0);
|
|
531
|
+
ClothObj->SetNumberField(TEXT("damping"), 0.0);
|
|
532
|
+
ClothObj->SetNumberField(TEXT("gravity_scale"), 1.0);
|
|
533
|
+
ClothObj->SetNumberField(TEXT("wind_drag"), 0.0);
|
|
534
|
+
ClothObj->SetNumberField(TEXT("wind_lift"), 0.0);
|
|
535
|
+
ClothObj->SetNumberField(TEXT("bend_stiffness"), 0.0);
|
|
536
|
+
ClothObj->SetNumberField(TEXT("stretch_stiffness"), 0.0);
|
|
537
|
+
ClothObj->SetNumberField(TEXT("shear_stiffness"), 0.0);
|
|
538
|
+
ClothObj->SetStringField(TEXT("config_note"), TEXT("No cloth config data available for this asset"));
|
|
539
|
+
ClothAssetsArray.Add(MakeShared<FJsonValueObject>(ClothObj));
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Extract parameters from the first available cloth config.
|
|
544
|
+
// In UE5, UClothConfigBase subclasses provide simulation parameters.
|
|
545
|
+
// UChaosClothConfig is the Chaos-specific config type.
|
|
546
|
+
bool bFoundConfig = false;
|
|
547
|
+
for (auto& ConfigPair : ClothAsset->ClothConfigs)
|
|
548
|
+
{
|
|
549
|
+
UClothConfigBase* ConfigBase = ConfigPair.Value;
|
|
550
|
+
if (!ConfigBase)
|
|
551
|
+
{
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Try to extract common parameters via reflection / known config structure.
|
|
556
|
+
// For Chaos cloth (UChaosClothConfig), parameters are available as properties.
|
|
557
|
+
// We use reflection to read float properties by name where available.
|
|
558
|
+
auto GetFloatProp = [&](const FString& PropName, float& OutVal) -> bool
|
|
559
|
+
{
|
|
560
|
+
const FProperty* Prop = ConfigBase->GetClass()->FindPropertyByName(*PropName);
|
|
561
|
+
if (!Prop)
|
|
562
|
+
{
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
const FFloatProperty* FloatProp = CastField<FFloatProperty>(Prop);
|
|
566
|
+
if (!FloatProp)
|
|
567
|
+
{
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
OutVal = FloatProp->GetPropertyValue_InContainer(ConfigBase);
|
|
571
|
+
return true;
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
float SelfCollisionThickness = 0.f;
|
|
575
|
+
float Friction = 0.f;
|
|
576
|
+
float Damping = 0.f;
|
|
577
|
+
float GravityScale = 1.f;
|
|
578
|
+
float WindDrag = 0.f;
|
|
579
|
+
float WindLift = 0.f;
|
|
580
|
+
float BendStiffness = 0.f;
|
|
581
|
+
float StretchStiffness = 0.f;
|
|
582
|
+
float ShearStiffness = 0.f;
|
|
583
|
+
|
|
584
|
+
GetFloatProp(TEXT("SelfCollisionThickness"), SelfCollisionThickness);
|
|
585
|
+
GetFloatProp(TEXT("FrictionCoefficient"), Friction);
|
|
586
|
+
GetFloatProp(TEXT("DampingCoefficient"), Damping);
|
|
587
|
+
GetFloatProp(TEXT("GravityScale"), GravityScale);
|
|
588
|
+
GetFloatProp(TEXT("WindDragCoefficient"), WindDrag);
|
|
589
|
+
GetFloatProp(TEXT("WindLiftCoefficient"), WindLift);
|
|
590
|
+
// Bend/stretch/shear stiffness vary by config type name in Chaos.
|
|
591
|
+
GetFloatProp(TEXT("BendStiffnessWeighted"), BendStiffness);
|
|
592
|
+
if (BendStiffness == 0.f)
|
|
593
|
+
{
|
|
594
|
+
GetFloatProp(TEXT("BendStiffness"), BendStiffness);
|
|
595
|
+
}
|
|
596
|
+
GetFloatProp(TEXT("StretchStiffness"), StretchStiffness);
|
|
597
|
+
GetFloatProp(TEXT("ShearStiffness"), ShearStiffness);
|
|
598
|
+
|
|
599
|
+
ClothObj->SetNumberField(TEXT("self_collision_thickness"), static_cast<double>(SelfCollisionThickness));
|
|
600
|
+
ClothObj->SetNumberField(TEXT("friction"), static_cast<double>(Friction));
|
|
601
|
+
ClothObj->SetNumberField(TEXT("damping"), static_cast<double>(Damping));
|
|
602
|
+
ClothObj->SetNumberField(TEXT("gravity_scale"), static_cast<double>(GravityScale));
|
|
603
|
+
ClothObj->SetNumberField(TEXT("wind_drag"), static_cast<double>(WindDrag));
|
|
604
|
+
ClothObj->SetNumberField(TEXT("wind_lift"), static_cast<double>(WindLift));
|
|
605
|
+
ClothObj->SetNumberField(TEXT("bend_stiffness"), static_cast<double>(BendStiffness));
|
|
606
|
+
ClothObj->SetNumberField(TEXT("stretch_stiffness"), static_cast<double>(StretchStiffness));
|
|
607
|
+
ClothObj->SetNumberField(TEXT("shear_stiffness"), static_cast<double>(ShearStiffness));
|
|
608
|
+
ClothObj->SetStringField(TEXT("config_class"), ConfigBase->GetClass()->GetName());
|
|
609
|
+
bFoundConfig = true;
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (!bFoundConfig)
|
|
614
|
+
{
|
|
615
|
+
ClothObj->SetStringField(TEXT("config_note"), TEXT("Config entries found but none readable"));
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
ClothAssetsArray.Add(MakeShared<FJsonValueObject>(ClothObj));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
622
|
+
Data->SetStringField(TEXT("actor_label"), ActorLabel);
|
|
623
|
+
Data->SetArrayField(TEXT("cloth_assets"), ClothAssetsArray);
|
|
624
|
+
Data->SetNumberField(TEXT("asset_count"), static_cast<double>(ClothAssetsArray.Num()));
|
|
625
|
+
|
|
626
|
+
SendResponse(BuildChaosSuccessResponse(CorrId, Data) + TEXT("\n"));
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
// -----------------------------------------------------------------------
|
|
630
|
+
// chaos.physicsCache (CHAOS-04)
|
|
631
|
+
// Manages Chaos physics cache recording: start, stop, or query status.
|
|
632
|
+
//
|
|
633
|
+
// Payload fields:
|
|
634
|
+
// action string (required) -- "start", "stop", or "query"
|
|
635
|
+
// actor_label string (optional) -- for actor-specific cache operations
|
|
636
|
+
//
|
|
637
|
+
// Threat T-26-04: action validated against explicit allowlist (start/stop/query only).
|
|
638
|
+
// Graceful fallback: returns "feature_not_available" if Chaos cache API is unavailable.
|
|
639
|
+
// -----------------------------------------------------------------------
|
|
640
|
+
Router.RegisterHandler(TEXT("chaos.physicsCache"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
|
|
641
|
+
{
|
|
642
|
+
const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
|
|
643
|
+
|
|
644
|
+
// Extract payload.
|
|
645
|
+
TSharedPtr<FJsonObject> Payload;
|
|
646
|
+
const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
|
|
647
|
+
if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
|
|
648
|
+
{
|
|
649
|
+
Payload = (*PayloadVal)->AsObject();
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Require action.
|
|
653
|
+
FString Action;
|
|
654
|
+
if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("action"), Action) || Action.IsEmpty())
|
|
655
|
+
{
|
|
656
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("missing_action")) + TEXT("\n"));
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Threat T-26-04: explicit allowlist for action strings.
|
|
661
|
+
const bool bIsStart = (Action == TEXT("start"));
|
|
662
|
+
const bool bIsStop = (Action == TEXT("stop"));
|
|
663
|
+
const bool bIsQuery = (Action == TEXT("query"));
|
|
664
|
+
|
|
665
|
+
if (!bIsStart && !bIsStop && !bIsQuery)
|
|
666
|
+
{
|
|
667
|
+
SendResponse(BuildChaosErrorResponse(CorrId,
|
|
668
|
+
TEXT("invalid_action: must be start, stop, or query")) + TEXT("\n"));
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Optional actor_label.
|
|
673
|
+
FString ActorLabel;
|
|
674
|
+
Payload->TryGetStringField(TEXT("actor_label"), ActorLabel);
|
|
675
|
+
|
|
676
|
+
// Access the Chaos physics scene via GEditor world.
|
|
677
|
+
if (!GEditor)
|
|
678
|
+
{
|
|
679
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("no_editor")) + TEXT("\n"));
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
UWorld* World = GEditor->GetEditorWorldContext().World();
|
|
683
|
+
if (!World)
|
|
684
|
+
{
|
|
685
|
+
SendResponse(BuildChaosErrorResponse(CorrId, TEXT("no_world_open")) + TEXT("\n"));
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Access the physics scene.
|
|
690
|
+
FPhysScene* PhysScene = World->GetPhysicsScene();
|
|
691
|
+
if (!PhysScene)
|
|
692
|
+
{
|
|
693
|
+
// Return informative error -- physics scene unavailable (e.g., editor config).
|
|
694
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
695
|
+
Data->SetStringField(TEXT("action"), Action);
|
|
696
|
+
Data->SetStringField(TEXT("status"), TEXT("not_available"));
|
|
697
|
+
Data->SetStringField(TEXT("message"), TEXT("Physics scene unavailable. The Chaos physics cache API requires an active simulation scene."));
|
|
698
|
+
Data->SetNumberField(TEXT("frame_count"), 0.0);
|
|
699
|
+
TSharedPtr<FJsonObject> TimeRange = MakeShared<FJsonObject>();
|
|
700
|
+
TimeRange->SetNumberField(TEXT("start"), 0.0);
|
|
701
|
+
TimeRange->SetNumberField(TEXT("end"), 0.0);
|
|
702
|
+
Data->SetObjectField(TEXT("time_range"), TimeRange);
|
|
703
|
+
if (!ActorLabel.IsEmpty())
|
|
704
|
+
{
|
|
705
|
+
Data->SetStringField(TEXT("actor_label"), ActorLabel);
|
|
706
|
+
}
|
|
707
|
+
SendResponse(BuildChaosSuccessResponse(CorrId, Data) + TEXT("\n"));
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// The Chaos physics cache recording API via FChaosSolversModule is optional
|
|
712
|
+
// and varies between UE configurations (ChaosSolvers may be experimental/unavailable).
|
|
713
|
+
// We implement the handler using FPhysScene_Chaos to access solver state where possible.
|
|
714
|
+
// For safety across all UE 5.7 configurations, we report cache status via available APIs
|
|
715
|
+
// and return a graceful "feature not available" response when cache-specific APIs are absent.
|
|
716
|
+
//
|
|
717
|
+
// Note: Direct physics cache recording (FPhysicsCache) in the editor is primarily exposed
|
|
718
|
+
// via PIE/simulation mode. In editor-at-rest, recording is not active.
|
|
719
|
+
// This handler reports the availability and provides action stubs for completeness.
|
|
720
|
+
|
|
721
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
722
|
+
Data->SetStringField(TEXT("action"), Action);
|
|
723
|
+
|
|
724
|
+
// In UE 5.7, physics cache management in-editor via C++ API is limited --
|
|
725
|
+
// cache recording is driven by UPhysicsSettings and activated during PIE.
|
|
726
|
+
// We report the current known state and log the requested action.
|
|
727
|
+
|
|
728
|
+
if (bIsStart)
|
|
729
|
+
{
|
|
730
|
+
// Request to start recording -- log and report status.
|
|
731
|
+
UE_LOG(LogTemp, Warning, TEXT("[MCPBridge] chaos.physicsCache start requested. Physics cache recording in editor requires PIE mode and UPhysicsSettings.bSupportImmediatePhysics configuration."));
|
|
732
|
+
|
|
733
|
+
Data->SetStringField(TEXT("status"), TEXT("start_requested"));
|
|
734
|
+
Data->SetStringField(TEXT("message"), TEXT("Physics cache recording start requested. Note: Cache recording is active during PIE simulation. In editor mode, start PIE to activate recording via UPhysicsSettings."));
|
|
735
|
+
Data->SetNumberField(TEXT("frame_count"), 0.0);
|
|
736
|
+
}
|
|
737
|
+
else if (bIsStop)
|
|
738
|
+
{
|
|
739
|
+
UE_LOG(LogTemp, Warning, TEXT("[MCPBridge] chaos.physicsCache stop requested."));
|
|
740
|
+
|
|
741
|
+
Data->SetStringField(TEXT("status"), TEXT("stop_requested"));
|
|
742
|
+
Data->SetStringField(TEXT("message"), TEXT("Physics cache recording stop requested. Ensure PIE simulation is running with cache recording enabled."));
|
|
743
|
+
Data->SetNumberField(TEXT("frame_count"), 0.0);
|
|
744
|
+
}
|
|
745
|
+
else // query
|
|
746
|
+
{
|
|
747
|
+
// Query current cache status.
|
|
748
|
+
// Without direct FPhysicsCache API access, we report a best-effort status.
|
|
749
|
+
const bool bPIEActive = (World->WorldType == EWorldType::PIE);
|
|
750
|
+
|
|
751
|
+
Data->SetStringField(TEXT("status"), bPIEActive ? TEXT("recording_possible") : TEXT("stopped"));
|
|
752
|
+
Data->SetStringField(TEXT("message"), bPIEActive
|
|
753
|
+
? TEXT("PIE is active. Physics cache recording may be enabled via UPhysicsSettings.")
|
|
754
|
+
: TEXT("No PIE session active. Physics cache recording is inactive."));
|
|
755
|
+
Data->SetNumberField(TEXT("frame_count"), 0.0);
|
|
756
|
+
Data->SetBoolField(TEXT("is_recording"), false);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
TSharedPtr<FJsonObject> TimeRange = MakeShared<FJsonObject>();
|
|
760
|
+
TimeRange->SetNumberField(TEXT("start"), 0.0);
|
|
761
|
+
TimeRange->SetNumberField(TEXT("end"), 0.0);
|
|
762
|
+
Data->SetObjectField(TEXT("time_range"), TimeRange);
|
|
763
|
+
|
|
764
|
+
if (!ActorLabel.IsEmpty())
|
|
765
|
+
{
|
|
766
|
+
Data->SetStringField(TEXT("actor_label"), ActorLabel);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
SendResponse(BuildChaosSuccessResponse(CorrId, Data) + TEXT("\n"));
|
|
770
|
+
});
|
|
771
|
+
}
|