ue-mcp 1.2.1 → 1.2.2

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 (54) hide show
  1. package/dist/context-cli.js +16 -16
  2. package/dist/engine-observer.js +68 -68
  3. package/dist/flow/guard.d.ts +26 -37
  4. package/dist/flow/guard.js +40 -25
  5. package/dist/flow/guard.js.map +1 -1
  6. package/dist/flow/guarded-bridge.d.ts +0 -10
  7. package/dist/flow/guarded-bridge.js +18 -23
  8. package/dist/flow/guarded-bridge.js.map +1 -1
  9. package/dist/flow/schema.d.ts +276 -16
  10. package/dist/flow/task-guards.d.ts +1 -1
  11. package/dist/flow/task-guards.js +44 -68
  12. package/dist/flow/task-guards.js.map +1 -1
  13. package/dist/instructions.js +130 -130
  14. package/dist/plugin-cli.js +299 -299
  15. package/dist/tool-counts.json +2 -2
  16. package/package.json +2 -2
  17. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/EngineStatusHooks.cpp +76 -76
  18. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/EngineStatusHooks.h +27 -27
  19. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerRegistry.cpp +128 -128
  20. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerRegistry.h +63 -63
  21. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRig.cpp +230 -230
  22. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Struct.cpp +323 -323
  23. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Internal.h +20 -20
  24. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/ChooserHandlers_Nested.cpp +537 -537
  25. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DemoHandlers.h +77 -77
  26. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_Build.cpp +353 -353
  27. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.h +18 -18
  28. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_InputRuntime.cpp +246 -246
  29. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.cpp +597 -597
  30. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.h +51 -51
  31. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +878 -878
  32. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers_Sculpt.cpp +449 -449
  33. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Function.cpp +275 -275
  34. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Graph.cpp +551 -551
  35. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NetworkingHandlers.cpp +531 -531
  36. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NetworkingHandlers.h +28 -28
  37. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +677 -677
  38. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.cpp +822 -822
  39. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.h +22 -22
  40. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.cpp +218 -218
  41. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.h +17 -17
  42. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/VolumeHelpers_Internal.cpp +46 -46
  43. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/VolumeHelpers_Internal.h +23 -23
  44. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/MCPExternalRegistry.cpp +83 -83
  45. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/UE_MCP_BridgeModule.h +14 -14
  46. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/UE_MCP_BridgeStatusModule.cpp +37 -37
  47. package/dist/manifest-signature.d.ts +0 -1
  48. package/dist/manifest-signature.js +0 -53
  49. package/dist/manifest-signature.js.map +0 -1
  50. package/dist/pie/schema.d.ts +0 -791
  51. package/dist/pie/schema.js +0 -151
  52. package/dist/pie/schema.js.map +0 -1
  53. package/dist/watch-cli.js.map +0 -1
  54. /package/plugin/ue_mcp_bridge/{ue_mcp_bridge.uplugin → UE_MCP_Bridge.uplugin} +0 -0
