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.
- package/dist/context-cli.js +16 -16
- package/dist/engine-observer.js +68 -68
- package/dist/flow/guard.d.ts +26 -37
- package/dist/flow/guard.js +40 -25
- package/dist/flow/guard.js.map +1 -1
- package/dist/flow/guarded-bridge.d.ts +0 -10
- package/dist/flow/guarded-bridge.js +18 -23
- package/dist/flow/guarded-bridge.js.map +1 -1
- package/dist/flow/schema.d.ts +276 -16
- package/dist/flow/task-guards.d.ts +1 -1
- package/dist/flow/task-guards.js +44 -68
- package/dist/flow/task-guards.js.map +1 -1
- package/dist/instructions.js +130 -130
- package/dist/plugin-cli.js +299 -299
- package/dist/tool-counts.json +2 -2
- package/package.json +2 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/EngineStatusHooks.cpp +76 -76
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/EngineStatusHooks.h +27 -27
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerRegistry.cpp +128 -128
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerRegistry.h +63 -63
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRig.cpp +230 -230
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Struct.cpp +323 -323
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Internal.h +20 -20
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/ChooserHandlers_Nested.cpp +537 -537
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DemoHandlers.h +77 -77
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_Build.cpp +353 -353
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.h +18 -18
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_InputRuntime.cpp +246 -246
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.cpp +597 -597
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.h +51 -51
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +878 -878
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers_Sculpt.cpp +449 -449
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Function.cpp +275 -275
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Graph.cpp +551 -551
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NetworkingHandlers.cpp +531 -531
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NetworkingHandlers.h +28 -28
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +677 -677
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.cpp +822 -822
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.h +22 -22
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.cpp +218 -218
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.h +17 -17
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/VolumeHelpers_Internal.cpp +46 -46
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/VolumeHelpers_Internal.h +23 -23
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/MCPExternalRegistry.cpp +83 -83
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/UE_MCP_BridgeModule.h +14 -14
- package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/UE_MCP_BridgeStatusModule.cpp +37 -37
- package/dist/manifest-signature.d.ts +0 -1
- package/dist/manifest-signature.js +0 -53
- package/dist/manifest-signature.js.map +0 -1
- package/dist/pie/schema.d.ts +0 -791
- package/dist/pie/schema.js +0 -151
- package/dist/pie/schema.js.map +0 -1
- package/dist/watch-cli.js.map +0 -1
- /package/plugin/ue_mcp_bridge/{ue_mcp_bridge.uplugin → UE_MCP_Bridge.uplugin} +0 -0
package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/ChooserHandlers_Nested.cpp
CHANGED
|
@@ -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
|
+
}
|