ue-mcp 1.0.38 → 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.
Files changed (26) hide show
  1. package/README.md +2 -2
  2. package/dist/instructions.d.ts +1 -1
  3. package/dist/instructions.js +1 -1
  4. package/dist/pie/schema.d.ts +10 -10
  5. package/dist/tool-counts.json +6 -6
  6. package/dist/tools/animation.js +3 -0
  7. package/dist/tools/animation.js.map +1 -1
  8. package/dist/tools/gameplay.js +21 -0
  9. package/dist/tools/gameplay.js.map +1 -1
  10. package/dist/ue-mcp.default.yml +72 -0
  11. package/package.json +2 -2
  12. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerUtils.h +13 -0
  13. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.cpp +2 -1
  14. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.h +1 -0
  15. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Sequence.cpp +127 -0
  16. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.cpp +11 -0
  17. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.h +13 -0
  18. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_PIEObserve.cpp +495 -0
  19. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_PIERecord.cpp +0 -12
  20. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/PIE/MCPObservationProfile.h +59 -0
  21. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/PIE/PIEInputRecorder.cpp +0 -7
  22. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/PIE/PIEInputReplayer.cpp +0 -8
  23. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/PIE/PIEObserver.cpp +379 -0
  24. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/PIE/PIEObserver.h +109 -0
  25. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/PIE/PIESequenceFormat.h +5 -0
  26. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/UE_MCP_Bridge.cpp +5 -1