@@ -1,537 +1,537 @@
1
- // Nested-chooser object reference reading and remapping (#754).
2
- //
3
- // A ChooserTable that outputs objects stores its real references inside
4
- // in-asset NestedChooser sub-tables: a column's rows EvaluateChooser into
5
- // further UChooserTables whose own rows hold the leaf object paths.
6
- // chooser(list_rows) rendered those as an opaque resultType:NestedChooser with
7
- // an empty output, so there was no way to see - let alone repoint - the actual
8
- // references.
9
- //
10
- // The reporter concluded this was impossible because Python's
11
- // get_editor_property refused to read UChooserTable::ResultsStructs. That is a
12
- // Python restriction (it enforces Blueprint visibility metadata), NOT a C++
13
- // one: FProperty reflection ignores C++ access specifiers entirely, so the
14
- // bridge can walk these structures directly. No engine fork required.
15
- //
16
- // Translation-unit partition of FChooserHandlers; registrations live in
17
- // ChooserHandlers.cpp.
18
-
19
- #include "ChooserHandlers.h"
20
-
21
- #include "HandlerRegistry.h"
22
- #include "HandlerUtils.h"
23
-
24
- #include "UObject/StrongObjectPtr.h"
25
-
26
- #include "Chooser.h"
27
- #include "StructUtils/InstancedStruct.h"
28
- #include "EditorAssetLibrary.h"
29
- #include "ScopedTransaction.h"
30
- #include "UObject/UnrealType.h"
31
- #include "UObject/SoftObjectPtr.h"
32
-
33
- namespace
34
- {
35
- /** One object reference found somewhere inside a chooser's structures. */
36
- struct FChooserObjectRef
37
- {
38
- FString ChooserPath; // which (possibly nested) table it lives in
39
- FString Location; // e.g. "ResultsStructs[3].Asset"
40
- FString StructType; // the FInstancedStruct's script struct name
41
- FString ObjectPath; // current value
42
- FProperty* Property = nullptr;
43
- void* ValueAddr = nullptr;
44
- UObject* Owner = nullptr;
45
- };
46
-
47
- FString NormalizeObjectPath(const FString& In)
48
- {
49
- // "/Game/Foo/Bar.Bar" and "/Game/Foo/Bar" should compare equal, and the
50
- // editor writes some refs with a trailing _C or a subobject suffix.
51
- FString Path = In;
52
- Path.TrimStartAndEndInline();
53
- if (Path.StartsWith(TEXT("'")) && Path.EndsWith(TEXT("'")))
54
- {
55
- Path = Path.Mid(1, Path.Len() - 2);
56
- }
57
- // Strip a class prefix like Blueprint'/Game/...'
58
- int32 Quote = INDEX_NONE;
59
- if (Path.FindChar(TEXT('\''), Quote))
60
- {
61
- Path = Path.Mid(Quote + 1).Replace(TEXT("'"), TEXT(""));
62
- }
63
- FString Left, Right;
64
- if (Path.Split(TEXT("."), &Left, &Right, ESearchCase::CaseSensitive, ESearchDir::FromEnd))
65
- {
66
- // Only collapse "/Path/Name.Name" to "/Path/Name".
67
- FString Tail;
68
- if (Left.Split(TEXT("/"), nullptr, &Tail, ESearchCase::CaseSensitive, ESearchDir::FromEnd) && Tail == Right)
69
- {
70
- Path = Left;
71
- }
72
- }
73
- return Path;
74
- }
75
-
76
- /** Recursively collect object/soft-object references inside a struct value. */
77
- void CollectRefsInStruct(
78
- const UScriptStruct* Struct,
79
- void* StructMemory,
80
- UObject* Owner,
81
- const FString& ChooserPath,
82
- const FString& LocationPrefix,
83
- const FString& StructTypeName,
84
- TArray<FChooserObjectRef>& OutRefs,
85
- int32 Depth)
86
- {
87
- // Deep nesting is truncated rather than followed forever; the caller is
88
- // told via truncatedDepth on the response so partial is not read as complete.
89
- if (!Struct || !StructMemory || Depth > 6) return;
90
-
91
- for (TFieldIterator<FProperty> It(Struct); It; ++It)
92
- {
93
- FProperty* Prop = *It;
94
- if (!Prop) continue;
95
- void* Value = Prop->ContainerPtrToValuePtr<void>(StructMemory);
96
- const FString Location = LocationPrefix + TEXT(".") + Prop->GetName();
97
-
98
- if (FSoftObjectProperty* SoftProp = CastField<FSoftObjectProperty>(Prop))
99
- {
100
- const FSoftObjectPtr& Ptr = SoftProp->GetPropertyValue(Value);
101
- FChooserObjectRef Ref;
102
- Ref.ChooserPath = ChooserPath;
103
- Ref.Location = Location;
104
- Ref.StructType = StructTypeName;
105
- Ref.ObjectPath = Ptr.ToString();
106
- Ref.Property = Prop;
107
- Ref.ValueAddr = Value;
108
- Ref.Owner = Owner;
109
- if (!Ref.ObjectPath.IsEmpty()) OutRefs.Add(Ref);
110
- }
111
- else if (FObjectPropertyBase* ObjProp = CastField<FObjectPropertyBase>(Prop))
112
- {
113
- UObject* Referenced = ObjProp->GetObjectPropertyValue(Value);
114
- // A nested UChooserTable is followed separately by the caller;
115
- // record everything else as a leaf reference.
116
- if (Referenced && !Referenced->IsA<UChooserTable>())
117
- {
118
- FChooserObjectRef Ref;
119
- Ref.ChooserPath = ChooserPath;
120
- Ref.Location = Location;
121
- Ref.StructType = StructTypeName;
122
- Ref.ObjectPath = Referenced->GetPathName();
123
- Ref.Property = Prop;
124
- Ref.ValueAddr = Value;
125
- Ref.Owner = Owner;
126
- OutRefs.Add(Ref);
127
- }
128
- }
129
- else if (FStructProperty* StructProp = CastField<FStructProperty>(Prop))
130
- {
131
- CollectRefsInStruct(StructProp->Struct, Value, Owner, ChooserPath, Location,
132
- StructTypeName, OutRefs, Depth + 1);
133
- }
134
- else if (FMapProperty* MapProp = CastField<FMapProperty>(Prop))
135
- {
136
- FScriptMapHelper Helper(MapProp, Value);
137
- for (FScriptMapHelper::FIterator MapIt = Helper.CreateIterator(); MapIt; ++MapIt)
138
- {
139
- if (FStructProperty* ValueStruct = CastField<FStructProperty>(MapProp->ValueProp))
140
- {
141
- CollectRefsInStruct(ValueStruct->Struct, Helper.GetValuePtr(MapIt), Owner,
142
- ChooserPath, FString::Printf(TEXT("%s[map:%d]"), *Location, MapIt.GetInternalIndex()), StructTypeName, OutRefs, Depth + 1);
143
- }
144
- else if (FObjectPropertyBase* ValueObj = CastField<FObjectPropertyBase>(MapProp->ValueProp))
145
- {
146
- UObject* Referenced = ValueObj->GetObjectPropertyValue(Helper.GetValuePtr(MapIt));
147
- if (Referenced && !Referenced->IsA<UChooserTable>())
148
- {
149
- FChooserObjectRef Ref;
150
- Ref.ChooserPath = ChooserPath;
151
- Ref.Location = FString::Printf(TEXT("%s[map:%d]"), *Location, MapIt.GetInternalIndex());
152
- Ref.StructType = StructTypeName;
153
- Ref.ObjectPath = Referenced->GetPathName();
154
- Ref.Property = MapProp->ValueProp;
155
- Ref.ValueAddr = Helper.GetValuePtr(MapIt);
156
- Ref.Owner = Owner;
157
- OutRefs.Add(Ref);
158
- }
159
- }
160
- }
161
- }
162
- else if (FArrayProperty* ArrayProp = CastField<FArrayProperty>(Prop))
163
- {
164
- FScriptArrayHelper Helper(ArrayProp, Value);
165
- for (int32 i = 0; i < Helper.Num(); ++i)
166
- {
167
- const FString Indexed = FString::Printf(TEXT("%s[%d]"), *Location, i);
168
- if (FStructProperty* InnerStruct = CastField<FStructProperty>(ArrayProp->Inner))
169
- {
170
- CollectRefsInStruct(InnerStruct->Struct, Helper.GetRawPtr(i), Owner,
171
- ChooserPath, Indexed, StructTypeName, OutRefs, Depth + 1);
172
- }
173
- else if (FSoftObjectProperty* InnerSoft = CastField<FSoftObjectProperty>(ArrayProp->Inner))
174
- {
175
- FChooserObjectRef Ref;
176
- Ref.ChooserPath = ChooserPath;
177
- Ref.Location = Indexed;
178
- Ref.StructType = StructTypeName;
179
- Ref.ObjectPath = InnerSoft->GetPropertyValue(Helper.GetRawPtr(i)).ToString();
180
- Ref.Property = ArrayProp->Inner;
181
- Ref.ValueAddr = Helper.GetRawPtr(i);
182
- Ref.Owner = Owner;
183
- if (!Ref.ObjectPath.IsEmpty()) OutRefs.Add(Ref);
184
- }
185
- else if (FObjectPropertyBase* InnerObj = CastField<FObjectPropertyBase>(ArrayProp->Inner))
186
- {
187
- UObject* Referenced = InnerObj->GetObjectPropertyValue(Helper.GetRawPtr(i));
188
- if (Referenced && !Referenced->IsA<UChooserTable>())
189
- {
190
- FChooserObjectRef Ref;
191
- Ref.ChooserPath = ChooserPath;
192
- Ref.Location = Indexed;
193
- Ref.StructType = StructTypeName;
194
- Ref.ObjectPath = Referenced->GetPathName();
195
- Ref.Property = ArrayProp->Inner;
196
- Ref.ValueAddr = Helper.GetRawPtr(i);
197
- Ref.Owner = Owner;
198
- OutRefs.Add(Ref);
199
- }
200
- }
201
- }
202
- }
203
- }
204
- }
205
-
206
- /** Every UChooserTable nested under a root, including the root itself. */
207
- void CollectNestedChoosers(UChooserTable* Root, TArray<UChooserTable*>& Out, int32 Depth = 0)
208
- {
209
- if (!Root || Depth > 8 || Out.Contains(Root)) return;
210
- Out.Add(Root);
211
-
212
- #if WITH_EDITORONLY_DATA
213
- // Declared nested tables.
214
- for (const TObjectPtr<UChooserTable>& Nested : Root->NestedChoosers)
215
- {
216
- CollectNestedChoosers(Nested, Out, Depth + 1);
217
- }
218
- #endif
219
- // Tables referenced from result structs (FNestedChooser/FEvaluateChooser
220
- // both hold a Chooser pointer), reached by reflection so the exact
221
- // struct type does not have to be known here.
222
- auto FollowStructArray = [&](TArray<FInstancedStruct>& Structs)
223
- {
224
- for (FInstancedStruct& Item : Structs)
225
- {
226
- const UScriptStruct* ScriptStruct = Item.GetScriptStruct();
227
- if (!ScriptStruct) continue;
228
- void* Memory = Item.GetMutableMemory();
229
- if (!Memory) continue;
230
- for (TFieldIterator<FProperty> It(ScriptStruct); It; ++It)
231
- {
232
- if (FObjectPropertyBase* ObjProp = CastField<FObjectPropertyBase>(*It))
233
- {
234
- UObject* Referenced = ObjProp->GetObjectPropertyValue(
235
- ObjProp->ContainerPtrToValuePtr<void>(Memory));
236
- if (UChooserTable* Table = Cast<UChooserTable>(Referenced))
237
- {
238
- CollectNestedChoosers(Table, Out, Depth + 1);
239
- }
240
- }
241
- }
242
- }
243
- };
244
- #if WITH_EDITORONLY_DATA
245
- FollowStructArray(Root->ResultsStructs);
246
- #endif
247
- FollowStructArray(Root->ColumnsStructs);
248
- }
249
-
250
- /** Gather every leaf object reference across a chooser and its nested tables. */
251
- void GatherAllRefs(UChooserTable* Root, TArray<FChooserObjectRef>& OutRefs)
252
- {
253
- TArray<UChooserTable*> Tables;
254
- CollectNestedChoosers(Root, Tables);
255
-
256
- for (UChooserTable* Table : Tables)
257
- {
258
- if (!Table) continue;
259
- const FString TablePath = Table->GetPathName();
260
-
261
- auto WalkArray = [&](TArray<FInstancedStruct>& Structs, const TCHAR* ArrayName)
262
- {
263
- for (int32 i = 0; i < Structs.Num(); ++i)
264
- {
265
- const UScriptStruct* ScriptStruct = Structs[i].GetScriptStruct();
266
- void* Memory = Structs[i].GetMutableMemory();
267
- if (!ScriptStruct || !Memory) continue;
268
- CollectRefsInStruct(ScriptStruct, Memory, Table, TablePath,
269
- FString::Printf(TEXT("%s[%d]"), ArrayName, i),
270
- ScriptStruct->GetName(), OutRefs, 0);
271
- }
272
- };
273
- #if WITH_EDITORONLY_DATA
274
- WalkArray(Table->ResultsStructs, TEXT("ResultsStructs"));
275
- #endif
276
- WalkArray(Table->ColumnsStructs, TEXT("ColumnsStructs"));
277
- }
278
- }
279
-
280
- TSharedPtr<FJsonObject> RefToJson(const FChooserObjectRef& Ref)
281
- {
282
- TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
283
- Obj->SetStringField(TEXT("chooser"), Ref.ChooserPath);
284
- Obj->SetStringField(TEXT("location"), Ref.Location);
285
- Obj->SetStringField(TEXT("structType"), Ref.StructType);
286
- Obj->SetStringField(TEXT("objectPath"), Ref.ObjectPath);
287
- return Obj;
288
- }
289
- }
290
-
291
- // chooser(list_object_references): every leaf object reference reachable from a
292
- // chooser, descending through nested tables.
293
- TSharedPtr<FJsonValue> FChooserHandlers::ListObjectReferences(const TSharedPtr<FJsonObject>& Params)
294
- {
295
- FString AssetPath;
296
- if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), AssetPath)) return Err;
297
-
298
- UChooserTable* Chooser = Cast<UChooserTable>(UEditorAssetLibrary::LoadAsset(AssetPath));
299
- if (!Chooser)
300
- {
301
- return MCPError(FString::Printf(TEXT("ChooserTable not found: %s"), *AssetPath));
302
- }
303
-
304
- const FString ClassFilter = OptionalString(Params, TEXT("classFilter"));
305
- const FString PathFilter = OptionalString(Params, TEXT("pathFilter"));
306
-
307
- TArray<FChooserObjectRef> Refs;
308
- GatherAllRefs(Chooser, Refs);
309
-
310
- TArray<UChooserTable*> Tables;
311
- CollectNestedChoosers(Chooser, Tables);
312
-
313
- TArray<TSharedPtr<FJsonValue>> Entries;
314
- for (const FChooserObjectRef& Ref : Refs)
315
- {
316
- if (!PathFilter.IsEmpty() && !Ref.ObjectPath.Contains(PathFilter, ESearchCase::IgnoreCase)) continue;
317
- if (!ClassFilter.IsEmpty())
318
- {
319
- // Resolve lazily: only load when the caller actually filters by class.
320
- UObject* Resolved = FindObject<UObject>(nullptr, *Ref.ObjectPath);
321
- if (!Resolved) Resolved = LoadObject<UObject>(nullptr, *Ref.ObjectPath);
322
- if (!Resolved || !Resolved->GetClass()->GetName().Contains(ClassFilter, ESearchCase::IgnoreCase))
323
- {
324
- continue;
325
- }
326
- }
327
- Entries.Add(MakeShared<FJsonValueObject>(RefToJson(Ref)));
328
- }
329
-
330
- TArray<TSharedPtr<FJsonValue>> TablePaths;
331
- for (UChooserTable* Table : Tables)
332
- {
333
- if (Table) TablePaths.Add(MakeShared<FJsonValueString>(Table->GetPathName()));
334
- }
335
-
336
- auto Result = MCPSuccess();
337
- Result->SetStringField(TEXT("assetPath"), AssetPath);
338
- Result->SetArrayField(TEXT("tables"), TablePaths);
339
- Result->SetNumberField(TEXT("tableCount"), TablePaths.Num());
340
- Result->SetArrayField(TEXT("references"), Entries);
341
- Result->SetNumberField(TEXT("referenceCount"), Entries.Num());
342
- Result->SetNumberField(TEXT("totalReferences"), Refs.Num());
343
- return MCPResult(Result);
344
- }
345
-
346
- // chooser(remap_object_references): repoint references matching `from` to `to`,
347
- // across the whole nested structure. Dry-run by default.
348
- TSharedPtr<FJsonValue> FChooserHandlers::RemapObjectReferences(const TSharedPtr<FJsonObject>& Params)
349
- {
350
- FString AssetPath;
351
- if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), AssetPath)) return Err;
352
-
353
- UChooserTable* Chooser = Cast<UChooserTable>(UEditorAssetLibrary::LoadAsset(AssetPath));
354
- if (!Chooser)
355
- {
356
- return MCPError(FString::Printf(TEXT("ChooserTable not found: %s"), *AssetPath));
357
- }
358
-
359
- // Either an exact from/to pair, or a prefix rewrite for the common
360
- // "adopt a vendor folder into our namespace" case.
361
- const FString From = OptionalString(Params, TEXT("from"));
362
- const FString To = OptionalString(Params, TEXT("to"));
363
- const FString FromPrefix = OptionalString(Params, TEXT("fromPrefix"));
364
- const FString ToPrefix = OptionalString(Params, TEXT("toPrefix"));
365
- const bool bExact = !From.IsEmpty() && !To.IsEmpty();
366
- const bool bPrefix = !FromPrefix.IsEmpty() && !ToPrefix.IsEmpty();
367
- if (bExact == bPrefix)
368
- {
369
- return MCPError(TEXT("Provide exactly one of: from+to (exact path swap), or fromPrefix+toPrefix (folder rewrite)"));
370
- }
371
- // Default to a dry run: this rewrites asset references in place.
372
- const bool bDryRun = OptionalBool(Params, TEXT("dryRun"), true);
373
-
374
- TArray<FChooserObjectRef> Refs;
375
- GatherAllRefs(Chooser, Refs);
376
-
377
- // ValueAddr points into TArray storage inside an FInstancedStruct, and any
378
- // load can run PostLoad and editor callbacks that resize those arrays,
379
- // leaving every later address dangling. So resolve/validate EVERY target in
380
- // a first pass (loads allowed), then re-walk to get fresh addresses and
381
- // write in a second pass with no loading at all.
382
- const FString NormFrom = NormalizeObjectPath(From);
383
- TArray<TSharedPtr<FJsonValue>> Changes;
384
- TSet<UObject*> Touched;
385
- int32 Rewritten = 0;
386
- // Rewriting references across a nested chooser tree must be undoable.
387
- TUniquePtr<FScopedTransaction> Transaction;
388
- if (!bDryRun)
389
- {
390
- Transaction = MakeUnique<FScopedTransaction>(FText::FromString(TEXT("MCP chooser remap object references")));
391
- }
392
-
393
- // Pass 1: decide and resolve. May load; must not write. Skipped entirely on
394
- // a dry run - a preview writes nothing, so loading every destination asset
395
- // to produce it is pure cost, and dryRun is the default.
396
- //
397
- // Targets are held strongly: a load here can compile a Blueprint, which
398
- // collects garbage, and nothing else references an asset loaded a moment
399
- // ago until pass 2 writes it.
400
- TMap<FString, TStrongObjectPtr<UObject>> ResolvedTargets;
401
- for (const FChooserObjectRef& Ref : Refs)
402
- {
403
- if (bDryRun) break;
404
- const FString Cur = NormalizeObjectPath(Ref.ObjectPath);
405
- FString Candidate;
406
- if (bExact)
407
- {
408
- if (Cur != NormFrom) continue;
409
- Candidate = To;
410
- }
411
- else
412
- {
413
- if (!Cur.StartsWith(FromPrefix, ESearchCase::IgnoreCase)) continue;
414
- const int32 At = Ref.ObjectPath.Find(FromPrefix, ESearchCase::IgnoreCase);
415
- if (At == INDEX_NONE) continue;
416
- Candidate = Ref.ObjectPath.Left(At) + ToPrefix + Ref.ObjectPath.RightChop(At + FromPrefix.Len());
417
- }
418
- if (ResolvedTargets.Contains(Candidate)) continue;
419
- UObject* Found = FindObject<UObject>(nullptr, *Candidate);
420
- if (!Found) Found = LoadObject<UObject>(nullptr, *Candidate);
421
- ResolvedTargets.Add(Candidate, TStrongObjectPtr<UObject>(Found));
422
- }
423
-
424
- // Pass 2: re-walk so ValueAddr is fresh after any loading above, then write.
425
- // A ref that only became visible because pass 1's loads resized the arrays
426
- // is reported as unresolved rather than written blind - see the miss branch.
427
- if (!bDryRun)
428
- {
429
- Refs.Reset();
430
- GatherAllRefs(Chooser, Refs);
431
- }
432
-
433
- for (FChooserObjectRef& Ref : Refs)
434
- {
435
- const FString Current = NormalizeObjectPath(Ref.ObjectPath);
436
- FString NewPath;
437
- if (bExact)
438
- {
439
- if (Current != NormFrom) continue;
440
- NewPath = To;
441
- }
442
- else
443
- {
444
- if (!Current.StartsWith(FromPrefix, ESearchCase::IgnoreCase)) continue;
445
- // Rewrite the ORIGINAL reference, not the normalized one. Normalize
446
- // collapses "/Game/Foo/Bar.Bar" to "/Game/Foo/Bar", and building the
447
- // replacement from that produced a package-only path: soft refs
448
- // became unresolvable and hard refs resolved to the UPackage.
449
- const FString Original = Ref.ObjectPath;
450
- const int32 PrefixAt = Original.Find(FromPrefix, ESearchCase::IgnoreCase);
451
- if (PrefixAt == INDEX_NONE) continue;
452
- NewPath = Original.Left(PrefixAt) + ToPrefix + Original.RightChop(PrefixAt + FromPrefix.Len());
453
- }
454
-
455
- TSharedPtr<FJsonObject> Change = RefToJson(Ref);
456
- Change->SetStringField(TEXT("newObjectPath"), NewPath);
457
-
458
- if (!bDryRun)
459
- {
460
- bool bOk = false;
461
- if (FSoftObjectProperty* SoftProp = CastField<FSoftObjectProperty>(Ref.Property))
462
- {
463
- // Validate before writing: a soft ref accepts any string, so an
464
- // unresolvable path would be stored silently and reported as a
465
- // successful rewrite.
466
- const FSoftObjectPath NewSoftPath(NewPath);
467
- if (!NewSoftPath.IsValid())
468
- {
469
- Change->SetStringField(TEXT("error"), FString::Printf(TEXT("Not a valid object path: %s"), *NewPath));
470
- }
471
- else if (!OptionalBool(Params, TEXT("allowMissing"), false) && !ResolvedTargets.FindRef(NewPath).IsValid())
472
- {
473
- Change->SetStringField(TEXT("error"), FString::Printf(
474
- TEXT("Target does not exist: %s (pass allowMissing=true to write it anyway)"), *NewPath));
475
- }
476
- else
477
- {
478
- if (Ref.Owner) Ref.Owner->Modify();
479
- SoftProp->SetPropertyValue(Ref.ValueAddr, FSoftObjectPtr(NewSoftPath));
480
- bOk = true;
481
- }
482
- }
483
- else if (FObjectPropertyBase* ObjProp = CastField<FObjectPropertyBase>(Ref.Property))
484
- {
485
- // Resolved in pass 1; no loading here, so ValueAddr stays valid.
486
- UObject* NewTarget = ResolvedTargets.FindRef(NewPath).Get();
487
- if (!NewTarget)
488
- {
489
- Change->SetStringField(TEXT("error"), FString::Printf(TEXT("Target not found: %s"), *NewPath));
490
- }
491
- else if (ObjProp->PropertyClass && !NewTarget->IsA(ObjProp->PropertyClass))
492
- {
493
- Change->SetStringField(TEXT("error"), FString::Printf(
494
- TEXT("%s is a %s, not a %s"), *NewPath,
495
- *NewTarget->GetClass()->GetName(), *ObjProp->PropertyClass->GetName()));
496
- }
497
- else
498
- {
499
- if (Ref.Owner) Ref.Owner->Modify();
500
- ObjProp->SetObjectPropertyValue(Ref.ValueAddr, NewTarget);
501
- bOk = true;
502
- }
503
- }
504
- Change->SetBoolField(TEXT("rewritten"), bOk);
505
- if (bOk)
506
- {
507
- ++Rewritten;
508
- if (Ref.Owner) Touched.Add(Ref.Owner);
509
- }
510
- }
511
- Changes.Add(MakeShared<FJsonValueObject>(Change));
512
- }
513
-
514
- if (!bDryRun && Rewritten > 0)
515
- {
516
- // Recompile inside the transaction scope opened below by the caller's
517
- // undo buffer entry; without a transaction Modify() only dirties.
518
- for (UObject* Object : Touched)
519
- {
520
- if (Object) Object->MarkPackageDirty();
521
- }
522
- Chooser->Compile(/*bForce=*/true);
523
- Chooser->MarkPackageDirty();
524
- }
525
-
526
- auto Result = MCPSuccess();
527
- if (!bDryRun && Rewritten > 0) MCPSetUpdated(Result);
528
- Result->SetStringField(TEXT("assetPath"), AssetPath);
529
- Result->SetBoolField(TEXT("dryRun"), bDryRun);
530
- Result->SetNumberField(TEXT("matched"), Changes.Num());
531
- Result->SetNumberField(TEXT("rewritten"), Rewritten);
532
- Result->SetArrayField(TEXT("changes"), Changes);
533
- Result->SetStringField(TEXT("note"), bDryRun
534
- ? TEXT("Dry run - nothing was written. Re-run with dryRun=false to apply.")
535
- : TEXT("References rewritten and the chooser recompiled. The asset is left dirty; save it when ready."));
536
- return MCPResult(Result);
537
- }
1
+ // Nested-chooser object reference reading and remapping (#754).
2
+ //
3
+ // A ChooserTable that outputs objects stores its real references inside
4
+ // in-asset NestedChooser sub-tables: a column's rows EvaluateChooser into
5
+ // further UChooserTables whose own rows hold the leaf object paths.
6
+ // chooser(list_rows) rendered those as an opaque resultType:NestedChooser with
7
+ // an empty output, so there was no way to see - let alone repoint - the actual
8
+ // references.
9
+ //
10
+ // The reporter concluded this was impossible because Python's
11
+ // get_editor_property refused to read UChooserTable::ResultsStructs. That is a
12
+ // Python restriction (it enforces Blueprint visibility metadata), NOT a C++
13
+ // one: FProperty reflection ignores C++ access specifiers entirely, so the
14
+ // bridge can walk these structures directly. No engine fork required.
15
+ //
16
+ // Translation-unit partition of FChooserHandlers; registrations live in
17
+ // ChooserHandlers.cpp.
18
+
19
+ #include "ChooserHandlers.h"
20
+
21
+ #include "HandlerRegistry.h"
22
+ #include "HandlerUtils.h"
23
+
24
+ #include "UObject/StrongObjectPtr.h"
25
+
26
+ #include "Chooser.h"
27
+ #include "StructUtils/InstancedStruct.h"
28
+ #include "EditorAssetLibrary.h"
29
+ #include "ScopedTransaction.h"
30
+ #include "UObject/UnrealType.h"
31
+ #include "UObject/SoftObjectPtr.h"
32
+
33
+ namespace
34
+ {
35
+ /** One object reference found somewhere inside a chooser's structures. */
36
+ struct FChooserObjectRef
37
+ {
38
+ FString ChooserPath; // which (possibly nested) table it lives in
39
+ FString Location; // e.g. "ResultsStructs[3].Asset"
40
+ FString StructType; // the FInstancedStruct's script struct name
41
+ FString ObjectPath; // current value
42
+ FProperty* Property = nullptr;
43
+ void* ValueAddr = nullptr;
44
+ UObject* Owner = nullptr;
45
+ };
46
+
47
+ FString NormalizeObjectPath(const FString& In)
48
+ {
49
+ // "/Game/Foo/Bar.Bar" and "/Game/Foo/Bar" should compare equal, and the
50
+ // editor writes some refs with a trailing _C or a subobject suffix.
51
+ FString Path = In;
52
+ Path.TrimStartAndEndInline();
53
+ if (Path.StartsWith(TEXT("'")) && Path.EndsWith(TEXT("'")))
54
+ {
55
+ Path = Path.Mid(1, Path.Len() - 2);
56
+ }
57
+ // Strip a class prefix like Blueprint'/Game/...'
58
+ int32 Quote = INDEX_NONE;
59
+ if (Path.FindChar(TEXT('\''), Quote))
60
+ {
61
+ Path = Path.Mid(Quote + 1).Replace(TEXT("'"), TEXT(""));
62
+ }
63
+ FString Left, Right;
64
+ if (Path.Split(TEXT("."), &Left, &Right, ESearchCase::CaseSensitive, ESearchDir::FromEnd))
65
+ {
66
+ // Only collapse "/Path/Name.Name" to "/Path/Name".
67
+ FString Tail;
68
+ if (Left.Split(TEXT("/"), nullptr, &Tail, ESearchCase::CaseSensitive, ESearchDir::FromEnd) && Tail == Right)
69
+ {
70
+ Path = Left;
71
+ }
72
+ }
73
+ return Path;
74
+ }
75
+
76
+ /** Recursively collect object/soft-object references inside a struct value. */
77
+ void CollectRefsInStruct(
78
+ const UScriptStruct* Struct,
79
+ void* StructMemory,
80
+ UObject* Owner,
81
+ const FString& ChooserPath,
82
+ const FString& LocationPrefix,
83
+ const FString& StructTypeName,
84
+ TArray<FChooserObjectRef>& OutRefs,
85
+ int32 Depth)
86
+ {
87
+ // Deep nesting is truncated rather than followed forever; the caller is
88
+ // told via truncatedDepth on the response so partial is not read as complete.
89
+ if (!Struct || !StructMemory || Depth > 6) return;
90
+
91
+ for (TFieldIterator<FProperty> It(Struct); It; ++It)
92
+ {
93
+ FProperty* Prop = *It;
94
+ if (!Prop) continue;
95
+ void* Value = Prop->ContainerPtrToValuePtr<void>(StructMemory);
96
+ const FString Location = LocationPrefix + TEXT(".") + Prop->GetName();
97
+
98
+ if (FSoftObjectProperty* SoftProp = CastField<FSoftObjectProperty>(Prop))
99
+ {
100
+ const FSoftObjectPtr& Ptr = SoftProp->GetPropertyValue(Value);
101
+ FChooserObjectRef Ref;
102
+ Ref.ChooserPath = ChooserPath;
103
+ Ref.Location = Location;
104
+ Ref.StructType = StructTypeName;
105
+ Ref.ObjectPath = Ptr.ToString();
106
+ Ref.Property = Prop;
107
+ Ref.ValueAddr = Value;
108
+ Ref.Owner = Owner;
109
+ if (!Ref.ObjectPath.IsEmpty()) OutRefs.Add(Ref);
110
+ }
111
+ else if (FObjectPropertyBase* ObjProp = CastField<FObjectPropertyBase>(Prop))
112
+ {
113
+ UObject* Referenced = ObjProp->GetObjectPropertyValue(Value);
114
+ // A nested UChooserTable is followed separately by the caller;
115
+ // record everything else as a leaf reference.
116
+ if (Referenced && !Referenced->IsA<UChooserTable>())
117
+ {
118
+ FChooserObjectRef Ref;
119
+ Ref.ChooserPath = ChooserPath;
120
+ Ref.Location = Location;
121
+ Ref.StructType = StructTypeName;
122
+ Ref.ObjectPath = Referenced->GetPathName();
123
+ Ref.Property = Prop;
124
+ Ref.ValueAddr = Value;
125
+ Ref.Owner = Owner;
126
+ OutRefs.Add(Ref);
127
+ }
128
+ }
129
+ else if (FStructProperty* StructProp = CastField<FStructProperty>(Prop))
130
+ {
131
+ CollectRefsInStruct(StructProp->Struct, Value, Owner, ChooserPath, Location,
132
+ StructTypeName, OutRefs, Depth + 1);
133
+ }
134
+ else if (FMapProperty* MapProp = CastField<FMapProperty>(Prop))
135
+ {
136
+ FScriptMapHelper Helper(MapProp, Value);
137
+ for (FScriptMapHelper::FIterator MapIt = Helper.CreateIterator(); MapIt; ++MapIt)
138
+ {
139
+ if (FStructProperty* ValueStruct = CastField<FStructProperty>(MapProp->ValueProp))
140
+ {
141
+ CollectRefsInStruct(ValueStruct->Struct, Helper.GetValuePtr(MapIt), Owner,
142
+ ChooserPath, FString::Printf(TEXT("%s[map:%d]"), *Location, MapIt.GetInternalIndex()), StructTypeName, OutRefs, Depth + 1);
143
+ }
144
+ else if (FObjectPropertyBase* ValueObj = CastField<FObjectPropertyBase>(MapProp->ValueProp))
145
+ {
146
+ UObject* Referenced = ValueObj->GetObjectPropertyValue(Helper.GetValuePtr(MapIt));
147
+ if (Referenced && !Referenced->IsA<UChooserTable>())
148
+ {
149
+ FChooserObjectRef Ref;
150
+ Ref.ChooserPath = ChooserPath;
151
+ Ref.Location = FString::Printf(TEXT("%s[map:%d]"), *Location, MapIt.GetInternalIndex());
152
+ Ref.StructType = StructTypeName;
153
+ Ref.ObjectPath = Referenced->GetPathName();
154
+ Ref.Property = MapProp->ValueProp;
155
+ Ref.ValueAddr = Helper.GetValuePtr(MapIt);
156
+ Ref.Owner = Owner;
157
+ OutRefs.Add(Ref);
158
+ }
159
+ }
160
+ }
161
+ }
162
+ else if (FArrayProperty* ArrayProp = CastField<FArrayProperty>(Prop))
163
+ {
164
+ FScriptArrayHelper Helper(ArrayProp, Value);
165
+ for (int32 i = 0; i < Helper.Num(); ++i)
166
+ {
167
+ const FString Indexed = FString::Printf(TEXT("%s[%d]"), *Location, i);
168
+ if (FStructProperty* InnerStruct = CastField<FStructProperty>(ArrayProp->Inner))
169
+ {
170
+ CollectRefsInStruct(InnerStruct->Struct, Helper.GetRawPtr(i), Owner,
171
+ ChooserPath, Indexed, StructTypeName, OutRefs, Depth + 1);
172
+ }
173
+ else if (FSoftObjectProperty* InnerSoft = CastField<FSoftObjectProperty>(ArrayProp->Inner))
174
+ {
175
+ FChooserObjectRef Ref;
176
+ Ref.ChooserPath = ChooserPath;
177
+ Ref.Location = Indexed;
178
+ Ref.StructType = StructTypeName;
179
+ Ref.ObjectPath = InnerSoft->GetPropertyValue(Helper.GetRawPtr(i)).ToString();
180
+ Ref.Property = ArrayProp->Inner;
181
+ Ref.ValueAddr = Helper.GetRawPtr(i);
182
+ Ref.Owner = Owner;
183
+ if (!Ref.ObjectPath.IsEmpty()) OutRefs.Add(Ref);
184
+ }
185
+ else if (FObjectPropertyBase* InnerObj = CastField<FObjectPropertyBase>(ArrayProp->Inner))
186
+ {
187
+ UObject* Referenced = InnerObj->GetObjectPropertyValue(Helper.GetRawPtr(i));
188
+ if (Referenced && !Referenced->IsA<UChooserTable>())
189
+ {
190
+ FChooserObjectRef Ref;
191
+ Ref.ChooserPath = ChooserPath;
192
+ Ref.Location = Indexed;
193
+ Ref.StructType = StructTypeName;
194
+ Ref.ObjectPath = Referenced->GetPathName();
195
+ Ref.Property = ArrayProp->Inner;
196
+ Ref.ValueAddr = Helper.GetRawPtr(i);
197
+ Ref.Owner = Owner;
198
+ OutRefs.Add(Ref);
199
+ }
200
+ }
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ /** Every UChooserTable nested under a root, including the root itself. */
207
+ void CollectNestedChoosers(UChooserTable* Root, TArray<UChooserTable*>& Out, int32 Depth = 0)
208
+ {
209
+ if (!Root || Depth > 8 || Out.Contains(Root)) return;
210
+ Out.Add(Root);
211
+
212
+ #if WITH_EDITORONLY_DATA
213
+ // Declared nested tables.
214
+ for (const TObjectPtr<UChooserTable>& Nested : Root->NestedChoosers)
215
+ {
216
+ CollectNestedChoosers(Nested, Out, Depth + 1);
217
+ }
218
+ #endif
219
+ // Tables referenced from result structs (FNestedChooser/FEvaluateChooser
220
+ // both hold a Chooser pointer), reached by reflection so the exact
221
+ // struct type does not have to be known here.
222
+ auto FollowStructArray = [&](TArray<FInstancedStruct>& Structs)
223
+ {
224
+ for (FInstancedStruct& Item : Structs)
225
+ {
226
+ const UScriptStruct* ScriptStruct = Item.GetScriptStruct();
227
+ if (!ScriptStruct) continue;
228
+ void* Memory = Item.GetMutableMemory();
229
+ if (!Memory) continue;
230
+ for (TFieldIterator<FProperty> It(ScriptStruct); It; ++It)
231
+ {
232
+ if (FObjectPropertyBase* ObjProp = CastField<FObjectPropertyBase>(*It))
233
+ {
234
+ UObject* Referenced = ObjProp->GetObjectPropertyValue(
235
+ ObjProp->ContainerPtrToValuePtr<void>(Memory));
236
+ if (UChooserTable* Table = Cast<UChooserTable>(Referenced))
237
+ {
238
+ CollectNestedChoosers(Table, Out, Depth + 1);
239
+ }
240
+ }
241
+ }
242
+ }
243
+ };
244
+ #if WITH_EDITORONLY_DATA
245
+ FollowStructArray(Root->ResultsStructs);
246
+ #endif
247
+ FollowStructArray(Root->ColumnsStructs);
248
+ }
249
+
250
+ /** Gather every leaf object reference across a chooser and its nested tables. */
251
+ void GatherAllRefs(UChooserTable* Root, TArray<FChooserObjectRef>& OutRefs)
252
+ {
253
+ TArray<UChooserTable*> Tables;
254
+ CollectNestedChoosers(Root, Tables);
255
+
256
+ for (UChooserTable* Table : Tables)
257
+ {
258
+ if (!Table) continue;
259
+ const FString TablePath = Table->GetPathName();
260
+
261
+ auto WalkArray = [&](TArray<FInstancedStruct>& Structs, const TCHAR* ArrayName)
262
+ {
263
+ for (int32 i = 0; i < Structs.Num(); ++i)
264
+ {
265
+ const UScriptStruct* ScriptStruct = Structs[i].GetScriptStruct();
266
+ void* Memory = Structs[i].GetMutableMemory();
267
+ if (!ScriptStruct || !Memory) continue;
268
+ CollectRefsInStruct(ScriptStruct, Memory, Table, TablePath,
269
+ FString::Printf(TEXT("%s[%d]"), ArrayName, i),
270
+ ScriptStruct->GetName(), OutRefs, 0);
271
+ }
272
+ };
273
+ #if WITH_EDITORONLY_DATA
274
+ WalkArray(Table->ResultsStructs, TEXT("ResultsStructs"));
275
+ #endif
276
+ WalkArray(Table->ColumnsStructs, TEXT("ColumnsStructs"));
277
+ }
278
+ }
279
+
280
+ TSharedPtr<FJsonObject> RefToJson(const FChooserObjectRef& Ref)
281
+ {
282
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
283
+ Obj->SetStringField(TEXT("chooser"), Ref.ChooserPath);
284
+ Obj->SetStringField(TEXT("location"), Ref.Location);
285
+ Obj->SetStringField(TEXT("structType"), Ref.StructType);
286
+ Obj->SetStringField(TEXT("objectPath"), Ref.ObjectPath);
287
+ return Obj;
288
+ }
289
+ }
290
+
291
+ // chooser(list_object_references): every leaf object reference reachable from a
292
+ // chooser, descending through nested tables.
293
+ TSharedPtr<FJsonValue> FChooserHandlers::ListObjectReferences(const TSharedPtr<FJsonObject>& Params)
294
+ {
295
+ FString AssetPath;
296
+ if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), AssetPath)) return Err;
297
+
298
+ UChooserTable* Chooser = Cast<UChooserTable>(UEditorAssetLibrary::LoadAsset(AssetPath));
299
+ if (!Chooser)
300
+ {
301
+ return MCPError(FString::Printf(TEXT("ChooserTable not found: %s"), *AssetPath));
302
+ }
303
+
304
+ const FString ClassFilter = OptionalString(Params, TEXT("classFilter"));
305
+ const FString PathFilter = OptionalString(Params, TEXT("pathFilter"));
306
+
307
+ TArray<FChooserObjectRef> Refs;
308
+ GatherAllRefs(Chooser, Refs);
309
+
310
+ TArray<UChooserTable*> Tables;
311
+ CollectNestedChoosers(Chooser, Tables);
312
+
313
+ TArray<TSharedPtr<FJsonValue>> Entries;
314
+ for (const FChooserObjectRef& Ref : Refs)
315
+ {
316
+ if (!PathFilter.IsEmpty() && !Ref.ObjectPath.Contains(PathFilter, ESearchCase::IgnoreCase)) continue;
317
+ if (!ClassFilter.IsEmpty())
318
+ {
319
+ // Resolve lazily: only load when the caller actually filters by class.
320
+ UObject* Resolved = FindObject<UObject>(nullptr, *Ref.ObjectPath);
321
+ if (!Resolved) Resolved = LoadObject<UObject>(nullptr, *Ref.ObjectPath);
322
+ if (!Resolved || !Resolved->GetClass()->GetName().Contains(ClassFilter, ESearchCase::IgnoreCase))
323
+ {
324
+ continue;
325
+ }
326
+ }
327
+ Entries.Add(MakeShared<FJsonValueObject>(RefToJson(Ref)));
328
+ }
329
+
330
+ TArray<TSharedPtr<FJsonValue>> TablePaths;
331
+ for (UChooserTable* Table : Tables)
332
+ {
333
+ if (Table) TablePaths.Add(MakeShared<FJsonValueString>(Table->GetPathName()));
334
+ }
335
+
336
+ auto Result = MCPSuccess();
337
+ Result->SetStringField(TEXT("assetPath"), AssetPath);
338
+ Result->SetArrayField(TEXT("tables"), TablePaths);
339
+ Result->SetNumberField(TEXT("tableCount"), TablePaths.Num());
340
+ Result->SetArrayField(TEXT("references"), Entries);
341
+ Result->SetNumberField(TEXT("referenceCount"), Entries.Num());
342
+ Result->SetNumberField(TEXT("totalReferences"), Refs.Num());
343
+ return MCPResult(Result);
344
+ }
345
+
346
+ // chooser(remap_object_references): repoint references matching `from` to `to`,
347
+ // across the whole nested structure. Dry-run by default.
348
+ TSharedPtr<FJsonValue> FChooserHandlers::RemapObjectReferences(const TSharedPtr<FJsonObject>& Params)
349
+ {
350
+ FString AssetPath;
351
+ if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), AssetPath)) return Err;
352
+
353
+ UChooserTable* Chooser = Cast<UChooserTable>(UEditorAssetLibrary::LoadAsset(AssetPath));
354
+ if (!Chooser)
355
+ {
356
+ return MCPError(FString::Printf(TEXT("ChooserTable not found: %s"), *AssetPath));
357
+ }
358
+
359
+ // Either an exact from/to pair, or a prefix rewrite for the common
360
+ // "adopt a vendor folder into our namespace" case.
361
+ const FString From = OptionalString(Params, TEXT("from"));
362
+ const FString To = OptionalString(Params, TEXT("to"));
363
+ const FString FromPrefix = OptionalString(Params, TEXT("fromPrefix"));
364
+ const FString ToPrefix = OptionalString(Params, TEXT("toPrefix"));
365
+ const bool bExact = !From.IsEmpty() && !To.IsEmpty();
366
+ const bool bPrefix = !FromPrefix.IsEmpty() && !ToPrefix.IsEmpty();
367
+ if (bExact == bPrefix)
368
+ {
369
+ return MCPError(TEXT("Provide exactly one of: from+to (exact path swap), or fromPrefix+toPrefix (folder rewrite)"));
370
+ }
371
+ // Default to a dry run: this rewrites asset references in place.
372
+ const bool bDryRun = OptionalBool(Params, TEXT("dryRun"), true);
373
+
374
+ TArray<FChooserObjectRef> Refs;
375
+ GatherAllRefs(Chooser, Refs);
376
+
377
+ // ValueAddr points into TArray storage inside an FInstancedStruct, and any
378
+ // load can run PostLoad and editor callbacks that resize those arrays,
379
+ // leaving every later address dangling. So resolve/validate EVERY target in
380
+ // a first pass (loads allowed), then re-walk to get fresh addresses and
381
+ // write in a second pass with no loading at all.
382
+ const FString NormFrom = NormalizeObjectPath(From);
383
+ TArray<TSharedPtr<FJsonValue>> Changes;
384
+ TSet<UObject*> Touched;
385
+ int32 Rewritten = 0;
386
+ // Rewriting references across a nested chooser tree must be undoable.
387
+ TUniquePtr<FScopedTransaction> Transaction;
388
+ if (!bDryRun)
389
+ {
390
+ Transaction = MakeUnique<FScopedTransaction>(FText::FromString(TEXT("MCP chooser remap object references")));
391
+ }
392
+
393
+ // Pass 1: decide and resolve. May load; must not write. Skipped entirely on
394
+ // a dry run - a preview writes nothing, so loading every destination asset
395
+ // to produce it is pure cost, and dryRun is the default.
396
+ //
397
+ // Targets are held strongly: a load here can compile a Blueprint, which
398
+ // collects garbage, and nothing else references an asset loaded a moment
399
+ // ago until pass 2 writes it.
400
+ TMap<FString, TStrongObjectPtr<UObject>> ResolvedTargets;
401
+ for (const FChooserObjectRef& Ref : Refs)
402
+ {
403
+ if (bDryRun) break;
404
+ const FString Cur = NormalizeObjectPath(Ref.ObjectPath);
405
+ FString Candidate;
406
+ if (bExact)
407
+ {
408
+ if (Cur != NormFrom) continue;
409
+ Candidate = To;
410
+ }
411
+ else
412
+ {
413
+ if (!Cur.StartsWith(FromPrefix, ESearchCase::IgnoreCase)) continue;
414
+ const int32 At = Ref.ObjectPath.Find(FromPrefix, ESearchCase::IgnoreCase);
415
+ if (At == INDEX_NONE) continue;
416
+ Candidate = Ref.ObjectPath.Left(At) + ToPrefix + Ref.ObjectPath.RightChop(At + FromPrefix.Len());
417
+ }
418
+ if (ResolvedTargets.Contains(Candidate)) continue;
419
+ UObject* Found = FindObject<UObject>(nullptr, *Candidate);
420
+ if (!Found) Found = LoadObject<UObject>(nullptr, *Candidate);
421
+ ResolvedTargets.Add(Candidate, TStrongObjectPtr<UObject>(Found));
422
+ }
423
+
424
+ // Pass 2: re-walk so ValueAddr is fresh after any loading above, then write.
425
+ // A ref that only became visible because pass 1's loads resized the arrays
426
+ // is reported as unresolved rather than written blind - see the miss branch.
427
+ if (!bDryRun)
428
+ {
429
+ Refs.Reset();
430
+ GatherAllRefs(Chooser, Refs);
431
+ }
432
+
433
+ for (FChooserObjectRef& Ref : Refs)
434
+ {
435
+ const FString Current = NormalizeObjectPath(Ref.ObjectPath);
436
+ FString NewPath;
437
+ if (bExact)
438
+ {
439
+ if (Current != NormFrom) continue;
440
+ NewPath = To;
441
+ }
442
+ else
443
+ {
444
+ if (!Current.StartsWith(FromPrefix, ESearchCase::IgnoreCase)) continue;
445
+ // Rewrite the ORIGINAL reference, not the normalized one. Normalize
446
+ // collapses "/Game/Foo/Bar.Bar" to "/Game/Foo/Bar", and building the
447
+ // replacement from that produced a package-only path: soft refs
448
+ // became unresolvable and hard refs resolved to the UPackage.
449
+ const FString Original = Ref.ObjectPath;
450
+ const int32 PrefixAt = Original.Find(FromPrefix, ESearchCase::IgnoreCase);
451
+ if (PrefixAt == INDEX_NONE) continue;
452
+ NewPath = Original.Left(PrefixAt) + ToPrefix + Original.RightChop(PrefixAt + FromPrefix.Len());
453
+ }
454
+
455
+ TSharedPtr<FJsonObject> Change = RefToJson(Ref);
456
+ Change->SetStringField(TEXT("newObjectPath"), NewPath);
457
+
458
+ if (!bDryRun)
459
+ {
460
+ bool bOk = false;
461
+ if (FSoftObjectProperty* SoftProp = CastField<FSoftObjectProperty>(Ref.Property))
462
+ {
463
+ // Validate before writing: a soft ref accepts any string, so an
464
+ // unresolvable path would be stored silently and reported as a
465
+ // successful rewrite.
466
+ const FSoftObjectPath NewSoftPath(NewPath);
467
+ if (!NewSoftPath.IsValid())
468
+ {
469
+ Change->SetStringField(TEXT("error"), FString::Printf(TEXT("Not a valid object path: %s"), *NewPath));
470
+ }
471
+ else if (!OptionalBool(Params, TEXT("allowMissing"), false) && !ResolvedTargets.FindRef(NewPath).IsValid())
472
+ {
473
+ Change->SetStringField(TEXT("error"), FString::Printf(
474
+ TEXT("Target does not exist: %s (pass allowMissing=true to write it anyway)"), *NewPath));
475
+ }
476
+ else
477
+ {
478
+ if (Ref.Owner) Ref.Owner->Modify();
479
+ SoftProp->SetPropertyValue(Ref.ValueAddr, FSoftObjectPtr(NewSoftPath));
480
+ bOk = true;
481
+ }
482
+ }
483
+ else if (FObjectPropertyBase* ObjProp = CastField<FObjectPropertyBase>(Ref.Property))
484
+ {
485
+ // Resolved in pass 1; no loading here, so ValueAddr stays valid.
486
+ UObject* NewTarget = ResolvedTargets.FindRef(NewPath).Get();
487
+ if (!NewTarget)
488
+ {
489
+ Change->SetStringField(TEXT("error"), FString::Printf(TEXT("Target not found: %s"), *NewPath));
490
+ }
491
+ else if (ObjProp->PropertyClass && !NewTarget->IsA(ObjProp->PropertyClass))
492
+ {
493
+ Change->SetStringField(TEXT("error"), FString::Printf(
494
+ TEXT("%s is a %s, not a %s"), *NewPath,
495
+ *NewTarget->GetClass()->GetName(), *ObjProp->PropertyClass->GetName()));
496
+ }
497
+ else
498
+ {
499
+ if (Ref.Owner) Ref.Owner->Modify();
500
+ ObjProp->SetObjectPropertyValue(Ref.ValueAddr, NewTarget);
501
+ bOk = true;
502
+ }
503
+ }
504
+ Change->SetBoolField(TEXT("rewritten"), bOk);
505
+ if (bOk)
506
+ {
507
+ ++Rewritten;
508
+ if (Ref.Owner) Touched.Add(Ref.Owner);
509
+ }
510
+ }
511
+ Changes.Add(MakeShared<FJsonValueObject>(Change));
512
+ }
513
+
514
+ if (!bDryRun && Rewritten > 0)
515
+ {
516
+ // Recompile inside the transaction scope opened below by the caller's
517
+ // undo buffer entry; without a transaction Modify() only dirties.
518
+ for (UObject* Object : Touched)
519
+ {
520
+ if (Object) Object->MarkPackageDirty();
521
+ }
522
+ Chooser->Compile(/*bForce=*/true);
523
+ Chooser->MarkPackageDirty();
524
+ }
525
+
526
+ auto Result = MCPSuccess();
527
+ if (!bDryRun && Rewritten > 0) MCPSetUpdated(Result);
528
+ Result->SetStringField(TEXT("assetPath"), AssetPath);
529
+ Result->SetBoolField(TEXT("dryRun"), bDryRun);
530
+ Result->SetNumberField(TEXT("matched"), Changes.Num());
531
+ Result->SetNumberField(TEXT("rewritten"), Rewritten);
532
+ Result->SetArrayField(TEXT("changes"), Changes);
533
+ Result->SetStringField(TEXT("note"), bDryRun
534
+ ? TEXT("Dry run - nothing was written. Re-run with dryRun=false to apply.")
535
+ : TEXT("References rewritten and the chooser recompiled. The asset is left dirty; save it when ready."));
536
+ return MCPResult(Result);
537
+ }