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,504 @@
|
|
|
1
|
+
// MCPLiveLinkCommands.cpp (Plan 27-01)
|
|
2
|
+
// Implements four Live Link inspection and control command handlers for the MCP bridge:
|
|
3
|
+
// livelink.sources -- list all active Live Link sources with type, machine name, status (LL-01)
|
|
4
|
+
// livelink.subjects -- list all Live Link subjects with roles and enabled state (LL-02)
|
|
5
|
+
// livelink.control -- pause/resume individual Live Link subjects (LL-03)
|
|
6
|
+
// livelink.preview -- inspect current frame data for any Live Link subject (LL-04)
|
|
7
|
+
//
|
|
8
|
+
// All handlers run on the game thread via FMCPCommandRouter::Dispatch.
|
|
9
|
+
// subject_name in livelink.control and livelink.preview is validated by exact match against
|
|
10
|
+
// GetSubjects() results -- never passed as a raw string to any API (T-27-01).
|
|
11
|
+
// livelink.preview animation bone output is capped at 10 bones with total_bones count (T-27-02).
|
|
12
|
+
// All handlers check IModularFeatures availability before accessing ILiveLinkClient for
|
|
13
|
+
// graceful degradation when Live Link plugin is not enabled.
|
|
14
|
+
|
|
15
|
+
#include "MCPLiveLinkCommands.h"
|
|
16
|
+
|
|
17
|
+
// Live Link headers
|
|
18
|
+
#include "ILiveLinkClient.h"
|
|
19
|
+
#include "LiveLinkTypes.h"
|
|
20
|
+
#include "LiveLinkSubjectSettings.h"
|
|
21
|
+
#include "Roles/LiveLinkAnimationRole.h"
|
|
22
|
+
#include "Roles/LiveLinkAnimationTypes.h"
|
|
23
|
+
#include "Roles/LiveLinkCameraRole.h"
|
|
24
|
+
#include "Roles/LiveLinkCameraTypes.h"
|
|
25
|
+
#include "Roles/LiveLinkTransformRole.h"
|
|
26
|
+
#include "Roles/LiveLinkTransformTypes.h"
|
|
27
|
+
|
|
28
|
+
// Modular features (ILiveLinkClient access)
|
|
29
|
+
#include "Features/IModularFeatures.h"
|
|
30
|
+
|
|
31
|
+
// JSON
|
|
32
|
+
#include "Serialization/JsonSerializer.h"
|
|
33
|
+
#include "Serialization/JsonWriter.h"
|
|
34
|
+
#include "Dom/JsonObject.h"
|
|
35
|
+
#include "Dom/JsonValue.h"
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Internal helpers
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/** Returns a JSON success response string. */
|
|
42
|
+
static FString BuildLiveLinkSuccessResponse(const FString& CorrId, TSharedPtr<FJsonObject> Data)
|
|
43
|
+
{
|
|
44
|
+
TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
|
|
45
|
+
Obj->SetBoolField(TEXT("success"), true);
|
|
46
|
+
Obj->SetStringField(TEXT("correlationId"), CorrId);
|
|
47
|
+
if (Data.IsValid())
|
|
48
|
+
{
|
|
49
|
+
Obj->SetObjectField(TEXT("data"), Data);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
FString Output;
|
|
53
|
+
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
|
|
54
|
+
FJsonSerializer::Serialize(Obj.ToSharedRef(), Writer);
|
|
55
|
+
return Output;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Returns a JSON error response string. */
|
|
59
|
+
static FString BuildLiveLinkErrorResponse(const FString& CorrId, const FString& Error)
|
|
60
|
+
{
|
|
61
|
+
TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
|
|
62
|
+
Obj->SetBoolField(TEXT("success"), false);
|
|
63
|
+
Obj->SetStringField(TEXT("correlationId"), CorrId);
|
|
64
|
+
Obj->SetStringField(TEXT("error"), Error);
|
|
65
|
+
|
|
66
|
+
FString Output;
|
|
67
|
+
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
|
|
68
|
+
FJsonSerializer::Serialize(Obj.ToSharedRef(), Writer);
|
|
69
|
+
return Output;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Returns a user-friendly role name from a Live Link role class. */
|
|
73
|
+
static FString GetRoleFriendlyName(TSubclassOf<ULiveLinkRole> RoleClass)
|
|
74
|
+
{
|
|
75
|
+
if (!RoleClass)
|
|
76
|
+
{
|
|
77
|
+
return TEXT("Unknown");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const FString ClassName = RoleClass->GetName();
|
|
81
|
+
|
|
82
|
+
if (ClassName == TEXT("LiveLinkAnimationRole"))
|
|
83
|
+
{
|
|
84
|
+
return TEXT("Animation");
|
|
85
|
+
}
|
|
86
|
+
if (ClassName == TEXT("LiveLinkTransformRole"))
|
|
87
|
+
{
|
|
88
|
+
return TEXT("Transform");
|
|
89
|
+
}
|
|
90
|
+
if (ClassName == TEXT("LiveLinkCameraRole"))
|
|
91
|
+
{
|
|
92
|
+
return TEXT("Camera");
|
|
93
|
+
}
|
|
94
|
+
if (ClassName == TEXT("LiveLinkLightRole"))
|
|
95
|
+
{
|
|
96
|
+
return TEXT("Light");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return ClassName;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Serialize a FVector as a JSON object with x, y, z fields. */
|
|
103
|
+
static TSharedPtr<FJsonObject> VectorToJson(const FVector& V)
|
|
104
|
+
{
|
|
105
|
+
TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
|
|
106
|
+
Obj->SetNumberField(TEXT("x"), V.X);
|
|
107
|
+
Obj->SetNumberField(TEXT("y"), V.Y);
|
|
108
|
+
Obj->SetNumberField(TEXT("z"), V.Z);
|
|
109
|
+
return Obj;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Serialize a FRotator as a JSON object with roll, pitch, yaw fields. */
|
|
113
|
+
static TSharedPtr<FJsonObject> RotatorToJson(const FRotator& R)
|
|
114
|
+
{
|
|
115
|
+
TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
|
|
116
|
+
Obj->SetNumberField(TEXT("roll"), R.Roll);
|
|
117
|
+
Obj->SetNumberField(TEXT("pitch"), R.Pitch);
|
|
118
|
+
Obj->SetNumberField(TEXT("yaw"), R.Yaw);
|
|
119
|
+
return Obj;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Serialize a FTransform location/rotation/scale into a JSON object. */
|
|
123
|
+
static TSharedPtr<FJsonObject> TransformToJson(const FTransform& T)
|
|
124
|
+
{
|
|
125
|
+
TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
|
|
126
|
+
Obj->SetObjectField(TEXT("location"), VectorToJson(T.GetLocation()));
|
|
127
|
+
Obj->SetObjectField(TEXT("rotation"), RotatorToJson(T.GetRotation().Rotator()));
|
|
128
|
+
Obj->SetObjectField(TEXT("scale"), VectorToJson(T.GetScale3D()));
|
|
129
|
+
return Obj;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Registration
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
void RegisterLiveLinkCommands(FMCPCommandRouter& Router)
|
|
137
|
+
{
|
|
138
|
+
// -------------------------------------------------------------------------
|
|
139
|
+
// livelink.sources (LL-01)
|
|
140
|
+
// List all active Live Link sources with connection status, type, and machine name.
|
|
141
|
+
// Payload: none (no required fields)
|
|
142
|
+
// -------------------------------------------------------------------------
|
|
143
|
+
Router.RegisterHandler(TEXT("livelink.sources"),
|
|
144
|
+
[](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
|
|
145
|
+
{
|
|
146
|
+
const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
|
|
147
|
+
|
|
148
|
+
// Graceful degradation: check if Live Link plugin is enabled.
|
|
149
|
+
if (!IModularFeatures::Get().IsModularFeatureAvailable(ILiveLinkClient::ModularFeatureName))
|
|
150
|
+
{
|
|
151
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
152
|
+
Data->SetArrayField(TEXT("sources"), TArray<TSharedPtr<FJsonValue>>());
|
|
153
|
+
Data->SetNumberField(TEXT("count"), 0);
|
|
154
|
+
Data->SetStringField(TEXT("message"),
|
|
155
|
+
TEXT("Live Link plugin is not enabled. Enable it in Plugins > Animation > Live Link to use this command."));
|
|
156
|
+
SendResponse(BuildLiveLinkSuccessResponse(CorrId, Data));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
ILiveLinkClient& Client = IModularFeatures::Get().GetModularFeature<ILiveLinkClient>(ILiveLinkClient::ModularFeatureName);
|
|
161
|
+
|
|
162
|
+
TArray<FGuid> SourceGuids = Client.GetSources();
|
|
163
|
+
|
|
164
|
+
TArray<TSharedPtr<FJsonValue>> SourceArray;
|
|
165
|
+
SourceArray.Reserve(SourceGuids.Num());
|
|
166
|
+
|
|
167
|
+
for (const FGuid& SourceGuid : SourceGuids)
|
|
168
|
+
{
|
|
169
|
+
TSharedPtr<FJsonObject> SourceObj = MakeShared<FJsonObject>();
|
|
170
|
+
SourceObj->SetStringField(TEXT("source_id"), SourceGuid.ToString());
|
|
171
|
+
SourceObj->SetStringField(TEXT("source_type"), Client.GetSourceType(SourceGuid).ToString());
|
|
172
|
+
SourceObj->SetStringField(TEXT("machine_name"), Client.GetSourceMachineName(SourceGuid).ToString());
|
|
173
|
+
SourceObj->SetStringField(TEXT("status"), Client.GetSourceStatus(SourceGuid).ToString());
|
|
174
|
+
SourceArray.Add(MakeShared<FJsonValueObject>(SourceObj));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
178
|
+
Data->SetArrayField(TEXT("sources"), SourceArray);
|
|
179
|
+
Data->SetNumberField(TEXT("count"), SourceGuids.Num());
|
|
180
|
+
|
|
181
|
+
SendResponse(BuildLiveLinkSuccessResponse(CorrId, Data));
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// -------------------------------------------------------------------------
|
|
185
|
+
// livelink.subjects (LL-02)
|
|
186
|
+
// List all Live Link subjects with their roles and enabled state.
|
|
187
|
+
// Payload: none (no required fields)
|
|
188
|
+
// -------------------------------------------------------------------------
|
|
189
|
+
Router.RegisterHandler(TEXT("livelink.subjects"),
|
|
190
|
+
[](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
|
|
191
|
+
{
|
|
192
|
+
const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
|
|
193
|
+
|
|
194
|
+
// Graceful degradation: check if Live Link plugin is enabled.
|
|
195
|
+
if (!IModularFeatures::Get().IsModularFeatureAvailable(ILiveLinkClient::ModularFeatureName))
|
|
196
|
+
{
|
|
197
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
198
|
+
Data->SetArrayField(TEXT("subjects"), TArray<TSharedPtr<FJsonValue>>());
|
|
199
|
+
Data->SetNumberField(TEXT("count"), 0);
|
|
200
|
+
Data->SetStringField(TEXT("message"),
|
|
201
|
+
TEXT("Live Link plugin is not enabled. Enable it in Plugins > Animation > Live Link to use this command."));
|
|
202
|
+
SendResponse(BuildLiveLinkSuccessResponse(CorrId, Data));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
ILiveLinkClient& Client = IModularFeatures::Get().GetModularFeature<ILiveLinkClient>(ILiveLinkClient::ModularFeatureName);
|
|
207
|
+
|
|
208
|
+
// Include disabled subjects so the full list is visible.
|
|
209
|
+
TArray<FLiveLinkSubjectKey> SubjectKeys = Client.GetSubjects(/*bIncludeDisabledSubjects=*/true);
|
|
210
|
+
|
|
211
|
+
TArray<TSharedPtr<FJsonValue>> SubjectArray;
|
|
212
|
+
SubjectArray.Reserve(SubjectKeys.Num());
|
|
213
|
+
|
|
214
|
+
for (const FLiveLinkSubjectKey& SubjectKey : SubjectKeys)
|
|
215
|
+
{
|
|
216
|
+
const FString SubjectName = SubjectKey.SubjectName.Name.ToString();
|
|
217
|
+
const FString SourceId = SubjectKey.Source.ToString();
|
|
218
|
+
|
|
219
|
+
FString RoleName = TEXT("Unknown");
|
|
220
|
+
bool bEnabled = true;
|
|
221
|
+
|
|
222
|
+
ULiveLinkSubjectSettings* Settings = Cast<ULiveLinkSubjectSettings>(Client.GetSubjectSettings(SubjectKey));
|
|
223
|
+
if (Settings)
|
|
224
|
+
{
|
|
225
|
+
RoleName = GetRoleFriendlyName(Settings->Role);
|
|
226
|
+
bEnabled = Settings->bEnabled;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
TSharedPtr<FJsonObject> SubjectObj = MakeShared<FJsonObject>();
|
|
230
|
+
SubjectObj->SetStringField(TEXT("subject_name"), SubjectName);
|
|
231
|
+
SubjectObj->SetStringField(TEXT("source_id"), SourceId);
|
|
232
|
+
SubjectObj->SetStringField(TEXT("role"), RoleName);
|
|
233
|
+
SubjectObj->SetBoolField (TEXT("enabled"), bEnabled);
|
|
234
|
+
SubjectArray.Add(MakeShared<FJsonValueObject>(SubjectObj));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
238
|
+
Data->SetArrayField(TEXT("subjects"), SubjectArray);
|
|
239
|
+
Data->SetNumberField(TEXT("count"), SubjectKeys.Num());
|
|
240
|
+
|
|
241
|
+
SendResponse(BuildLiveLinkSuccessResponse(CorrId, Data));
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// -------------------------------------------------------------------------
|
|
245
|
+
// livelink.control (LL-03)
|
|
246
|
+
// Pause/resume an individual Live Link subject by toggling its enabled state.
|
|
247
|
+
// Payload: subject_name (string, required), enabled (bool, required)
|
|
248
|
+
// T-27-01: subject_name is validated against GetSubjects() result before use.
|
|
249
|
+
// -------------------------------------------------------------------------
|
|
250
|
+
Router.RegisterHandler(TEXT("livelink.control"),
|
|
251
|
+
[](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
|
|
252
|
+
{
|
|
253
|
+
const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
|
|
254
|
+
|
|
255
|
+
// Validate required fields.
|
|
256
|
+
FString SubjectName;
|
|
257
|
+
if (!Cmd->TryGetStringField(TEXT("subject_name"), SubjectName) || SubjectName.IsEmpty())
|
|
258
|
+
{
|
|
259
|
+
SendResponse(BuildLiveLinkErrorResponse(CorrId, TEXT("Missing required field: subject_name")));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
bool bEnabled = true;
|
|
264
|
+
if (!Cmd->TryGetBoolField(TEXT("enabled"), bEnabled))
|
|
265
|
+
{
|
|
266
|
+
SendResponse(BuildLiveLinkErrorResponse(CorrId, TEXT("Missing required field: enabled")));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Graceful degradation: check if Live Link plugin is enabled.
|
|
271
|
+
if (!IModularFeatures::Get().IsModularFeatureAvailable(ILiveLinkClient::ModularFeatureName))
|
|
272
|
+
{
|
|
273
|
+
SendResponse(BuildLiveLinkErrorResponse(CorrId,
|
|
274
|
+
TEXT("Live Link plugin is not enabled. Enable it in Plugins > Animation > Live Link to use this command.")));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
ILiveLinkClient& Client = IModularFeatures::Get().GetModularFeature<ILiveLinkClient>(ILiveLinkClient::ModularFeatureName);
|
|
279
|
+
|
|
280
|
+
// T-27-01: Find subject by exact name match from GetSubjects() -- never use raw input as key.
|
|
281
|
+
TArray<FLiveLinkSubjectKey> SubjectKeys = Client.GetSubjects(/*bIncludeDisabledSubjects=*/true);
|
|
282
|
+
|
|
283
|
+
bool bFound = false;
|
|
284
|
+
for (const FLiveLinkSubjectKey& SubjectKey : SubjectKeys)
|
|
285
|
+
{
|
|
286
|
+
if (SubjectKey.SubjectName.Name.ToString() == SubjectName)
|
|
287
|
+
{
|
|
288
|
+
ULiveLinkSubjectSettings* Settings = Cast<ULiveLinkSubjectSettings>(Client.GetSubjectSettings(SubjectKey));
|
|
289
|
+
if (!Settings)
|
|
290
|
+
{
|
|
291
|
+
SendResponse(BuildLiveLinkErrorResponse(CorrId,
|
|
292
|
+
FString::Printf(TEXT("Could not access settings for subject: %s"), *SubjectName)));
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Toggle the enabled state (runtime-only; no Modify() needed for Live Link subject state).
|
|
297
|
+
Settings->bEnabled = bEnabled;
|
|
298
|
+
|
|
299
|
+
const FString StateMessage = bEnabled
|
|
300
|
+
? TEXT("Subject resumed")
|
|
301
|
+
: TEXT("Subject paused");
|
|
302
|
+
|
|
303
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
304
|
+
Data->SetStringField(TEXT("subject_name"), SubjectName);
|
|
305
|
+
Data->SetBoolField (TEXT("enabled"), bEnabled);
|
|
306
|
+
Data->SetStringField(TEXT("message"), StateMessage);
|
|
307
|
+
|
|
308
|
+
SendResponse(BuildLiveLinkSuccessResponse(CorrId, Data));
|
|
309
|
+
bFound = true;
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!bFound)
|
|
315
|
+
{
|
|
316
|
+
SendResponse(BuildLiveLinkErrorResponse(CorrId,
|
|
317
|
+
FString::Printf(TEXT("Subject not found: %s"), *SubjectName)));
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// -------------------------------------------------------------------------
|
|
322
|
+
// livelink.preview (LL-04)
|
|
323
|
+
// Inspect the current frame data for any Live Link subject.
|
|
324
|
+
// Payload: subject_name (string, required)
|
|
325
|
+
// T-27-01: subject_name validated against GetSubjects() result.
|
|
326
|
+
// T-27-02: Animation bone output capped at 10 bones; total_bones count always included.
|
|
327
|
+
// -------------------------------------------------------------------------
|
|
328
|
+
Router.RegisterHandler(TEXT("livelink.preview"),
|
|
329
|
+
[](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
|
|
330
|
+
{
|
|
331
|
+
const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
|
|
332
|
+
|
|
333
|
+
// Validate required field.
|
|
334
|
+
FString SubjectName;
|
|
335
|
+
if (!Cmd->TryGetStringField(TEXT("subject_name"), SubjectName) || SubjectName.IsEmpty())
|
|
336
|
+
{
|
|
337
|
+
SendResponse(BuildLiveLinkErrorResponse(CorrId, TEXT("Missing required field: subject_name")));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Graceful degradation: check if Live Link plugin is enabled.
|
|
342
|
+
if (!IModularFeatures::Get().IsModularFeatureAvailable(ILiveLinkClient::ModularFeatureName))
|
|
343
|
+
{
|
|
344
|
+
SendResponse(BuildLiveLinkErrorResponse(CorrId,
|
|
345
|
+
TEXT("Live Link plugin is not enabled. Enable it in Plugins > Animation > Live Link to use this command.")));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
ILiveLinkClient& Client = IModularFeatures::Get().GetModularFeature<ILiveLinkClient>(ILiveLinkClient::ModularFeatureName);
|
|
350
|
+
|
|
351
|
+
// T-27-01: Find subject by exact name match from GetSubjects() -- never use raw input as key.
|
|
352
|
+
TArray<FLiveLinkSubjectKey> SubjectKeys = Client.GetSubjects(/*bIncludeDisabledSubjects=*/true);
|
|
353
|
+
|
|
354
|
+
bool bFound = false;
|
|
355
|
+
for (const FLiveLinkSubjectKey& SubjectKey : SubjectKeys)
|
|
356
|
+
{
|
|
357
|
+
if (SubjectKey.SubjectName.Name.ToString() != SubjectName)
|
|
358
|
+
{
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
bFound = true;
|
|
363
|
+
|
|
364
|
+
// Determine role from settings.
|
|
365
|
+
FString RoleName = TEXT("Unknown");
|
|
366
|
+
TSubclassOf<ULiveLinkRole> RoleClass = nullptr;
|
|
367
|
+
|
|
368
|
+
ULiveLinkSubjectSettings* Settings = Cast<ULiveLinkSubjectSettings>(Client.GetSubjectSettings(SubjectKey));
|
|
369
|
+
if (Settings)
|
|
370
|
+
{
|
|
371
|
+
RoleClass = Settings->Role;
|
|
372
|
+
RoleName = GetRoleFriendlyName(RoleClass);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Retrieve the latest frame data.
|
|
376
|
+
FLiveLinkSubjectFrameData FrameData;
|
|
377
|
+
const bool bHasData = Client.EvaluateFrame_AnyThread(SubjectKey, RoleClass, FrameData);
|
|
378
|
+
|
|
379
|
+
if (!bHasData || !FrameData.StaticData.IsValid() || !FrameData.FrameData.IsValid())
|
|
380
|
+
{
|
|
381
|
+
TSharedPtr<FJsonObject> NoData = MakeShared<FJsonObject>();
|
|
382
|
+
NoData->SetBoolField (TEXT("available"), false);
|
|
383
|
+
NoData->SetStringField(TEXT("message"),
|
|
384
|
+
TEXT("No frame data available for subject. Source may be disconnected or not streaming."));
|
|
385
|
+
|
|
386
|
+
TSharedPtr<FJsonObject> ResponseData = MakeShared<FJsonObject>();
|
|
387
|
+
ResponseData->SetStringField(TEXT("subject_name"), SubjectName);
|
|
388
|
+
ResponseData->SetStringField(TEXT("role"), RoleName);
|
|
389
|
+
ResponseData->SetObjectField(TEXT("frame_data"), NoData);
|
|
390
|
+
|
|
391
|
+
SendResponse(BuildLiveLinkSuccessResponse(CorrId, ResponseData));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Build role-specific frame_data object.
|
|
396
|
+
TSharedPtr<FJsonObject> FrameDataObj = MakeShared<FJsonObject>();
|
|
397
|
+
FrameDataObj->SetBoolField(TEXT("available"), true);
|
|
398
|
+
|
|
399
|
+
if (RoleName == TEXT("Transform"))
|
|
400
|
+
{
|
|
401
|
+
// Extract transform from FLiveLinkTransformFrameData.
|
|
402
|
+
FLiveLinkTransformFrameData* TransformFrame =
|
|
403
|
+
FrameData.FrameData.Cast<FLiveLinkTransformFrameData>();
|
|
404
|
+
if (TransformFrame)
|
|
405
|
+
{
|
|
406
|
+
FrameDataObj->SetObjectField(TEXT("transform"), TransformToJson(TransformFrame->Transform));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
else if (RoleName == TEXT("Camera"))
|
|
410
|
+
{
|
|
411
|
+
// Extract camera properties from FLiveLinkCameraFrameData and static data.
|
|
412
|
+
FLiveLinkCameraFrameData* CameraFrame =
|
|
413
|
+
FrameData.FrameData.Cast<FLiveLinkCameraFrameData>();
|
|
414
|
+
FLiveLinkCameraStaticData* CameraStatic =
|
|
415
|
+
FrameData.StaticData.Cast<FLiveLinkCameraStaticData>();
|
|
416
|
+
|
|
417
|
+
if (CameraFrame)
|
|
418
|
+
{
|
|
419
|
+
FrameDataObj->SetObjectField(TEXT("transform"), TransformToJson(CameraFrame->Transform));
|
|
420
|
+
FrameDataObj->SetNumberField (TEXT("field_of_view"), CameraFrame->FieldOfView);
|
|
421
|
+
FrameDataObj->SetNumberField (TEXT("aspect_ratio"), CameraFrame->AspectRatio);
|
|
422
|
+
FrameDataObj->SetNumberField (TEXT("focal_length"), CameraFrame->FocalLength);
|
|
423
|
+
FrameDataObj->SetNumberField (TEXT("aperture"), CameraFrame->Aperture);
|
|
424
|
+
FrameDataObj->SetNumberField (TEXT("focus_distance"), CameraFrame->FocusDistance);
|
|
425
|
+
}
|
|
426
|
+
if (CameraStatic)
|
|
427
|
+
{
|
|
428
|
+
FrameDataObj->SetBoolField(TEXT("film_back_override"), CameraStatic->bIsFieldOfViewSupported);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
else if (RoleName == TEXT("Animation"))
|
|
432
|
+
{
|
|
433
|
+
// Extract bone data from FLiveLinkAnimationFrameData.
|
|
434
|
+
// T-27-02: Cap output at 10 bones; always include total_bones count.
|
|
435
|
+
FLiveLinkAnimationFrameData* AnimFrame =
|
|
436
|
+
FrameData.FrameData.Cast<FLiveLinkAnimationFrameData>();
|
|
437
|
+
FLiveLinkSkeletonStaticData* AnimStatic =
|
|
438
|
+
FrameData.StaticData.Cast<FLiveLinkSkeletonStaticData>();
|
|
439
|
+
|
|
440
|
+
const int32 TotalBones = AnimStatic ? AnimStatic->BoneNames.Num() : 0;
|
|
441
|
+
FrameDataObj->SetNumberField(TEXT("total_bones"), TotalBones);
|
|
442
|
+
|
|
443
|
+
TArray<TSharedPtr<FJsonValue>> BoneNamesArray;
|
|
444
|
+
if (AnimStatic)
|
|
445
|
+
{
|
|
446
|
+
for (const FName& BoneName : AnimStatic->BoneNames)
|
|
447
|
+
{
|
|
448
|
+
BoneNamesArray.Add(MakeShared<FJsonValueString>(BoneName.ToString()));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
FrameDataObj->SetArrayField(TEXT("bone_names"), BoneNamesArray);
|
|
452
|
+
FrameDataObj->SetNumberField(TEXT("bone_count"), TotalBones);
|
|
453
|
+
|
|
454
|
+
if (AnimFrame && AnimStatic)
|
|
455
|
+
{
|
|
456
|
+
// Cap at first 10 bone transforms (T-27-02).
|
|
457
|
+
const int32 PreviewCount = FMath::Min(AnimFrame->Transforms.Num(), 10);
|
|
458
|
+
|
|
459
|
+
TArray<TSharedPtr<FJsonValue>> BoneTransforms;
|
|
460
|
+
BoneTransforms.Reserve(PreviewCount);
|
|
461
|
+
|
|
462
|
+
for (int32 BoneIdx = 0; BoneIdx < PreviewCount; ++BoneIdx)
|
|
463
|
+
{
|
|
464
|
+
const FName BoneName = (BoneIdx < AnimStatic->BoneNames.Num())
|
|
465
|
+
? AnimStatic->BoneNames[BoneIdx]
|
|
466
|
+
: FName(*FString::Printf(TEXT("Bone_%d"), BoneIdx));
|
|
467
|
+
|
|
468
|
+
TSharedPtr<FJsonObject> BoneObj = MakeShared<FJsonObject>();
|
|
469
|
+
BoneObj->SetStringField(TEXT("bone_name"), BoneName.ToString());
|
|
470
|
+
BoneObj->SetObjectField(TEXT("location"), VectorToJson(AnimFrame->Transforms[BoneIdx].GetLocation()));
|
|
471
|
+
BoneObj->SetObjectField(TEXT("rotation"), RotatorToJson(AnimFrame->Transforms[BoneIdx].GetRotation().Rotator()));
|
|
472
|
+
|
|
473
|
+
BoneTransforms.Add(MakeShared<FJsonValueObject>(BoneObj));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
FrameDataObj->SetArrayField(TEXT("bone_transforms"), BoneTransforms);
|
|
477
|
+
FrameDataObj->SetNumberField(TEXT("preview_bone_count"), PreviewCount);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else
|
|
481
|
+
{
|
|
482
|
+
// Generic/unknown role: report what we know.
|
|
483
|
+
FrameDataObj->SetStringField(TEXT("role_class"),
|
|
484
|
+
RoleClass ? RoleClass->GetName() : TEXT("None"));
|
|
485
|
+
FrameDataObj->SetStringField(TEXT("note"),
|
|
486
|
+
TEXT("Role-specific frame extraction not implemented for this role type."));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
TSharedPtr<FJsonObject> ResponseData = MakeShared<FJsonObject>();
|
|
490
|
+
ResponseData->SetStringField(TEXT("subject_name"), SubjectName);
|
|
491
|
+
ResponseData->SetStringField(TEXT("role"), RoleName);
|
|
492
|
+
ResponseData->SetObjectField(TEXT("frame_data"), FrameDataObj);
|
|
493
|
+
|
|
494
|
+
SendResponse(BuildLiveLinkSuccessResponse(CorrId, ResponseData));
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (!bFound)
|
|
499
|
+
{
|
|
500
|
+
SendResponse(BuildLiveLinkErrorResponse(CorrId,
|
|
501
|
+
FString::Printf(TEXT("Subject not found: %s"), *SubjectName)));
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// MCPLiveLinkCommands.h (Plan 27-01)
|
|
2
|
+
// Declares the registration function for all Live Link MCP command handlers.
|
|
3
|
+
// Handlers: livelink.sources, livelink.subjects, livelink.control, livelink.preview
|
|
4
|
+
//
|
|
5
|
+
// Call RegisterLiveLinkCommands(*Router) in MCPBridgeSubsystem::Initialize()
|
|
6
|
+
// BEFORE the TCP server starts accepting connections.
|
|
7
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
|
|
10
|
+
#include "MCPCommandRouter.h"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Register all four Live Link command handlers into the given router.
|
|
14
|
+
* Must be called on the game thread before connections arrive.
|
|
15
|
+
*
|
|
16
|
+
* Registered commands:
|
|
17
|
+
* livelink.sources -- list all active Live Link sources with type, machine name, status (LL-01)
|
|
18
|
+
* livelink.subjects -- list all Live Link subjects with roles and enabled state (LL-02)
|
|
19
|
+
* livelink.control -- pause/resume individual Live Link subjects by toggling enabled state (LL-03)
|
|
20
|
+
* livelink.preview -- inspect current frame data for any Live Link subject (LL-04)
|
|
21
|
+
*/
|
|
22
|
+
void RegisterLiveLinkCommands(FMCPCommandRouter& Router);
|