ue-mcp 1.0.37 → 1.0.39
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 +2 -2
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.js +1 -1
- package/dist/pie/schema.d.ts +10 -10
- package/dist/tool-counts.json +6 -6
- package/dist/tools/animation.js +3 -0
- package/dist/tools/animation.js.map +1 -1
- package/dist/tools/gameplay.js +21 -0
- package/dist/tools/gameplay.js.map +1 -1
- package/dist/ue-mcp.default.yml +72 -0
- package/package.json +2 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerUtils.h +13 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.cpp +2 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.h +1 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Sequence.cpp +127 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.cpp +11 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.h +13 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_PIEObserve.cpp +495 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_PIERecord.cpp +0 -12
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/ReflectionHandlers.cpp +236 -92
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/PIE/MCPObservationProfile.h +59 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/PIE/PIEInputRecorder.cpp +0 -7
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/PIE/PIEInputReplayer.cpp +0 -8
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/PIE/PIEObserver.cpp +379 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/PIE/PIEObserver.h +109 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/PIE/PIESequenceFormat.h +5 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/UE_MCP_Bridge.cpp +5 -1
package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Sequence.cpp
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
#include "Animation/AnimSequence.h"
|
|
11
11
|
#include "Animation/AnimSequenceBase.h"
|
|
12
12
|
#include "Animation/AnimComposite.h"
|
|
13
|
+
#include "Animation/AnimData/IAnimationDataModel.h"
|
|
13
14
|
#include "Animation/Skeleton.h"
|
|
14
15
|
#include "AnimationBlueprintLibrary.h"
|
|
15
16
|
#include "Engine/SkeletalMesh.h"
|
|
@@ -127,6 +128,132 @@ TSharedPtr<FJsonValue> FAnimationHandlers::ReadAnimSequence(const TSharedPtr<FJs
|
|
|
127
128
|
return MCPResult(Result);
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// scan_animation_tracks
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
TSharedPtr<FJsonValue> FAnimationHandlers::ScanAnimationTracks(const TSharedPtr<FJsonObject>& Params)
|
|
135
|
+
{
|
|
136
|
+
const int32 TargetTrackCount = OptionalInt(Params, TEXT("targetTrackCount"), 0);
|
|
137
|
+
const bool bIncludeTrackNames = OptionalBool(Params, TEXT("includeTrackNames"), false);
|
|
138
|
+
const bool bRecursive = OptionalBool(Params, TEXT("recursive"), true);
|
|
139
|
+
const FString Directory = OptionalString(Params, TEXT("directory"), TEXT("/Game"));
|
|
140
|
+
const FString SkeletonFilter = OptionalString(Params, TEXT("skeletonPath"));
|
|
141
|
+
|
|
142
|
+
TArray<FString> AssetPaths;
|
|
143
|
+
const TArray<TSharedPtr<FJsonValue>>* PathsArray = nullptr;
|
|
144
|
+
if (Params->TryGetArrayField(TEXT("assetPaths"), PathsArray))
|
|
145
|
+
{
|
|
146
|
+
for (const TSharedPtr<FJsonValue>& PathValue : *PathsArray)
|
|
147
|
+
{
|
|
148
|
+
FString Path;
|
|
149
|
+
if (PathValue.IsValid() && PathValue->TryGetString(Path) && !Path.IsEmpty())
|
|
150
|
+
{
|
|
151
|
+
AssetPaths.Add(Path);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else
|
|
156
|
+
{
|
|
157
|
+
IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
|
|
158
|
+
TArray<FAssetData> AssetDataList;
|
|
159
|
+
AssetRegistry.GetAssetsByClass(FTopLevelAssetPath(TEXT("/Script/Engine"), TEXT("AnimSequence")), AssetDataList, true);
|
|
160
|
+
for (const FAssetData& AssetData : AssetDataList)
|
|
161
|
+
{
|
|
162
|
+
if (!Directory.IsEmpty() && !AssetData.PackageName.ToString().StartsWith(Directory))
|
|
163
|
+
{
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (!bRecursive && AssetData.PackagePath.ToString() != Directory)
|
|
167
|
+
{
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
AssetPaths.Add(AssetData.GetObjectPathString());
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
TArray<TSharedPtr<FJsonValue>> Sequences;
|
|
175
|
+
int32 ProblemCount = 0;
|
|
176
|
+
int32 InspectedCount = 0;
|
|
177
|
+
int32 FailureCount = 0;
|
|
178
|
+
|
|
179
|
+
for (const FString& AssetPath : AssetPaths)
|
|
180
|
+
{
|
|
181
|
+
UObject* LoadedAsset = UEditorAssetLibrary::LoadAsset(AssetPath);
|
|
182
|
+
UAnimSequence* AnimSeq = Cast<UAnimSequence>(LoadedAsset);
|
|
183
|
+
if (!AnimSeq)
|
|
184
|
+
{
|
|
185
|
+
++FailureCount;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const IAnimationDataModel* DataModel = AnimSeq->GetDataModel();
|
|
190
|
+
if (!DataModel)
|
|
191
|
+
{
|
|
192
|
+
++FailureCount;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
USkeleton* Skeleton = AnimSeq->GetSkeleton();
|
|
197
|
+
const FString SequenceSkeletonPath = Skeleton ? Skeleton->GetPathName() : FString();
|
|
198
|
+
if (!SkeletonFilter.IsEmpty() && SequenceSkeletonPath != SkeletonFilter)
|
|
199
|
+
{
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
++InspectedCount;
|
|
204
|
+
TArray<FName> BoneTrackNames;
|
|
205
|
+
DataModel->GetBoneTrackNames(BoneTrackNames);
|
|
206
|
+
const int32 TrackCount = BoneTrackNames.Num();
|
|
207
|
+
const bool bOverTarget = TargetTrackCount > 0 && TrackCount > TargetTrackCount;
|
|
208
|
+
if (bOverTarget)
|
|
209
|
+
{
|
|
210
|
+
++ProblemCount;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
TSharedPtr<FJsonObject> Entry = MakeShared<FJsonObject>();
|
|
214
|
+
Entry->SetStringField(TEXT("assetPath"), AnimSeq->GetPathName());
|
|
215
|
+
Entry->SetStringField(TEXT("packagePath"), AnimSeq->GetOutermost()->GetName());
|
|
216
|
+
Entry->SetStringField(TEXT("name"), AnimSeq->GetName());
|
|
217
|
+
Entry->SetStringField(TEXT("skeleton"), SequenceSkeletonPath);
|
|
218
|
+
Entry->SetNumberField(TEXT("numBoneTracks"), TrackCount);
|
|
219
|
+
Entry->SetBoolField(TEXT("overTarget"), bOverTarget);
|
|
220
|
+
|
|
221
|
+
if (bIncludeTrackNames)
|
|
222
|
+
{
|
|
223
|
+
TArray<TSharedPtr<FJsonValue>> TrackArray;
|
|
224
|
+
for (const FName& TrackName : BoneTrackNames)
|
|
225
|
+
{
|
|
226
|
+
TrackArray.Add(MakeShared<FJsonValueString>(TrackName.ToString()));
|
|
227
|
+
}
|
|
228
|
+
Entry->SetArrayField(TEXT("boneTrackNames"), TrackArray);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (TargetTrackCount > 0)
|
|
232
|
+
{
|
|
233
|
+
TArray<TSharedPtr<FJsonValue>> OverflowArray;
|
|
234
|
+
for (int32 Index = TargetTrackCount; Index < BoneTrackNames.Num(); ++Index)
|
|
235
|
+
{
|
|
236
|
+
OverflowArray.Add(MakeShared<FJsonValueString>(BoneTrackNames[Index].ToString()));
|
|
237
|
+
}
|
|
238
|
+
Entry->SetArrayField(TEXT("overflowTrackNames"), OverflowArray);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (bOverTarget || TargetTrackCount <= 0)
|
|
242
|
+
{
|
|
243
|
+
Sequences.Add(MakeShared<FJsonValueObject>(Entry));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
auto Result = MCPSuccess();
|
|
248
|
+
Result->SetStringField(TEXT("directory"), Directory);
|
|
249
|
+
Result->SetNumberField(TEXT("targetTrackCount"), TargetTrackCount);
|
|
250
|
+
Result->SetNumberField(TEXT("inspectedCount"), InspectedCount);
|
|
251
|
+
Result->SetNumberField(TEXT("problemCount"), ProblemCount);
|
|
252
|
+
Result->SetNumberField(TEXT("failureCount"), FailureCount);
|
|
253
|
+
Result->SetArrayField(TEXT("sequences"), Sequences);
|
|
254
|
+
return MCPResult(Result);
|
|
255
|
+
}
|
|
256
|
+
|
|
130
257
|
// ---------------------------------------------------------------------------
|
|
131
258
|
// create_anim_blueprint
|
|
132
259
|
// ---------------------------------------------------------------------------
|
|
@@ -153,6 +153,17 @@ void FGameplayHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
|
|
|
153
153
|
Registry.RegisterHandler(TEXT("pie_replay_status"), &PieReplayStatus);
|
|
154
154
|
Registry.RegisterHandler(TEXT("pie_record_diff"), &PieRecordDiff);
|
|
155
155
|
Registry.RegisterHandler(TEXT("pie_snapshot"), &PieSnapshot);
|
|
156
|
+
Registry.RegisterHandler(TEXT("pie_profile_create"), &PieProfileCreate);
|
|
157
|
+
Registry.RegisterHandler(TEXT("pie_profile_read"), &PieProfileRead);
|
|
158
|
+
Registry.RegisterHandler(TEXT("pie_profile_update"), &PieProfileUpdate);
|
|
159
|
+
Registry.RegisterHandler(TEXT("pie_profile_delete"), &PieProfileDelete);
|
|
160
|
+
Registry.RegisterHandler(TEXT("pie_profile_list"), &PieProfileList);
|
|
161
|
+
Registry.RegisterHandler(TEXT("pie_observe_arm"), &PieObserveArm);
|
|
162
|
+
Registry.RegisterHandler(TEXT("pie_observe_disarm"), &PieObserveDisarm);
|
|
163
|
+
Registry.RegisterHandler(TEXT("pie_observe_stop"), &PieObserveStop);
|
|
164
|
+
Registry.RegisterHandler(TEXT("pie_observe_status"), &PieObserveStatus);
|
|
165
|
+
Registry.RegisterHandler(TEXT("pie_observe_list"), &PieObserveList);
|
|
166
|
+
Registry.RegisterHandler(TEXT("pie_observe_read"), &PieObserveRead);
|
|
156
167
|
}
|
|
157
168
|
|
|
158
169
|
TSharedPtr<FJsonValue> FGameplayHandlers::CreateSmartObjectDefinition(const TSharedPtr<FJsonObject>& Params)
|
|
@@ -110,4 +110,17 @@ private:
|
|
|
110
110
|
|
|
111
111
|
// Full UProperty dump of a live PIE actor to JSON (snapshots/<name>.json).
|
|
112
112
|
static TSharedPtr<FJsonValue> PieSnapshot(const TSharedPtr<FJsonObject>& Params);
|
|
113
|
+
|
|
114
|
+
// Observation profile CRUD + observer lifecycle.
|
|
115
|
+
static TSharedPtr<FJsonValue> PieProfileCreate(const TSharedPtr<FJsonObject>& Params);
|
|
116
|
+
static TSharedPtr<FJsonValue> PieProfileRead(const TSharedPtr<FJsonObject>& Params);
|
|
117
|
+
static TSharedPtr<FJsonValue> PieProfileUpdate(const TSharedPtr<FJsonObject>& Params);
|
|
118
|
+
static TSharedPtr<FJsonValue> PieProfileDelete(const TSharedPtr<FJsonObject>& Params);
|
|
119
|
+
static TSharedPtr<FJsonValue> PieProfileList(const TSharedPtr<FJsonObject>& Params);
|
|
120
|
+
static TSharedPtr<FJsonValue> PieObserveArm(const TSharedPtr<FJsonObject>& Params);
|
|
121
|
+
static TSharedPtr<FJsonValue> PieObserveDisarm(const TSharedPtr<FJsonObject>& Params);
|
|
122
|
+
static TSharedPtr<FJsonValue> PieObserveStop(const TSharedPtr<FJsonObject>& Params);
|
|
123
|
+
static TSharedPtr<FJsonValue> PieObserveStatus(const TSharedPtr<FJsonObject>& Params);
|
|
124
|
+
static TSharedPtr<FJsonValue> PieObserveList(const TSharedPtr<FJsonObject>& Params);
|
|
125
|
+
static TSharedPtr<FJsonValue> PieObserveRead(const TSharedPtr<FJsonObject>& Params);
|
|
113
126
|
};
|
package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_PIEObserve.cpp
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
// Observation profile CRUD + observer lifecycle handlers.
|
|
2
|
+
// Members of FGameplayHandlers.
|
|
3
|
+
|
|
4
|
+
#include "GameplayHandlers.h"
|
|
5
|
+
#include "HandlerRegistry.h"
|
|
6
|
+
#include "HandlerUtils.h"
|
|
7
|
+
#include "HandlerAssetCreate.h"
|
|
8
|
+
#include "PIE/PIEObserver.h"
|
|
9
|
+
#include "PIE/MCPObservationProfile.h"
|
|
10
|
+
#include "PIE/PIESequenceFormat.h"
|
|
11
|
+
#include "AssetRegistry/AssetRegistryModule.h"
|
|
12
|
+
#include "EditorAssetLibrary.h"
|
|
13
|
+
#include "HAL/FileManager.h"
|
|
14
|
+
#include "Misc/FileHelper.h"
|
|
15
|
+
#include "Misc/Paths.h"
|
|
16
|
+
#include "UObject/SavePackage.h"
|
|
17
|
+
|
|
18
|
+
namespace
|
|
19
|
+
{
|
|
20
|
+
using namespace UEMCPPIE;
|
|
21
|
+
|
|
22
|
+
FString ObserverStateToString(EObserverState S)
|
|
23
|
+
{
|
|
24
|
+
switch (S)
|
|
25
|
+
{
|
|
26
|
+
case EObserverState::Idle: return TEXT("idle");
|
|
27
|
+
case EObserverState::Armed: return TEXT("armed");
|
|
28
|
+
case EObserverState::WaitingForPawn: return TEXT("waiting_for_pawn");
|
|
29
|
+
case EObserverState::Observing: return TEXT("observing");
|
|
30
|
+
case EObserverState::Completed: return TEXT("completed");
|
|
31
|
+
}
|
|
32
|
+
return TEXT("idle");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
void WriteObserverStatusFields(TSharedPtr<FJsonObject> R, const FObserverStatus& S)
|
|
36
|
+
{
|
|
37
|
+
R->SetStringField(TEXT("state"), ObserverStateToString(S.State));
|
|
38
|
+
R->SetStringField(TEXT("run_id"), S.RunId);
|
|
39
|
+
R->SetStringField(TEXT("profile"), S.ProfilePath);
|
|
40
|
+
R->SetNumberField(TEXT("frames_sampled"), S.FramesSampled);
|
|
41
|
+
R->SetNumberField(TEXT("elapsed_seconds"), S.ElapsedSeconds);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
void ProfileToJson(const UMCPObservationProfile* P, TSharedPtr<FJsonObject> R)
|
|
45
|
+
{
|
|
46
|
+
TArray<TSharedPtr<FJsonValue>> Vals;
|
|
47
|
+
for (const FMCPTrackedValueEntry& E : P->TrackedValues)
|
|
48
|
+
{
|
|
49
|
+
TSharedRef<FJsonObject> V = MakeShared<FJsonObject>();
|
|
50
|
+
V->SetStringField(TEXT("path"), E.Path);
|
|
51
|
+
V->SetNumberField(TEXT("drift_threshold"), E.DriftThreshold);
|
|
52
|
+
Vals.Add(MakeShared<FJsonValueObject>(V));
|
|
53
|
+
}
|
|
54
|
+
R->SetArrayField(TEXT("tracked_values"), Vals);
|
|
55
|
+
|
|
56
|
+
TArray<TSharedPtr<FJsonValue>> Acts;
|
|
57
|
+
for (const FMCPTrackedActorEntry& E : P->TrackedActors)
|
|
58
|
+
{
|
|
59
|
+
Acts.Add(MakeShared<FJsonValueString>(E.ActorId));
|
|
60
|
+
}
|
|
61
|
+
R->SetArrayField(TEXT("tracked_actors"), Acts);
|
|
62
|
+
|
|
63
|
+
R->SetBoolField(TEXT("capture_pawn_state"), P->bCapturePawnState);
|
|
64
|
+
R->SetBoolField(TEXT("capture_montage"), P->bCaptureMontage);
|
|
65
|
+
R->SetNumberField(TEXT("position_threshold_cm"), P->PositionThresholdCm);
|
|
66
|
+
R->SetNumberField(TEXT("rotation_threshold_deg"), P->RotationThresholdDeg);
|
|
67
|
+
R->SetNumberField(TEXT("velocity_threshold_cms"), P->VelocityThresholdCms);
|
|
68
|
+
R->SetNumberField(TEXT("tracked_value_default_threshold"), P->TrackedValueDefaultThreshold);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
TSharedPtr<FJsonValue> FGameplayHandlers::PieProfileCreate(const TSharedPtr<FJsonObject>& Params)
|
|
73
|
+
{
|
|
74
|
+
MCP_CHECK_GAME_THREAD();
|
|
75
|
+
FString Name;
|
|
76
|
+
if (auto Err = RequireString(Params, TEXT("name"), Name)) return Err;
|
|
77
|
+
|
|
78
|
+
const FString PackagePath = OptionalString(Params, TEXT("package_path"), TEXT("/Game/Observation"));
|
|
79
|
+
const FString OnConflict = OptionalString(Params, TEXT("onConflict"), TEXT("skip"));
|
|
80
|
+
|
|
81
|
+
auto Create = MCPCreateAssetIdempotentNewObject<UMCPObservationProfile>(
|
|
82
|
+
Name, PackagePath, OnConflict, TEXT("ObservationProfile"));
|
|
83
|
+
if (Create.EarlyReturn) return Create.EarlyReturn;
|
|
84
|
+
|
|
85
|
+
UMCPObservationProfile* Profile = Create.Asset;
|
|
86
|
+
|
|
87
|
+
const TArray<TSharedPtr<FJsonValue>>* TVArr = nullptr;
|
|
88
|
+
if (Params->TryGetArrayField(TEXT("tracked_values"), TVArr) && TVArr)
|
|
89
|
+
{
|
|
90
|
+
for (const TSharedPtr<FJsonValue>& V : *TVArr)
|
|
91
|
+
{
|
|
92
|
+
if (!V.IsValid()) continue;
|
|
93
|
+
const TSharedPtr<FJsonObject>* Obj = nullptr;
|
|
94
|
+
FString Str;
|
|
95
|
+
if (V->TryGetObject(Obj) && Obj)
|
|
96
|
+
{
|
|
97
|
+
FMCPTrackedValueEntry E;
|
|
98
|
+
(*Obj)->TryGetStringField(TEXT("path"), E.Path);
|
|
99
|
+
double Thr = 0;
|
|
100
|
+
if ((*Obj)->TryGetNumberField(TEXT("drift_threshold"), Thr))
|
|
101
|
+
E.DriftThreshold = static_cast<float>(Thr);
|
|
102
|
+
if (!E.Path.IsEmpty()) Profile->TrackedValues.Add(E);
|
|
103
|
+
}
|
|
104
|
+
else if (V->TryGetString(Str))
|
|
105
|
+
{
|
|
106
|
+
FMCPTrackedValueEntry E;
|
|
107
|
+
E.Path = Str;
|
|
108
|
+
Profile->TrackedValues.Add(E);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const TArray<TSharedPtr<FJsonValue>>* TAArr = nullptr;
|
|
114
|
+
if (Params->TryGetArrayField(TEXT("tracked_actors"), TAArr) && TAArr)
|
|
115
|
+
{
|
|
116
|
+
for (const TSharedPtr<FJsonValue>& V : *TAArr)
|
|
117
|
+
{
|
|
118
|
+
FString S;
|
|
119
|
+
if (V.IsValid() && V->TryGetString(S))
|
|
120
|
+
{
|
|
121
|
+
FMCPTrackedActorEntry E;
|
|
122
|
+
E.ActorId = S;
|
|
123
|
+
Profile->TrackedActors.Add(E);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
bool BV;
|
|
129
|
+
if (Params->TryGetBoolField(TEXT("capture_pawn_state"), BV)) Profile->bCapturePawnState = BV;
|
|
130
|
+
if (Params->TryGetBoolField(TEXT("capture_montage"), BV)) Profile->bCaptureMontage = BV;
|
|
131
|
+
|
|
132
|
+
double D;
|
|
133
|
+
if (Params->TryGetNumberField(TEXT("position_threshold_cm"), D))
|
|
134
|
+
Profile->PositionThresholdCm = static_cast<float>(D);
|
|
135
|
+
if (Params->TryGetNumberField(TEXT("rotation_threshold_deg"), D))
|
|
136
|
+
Profile->RotationThresholdDeg = static_cast<float>(D);
|
|
137
|
+
if (Params->TryGetNumberField(TEXT("velocity_threshold_cms"), D))
|
|
138
|
+
Profile->VelocityThresholdCms = static_cast<float>(D);
|
|
139
|
+
if (Params->TryGetNumberField(TEXT("tracked_value_default_threshold"), D))
|
|
140
|
+
Profile->TrackedValueDefaultThreshold = static_cast<float>(D);
|
|
141
|
+
|
|
142
|
+
SaveAssetPackage(Profile);
|
|
143
|
+
|
|
144
|
+
const FString AssetPath = PackagePath / Name;
|
|
145
|
+
auto Result = MCPSuccess();
|
|
146
|
+
MCPSetCreated(Result);
|
|
147
|
+
Result->SetStringField(TEXT("path"), AssetPath);
|
|
148
|
+
ProfileToJson(Profile, Result);
|
|
149
|
+
MCPSetDeleteAssetRollback(Result, AssetPath);
|
|
150
|
+
return MCPResult(Result);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
TSharedPtr<FJsonValue> FGameplayHandlers::PieProfileRead(const TSharedPtr<FJsonObject>& Params)
|
|
154
|
+
{
|
|
155
|
+
MCP_CHECK_GAME_THREAD();
|
|
156
|
+
FString Path;
|
|
157
|
+
if (auto Err = RequireString(Params, TEXT("path"), Path)) return Err;
|
|
158
|
+
|
|
159
|
+
UMCPObservationProfile* Profile = LoadObject<UMCPObservationProfile>(nullptr, *Path);
|
|
160
|
+
if (!Profile) return MCPError(FString::Printf(TEXT("Profile not found: %s"), *Path));
|
|
161
|
+
|
|
162
|
+
auto Result = MCPSuccess();
|
|
163
|
+
Result->SetStringField(TEXT("path"), Path);
|
|
164
|
+
ProfileToJson(Profile, Result);
|
|
165
|
+
return MCPResult(Result);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
TSharedPtr<FJsonValue> FGameplayHandlers::PieProfileUpdate(const TSharedPtr<FJsonObject>& Params)
|
|
169
|
+
{
|
|
170
|
+
MCP_CHECK_GAME_THREAD();
|
|
171
|
+
FString Path;
|
|
172
|
+
if (auto Err = RequireString(Params, TEXT("path"), Path)) return Err;
|
|
173
|
+
|
|
174
|
+
UMCPObservationProfile* Profile = LoadObject<UMCPObservationProfile>(nullptr, *Path);
|
|
175
|
+
if (!Profile) return MCPError(FString::Printf(TEXT("Profile not found: %s"), *Path));
|
|
176
|
+
|
|
177
|
+
const TArray<TSharedPtr<FJsonValue>>* TVArr = nullptr;
|
|
178
|
+
if (Params->TryGetArrayField(TEXT("tracked_values"), TVArr) && TVArr)
|
|
179
|
+
{
|
|
180
|
+
Profile->TrackedValues.Reset();
|
|
181
|
+
for (const TSharedPtr<FJsonValue>& V : *TVArr)
|
|
182
|
+
{
|
|
183
|
+
if (!V.IsValid()) continue;
|
|
184
|
+
const TSharedPtr<FJsonObject>* Obj = nullptr;
|
|
185
|
+
FString Str;
|
|
186
|
+
if (V->TryGetObject(Obj) && Obj)
|
|
187
|
+
{
|
|
188
|
+
FMCPTrackedValueEntry E;
|
|
189
|
+
(*Obj)->TryGetStringField(TEXT("path"), E.Path);
|
|
190
|
+
double Thr = 0;
|
|
191
|
+
if ((*Obj)->TryGetNumberField(TEXT("drift_threshold"), Thr))
|
|
192
|
+
E.DriftThreshold = static_cast<float>(Thr);
|
|
193
|
+
if (!E.Path.IsEmpty()) Profile->TrackedValues.Add(E);
|
|
194
|
+
}
|
|
195
|
+
else if (V->TryGetString(Str))
|
|
196
|
+
{
|
|
197
|
+
FMCPTrackedValueEntry E;
|
|
198
|
+
E.Path = Str;
|
|
199
|
+
Profile->TrackedValues.Add(E);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const TArray<TSharedPtr<FJsonValue>>* TAArr = nullptr;
|
|
205
|
+
if (Params->TryGetArrayField(TEXT("tracked_actors"), TAArr) && TAArr)
|
|
206
|
+
{
|
|
207
|
+
Profile->TrackedActors.Reset();
|
|
208
|
+
for (const TSharedPtr<FJsonValue>& V : *TAArr)
|
|
209
|
+
{
|
|
210
|
+
FString S;
|
|
211
|
+
if (V.IsValid() && V->TryGetString(S))
|
|
212
|
+
{
|
|
213
|
+
FMCPTrackedActorEntry E;
|
|
214
|
+
E.ActorId = S;
|
|
215
|
+
Profile->TrackedActors.Add(E);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
bool BV;
|
|
221
|
+
if (Params->TryGetBoolField(TEXT("capture_pawn_state"), BV)) Profile->bCapturePawnState = BV;
|
|
222
|
+
if (Params->TryGetBoolField(TEXT("capture_montage"), BV)) Profile->bCaptureMontage = BV;
|
|
223
|
+
|
|
224
|
+
double D;
|
|
225
|
+
if (Params->TryGetNumberField(TEXT("position_threshold_cm"), D))
|
|
226
|
+
Profile->PositionThresholdCm = static_cast<float>(D);
|
|
227
|
+
if (Params->TryGetNumberField(TEXT("rotation_threshold_deg"), D))
|
|
228
|
+
Profile->RotationThresholdDeg = static_cast<float>(D);
|
|
229
|
+
if (Params->TryGetNumberField(TEXT("velocity_threshold_cms"), D))
|
|
230
|
+
Profile->VelocityThresholdCms = static_cast<float>(D);
|
|
231
|
+
if (Params->TryGetNumberField(TEXT("tracked_value_default_threshold"), D))
|
|
232
|
+
Profile->TrackedValueDefaultThreshold = static_cast<float>(D);
|
|
233
|
+
|
|
234
|
+
SaveAssetPackage(Profile);
|
|
235
|
+
|
|
236
|
+
auto Result = MCPSuccess();
|
|
237
|
+
MCPSetUpdated(Result);
|
|
238
|
+
Result->SetStringField(TEXT("path"), Path);
|
|
239
|
+
ProfileToJson(Profile, Result);
|
|
240
|
+
return MCPResult(Result);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
TSharedPtr<FJsonValue> FGameplayHandlers::PieProfileDelete(const TSharedPtr<FJsonObject>& Params)
|
|
244
|
+
{
|
|
245
|
+
MCP_CHECK_GAME_THREAD();
|
|
246
|
+
FString Path;
|
|
247
|
+
if (auto Err = RequireString(Params, TEXT("path"), Path)) return Err;
|
|
248
|
+
|
|
249
|
+
if (!OptionalBool(Params, TEXT("confirm"), false))
|
|
250
|
+
{
|
|
251
|
+
return MCPError(TEXT("confirm=true required to delete a profile"));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!UEditorAssetLibrary::DoesAssetExist(Path))
|
|
255
|
+
{
|
|
256
|
+
auto Result = MCPSuccess();
|
|
257
|
+
Result->SetBoolField(TEXT("already_deleted"), true);
|
|
258
|
+
return MCPResult(Result);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const bool bDeleted = UEditorAssetLibrary::DeleteAsset(Path);
|
|
262
|
+
if (!bDeleted) return MCPError(FString::Printf(TEXT("Failed to delete: %s"), *Path));
|
|
263
|
+
|
|
264
|
+
auto Result = MCPSuccess();
|
|
265
|
+
Result->SetBoolField(TEXT("deleted"), true);
|
|
266
|
+
Result->SetStringField(TEXT("path"), Path);
|
|
267
|
+
return MCPResult(Result);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
TSharedPtr<FJsonValue> FGameplayHandlers::PieProfileList(const TSharedPtr<FJsonObject>& Params)
|
|
271
|
+
{
|
|
272
|
+
MCP_CHECK_GAME_THREAD();
|
|
273
|
+
const FString Directory = OptionalString(Params, TEXT("directory"), TEXT("/Game"));
|
|
274
|
+
|
|
275
|
+
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
|
|
276
|
+
TArray<FAssetData> Assets;
|
|
277
|
+
ARM.Get().GetAssetsByClass(UMCPObservationProfile::StaticClass()->GetClassPathName(), Assets, true);
|
|
278
|
+
|
|
279
|
+
TArray<TSharedPtr<FJsonValue>> Items;
|
|
280
|
+
for (const FAssetData& A : Assets)
|
|
281
|
+
{
|
|
282
|
+
const FString AP = A.GetObjectPathString();
|
|
283
|
+
if (!AP.StartsWith(Directory)) continue;
|
|
284
|
+
TSharedRef<FJsonObject> Item = MakeShared<FJsonObject>();
|
|
285
|
+
Item->SetStringField(TEXT("path"), AP);
|
|
286
|
+
Item->SetStringField(TEXT("name"), A.AssetName.ToString());
|
|
287
|
+
Items.Add(MakeShared<FJsonValueObject>(Item));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
auto Result = MCPSuccess();
|
|
291
|
+
Result->SetArrayField(TEXT("profiles"), Items);
|
|
292
|
+
Result->SetNumberField(TEXT("count"), Items.Num());
|
|
293
|
+
return MCPResult(Result);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
TSharedPtr<FJsonValue> FGameplayHandlers::PieObserveArm(const TSharedPtr<FJsonObject>& Params)
|
|
297
|
+
{
|
|
298
|
+
MCP_CHECK_GAME_THREAD();
|
|
299
|
+
FString ProfilePath;
|
|
300
|
+
if (auto Err = RequireString(Params, TEXT("profile"), ProfilePath)) return Err;
|
|
301
|
+
|
|
302
|
+
FObserverArmConfig Cfg;
|
|
303
|
+
Cfg.ProfilePath = ProfilePath;
|
|
304
|
+
Cfg.OutputDir = OptionalString(Params, TEXT("output_dir"));
|
|
305
|
+
Cfg.SampleHz = OptionalInt(Params, TEXT("sample_hz"), 60);
|
|
306
|
+
Cfg.ClientId = OptionalInt(Params, TEXT("client_id"), 0);
|
|
307
|
+
|
|
308
|
+
if (Params->HasField(TEXT("pin_fps")))
|
|
309
|
+
{
|
|
310
|
+
int32 Hz = 60;
|
|
311
|
+
Params->TryGetNumberField(TEXT("pin_fps"), Hz);
|
|
312
|
+
Cfg.PinFPS = Hz;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
FString Err, Msg;
|
|
316
|
+
if (!FPIEObserver::Get().Arm(Cfg, Err, Msg))
|
|
317
|
+
{
|
|
318
|
+
return MCPError(Err);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const FObserverStatus S = FPIEObserver::Get().GetStatus();
|
|
322
|
+
auto Result = MCPSuccess();
|
|
323
|
+
Result->SetBoolField(TEXT("armed"), true);
|
|
324
|
+
Result->SetStringField(TEXT("message"), Msg);
|
|
325
|
+
WriteObserverStatusFields(Result, S);
|
|
326
|
+
|
|
327
|
+
TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
|
|
328
|
+
MCPSetRollback(Result, TEXT("pie_observe_disarm"), Payload);
|
|
329
|
+
return MCPResult(Result);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
TSharedPtr<FJsonValue> FGameplayHandlers::PieObserveDisarm(const TSharedPtr<FJsonObject>& /*Params*/)
|
|
333
|
+
{
|
|
334
|
+
MCP_CHECK_GAME_THREAD();
|
|
335
|
+
FString Err;
|
|
336
|
+
if (!FPIEObserver::Get().Disarm(Err)) return MCPError(Err);
|
|
337
|
+
auto Result = MCPSuccess();
|
|
338
|
+
Result->SetBoolField(TEXT("disarmed"), true);
|
|
339
|
+
return MCPResult(Result);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
TSharedPtr<FJsonValue> FGameplayHandlers::PieObserveStop(const TSharedPtr<FJsonObject>& /*Params*/)
|
|
343
|
+
{
|
|
344
|
+
MCP_CHECK_GAME_THREAD();
|
|
345
|
+
const FObserverFinishResult F = FPIEObserver::Get().ForceStop();
|
|
346
|
+
if (!F.bSuccess && !F.Error.IsEmpty()) return MCPError(F.Error);
|
|
347
|
+
auto Result = MCPSuccess();
|
|
348
|
+
Result->SetBoolField(TEXT("stopped"), true);
|
|
349
|
+
Result->SetStringField(TEXT("run_id"), F.RunId);
|
|
350
|
+
Result->SetStringField(TEXT("output_dir"), F.OutputDir);
|
|
351
|
+
Result->SetNumberField(TEXT("frames_sampled"), F.FramesSampled);
|
|
352
|
+
if (!F.CSVPath.IsEmpty()) Result->SetStringField(TEXT("csv_path"), F.CSVPath);
|
|
353
|
+
if (!F.TrackedActorsPath.IsEmpty()) Result->SetStringField(TEXT("tracked_actors_path"), F.TrackedActorsPath);
|
|
354
|
+
return MCPResult(Result);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
TSharedPtr<FJsonValue> FGameplayHandlers::PieObserveStatus(const TSharedPtr<FJsonObject>& /*Params*/)
|
|
358
|
+
{
|
|
359
|
+
MCP_CHECK_GAME_THREAD();
|
|
360
|
+
const FObserverStatus S = FPIEObserver::Get().GetStatus();
|
|
361
|
+
auto Result = MCPSuccess();
|
|
362
|
+
WriteObserverStatusFields(Result, S);
|
|
363
|
+
return MCPResult(Result);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
TSharedPtr<FJsonValue> FGameplayHandlers::PieObserveList(const TSharedPtr<FJsonObject>& Params)
|
|
367
|
+
{
|
|
368
|
+
MCP_CHECK_GAME_THREAD();
|
|
369
|
+
const FString Root = OptionalString(Params, TEXT("output_dir"),
|
|
370
|
+
FPaths::ProjectSavedDir() / TEXT("MCPObservations"));
|
|
371
|
+
const int32 Limit = OptionalInt(Params, TEXT("limit"), 200);
|
|
372
|
+
|
|
373
|
+
TArray<FString> Dirs;
|
|
374
|
+
IFileManager::Get().FindFiles(Dirs, *(Root / TEXT("*")), false, true);
|
|
375
|
+
|
|
376
|
+
Dirs.Sort([](const FString& A, const FString& B) { return A > B; });
|
|
377
|
+
if (Dirs.Num() > Limit) Dirs.SetNum(Limit);
|
|
378
|
+
|
|
379
|
+
TArray<TSharedPtr<FJsonValue>> Items;
|
|
380
|
+
for (const FString& D : Dirs)
|
|
381
|
+
{
|
|
382
|
+
const FString ManifestPath = Root / D / TEXT("manifest.json");
|
|
383
|
+
if (!FPaths::FileExists(ManifestPath)) continue;
|
|
384
|
+
|
|
385
|
+
FString JsonStr;
|
|
386
|
+
if (!FFileHelper::LoadFileToString(JsonStr, *ManifestPath)) continue;
|
|
387
|
+
|
|
388
|
+
TSharedPtr<FJsonObject> Obj;
|
|
389
|
+
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonStr);
|
|
390
|
+
if (!FJsonSerializer::Deserialize(Reader, Obj) || !Obj.IsValid()) continue;
|
|
391
|
+
|
|
392
|
+
TSharedRef<FJsonObject> Item = MakeShared<FJsonObject>();
|
|
393
|
+
Item->SetStringField(TEXT("run_id"), D);
|
|
394
|
+
FString Profile;
|
|
395
|
+
if (Obj->TryGetStringField(TEXT("profile"), Profile))
|
|
396
|
+
Item->SetStringField(TEXT("profile"), Profile);
|
|
397
|
+
int32 Frames = 0;
|
|
398
|
+
if (Obj->TryGetNumberField(TEXT("frames_sampled"), Frames))
|
|
399
|
+
Item->SetNumberField(TEXT("frames_sampled"), Frames);
|
|
400
|
+
FString StartedAt;
|
|
401
|
+
if (Obj->TryGetStringField(TEXT("started_at"), StartedAt))
|
|
402
|
+
Item->SetStringField(TEXT("started_at"), StartedAt);
|
|
403
|
+
|
|
404
|
+
Items.Add(MakeShared<FJsonValueObject>(Item));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
auto Result = MCPSuccess();
|
|
408
|
+
Result->SetArrayField(TEXT("observations"), Items);
|
|
409
|
+
Result->SetNumberField(TEXT("count"), Items.Num());
|
|
410
|
+
return MCPResult(Result);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
TSharedPtr<FJsonValue> FGameplayHandlers::PieObserveRead(const TSharedPtr<FJsonObject>& Params)
|
|
414
|
+
{
|
|
415
|
+
MCP_CHECK_GAME_THREAD();
|
|
416
|
+
FString RunId;
|
|
417
|
+
if (auto Err = RequireString(Params, TEXT("run_id"), RunId)) return Err;
|
|
418
|
+
|
|
419
|
+
const FString Root = OptionalString(Params, TEXT("output_dir"),
|
|
420
|
+
FPaths::ProjectSavedDir() / TEXT("MCPObservations"));
|
|
421
|
+
const FString File = OptionalString(Params, TEXT("file"), TEXT("manifest"));
|
|
422
|
+
const int32 Limit = OptionalInt(Params, TEXT("limit"), 1000);
|
|
423
|
+
const int32 Offset = OptionalInt(Params, TEXT("offset"), 0);
|
|
424
|
+
|
|
425
|
+
const FString RunDir = Root / RunId;
|
|
426
|
+
|
|
427
|
+
if (File == TEXT("manifest"))
|
|
428
|
+
{
|
|
429
|
+
FString JsonStr;
|
|
430
|
+
if (!FFileHelper::LoadFileToString(JsonStr, *(RunDir / TEXT("manifest.json"))))
|
|
431
|
+
return MCPError(FString::Printf(TEXT("manifest.json not found in %s"), *RunDir));
|
|
432
|
+
|
|
433
|
+
TSharedPtr<FJsonObject> Obj;
|
|
434
|
+
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonStr);
|
|
435
|
+
if (!FJsonSerializer::Deserialize(Reader, Obj) || !Obj.IsValid())
|
|
436
|
+
return MCPError(TEXT("Failed to parse manifest.json"));
|
|
437
|
+
|
|
438
|
+
return MCPResult(Obj.ToSharedRef());
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (File == TEXT("csv"))
|
|
442
|
+
{
|
|
443
|
+
FString CSV;
|
|
444
|
+
if (!FFileHelper::LoadFileToString(CSV, *(RunDir / TEXT("observation.csv"))))
|
|
445
|
+
return MCPError(TEXT("observation.csv not found"));
|
|
446
|
+
|
|
447
|
+
TArray<FString> Lines;
|
|
448
|
+
CSV.ParseIntoArrayLines(Lines);
|
|
449
|
+
const int32 Start = FMath::Max(0, Offset);
|
|
450
|
+
const int32 End = FMath::Min(Lines.Num(), Start + Limit);
|
|
451
|
+
|
|
452
|
+
TArray<TSharedPtr<FJsonValue>> Rows;
|
|
453
|
+
for (int32 i = Start; i < End; ++i)
|
|
454
|
+
{
|
|
455
|
+
Rows.Add(MakeShared<FJsonValueString>(Lines[i]));
|
|
456
|
+
}
|
|
457
|
+
auto Result = MCPSuccess();
|
|
458
|
+
Result->SetArrayField(TEXT("rows"), Rows);
|
|
459
|
+
Result->SetNumberField(TEXT("total_lines"), Lines.Num());
|
|
460
|
+
Result->SetNumberField(TEXT("offset"), Start);
|
|
461
|
+
Result->SetNumberField(TEXT("count"), Rows.Num());
|
|
462
|
+
return MCPResult(Result);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (File == TEXT("tracked"))
|
|
466
|
+
{
|
|
467
|
+
FString JSONL;
|
|
468
|
+
if (!FFileHelper::LoadFileToString(JSONL, *(RunDir / TEXT("tracked.jsonl"))))
|
|
469
|
+
return MCPError(TEXT("tracked.jsonl not found"));
|
|
470
|
+
|
|
471
|
+
TArray<FString> Lines;
|
|
472
|
+
JSONL.ParseIntoArrayLines(Lines);
|
|
473
|
+
const int32 Start = FMath::Max(0, Offset);
|
|
474
|
+
const int32 End = FMath::Min(Lines.Num(), Start + Limit);
|
|
475
|
+
|
|
476
|
+
TArray<TSharedPtr<FJsonValue>> Rows;
|
|
477
|
+
for (int32 i = Start; i < End; ++i)
|
|
478
|
+
{
|
|
479
|
+
TSharedPtr<FJsonObject> Row;
|
|
480
|
+
TSharedRef<TJsonReader<>> R = TJsonReaderFactory<>::Create(Lines[i]);
|
|
481
|
+
if (FJsonSerializer::Deserialize(R, Row) && Row.IsValid())
|
|
482
|
+
{
|
|
483
|
+
Rows.Add(MakeShared<FJsonValueObject>(Row.ToSharedRef()));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
auto Result = MCPSuccess();
|
|
487
|
+
Result->SetArrayField(TEXT("rows"), Rows);
|
|
488
|
+
Result->SetNumberField(TEXT("total_lines"), Lines.Num());
|
|
489
|
+
Result->SetNumberField(TEXT("offset"), Start);
|
|
490
|
+
Result->SetNumberField(TEXT("count"), Rows.Num());
|
|
491
|
+
return MCPResult(Result);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return MCPError(FString::Printf(TEXT("Unknown file type: %s (use manifest, csv, or tracked)"), *File));
|
|
495
|
+
}
|
package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_PIERecord.cpp
CHANGED
|
@@ -26,18 +26,6 @@ namespace
|
|
|
26
26
|
return TEXT("idle");
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
TArray<FString> JsonArrayToStringList(const TArray<TSharedPtr<FJsonValue>>* Arr)
|
|
30
|
-
{
|
|
31
|
-
TArray<FString> Out;
|
|
32
|
-
if (!Arr) return Out;
|
|
33
|
-
for (const TSharedPtr<FJsonValue>& V : *Arr)
|
|
34
|
-
{
|
|
35
|
-
FString S;
|
|
36
|
-
if (V.IsValid() && V->TryGetString(S)) Out.Add(S);
|
|
37
|
-
}
|
|
38
|
-
return Out;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
29
|
void WriteStatusFields(TSharedPtr<FJsonObject> R, const FRecorderStatus& S)
|
|
42
30
|
{
|
|
43
31
|
R->SetStringField(TEXT("state"), StateToString(S.State));
|