ue-mcp 1.1.19 → 1.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bridge.d.ts +18 -5
- package/dist/bridge.js +57 -14
- package/dist/bridge.js.map +1 -1
- package/dist/errors.d.ts +4 -1
- package/dist/errors.js +3 -0
- package/dist/errors.js.map +1 -1
- package/dist/global-config.d.ts +2 -2
- package/dist/global-config.js +2 -2
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/dist/init.js +7 -0
- package/dist/init.js.map +1 -1
- package/dist/locking.d.ts +29 -0
- package/dist/locking.js +136 -0
- package/dist/locking.js.map +1 -0
- package/dist/port.d.ts +16 -0
- package/dist/port.js +42 -0
- package/dist/port.js.map +1 -0
- package/dist/project.d.ts +11 -0
- package/dist/project.js +1 -1
- package/dist/project.js.map +1 -1
- package/dist/schemas.d.ts +51 -0
- package/dist/schemas.js +19 -0
- package/dist/schemas.js.map +1 -1
- package/dist/tool-counts.json +6 -6
- package/dist/tools/asset.js +8 -0
- package/dist/tools/asset.js.map +1 -1
- package/dist/tools/blueprint.js +1 -0
- package/dist/tools/blueprint.js.map +1 -1
- package/dist/tools/feedback.d.ts +1 -1
- package/dist/tools/feedback.js +17 -14
- package/dist/tools/feedback.js.map +1 -1
- package/dist/ue-mcp.default.yml +25 -1
- package/package.json +2 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/BridgeServer.cpp +65 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/BridgeServer.h +11 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.cpp +328 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.h +36 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LockHandlers.cpp +107 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LockHandlers.h +25 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NiagaraHandlers.cpp +66 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/LockRegistry.cpp +121 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/LockRegistry.h +67 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/UE_MCP_Bridge.cpp +8 -3
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
#include "Handlers/DiffHandlers.h"
|
|
2
|
+
#include "HandlerRegistry.h"
|
|
3
|
+
#include "HandlerUtils.h"
|
|
4
|
+
#include "Engine/Blueprint.h"
|
|
5
|
+
#include "EdGraph/EdGraph.h"
|
|
6
|
+
#include "EdGraph/EdGraphNode.h"
|
|
7
|
+
#include "EdGraph/EdGraphPin.h"
|
|
8
|
+
#include "Engine/SimpleConstructionScript.h"
|
|
9
|
+
#include "Engine/SCS_Node.h"
|
|
10
|
+
|
|
11
|
+
// ── Structural extraction helpers ────────────────────────────────────────────
|
|
12
|
+
namespace
|
|
13
|
+
{
|
|
14
|
+
/** Compact, stable string for a pin type, e.g. "int", "object(Actor)",
|
|
15
|
+
* "struct(Vector)[]". Used to detect variable type changes. */
|
|
16
|
+
FString PinTypeToShortString(const FEdGraphPinType& T)
|
|
17
|
+
{
|
|
18
|
+
FString S = T.PinCategory.ToString();
|
|
19
|
+
if (const UObject* Sub = T.PinSubCategoryObject.Get())
|
|
20
|
+
{
|
|
21
|
+
S += TEXT("(") + Sub->GetName() + TEXT(")");
|
|
22
|
+
}
|
|
23
|
+
else if (!T.PinSubCategory.IsNone())
|
|
24
|
+
{
|
|
25
|
+
S += TEXT("(") + T.PinSubCategory.ToString() + TEXT(")");
|
|
26
|
+
}
|
|
27
|
+
switch (T.ContainerType)
|
|
28
|
+
{
|
|
29
|
+
case EPinContainerType::Array: S += TEXT("[]"); break;
|
|
30
|
+
case EPinContainerType::Set: S += TEXT("{}"); break;
|
|
31
|
+
case EPinContainerType::Map: S += TEXT("{:}"); break;
|
|
32
|
+
default: break;
|
|
33
|
+
}
|
|
34
|
+
return S;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** All named graphs of a Blueprint keyed by graph name (event graphs +
|
|
38
|
+
* function graphs + macro graphs). */
|
|
39
|
+
TMap<FString, UEdGraph*> CollectGraphs(UBlueprint* BP)
|
|
40
|
+
{
|
|
41
|
+
TMap<FString, UEdGraph*> Out;
|
|
42
|
+
auto Add = [&Out](const TArray<UEdGraph*>& Graphs)
|
|
43
|
+
{
|
|
44
|
+
for (UEdGraph* G : Graphs)
|
|
45
|
+
{
|
|
46
|
+
if (G) Out.Add(G->GetName(), G);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
Add(BP->UbergraphPages);
|
|
50
|
+
Add(BP->FunctionGraphs);
|
|
51
|
+
Add(BP->MacroGraphs);
|
|
52
|
+
return Out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
TSharedPtr<FJsonObject> NodeSummary(UEdGraphNode* Node)
|
|
56
|
+
{
|
|
57
|
+
TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
|
|
58
|
+
Obj->SetStringField(TEXT("id"), Node->NodeGuid.ToString());
|
|
59
|
+
Obj->SetStringField(TEXT("class"), Node->GetClass()->GetName());
|
|
60
|
+
Obj->SetStringField(TEXT("title"), Node->GetNodeTitle(ENodeTitleType::ListView).ToString());
|
|
61
|
+
return Obj;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Canonical strings for every connection in a graph, emitted once per link
|
|
65
|
+
* (from the output side). Format: "SrcGuid.Pin -> DstGuid.Pin". */
|
|
66
|
+
TSet<FString> CollectConnections(UEdGraph* Graph)
|
|
67
|
+
{
|
|
68
|
+
TSet<FString> Out;
|
|
69
|
+
for (UEdGraphNode* Node : Graph->Nodes)
|
|
70
|
+
{
|
|
71
|
+
if (!Node) continue;
|
|
72
|
+
for (UEdGraphPin* Pin : Node->Pins)
|
|
73
|
+
{
|
|
74
|
+
if (!Pin || Pin->Direction != EGPD_Output) continue;
|
|
75
|
+
for (UEdGraphPin* Linked : Pin->LinkedTo)
|
|
76
|
+
{
|
|
77
|
+
if (!Linked || !Linked->GetOwningNodeUnchecked()) continue;
|
|
78
|
+
Out.Add(FString::Printf(TEXT("%s.%s -> %s.%s"),
|
|
79
|
+
*Node->NodeGuid.ToString(), *Pin->PinName.ToString(),
|
|
80
|
+
*Linked->GetOwningNode()->NodeGuid.ToString(), *Linked->PinName.ToString()));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return Out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
TArray<TSharedPtr<FJsonValue>> StringsToJson(const TArray<FString>& In)
|
|
88
|
+
{
|
|
89
|
+
TArray<TSharedPtr<FJsonValue>> Out;
|
|
90
|
+
Out.Reserve(In.Num());
|
|
91
|
+
for (const FString& S : In) Out.Add(MakeShared<FJsonValueString>(S));
|
|
92
|
+
return Out;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
void FDiffHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
|
|
97
|
+
{
|
|
98
|
+
Registry.RegisterHandler(TEXT("diff_blueprint"), &FDiffHandlers::DiffBlueprint);
|
|
99
|
+
Registry.RegisterHandler(TEXT("diff_asset"), &FDiffHandlers::DiffAsset);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
TSharedPtr<FJsonValue> FDiffHandlers::DiffBlueprint(const TSharedPtr<FJsonObject>& Params)
|
|
103
|
+
{
|
|
104
|
+
FString PathA;
|
|
105
|
+
if (TSharedPtr<FJsonValue> Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), PathA)) return Err;
|
|
106
|
+
FString PathB;
|
|
107
|
+
if (TSharedPtr<FJsonValue> Err = RequireString(Params, TEXT("otherPath"), PathB)) return Err;
|
|
108
|
+
|
|
109
|
+
// Revision-based diffing (loading a depot revision into a transient package)
|
|
110
|
+
// is a documented follow-up; for now both sides are loadable asset paths.
|
|
111
|
+
if (Params->HasField(TEXT("fromRevision")) || Params->HasField(TEXT("toRevision")))
|
|
112
|
+
{
|
|
113
|
+
return MCPError(TEXT("Revision-based diffing (fromRevision/toRevision via source control) is not wired yet - pass 'otherPath' to diff two loaded Blueprint assets. Revision loading is a staged follow-up."));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
UBlueprint* A = LoadAssetByPath<UBlueprint>(PathA);
|
|
117
|
+
if (!A) return MCPError(FString::Printf(TEXT("Blueprint not found: %s"), *PathA));
|
|
118
|
+
UBlueprint* B = LoadAssetByPath<UBlueprint>(PathB);
|
|
119
|
+
if (!B) return MCPError(FString::Printf(TEXT("Blueprint not found: %s"), *PathB));
|
|
120
|
+
|
|
121
|
+
TSharedPtr<FJsonObject> Result = MCPSuccess();
|
|
122
|
+
Result->SetStringField(TEXT("assetType"), TEXT("Blueprint"));
|
|
123
|
+
Result->SetStringField(TEXT("from"), PathA);
|
|
124
|
+
Result->SetStringField(TEXT("to"), PathB);
|
|
125
|
+
|
|
126
|
+
int32 ChangeCount = 0;
|
|
127
|
+
|
|
128
|
+
// ── Parent class ────────────────────────────────────────────────────────
|
|
129
|
+
const FString ParentA = A->ParentClass ? A->ParentClass->GetPathName() : TEXT("");
|
|
130
|
+
const FString ParentB = B->ParentClass ? B->ParentClass->GetPathName() : TEXT("");
|
|
131
|
+
if (ParentA != ParentB)
|
|
132
|
+
{
|
|
133
|
+
TSharedPtr<FJsonObject> PC = MakeShared<FJsonObject>();
|
|
134
|
+
PC->SetStringField(TEXT("from"), ParentA);
|
|
135
|
+
PC->SetStringField(TEXT("to"), ParentB);
|
|
136
|
+
Result->SetObjectField(TEXT("parentClassChanged"), PC);
|
|
137
|
+
++ChangeCount;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Variables ───────────────────────────────────────────────────────────
|
|
141
|
+
auto VarMap = [](UBlueprint* BP)
|
|
142
|
+
{
|
|
143
|
+
TMap<FString, TPair<FString, FString>> M; // name -> (type, default)
|
|
144
|
+
for (const FBPVariableDescription& V : BP->NewVariables)
|
|
145
|
+
{
|
|
146
|
+
M.Add(V.VarName.ToString(), TPair<FString, FString>(PinTypeToShortString(V.VarType), V.DefaultValue));
|
|
147
|
+
}
|
|
148
|
+
return M;
|
|
149
|
+
};
|
|
150
|
+
const TMap<FString, TPair<FString, FString>> VA = VarMap(A);
|
|
151
|
+
const TMap<FString, TPair<FString, FString>> VB = VarMap(B);
|
|
152
|
+
{
|
|
153
|
+
TArray<FString> Added, Removed;
|
|
154
|
+
TArray<TSharedPtr<FJsonValue>> Changed;
|
|
155
|
+
for (const auto& Pair : VB) if (!VA.Contains(Pair.Key)) Added.Add(Pair.Key);
|
|
156
|
+
for (const auto& Pair : VA)
|
|
157
|
+
{
|
|
158
|
+
if (const TPair<FString, FString>* Bv = VB.Find(Pair.Key))
|
|
159
|
+
{
|
|
160
|
+
if (Bv->Key != Pair.Value.Key || Bv->Value != Pair.Value.Value)
|
|
161
|
+
{
|
|
162
|
+
TSharedPtr<FJsonObject> C = MakeShared<FJsonObject>();
|
|
163
|
+
C->SetStringField(TEXT("name"), Pair.Key);
|
|
164
|
+
C->SetStringField(TEXT("fromType"), Pair.Value.Key);
|
|
165
|
+
C->SetStringField(TEXT("toType"), Bv->Key);
|
|
166
|
+
C->SetStringField(TEXT("fromDefault"), Pair.Value.Value);
|
|
167
|
+
C->SetStringField(TEXT("toDefault"), Bv->Value);
|
|
168
|
+
Changed.Add(MakeShared<FJsonValueObject>(C));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else
|
|
172
|
+
{
|
|
173
|
+
Removed.Add(Pair.Key);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (Added.Num() || Removed.Num() || Changed.Num())
|
|
177
|
+
{
|
|
178
|
+
TSharedPtr<FJsonObject> V = MakeShared<FJsonObject>();
|
|
179
|
+
V->SetArrayField(TEXT("added"), StringsToJson(Added));
|
|
180
|
+
V->SetArrayField(TEXT("removed"), StringsToJson(Removed));
|
|
181
|
+
V->SetArrayField(TEXT("changed"), Changed);
|
|
182
|
+
Result->SetObjectField(TEXT("variables"), V);
|
|
183
|
+
ChangeCount += Added.Num() + Removed.Num() + Changed.Num();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Components (SimpleConstructionScript) ───────────────────────────────
|
|
188
|
+
auto CompMap = [](UBlueprint* BP)
|
|
189
|
+
{
|
|
190
|
+
TMap<FString, FString> M; // name -> class
|
|
191
|
+
if (BP->SimpleConstructionScript)
|
|
192
|
+
{
|
|
193
|
+
for (USCS_Node* Node : BP->SimpleConstructionScript->GetAllNodes())
|
|
194
|
+
{
|
|
195
|
+
if (!Node) continue;
|
|
196
|
+
const FString Name = Node->GetVariableName().ToString();
|
|
197
|
+
const FString Cls = Node->ComponentClass ? Node->ComponentClass->GetName() : TEXT("");
|
|
198
|
+
M.Add(Name, Cls);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return M;
|
|
202
|
+
};
|
|
203
|
+
const TMap<FString, FString> CA = CompMap(A);
|
|
204
|
+
const TMap<FString, FString> CB = CompMap(B);
|
|
205
|
+
{
|
|
206
|
+
TArray<TSharedPtr<FJsonValue>> Added, Removed, Changed;
|
|
207
|
+
for (const auto& Pair : CB) if (!CA.Contains(Pair.Key))
|
|
208
|
+
{
|
|
209
|
+
TSharedPtr<FJsonObject> O = MakeShared<FJsonObject>();
|
|
210
|
+
O->SetStringField(TEXT("name"), Pair.Key);
|
|
211
|
+
O->SetStringField(TEXT("class"), Pair.Value);
|
|
212
|
+
Added.Add(MakeShared<FJsonValueObject>(O));
|
|
213
|
+
}
|
|
214
|
+
for (const auto& Pair : CA)
|
|
215
|
+
{
|
|
216
|
+
if (const FString* Bc = CB.Find(Pair.Key))
|
|
217
|
+
{
|
|
218
|
+
if (*Bc != Pair.Value)
|
|
219
|
+
{
|
|
220
|
+
TSharedPtr<FJsonObject> O = MakeShared<FJsonObject>();
|
|
221
|
+
O->SetStringField(TEXT("name"), Pair.Key);
|
|
222
|
+
O->SetStringField(TEXT("fromClass"), Pair.Value);
|
|
223
|
+
O->SetStringField(TEXT("toClass"), *Bc);
|
|
224
|
+
Changed.Add(MakeShared<FJsonValueObject>(O));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else
|
|
228
|
+
{
|
|
229
|
+
TSharedPtr<FJsonObject> O = MakeShared<FJsonObject>();
|
|
230
|
+
O->SetStringField(TEXT("name"), Pair.Key);
|
|
231
|
+
O->SetStringField(TEXT("class"), Pair.Value);
|
|
232
|
+
Removed.Add(MakeShared<FJsonValueObject>(O));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (Added.Num() || Removed.Num() || Changed.Num())
|
|
236
|
+
{
|
|
237
|
+
TSharedPtr<FJsonObject> C = MakeShared<FJsonObject>();
|
|
238
|
+
C->SetArrayField(TEXT("added"), Added);
|
|
239
|
+
C->SetArrayField(TEXT("removed"), Removed);
|
|
240
|
+
C->SetArrayField(TEXT("changed"), Changed);
|
|
241
|
+
Result->SetObjectField(TEXT("components"), C);
|
|
242
|
+
ChangeCount += Added.Num() + Removed.Num() + Changed.Num();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── Graphs: node + connection deltas keyed on stable NodeGuid ───────────
|
|
247
|
+
const TMap<FString, UEdGraph*> GA = CollectGraphs(A);
|
|
248
|
+
const TMap<FString, UEdGraph*> GB = CollectGraphs(B);
|
|
249
|
+
{
|
|
250
|
+
// Functions/macros/event-graphs present on only one side.
|
|
251
|
+
TArray<FString> GraphsAdded, GraphsRemoved;
|
|
252
|
+
for (const auto& Pair : GB) if (!GA.Contains(Pair.Key)) GraphsAdded.Add(Pair.Key);
|
|
253
|
+
for (const auto& Pair : GA) if (!GB.Contains(Pair.Key)) GraphsRemoved.Add(Pair.Key);
|
|
254
|
+
if (GraphsAdded.Num() || GraphsRemoved.Num())
|
|
255
|
+
{
|
|
256
|
+
TSharedPtr<FJsonObject> G = MakeShared<FJsonObject>();
|
|
257
|
+
G->SetArrayField(TEXT("added"), StringsToJson(GraphsAdded));
|
|
258
|
+
G->SetArrayField(TEXT("removed"), StringsToJson(GraphsRemoved));
|
|
259
|
+
Result->SetObjectField(TEXT("graphsAddedRemoved"), G);
|
|
260
|
+
ChangeCount += GraphsAdded.Num() + GraphsRemoved.Num();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
TArray<TSharedPtr<FJsonValue>> GraphDeltas;
|
|
264
|
+
for (const auto& Pair : GA)
|
|
265
|
+
{
|
|
266
|
+
UEdGraph* GraphA = Pair.Value;
|
|
267
|
+
UEdGraph* const* GraphBPtr = GB.Find(Pair.Key);
|
|
268
|
+
if (!GraphBPtr) continue; // whole-graph add/remove handled above
|
|
269
|
+
UEdGraph* GraphB = *GraphBPtr;
|
|
270
|
+
|
|
271
|
+
TMap<FString, UEdGraphNode*> NodesA, NodesB;
|
|
272
|
+
for (UEdGraphNode* N : GraphA->Nodes) if (N) NodesA.Add(N->NodeGuid.ToString(), N);
|
|
273
|
+
for (UEdGraphNode* N : GraphB->Nodes) if (N) NodesB.Add(N->NodeGuid.ToString(), N);
|
|
274
|
+
|
|
275
|
+
TArray<TSharedPtr<FJsonValue>> NodesAdded, NodesRemoved;
|
|
276
|
+
for (const auto& NB : NodesB) if (!NodesA.Contains(NB.Key)) NodesAdded.Add(MakeShared<FJsonValueObject>(NodeSummary(NB.Value)));
|
|
277
|
+
for (const auto& NA : NodesA) if (!NodesB.Contains(NA.Key)) NodesRemoved.Add(MakeShared<FJsonValueObject>(NodeSummary(NA.Value)));
|
|
278
|
+
|
|
279
|
+
const TSet<FString> ConnA = CollectConnections(GraphA);
|
|
280
|
+
const TSet<FString> ConnB = CollectConnections(GraphB);
|
|
281
|
+
TArray<FString> ConnAdded, ConnRemoved;
|
|
282
|
+
for (const FString& C : ConnB) if (!ConnA.Contains(C)) ConnAdded.Add(C);
|
|
283
|
+
for (const FString& C : ConnA) if (!ConnB.Contains(C)) ConnRemoved.Add(C);
|
|
284
|
+
|
|
285
|
+
if (NodesAdded.Num() || NodesRemoved.Num() || ConnAdded.Num() || ConnRemoved.Num())
|
|
286
|
+
{
|
|
287
|
+
TSharedPtr<FJsonObject> GD = MakeShared<FJsonObject>();
|
|
288
|
+
GD->SetStringField(TEXT("graph"), Pair.Key);
|
|
289
|
+
GD->SetArrayField(TEXT("nodesAdded"), NodesAdded);
|
|
290
|
+
GD->SetArrayField(TEXT("nodesRemoved"), NodesRemoved);
|
|
291
|
+
GD->SetArrayField(TEXT("connectionsAdded"), StringsToJson(ConnAdded));
|
|
292
|
+
GD->SetArrayField(TEXT("connectionsRemoved"), StringsToJson(ConnRemoved));
|
|
293
|
+
GraphDeltas.Add(MakeShared<FJsonValueObject>(GD));
|
|
294
|
+
ChangeCount += NodesAdded.Num() + NodesRemoved.Num() + ConnAdded.Num() + ConnRemoved.Num();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (GraphDeltas.Num())
|
|
298
|
+
{
|
|
299
|
+
Result->SetArrayField(TEXT("graphs"), GraphDeltas);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
Result->SetNumberField(TEXT("changeCount"), ChangeCount);
|
|
304
|
+
Result->SetBoolField(TEXT("identical"), ChangeCount == 0);
|
|
305
|
+
Result->SetStringField(TEXT("summary"), ChangeCount == 0
|
|
306
|
+
? FString::Printf(TEXT("%s and %s are structurally identical"), *PathA, *PathB)
|
|
307
|
+
: FString::Printf(TEXT("%d structural change(s) from %s to %s"), ChangeCount, *PathA, *PathB));
|
|
308
|
+
|
|
309
|
+
return MCPResult(Result);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
TSharedPtr<FJsonValue> FDiffHandlers::DiffAsset(const TSharedPtr<FJsonObject>& Params)
|
|
313
|
+
{
|
|
314
|
+
FString PathA;
|
|
315
|
+
if (TSharedPtr<FJsonValue> Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), PathA)) return Err;
|
|
316
|
+
|
|
317
|
+
if (UObject* Asset = LoadAssetByPath<UObject>(PathA))
|
|
318
|
+
{
|
|
319
|
+
if (Asset->IsA<UBlueprint>())
|
|
320
|
+
{
|
|
321
|
+
return DiffBlueprint(Params);
|
|
322
|
+
}
|
|
323
|
+
return MCPError(FString::Printf(
|
|
324
|
+
TEXT("Diffing '%s' assets is not supported yet. Blueprint diffing is available now; StateTree and other graph assets are staged follow-ups."),
|
|
325
|
+
*Asset->GetClass()->GetName()));
|
|
326
|
+
}
|
|
327
|
+
return MCPError(FString::Printf(TEXT("Asset not found: %s"), *PathA));
|
|
328
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "CoreMinimal.h"
|
|
4
|
+
#include "Dom/JsonValue.h"
|
|
5
|
+
#include "Dom/JsonObject.h"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Semantic revision diffing for graph-based binary assets. Blueprint and
|
|
9
|
+
* StateTree assets are opaque to git, so an agent's edits are unreviewable:
|
|
10
|
+
* there is no way to answer "what did this change between two versions".
|
|
11
|
+
*
|
|
12
|
+
* Phase 1 (here): Blueprint structural diff between two loadable asset paths —
|
|
13
|
+
* parent class, variables, functions/macros, components, and per-graph node and
|
|
14
|
+
* connection deltas, keyed on the stable node GUID so a moved or edited node is
|
|
15
|
+
* matched rather than reported as remove+add. Output is a structured JSON delta
|
|
16
|
+
* plus a human-readable summary.
|
|
17
|
+
*
|
|
18
|
+
* Follow-ups (documented, not yet wired): loading an arbitrary source-control
|
|
19
|
+
* revision into a transient package so the "to" side can be a depot revision
|
|
20
|
+
* rather than a second on-disk asset (needs a live SCC spike), then StateTree
|
|
21
|
+
* assets on the same diff spine.
|
|
22
|
+
*/
|
|
23
|
+
class FDiffHandlers
|
|
24
|
+
{
|
|
25
|
+
public:
|
|
26
|
+
static void RegisterHandlers(class FMCPHandlerRegistry& Registry);
|
|
27
|
+
|
|
28
|
+
private:
|
|
29
|
+
// Diff two Blueprints loaded from asset paths. Params: assetPath (base/A),
|
|
30
|
+
// otherPath (compare/B). Reports changes as A -> B.
|
|
31
|
+
static TSharedPtr<FJsonValue> DiffBlueprint(const TSharedPtr<FJsonObject>& Params);
|
|
32
|
+
|
|
33
|
+
// Type-routing entry point. Blueprints delegate to DiffBlueprint; other
|
|
34
|
+
// asset classes return a clear "not yet supported" until their phase lands.
|
|
35
|
+
static TSharedPtr<FJsonValue> DiffAsset(const TSharedPtr<FJsonObject>& Params);
|
|
36
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#include "Handlers/LockHandlers.h"
|
|
2
|
+
#include "HandlerRegistry.h"
|
|
3
|
+
#include "HandlerUtils.h"
|
|
4
|
+
#include "LockRegistry.h"
|
|
5
|
+
#include "HAL/PlatformTime.h"
|
|
6
|
+
|
|
7
|
+
namespace
|
|
8
|
+
{
|
|
9
|
+
/** Serialize a lock record to the wire shape shared by list/acquire/release. */
|
|
10
|
+
TSharedPtr<FJsonObject> LockToJson(const FMCPAssetLock& Lock)
|
|
11
|
+
{
|
|
12
|
+
const double Remaining = FMath::Max(0.0, Lock.ExpiresMono - FPlatformTime::Seconds());
|
|
13
|
+
TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
|
|
14
|
+
Obj->SetStringField(TEXT("path"), Lock.AssetPath);
|
|
15
|
+
Obj->SetStringField(TEXT("sessionId"), Lock.SessionId);
|
|
16
|
+
Obj->SetStringField(TEXT("acquiredAt"), Lock.AcquiredAtUtc.ToIso8601());
|
|
17
|
+
Obj->SetNumberField(TEXT("ttlSeconds"), Lock.TtlSeconds);
|
|
18
|
+
Obj->SetNumberField(TEXT("ttlSecondsRemaining"), Remaining);
|
|
19
|
+
return Obj;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
void FLockHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
|
|
24
|
+
{
|
|
25
|
+
Registry.RegisterHandler(TEXT("acquire_lock"), &FLockHandlers::AcquireLock);
|
|
26
|
+
Registry.RegisterHandler(TEXT("release_lock"), &FLockHandlers::ReleaseLock);
|
|
27
|
+
Registry.RegisterHandler(TEXT("release_session_locks"), &FLockHandlers::ReleaseSessionLocks);
|
|
28
|
+
Registry.RegisterHandler(TEXT("list_locks"), &FLockHandlers::ListLocks);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
TSharedPtr<FJsonValue> FLockHandlers::AcquireLock(const TSharedPtr<FJsonObject>& Params)
|
|
32
|
+
{
|
|
33
|
+
FString Path;
|
|
34
|
+
if (TSharedPtr<FJsonValue> Err = RequireStringAlt(Params, TEXT("path"), TEXT("assetPath"), Path)) return Err;
|
|
35
|
+
FString SessionId;
|
|
36
|
+
if (TSharedPtr<FJsonValue> Err = RequireString(Params, TEXT("sessionId"), SessionId)) return Err;
|
|
37
|
+
|
|
38
|
+
double Ttl = OptionalNumber(Params, TEXT("ttlSeconds"), 300.0);
|
|
39
|
+
if (Ttl <= 0.0) Ttl = 300.0;
|
|
40
|
+
|
|
41
|
+
FMCPAssetLock Holder;
|
|
42
|
+
const bool bAcquired = FMCPLockRegistry::Get().Acquire(Path, SessionId, Ttl, Holder);
|
|
43
|
+
|
|
44
|
+
TSharedPtr<FJsonObject> Res = MCPSuccess();
|
|
45
|
+
Res->SetBoolField(TEXT("acquired"), bAcquired);
|
|
46
|
+
Res->SetStringField(TEXT("path"), Path);
|
|
47
|
+
Res->SetStringField(TEXT("sessionId"), SessionId);
|
|
48
|
+
if (bAcquired)
|
|
49
|
+
{
|
|
50
|
+
Res->SetNumberField(TEXT("ttlSeconds"), Ttl);
|
|
51
|
+
Res->SetNumberField(TEXT("ttlSecondsRemaining"), FMath::Max(0.0, Holder.ExpiresMono - FPlatformTime::Seconds()));
|
|
52
|
+
}
|
|
53
|
+
else
|
|
54
|
+
{
|
|
55
|
+
// Busy: describe the current holder so the caller (or the Node
|
|
56
|
+
// middleware) can surface a retryable error.
|
|
57
|
+
Res->SetObjectField(TEXT("holder"), LockToJson(Holder));
|
|
58
|
+
}
|
|
59
|
+
return MCPResult(Res);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
TSharedPtr<FJsonValue> FLockHandlers::ReleaseLock(const TSharedPtr<FJsonObject>& Params)
|
|
63
|
+
{
|
|
64
|
+
FString Path;
|
|
65
|
+
if (TSharedPtr<FJsonValue> Err = RequireStringAlt(Params, TEXT("path"), TEXT("assetPath"), Path)) return Err;
|
|
66
|
+
FString SessionId;
|
|
67
|
+
if (TSharedPtr<FJsonValue> Err = RequireString(Params, TEXT("sessionId"), SessionId)) return Err;
|
|
68
|
+
const bool bForce = OptionalBool(Params, TEXT("force"), false);
|
|
69
|
+
|
|
70
|
+
FMCPAssetLock Released;
|
|
71
|
+
const bool bReleased = FMCPLockRegistry::Get().Release(Path, SessionId, bForce, Released);
|
|
72
|
+
|
|
73
|
+
TSharedPtr<FJsonObject> Res = MCPSuccess();
|
|
74
|
+
Res->SetBoolField(TEXT("released"), bReleased);
|
|
75
|
+
Res->SetStringField(TEXT("path"), Path);
|
|
76
|
+
return MCPResult(Res);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
TSharedPtr<FJsonValue> FLockHandlers::ReleaseSessionLocks(const TSharedPtr<FJsonObject>& Params)
|
|
80
|
+
{
|
|
81
|
+
FString SessionId;
|
|
82
|
+
if (TSharedPtr<FJsonValue> Err = RequireString(Params, TEXT("sessionId"), SessionId)) return Err;
|
|
83
|
+
|
|
84
|
+
const int32 Count = FMCPLockRegistry::Get().ReleaseSession(SessionId);
|
|
85
|
+
|
|
86
|
+
TSharedPtr<FJsonObject> Res = MCPSuccess();
|
|
87
|
+
Res->SetStringField(TEXT("sessionId"), SessionId);
|
|
88
|
+
Res->SetNumberField(TEXT("released"), Count);
|
|
89
|
+
return MCPResult(Res);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
TSharedPtr<FJsonValue> FLockHandlers::ListLocks(const TSharedPtr<FJsonObject>& Params)
|
|
93
|
+
{
|
|
94
|
+
const TArray<FMCPAssetLock> Locks = FMCPLockRegistry::Get().List();
|
|
95
|
+
|
|
96
|
+
TArray<TSharedPtr<FJsonValue>> Arr;
|
|
97
|
+
Arr.Reserve(Locks.Num());
|
|
98
|
+
for (const FMCPAssetLock& Lock : Locks)
|
|
99
|
+
{
|
|
100
|
+
Arr.Add(MakeShared<FJsonValueObject>(LockToJson(Lock)));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
TSharedPtr<FJsonObject> Res = MCPSuccess();
|
|
104
|
+
Res->SetArrayField(TEXT("locks"), Arr);
|
|
105
|
+
Res->SetNumberField(TEXT("count"), Locks.Num());
|
|
106
|
+
return MCPResult(Res);
|
|
107
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "CoreMinimal.h"
|
|
4
|
+
#include "Dom/JsonValue.h"
|
|
5
|
+
#include "Dom/JsonObject.h"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Per-asset exclusive locking so concurrent agents (or an agent plus a human)
|
|
9
|
+
* don't issue conflicting mutations against the same asset. Backed by
|
|
10
|
+
* FMCPLockRegistry, which lives in the bridge — the single shared editor every
|
|
11
|
+
* agent connects to. Exposed as explicit acquire/release/list actions; the
|
|
12
|
+
* Node dispatch layer also wraps mutating calls with these when locking is
|
|
13
|
+
* enabled (see src/locking.ts).
|
|
14
|
+
*/
|
|
15
|
+
class FLockHandlers
|
|
16
|
+
{
|
|
17
|
+
public:
|
|
18
|
+
static void RegisterHandlers(class FMCPHandlerRegistry& Registry);
|
|
19
|
+
|
|
20
|
+
private:
|
|
21
|
+
static TSharedPtr<FJsonValue> AcquireLock(const TSharedPtr<FJsonObject>& Params);
|
|
22
|
+
static TSharedPtr<FJsonValue> ReleaseLock(const TSharedPtr<FJsonObject>& Params);
|
|
23
|
+
static TSharedPtr<FJsonValue> ReleaseSessionLocks(const TSharedPtr<FJsonObject>& Params);
|
|
24
|
+
static TSharedPtr<FJsonValue> ListLocks(const TSharedPtr<FJsonObject>& Params);
|
|
25
|
+
};
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
#include "NiagaraNodeCustomHlsl.h"
|
|
28
28
|
#include "NiagaraEmitterHandle.h"
|
|
29
29
|
#include "NiagaraEditorUtilities.h"
|
|
30
|
+
#include "NiagaraEmitterFactoryNew.h"
|
|
30
31
|
#include "Editor.h"
|
|
31
32
|
#include "Engine/World.h"
|
|
32
33
|
#include "EngineUtils.h"
|
|
@@ -251,7 +252,18 @@ TSharedPtr<FJsonValue> FNiagaraHandlers::CreateNiagaraEmitter(const TSharedPtr<F
|
|
|
251
252
|
return MCPError(TEXT("NiagaraEmitter class not found - factory not available"));
|
|
252
253
|
}
|
|
253
254
|
|
|
254
|
-
|
|
255
|
+
// Build the emitter through UNiagaraEmitterFactoryNew, not a bare NewObject.
|
|
256
|
+
// The factory's empty-emitter path initializes a UNiagaraScriptSource +
|
|
257
|
+
// graph (GraphSource) plus default modules and a sprite renderer - the same
|
|
258
|
+
// asset the "Niagara Emitter" content-browser action produces. A bare
|
|
259
|
+
// NewObject'd emitter has GraphSource==nullptr, "succeeds" here, then
|
|
260
|
+
// crashes the editor the moment it is added to a system. IAssetTools::
|
|
261
|
+
// CreateAsset runs FactoryCreateNew headlessly (no ConfigureProperties UI),
|
|
262
|
+
// and the factory defaults (EmitterToCopy=null) select the empty path.
|
|
263
|
+
UNiagaraEmitterFactoryNew* Factory = NewObject<UNiagaraEmitterFactoryNew>();
|
|
264
|
+
FGCRootScope FactoryRoot(Factory);
|
|
265
|
+
|
|
266
|
+
auto Created = MCPCreateAssetIdempotent<UObject>(Name, PackagePath, OnConflict, TEXT("NiagaraEmitter"), EmitterClass, Factory);
|
|
255
267
|
if (Created.EarlyReturn) return Created.EarlyReturn;
|
|
256
268
|
|
|
257
269
|
UEditorAssetLibrary::SaveAsset(Created.Asset->GetPathName());
|
|
@@ -532,6 +544,22 @@ TSharedPtr<FJsonValue> FNiagaraHandlers::SetNiagaraParameter(const TSharedPtr<FJ
|
|
|
532
544
|
return MCPResult(Result);
|
|
533
545
|
}
|
|
534
546
|
|
|
547
|
+
// An emitter is only safely addable to a system when its target version has a
|
|
548
|
+
// GraphSource. A bare NewObject'd UNiagaraEmitter (how create_niagara_emitter
|
|
549
|
+
// used to build them) or any asset left in a broken state has
|
|
550
|
+
// GraphSource==nullptr; passing one to FNiagaraEditorUtilities::AddEmitterToSystem
|
|
551
|
+
// makes Niagara's RebuildEmitterNodes dereference it and hard-crashes the editor
|
|
552
|
+
// (ensure GraphSource != nullptr at NiagaraEmitter.cpp, then an access violation).
|
|
553
|
+
// Every add-path must gate on this instead of trusting the emitter loaded clean.
|
|
554
|
+
static bool EmitterHasGraphSource(UNiagaraEmitter* Emitter, const FGuid& Version)
|
|
555
|
+
{
|
|
556
|
+
if (!Emitter) return false;
|
|
557
|
+
const FVersionedNiagaraEmitterData* Data = Version.IsValid()
|
|
558
|
+
? Emitter->GetEmitterData(Version)
|
|
559
|
+
: Emitter->GetLatestEmitterData();
|
|
560
|
+
return Data != nullptr && Data->GraphSource != nullptr;
|
|
561
|
+
}
|
|
562
|
+
|
|
535
563
|
TSharedPtr<FJsonValue> FNiagaraHandlers::AddEmitterToSystem(const TSharedPtr<FJsonObject>& Params)
|
|
536
564
|
{
|
|
537
565
|
FString SystemPath;
|
|
@@ -591,6 +619,13 @@ TSharedPtr<FJsonValue> FNiagaraHandlers::AddEmitterToSystem(const TSharedPtr<FJs
|
|
|
591
619
|
// system's overview graph. This is the same path the editor uses for
|
|
592
620
|
// "Add Emitter to System" in the Niagara System editor.
|
|
593
621
|
const FGuid EmitterVersion = Emitter->GetExposedVersion().VersionGuid;
|
|
622
|
+
// Guard: a GraphSource-less emitter crashes AddEmitterToSystem. Fail cleanly.
|
|
623
|
+
if (!EmitterHasGraphSource(Emitter, EmitterVersion))
|
|
624
|
+
{
|
|
625
|
+
return MCPError(FString::Printf(
|
|
626
|
+
TEXT("Emitter '%s' has no GraphSource - it was created empty/invalid and cannot be added to a system (adding it would crash the editor). Recreate it with create_niagara_emitter, which now initializes a valid graph."),
|
|
627
|
+
*EmitterPath));
|
|
628
|
+
}
|
|
594
629
|
const FGuid HandleId = FNiagaraEditorUtilities::AddEmitterToSystem(*System, *Emitter, EmitterVersion);
|
|
595
630
|
if (!HandleId.IsValid())
|
|
596
631
|
{
|
|
@@ -1041,11 +1076,23 @@ TSharedPtr<FJsonValue> FNiagaraHandlers::CreateNiagaraSystemFromSpec(const TShar
|
|
|
1041
1076
|
const TArray<TSharedPtr<FJsonValue>>* EmittersArr = nullptr;
|
|
1042
1077
|
Params->TryGetArrayField(TEXT("emitters"), EmittersArr);
|
|
1043
1078
|
|
|
1044
|
-
|
|
1079
|
+
// Create the system through UNiagaraSystemFactoryNew, not a bare NewObject.
|
|
1080
|
+
// A NewObject'd UNiagaraSystem has no system spawn/update scripts and no
|
|
1081
|
+
// editor data; FNiagaraEditorUtilities::AddEmitterToSystem then dereferences
|
|
1082
|
+
// that missing state and hard-crashes the editor (access violation). The
|
|
1083
|
+
// factory's InitializeSystem sets up the scripts + editor data + default
|
|
1084
|
+
// nodes - the same asset the plain `create` action produces. (Mirrors
|
|
1085
|
+
// CreateNiagaraSystem.)
|
|
1086
|
+
UClass* SystemFactoryClass = FindObject<UClass>(nullptr, TEXT("/Script/NiagaraEditor.NiagaraSystemFactoryNew"));
|
|
1087
|
+
UFactory* SystemFactory = SystemFactoryClass
|
|
1088
|
+
? Cast<UFactory>(NewObject<UObject>(GetTransientPackage(), SystemFactoryClass))
|
|
1089
|
+
: nullptr;
|
|
1090
|
+
auto Created = MCPCreateAssetIdempotent<UNiagaraSystem>(Name, PackagePath, OptionalString(Params, TEXT("onConflict"), TEXT("skip")), TEXT("NiagaraSystem"), SystemFactory);
|
|
1045
1091
|
if (Created.EarlyReturn) return Created.EarlyReturn;
|
|
1046
1092
|
UNiagaraSystem* System = Created.Asset;
|
|
1047
1093
|
|
|
1048
1094
|
int32 AddedEmitters = 0;
|
|
1095
|
+
TArray<FString> SkippedEmitters;
|
|
1049
1096
|
if (EmittersArr)
|
|
1050
1097
|
{
|
|
1051
1098
|
for (const TSharedPtr<FJsonValue>& V : *EmittersArr)
|
|
@@ -1065,6 +1112,14 @@ TSharedPtr<FJsonValue> FNiagaraHandlers::CreateNiagaraSystemFromSpec(const TShar
|
|
|
1065
1112
|
if (!Source) Source = Cast<UNiagaraEmitter>(UEditorAssetLibrary::LoadAsset(EmitterPath));
|
|
1066
1113
|
if (!Source) continue;
|
|
1067
1114
|
const FGuid Version = Source->GetExposedVersion().VersionGuid;
|
|
1115
|
+
// Guard: skip GraphSource-less emitters instead of crashing the
|
|
1116
|
+
// editor. Surfaced in the result so the caller knows which ones
|
|
1117
|
+
// failed and can recreate them.
|
|
1118
|
+
if (!EmitterHasGraphSource(Source, Version))
|
|
1119
|
+
{
|
|
1120
|
+
SkippedEmitters.Add(EmitterPath);
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1068
1123
|
// #275: route through FNiagaraEditorUtilities so we get
|
|
1069
1124
|
// KillSystemInstances + RebuildEmitterNodes + unique-name
|
|
1070
1125
|
// resolution instead of mutating the handle list raw.
|
|
@@ -1080,6 +1135,15 @@ TSharedPtr<FJsonValue> FNiagaraHandlers::CreateNiagaraSystemFromSpec(const TShar
|
|
|
1080
1135
|
MCPSetCreated(Res);
|
|
1081
1136
|
Res->SetStringField(TEXT("path"), System->GetPathName());
|
|
1082
1137
|
Res->SetNumberField(TEXT("emittersAdded"), AddedEmitters);
|
|
1138
|
+
if (SkippedEmitters.Num() > 0)
|
|
1139
|
+
{
|
|
1140
|
+
TArray<TSharedPtr<FJsonValue>> Skipped;
|
|
1141
|
+
for (const FString& S : SkippedEmitters) Skipped.Add(MakeShared<FJsonValueString>(S));
|
|
1142
|
+
Res->SetArrayField(TEXT("skippedEmitters"), Skipped);
|
|
1143
|
+
Res->SetStringField(TEXT("warning"), FString::Printf(
|
|
1144
|
+
TEXT("%d emitter(s) were skipped because they have no GraphSource (created empty/invalid). Recreate them with create_niagara_emitter."),
|
|
1145
|
+
SkippedEmitters.Num()));
|
|
1146
|
+
}
|
|
1083
1147
|
MCPSetDeleteAssetRollback(Res, System->GetPathName());
|
|
1084
1148
|
return MCPResult(Res);
|
|
1085
1149
|
}
|