@@ -0,0 +1,59 @@
1
+ #pragma once
2
+
3
+ #include "CoreMinimal.h"
4
+ #include "Engine/DataAsset.h"
5
+ #include "MCPObservationProfile.generated.h"
6
+
7
+ USTRUCT(BlueprintType)
8
+ struct FMCPTrackedValueEntry
9
+ {
10
+ GENERATED_BODY()
11
+
12
+ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Tracking")
13
+ FString Path;
14
+
15
+ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Tracking", meta=(ClampMin="0"))
16
+ float DriftThreshold = 0.f;
17
+ };
18
+
19
+ USTRUCT(BlueprintType)
20
+ struct FMCPTrackedActorEntry
21
+ {
22
+ GENERATED_BODY()
23
+
24
+ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Tracking")
25
+ FString ActorId;
26
+ };
27
+
28
+ UCLASS(BlueprintType)
29
+ class UMCPObservationProfile : public UDataAsset
30
+ {
31
+ GENERATED_BODY()
32
+ public:
33
+
34
+ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Values",
35
+ meta=(TitleProperty="Path"))
36
+ TArray<FMCPTrackedValueEntry> TrackedValues;
37
+
38
+ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Actors",
39
+ meta=(TitleProperty="ActorId"))
40
+ TArray<FMCPTrackedActorEntry> TrackedActors;
41
+
42
+ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Sampling")
43
+ bool bCapturePawnState = true;
44
+
45
+ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Sampling")
46
+ bool bCaptureMontage = true;
47
+
48
+ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Thresholds")
49
+ float PositionThresholdCm = 5.f;
50
+
51
+ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Thresholds")
52
+ float RotationThresholdDeg = 2.f;
53
+
54
+ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Thresholds")
55
+ float VelocityThresholdCms = 25.f;
56
+
57
+ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Thresholds")
58
+ float TrackedValueDefaultThreshold = 0.f;
59
+ };
@@ -19,13 +19,6 @@ namespace UEMCPPIE
19
19
  {
20
20
  namespace
21
21
  {
22
- FString ISOTimestampNow()
23
- {
24
- // Approximate ISO 8601 UTC. UE's FDateTime has no timezone awareness,
25
- // so we treat it as local and append the local offset.
26
- return FDateTime::Now().ToString(TEXT("%Y-%m-%dT%H:%M:%S"));
27
- }
28
-
29
22
  void SampleTrackedActors(UWorld* World,
30
23
  const TArray<FString>& Ids,
31
24
  TMap<FString, TWeakObjectPtr<AActor>>& Cache,
@@ -23,11 +23,6 @@ namespace UEMCPPIE
23
23
  {
24
24
  namespace
25
25
  {
26
- FString ISOTimestampNow()
27
- {
28
- return FDateTime::Now().ToString(TEXT("%Y-%m-%dT%H:%M:%S"));
29
- }
30
-
31
26
  UInputAction* LoadAction(const FString& Path)
32
27
  {
33
28
  if (Path.IsEmpty()) return nullptr;
@@ -338,9 +333,6 @@ namespace UEMCPPIE
338
333
  SC.AxisThreshold = 0.15f;
339
334
  SC.bCapturePawnState = true;
340
335
  SC.bCaptureMontage = true;
341
- // Replay drift compares tracked-value columns recovered from the
342
- // source CSV header. Telling the sampler about those paths makes
343
- // Row.TrackedValues populated on each sampled frame.
344
336
  SC.TrackedValuePaths = SourceTrackedPaths;
345
337
  SC.ClientIndex = Pending.ClientId;
346
338
  Sampler.Reset();
@@ -0,0 +1,379 @@
1
+ #include "PIEObserver.h"
2
+ #include "MCPObservationProfile.h"
3
+ #include "PIESequenceFormat.h"
4
+ #include "UE_MCP_BridgeModule.h"
5
+ #include "Editor.h"
6
+ #include "Engine/World.h"
7
+ #include "HAL/FileManager.h"
8
+ #include "Misc/FileHelper.h"
9
+ #include "Misc/Paths.h"
10
+
11
+ namespace UEMCPPIE
12
+ {
13
+ namespace
14
+ {
15
+ void SampleActors(UWorld* World,
16
+ const TArray<FString>& Ids,
17
+ TMap<FString, TWeakObjectPtr<AActor>>& Cache,
18
+ FTrackedActorRow& OutRow)
19
+ {
20
+ for (const FString& Id : Ids)
21
+ {
22
+ FActorState AS;
23
+ AActor* A = nullptr;
24
+ if (TWeakObjectPtr<AActor>* Cached = Cache.Find(Id))
25
+ {
26
+ A = Cached->Get();
27
+ }
28
+ if (!A)
29
+ {
30
+ A = FindActorById(World, Id);
31
+ if (A) Cache.Add(Id, A);
32
+ }
33
+ if (A)
34
+ {
35
+ AS.Location = A->GetActorLocation();
36
+ AS.Rotation = A->GetActorRotation();
37
+ AS.Velocity = A->GetVelocity();
38
+ AS.bResolved = true;
39
+ }
40
+ OutRow.Actors.Add(Id, AS);
41
+ }
42
+ }
43
+ }
44
+
45
+ FPIEObserver& FPIEObserver::Get()
46
+ {
47
+ static FPIEObserver Instance;
48
+ return Instance;
49
+ }
50
+
51
+ void FPIEObserver::Init()
52
+ {
53
+ if (BeginPIEHandle.IsValid()) return;
54
+ BeginPIEHandle = FEditorDelegates::BeginPIE.AddLambda([this](bool bSim)
55
+ {
56
+ this->OnBeginPIE(bSim);
57
+ });
58
+ EndPIEHandle = FEditorDelegates::EndPIE.AddLambda([this](bool bSim)
59
+ {
60
+ this->OnEndPIE(bSim);
61
+ });
62
+ }
63
+
64
+ void FPIEObserver::Shutdown()
65
+ {
66
+ if (BeginPIEHandle.IsValid()) FEditorDelegates::BeginPIE.Remove(BeginPIEHandle);
67
+ if (EndPIEHandle.IsValid()) FEditorDelegates::EndPIE.Remove(EndPIEHandle);
68
+ BeginPIEHandle.Reset();
69
+ EndPIEHandle.Reset();
70
+ if (bEndFrameBound && OnEndFrameHandle.IsValid())
71
+ {
72
+ FCoreDelegates::OnEndFrame.Remove(OnEndFrameHandle);
73
+ }
74
+ OnEndFrameHandle.Reset();
75
+ bEndFrameBound = false;
76
+ State = EObserverState::Idle;
77
+ bArmed = false;
78
+ }
79
+
80
+ bool FPIEObserver::Arm(const FObserverArmConfig& Cfg, FString& OutError, FString& OutMessage)
81
+ {
82
+ if (State == EObserverState::Observing || State == EObserverState::WaitingForPawn)
83
+ {
84
+ OutError = TEXT("Observation already in flight; pie_observe_stop first.");
85
+ return false;
86
+ }
87
+
88
+ UMCPObservationProfile* Profile = LoadObject<UMCPObservationProfile>(
89
+ nullptr, *Cfg.ProfilePath);
90
+ if (!Profile)
91
+ {
92
+ OutError = FString::Printf(TEXT("Profile not found: %s"), *Cfg.ProfilePath);
93
+ return false;
94
+ }
95
+
96
+ Pending = Cfg;
97
+
98
+ TrackedValuePaths.Reset();
99
+ TrackedThresholds.Reset();
100
+ for (const FMCPTrackedValueEntry& E : Profile->TrackedValues)
101
+ {
102
+ TrackedValuePaths.Add(E.Path);
103
+ if (E.DriftThreshold > 0.f)
104
+ {
105
+ TrackedThresholds.Add(E.Path, E.DriftThreshold);
106
+ }
107
+ }
108
+
109
+ TrackedActorIds.Reset();
110
+ for (const FMCPTrackedActorEntry& E : Profile->TrackedActors)
111
+ {
112
+ TrackedActorIds.Add(E.ActorId);
113
+ }
114
+
115
+ bCapturePawnState = Profile->bCapturePawnState;
116
+ bCaptureMontage = Profile->bCaptureMontage;
117
+ ThrPosCm = Profile->PositionThresholdCm;
118
+ ThrRotDeg = Profile->RotationThresholdDeg;
119
+ ThrVelCms = Profile->VelocityThresholdCms;
120
+ ThrTrackedDefault = Profile->TrackedValueDefaultThreshold;
121
+ CurrentProfilePath = Cfg.ProfilePath;
122
+
123
+ CurrentRunId = FString::Printf(TEXT("obs_%s"), *FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S")));
124
+ const FString Root = Cfg.OutputDir.IsEmpty()
125
+ ? (FPaths::ProjectSavedDir() / TEXT("MCPObservations"))
126
+ : Cfg.OutputDir;
127
+ CurrentOutputDir = Root / CurrentRunId;
128
+
129
+ ActorRows.Reset();
130
+ ActorCache.Reset();
131
+ CSVBody.Reset();
132
+ FramesSampled = 0;
133
+
134
+ bArmed = true;
135
+ State = EObserverState::Armed;
136
+ OutMessage = FString::Printf(TEXT("Armed: profile=%s run=%s values=%d actors=%d"),
137
+ *FPaths::GetBaseFilename(Cfg.ProfilePath),
138
+ *CurrentRunId,
139
+ TrackedValuePaths.Num(),
140
+ TrackedActorIds.Num());
141
+
142
+ if (GEditor && GEditor->PlayWorld)
143
+ {
144
+ OnBeginPIE(false);
145
+ }
146
+ return true;
147
+ }
148
+
149
+ bool FPIEObserver::Disarm(FString& OutError)
150
+ {
151
+ if (State == EObserverState::Observing || State == EObserverState::WaitingForPawn)
152
+ {
153
+ OutError = TEXT("Observation is in flight; pie_observe_stop to finalize.");
154
+ return false;
155
+ }
156
+ bArmed = false;
157
+ State = EObserverState::Idle;
158
+ return true;
159
+ }
160
+
161
+ void FPIEObserver::OnBeginPIE(bool /*bIsSimulating*/)
162
+ {
163
+ if (!bArmed) return;
164
+ bArmed = false;
165
+
166
+ FPIEFrameSampler::FConfig SC;
167
+ SC.AxisThreshold = 0.15f;
168
+ SC.bCapturePawnState = bCapturePawnState;
169
+ SC.bCaptureMontage = bCaptureMontage;
170
+ SC.TrackedValuePaths = TrackedValuePaths;
171
+ SC.ClientIndex = Pending.ClientId;
172
+ Sampler.Reset();
173
+ Sampler.SetConfig(SC);
174
+
175
+ State = EObserverState::WaitingForPawn;
176
+ AttachTime = 0.0;
177
+ StartedAt = ISOTimestampNow();
178
+
179
+ if (!bEndFrameBound)
180
+ {
181
+ OnEndFrameHandle = FCoreDelegates::OnEndFrame.AddLambda([this]()
182
+ {
183
+ this->OnEndFrame();
184
+ });
185
+ bEndFrameBound = true;
186
+ }
187
+
188
+ UE_LOG(LogMCPBridge, Log, TEXT("[PIE-OBS] Armed -> BeginPIE: profile=%s run=%s"),
189
+ *FPaths::GetBaseFilename(CurrentProfilePath), *CurrentRunId);
190
+ }
191
+
192
+ void FPIEObserver::OnEndFrame()
193
+ {
194
+ if (State == EObserverState::Idle || State == EObserverState::Completed) return;
195
+ UWorld* PIEWorld = nullptr;
196
+ if (GEditor) PIEWorld = GEditor->PlayWorld;
197
+ if (!PIEWorld) return;
198
+
199
+ if (State == EObserverState::WaitingForPawn)
200
+ {
201
+ if (Sampler.AttachToPIE(PIEWorld))
202
+ {
203
+ AttachTime = PIEWorld->GetTimeSeconds();
204
+
205
+ CSVHdr = FCSVHeader();
206
+ CSVHdr.RecordingId = CurrentRunId;
207
+ CSVHdr.SampleHz = Pending.SampleHz > 0 ? Pending.SampleHz : 60;
208
+ CSVHdr.Actions = Sampler.GetActions();
209
+ CSVHdr.TrackedValues = Sampler.GetTrackedValues();
210
+ CSVHeaderStr = BuildCSVHeader(CSVHdr);
211
+ CSVBody.Reset();
212
+
213
+ State = EObserverState::Observing;
214
+ UE_LOG(LogMCPBridge, Log, TEXT("[PIE-OBS] Pawn attached, observing"));
215
+ }
216
+ return;
217
+ }
218
+
219
+ if (State == EObserverState::Observing)
220
+ {
221
+ const double GameTime = PIEWorld->GetTimeSeconds();
222
+ const double Dt = PIEWorld->GetDeltaSeconds();
223
+ const uint64 FrameNum = static_cast<uint64>(FramesSampled);
224
+ FCSVRow Row = Sampler.SampleFrame(PIEWorld, FrameNum, GameTime, Dt);
225
+
226
+ if (TrackedActorIds.Num() > 0)
227
+ {
228
+ FTrackedActorRow AR;
229
+ AR.Frame = FrameNum;
230
+ AR.Time = GameTime;
231
+ SampleActors(PIEWorld, TrackedActorIds, ActorCache, AR);
232
+ ActorRows.Add(MoveTemp(AR));
233
+ }
234
+
235
+ AppendCSVRow(CSVBody, Row, CSVHdr);
236
+ FramesSampled++;
237
+ }
238
+ }
239
+
240
+ void FPIEObserver::OnEndPIE(bool /*bIsSimulating*/)
241
+ {
242
+ if (State == EObserverState::Idle) return;
243
+ FinaliseCurrent();
244
+ }
245
+
246
+ FObserverFinishResult FPIEObserver::FinaliseCurrent()
247
+ {
248
+ FObserverFinishResult R;
249
+ if (State == EObserverState::Idle)
250
+ {
251
+ R.Error = TEXT("Not observing");
252
+ return R;
253
+ }
254
+
255
+ R.RunId = CurrentRunId;
256
+ R.OutputDir = CurrentOutputDir;
257
+ R.FramesSampled = FramesSampled;
258
+
259
+ if (FramesSampled == 0)
260
+ {
261
+ R.bSuccess = true;
262
+ State = EObserverState::Idle;
263
+ return R;
264
+ }
265
+
266
+ IFileManager::Get().MakeDirectory(*CurrentOutputDir, true);
267
+
268
+ // Write observation CSV
269
+ {
270
+ const FString FullCSV = CSVHeaderStr + CSVBody;
271
+ FString Err;
272
+ if (SaveCSV(CurrentOutputDir / TEXT("observation.csv"), FullCSV, Err))
273
+ {
274
+ R.CSVPath = CurrentOutputDir / TEXT("observation.csv");
275
+ }
276
+ else
277
+ {
278
+ UE_LOG(LogMCPBridge, Warning, TEXT("[PIE-OBS] CSV write failed: %s"), *Err);
279
+ }
280
+ }
281
+
282
+ // Write tracked actors
283
+ if (ActorRows.Num() > 0)
284
+ {
285
+ FString Err;
286
+ if (SaveTrackedActorsJSONL(CurrentOutputDir / TEXT("tracked.jsonl"), ActorRows, Err))
287
+ {
288
+ R.TrackedActorsPath = CurrentOutputDir / TEXT("tracked.jsonl");
289
+ }
290
+ else
291
+ {
292
+ UE_LOG(LogMCPBridge, Warning, TEXT("[PIE-OBS] tracked.jsonl write failed: %s"), *Err);
293
+ }
294
+ }
295
+
296
+ // Write manifest
297
+ {
298
+ TSharedRef<FJsonObject> M = MakeShared<FJsonObject>();
299
+ M->SetNumberField(TEXT("version"), kFormatVersion);
300
+ M->SetStringField(TEXT("type"), TEXT("observation"));
301
+ M->SetStringField(TEXT("run_id"), CurrentRunId);
302
+ M->SetStringField(TEXT("profile"), CurrentProfilePath);
303
+ M->SetStringField(TEXT("started_at"), StartedAt);
304
+ M->SetStringField(TEXT("ended_at"), ISOTimestampNow());
305
+ M->SetNumberField(TEXT("frames_sampled"), FramesSampled);
306
+ M->SetNumberField(TEXT("sample_hz"), CSVHdr.SampleHz);
307
+
308
+ TArray<TSharedPtr<FJsonValue>> Vals;
309
+ for (const FString& P : TrackedValuePaths)
310
+ {
311
+ Vals.Add(MakeShared<FJsonValueString>(P));
312
+ }
313
+ M->SetArrayField(TEXT("tracked_values"), Vals);
314
+
315
+ TArray<TSharedPtr<FJsonValue>> Acts;
316
+ for (const FString& A : TrackedActorIds)
317
+ {
318
+ Acts.Add(MakeShared<FJsonValueString>(A));
319
+ }
320
+ M->SetArrayField(TEXT("tracked_actors"), Acts);
321
+
322
+ TSharedRef<FJsonObject> Thr = MakeShared<FJsonObject>();
323
+ Thr->SetNumberField(TEXT("position_cm"), ThrPosCm);
324
+ Thr->SetNumberField(TEXT("rotation_deg"), ThrRotDeg);
325
+ Thr->SetNumberField(TEXT("velocity_cms"), ThrVelCms);
326
+ Thr->SetNumberField(TEXT("tracked_default"), ThrTrackedDefault);
327
+ if (TrackedThresholds.Num() > 0)
328
+ {
329
+ TSharedRef<FJsonObject> PerPath = MakeShared<FJsonObject>();
330
+ for (const TPair<FString, float>& KV : TrackedThresholds)
331
+ {
332
+ PerPath->SetNumberField(KV.Key, KV.Value);
333
+ }
334
+ Thr->SetObjectField(TEXT("tracked"), PerPath);
335
+ }
336
+ M->SetObjectField(TEXT("thresholds"), Thr);
337
+
338
+ FString JsonStr;
339
+ TSharedRef<TJsonWriter<>> W = TJsonWriterFactory<>::Create(&JsonStr);
340
+ FJsonSerializer::Serialize(M, W);
341
+ FFileHelper::SaveStringToFile(JsonStr, *(CurrentOutputDir / TEXT("manifest.json")),
342
+ FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);
343
+ }
344
+
345
+ R.bSuccess = true;
346
+ R.DurationSeconds = 0.0;
347
+
348
+ if (bEndFrameBound && OnEndFrameHandle.IsValid())
349
+ {
350
+ FCoreDelegates::OnEndFrame.Remove(OnEndFrameHandle);
351
+ OnEndFrameHandle.Reset();
352
+ bEndFrameBound = false;
353
+ }
354
+ State = EObserverState::Idle;
355
+
356
+ UE_LOG(LogMCPBridge, Log, TEXT("[PIE-OBS] Finalized: %d frames -> %s"),
357
+ FramesSampled, *CurrentOutputDir);
358
+ return R;
359
+ }
360
+
361
+ FObserverFinishResult FPIEObserver::ForceStop()
362
+ {
363
+ return FinaliseCurrent();
364
+ }
365
+
366
+ FObserverStatus FPIEObserver::GetStatus() const
367
+ {
368
+ FObserverStatus S;
369
+ S.State = State;
370
+ S.RunId = CurrentRunId;
371
+ S.ProfilePath = CurrentProfilePath;
372
+ S.FramesSampled = FramesSampled;
373
+ if (GEditor && GEditor->PlayWorld && AttachTime > 0.0)
374
+ {
375
+ S.ElapsedSeconds = GEditor->PlayWorld->GetTimeSeconds() - AttachTime;
376
+ }
377
+ return S;
378
+ }
379
+ }
@@ -0,0 +1,109 @@
1
+ #pragma once
2
+
3
+ #include "CoreMinimal.h"
4
+ #include "PIEFrameSampler.h"
5
+ #include "PIESequenceFormat.h"
6
+ #include "UObject/WeakObjectPtrTemplates.h"
7
+
8
+ class UMCPObservationProfile;
9
+ class UWorld;
10
+ class AActor;
11
+
12
+ namespace UEMCPPIE
13
+ {
14
+ enum class EObserverState : uint8
15
+ {
16
+ Idle,
17
+ Armed,
18
+ WaitingForPawn,
19
+ Observing,
20
+ Completed
21
+ };
22
+
23
+ struct FObserverArmConfig
24
+ {
25
+ FString ProfilePath;
26
+ FString OutputDir;
27
+ int32 SampleHz = 60;
28
+ int32 PinFPS = -1;
29
+ int32 ClientId = 0;
30
+ };
31
+
32
+ struct FObserverStatus
33
+ {
34
+ EObserverState State = EObserverState::Idle;
35
+ FString RunId;
36
+ FString ProfilePath;
37
+ int32 FramesSampled = 0;
38
+ double ElapsedSeconds = 0.0;
39
+ };
40
+
41
+ struct FObserverFinishResult
42
+ {
43
+ bool bSuccess = false;
44
+ FString Error;
45
+ FString RunId;
46
+ FString OutputDir;
47
+ FString CSVPath;
48
+ FString TrackedActorsPath;
49
+ int32 FramesSampled = 0;
50
+ double DurationSeconds = 0.0;
51
+ };
52
+
53
+ class FPIEObserver
54
+ {
55
+ public:
56
+ static FPIEObserver& Get();
57
+
58
+ void Init();
59
+ void Shutdown();
60
+
61
+ bool Arm(const FObserverArmConfig& Cfg, FString& OutError, FString& OutMessage);
62
+ bool Disarm(FString& OutError);
63
+ FObserverFinishResult ForceStop();
64
+ FObserverStatus GetStatus() const;
65
+ bool IsActive() const { return State != EObserverState::Idle && State != EObserverState::Completed; }
66
+
67
+ private:
68
+ void OnBeginPIE(bool bIsSimulating);
69
+ void OnEndPIE(bool bIsSimulating);
70
+ void OnEndFrame();
71
+ FObserverFinishResult FinaliseCurrent();
72
+
73
+ FObserverArmConfig Pending;
74
+ EObserverState State = EObserverState::Idle;
75
+ bool bArmed = false;
76
+
77
+ FString CurrentRunId;
78
+ FString CurrentOutputDir;
79
+ FString CurrentProfilePath;
80
+
81
+ FPIEFrameSampler Sampler;
82
+ FCSVHeader CSVHdr;
83
+ FString CSVHeaderStr;
84
+ FString CSVBody;
85
+
86
+ TArray<FString> TrackedActorIds;
87
+ TArray<FTrackedActorRow> ActorRows;
88
+ TMap<FString, TWeakObjectPtr<AActor>> ActorCache;
89
+
90
+ double AttachTime = 0.0;
91
+ int32 FramesSampled = 0;
92
+ FString StartedAt;
93
+
94
+ // Profile config extracted at arm time
95
+ TArray<FString> TrackedValuePaths;
96
+ bool bCapturePawnState = true;
97
+ bool bCaptureMontage = true;
98
+ float ThrPosCm = 5.f;
99
+ float ThrRotDeg = 2.f;
100
+ float ThrVelCms = 25.f;
101
+ float ThrTrackedDefault = 0.f;
102
+ TMap<FString, float> TrackedThresholds;
103
+
104
+ FDelegateHandle BeginPIEHandle;
105
+ FDelegateHandle EndPIEHandle;
106
+ FDelegateHandle OnEndFrameHandle;
107
+ bool bEndFrameBound = false;
108
+ };
109
+ }
@@ -247,4 +247,9 @@ namespace UEMCPPIE
247
247
  // recorder (initial resolve), the replayer (re-resolve in the new world),
248
248
  // and pie_snapshot.
249
249
  AActor* FindActorById(UWorld* World, const FString& Id);
250
+
251
+ inline FString ISOTimestampNow()
252
+ {
253
+ return FDateTime::Now().ToString(TEXT("%Y-%m-%dT%H:%M:%S"));
254
+ }
250
255
  }
@@ -5,6 +5,7 @@
5
5
  #include "PIE/PIEInputInjector.h"
6
6
  #include "PIE/PIEInputRecorder.h"
7
7
  #include "PIE/PIEInputReplayer.h"
8
+ #include "PIE/PIEObserver.h"
8
9
  #include "Editor.h"
9
10
  #include "Editor/EditorEngine.h"
10
11
  #include "Misc/ConfigCacheIni.h"
@@ -24,6 +25,7 @@ void FUE_MCP_BridgeModule::StartupModule()
24
25
  UEMCPPIE::FPIEInputInjector::Init();
25
26
  UEMCPPIE::FPIEInputRecorder::Get().Init();
26
27
  UEMCPPIE::FPIEInputReplayer::Get().Init();
28
+ UEMCPPIE::FPIEObserver::Get().Init();
27
29
  // Clear any leftover injections from a previous PIE session so a fresh
28
30
  // EndPIE-BeginPIE pair starts with no ghost holds in the queue.
29
31
  FEditorDelegates::EndPIE.AddLambda([](bool /*bIsSimulating*/)
@@ -100,7 +102,8 @@ void FUE_MCP_BridgeModule::StartupModule()
100
102
  Suppress.BindLambda([]() -> bool
101
103
  {
102
104
  return UEMCPPIE::FPIEInputRecorder::Get().IsActive()
103
- || UEMCPPIE::FPIEInputReplayer::Get().IsActive();
105
+ || UEMCPPIE::FPIEInputReplayer::Get().IsActive()
106
+ || UEMCPPIE::FPIEObserver::Get().IsActive();
104
107
  });
105
108
  GEditor->ShouldDisableCPUThrottlingDelegates.Add(Suppress);
106
109
 
@@ -113,6 +116,7 @@ void FUE_MCP_BridgeModule::ShutdownModule()
113
116
  {
114
117
  // Stop bridge server
115
118
  FDialogHandlers::RemoveDialogHook();
119
+ UEMCPPIE::FPIEObserver::Get().Shutdown();
116
120
  UEMCPPIE::FPIEInputReplayer::Get().Shutdown();
117
121
  UEMCPPIE::FPIEInputRecorder::Get().Shutdown();
118
122
  UEMCPPIE::FPIEInputInjector::Shutdown();