ue-mcp 0.7.9 → 0.7.10
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/package.json +1 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.cpp +208 -26
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.cpp +9 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AudioHandlers.cpp +3 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.cpp +284 -27
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DialogHandlers.cpp +20 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.cpp +46 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.cpp +10 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.cpp +192 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.cpp +62 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +22 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.cpp +221 -16
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NetworkingHandlers.cpp +177 -41
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NiagaraHandlers.cpp +45 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PCGHandlers.cpp +18 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +145 -15
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.cpp +28 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.cpp +20 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.cpp +60 -2
package/package.json
CHANGED
|
@@ -769,6 +769,21 @@ TSharedPtr<FJsonValue> FAnimationHandlers::AddAnimNotify(const TSharedPtr<FJsonO
|
|
|
769
769
|
float PlayLength = AnimAsset->GetPlayLength();
|
|
770
770
|
float ClampedTime = FMath::Clamp(static_cast<float>(TriggerTime), 0.0f, PlayLength);
|
|
771
771
|
|
|
772
|
+
// Idempotency: check for existing notify with same name at same trigger time
|
|
773
|
+
const FName NotifyFName(*NotifyName);
|
|
774
|
+
for (const FAnimNotifyEvent& Existing : AnimAsset->Notifies)
|
|
775
|
+
{
|
|
776
|
+
if (Existing.NotifyName == NotifyFName && FMath::IsNearlyEqual(Existing.GetTime(), ClampedTime, 0.001f))
|
|
777
|
+
{
|
|
778
|
+
auto ExistedRes = MCPSuccess();
|
|
779
|
+
MCPSetExisted(ExistedRes);
|
|
780
|
+
ExistedRes->SetStringField(TEXT("assetPath"), AssetPath);
|
|
781
|
+
ExistedRes->SetStringField(TEXT("notifyName"), NotifyName);
|
|
782
|
+
ExistedRes->SetNumberField(TEXT("triggerTime"), ClampedTime);
|
|
783
|
+
return MCPResult(ExistedRes);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
772
787
|
// If a notify class is specified, try to find and instantiate it
|
|
773
788
|
UAnimNotify* NewNotify = nullptr;
|
|
774
789
|
if (!NotifyClassName.IsEmpty())
|
|
@@ -805,6 +820,7 @@ TSharedPtr<FJsonValue> FAnimationHandlers::AddAnimNotify(const TSharedPtr<FJsonO
|
|
|
805
820
|
UEditorAssetLibrary::SaveAsset(AssetPath);
|
|
806
821
|
|
|
807
822
|
auto Result = MCPSuccess();
|
|
823
|
+
MCPSetCreated(Result);
|
|
808
824
|
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
|
809
825
|
Result->SetStringField(TEXT("notifyName"), NotifyName);
|
|
810
826
|
Result->SetNumberField(TEXT("triggerTime"), ClampedTime);
|
|
@@ -812,6 +828,7 @@ TSharedPtr<FJsonValue> FAnimationHandlers::AddAnimNotify(const TSharedPtr<FJsonO
|
|
|
812
828
|
{
|
|
813
829
|
Result->SetStringField(TEXT("notifyClass"), NewNotify->GetClass()->GetName());
|
|
814
830
|
}
|
|
831
|
+
// No rollback: no paired remove_anim_notify handler yet.
|
|
815
832
|
|
|
816
833
|
return MCPResult(Result);
|
|
817
834
|
}
|
|
@@ -1341,58 +1358,96 @@ TSharedPtr<FJsonValue> FAnimationHandlers::SetMontageProperties(const TSharedPtr
|
|
|
1341
1358
|
return MCPError(FString::Printf(TEXT("Failed to load AnimMontage at '%s'"), *AssetPath));
|
|
1342
1359
|
}
|
|
1343
1360
|
|
|
1361
|
+
// Capture previous values for rollback
|
|
1362
|
+
const float PrevSeqLen = Montage->GetPlayLength();
|
|
1363
|
+
const float PrevRateScale = Montage->RateScale;
|
|
1364
|
+
const float PrevBlendIn = Montage->BlendIn.GetBlendTime();
|
|
1365
|
+
const float PrevBlendOut = Montage->BlendOut.GetBlendTime();
|
|
1366
|
+
|
|
1344
1367
|
TArray<FString> Modified;
|
|
1368
|
+
bool bAnyChanged = false;
|
|
1345
1369
|
|
|
1346
1370
|
// sequenceLength — update via property reflection (SequenceLength is protected)
|
|
1347
1371
|
double SeqLen;
|
|
1348
|
-
|
|
1372
|
+
const bool bHasSeqLen = Params->TryGetNumberField(TEXT("sequenceLength"), SeqLen);
|
|
1373
|
+
if (bHasSeqLen)
|
|
1349
1374
|
{
|
|
1350
1375
|
float NewLength = static_cast<float>(SeqLen);
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
// Also update composite sections' segment lengths to match
|
|
1354
|
-
for (FCompositeSection& Section : Montage->CompositeSections)
|
|
1376
|
+
if (!FMath::IsNearlyEqual(NewLength, PrevSeqLen))
|
|
1355
1377
|
{
|
|
1356
|
-
|
|
1378
|
+
SetMontageSequenceLength(Montage, NewLength);
|
|
1379
|
+
for (FCompositeSection& Section : Montage->CompositeSections)
|
|
1380
|
+
{
|
|
1381
|
+
SetSegmentLength(Section, NewLength);
|
|
1382
|
+
}
|
|
1383
|
+
Modified.Add(TEXT("sequenceLength"));
|
|
1384
|
+
bAnyChanged = true;
|
|
1357
1385
|
}
|
|
1358
|
-
Modified.Add(TEXT("sequenceLength"));
|
|
1359
1386
|
}
|
|
1360
1387
|
|
|
1361
1388
|
// rateScale
|
|
1362
1389
|
double RateScale;
|
|
1363
|
-
|
|
1390
|
+
const bool bHasRate = Params->TryGetNumberField(TEXT("rateScale"), RateScale);
|
|
1391
|
+
if (bHasRate)
|
|
1364
1392
|
{
|
|
1365
|
-
|
|
1366
|
-
|
|
1393
|
+
float NewRate = static_cast<float>(RateScale);
|
|
1394
|
+
if (!FMath::IsNearlyEqual(NewRate, PrevRateScale))
|
|
1395
|
+
{
|
|
1396
|
+
Montage->RateScale = NewRate;
|
|
1397
|
+
Modified.Add(TEXT("rateScale"));
|
|
1398
|
+
bAnyChanged = true;
|
|
1399
|
+
}
|
|
1367
1400
|
}
|
|
1368
1401
|
|
|
1369
1402
|
// blendIn
|
|
1370
1403
|
double BlendIn;
|
|
1371
|
-
|
|
1404
|
+
const bool bHasBlendIn = Params->TryGetNumberField(TEXT("blendIn"), BlendIn);
|
|
1405
|
+
if (bHasBlendIn)
|
|
1372
1406
|
{
|
|
1373
|
-
|
|
1374
|
-
|
|
1407
|
+
float NewIn = static_cast<float>(BlendIn);
|
|
1408
|
+
if (!FMath::IsNearlyEqual(NewIn, PrevBlendIn))
|
|
1409
|
+
{
|
|
1410
|
+
Montage->BlendIn.SetBlendTime(NewIn);
|
|
1411
|
+
Modified.Add(TEXT("blendIn"));
|
|
1412
|
+
bAnyChanged = true;
|
|
1413
|
+
}
|
|
1375
1414
|
}
|
|
1376
1415
|
|
|
1377
1416
|
// blendOut
|
|
1378
1417
|
double BlendOut;
|
|
1379
|
-
|
|
1418
|
+
const bool bHasBlendOut = Params->TryGetNumberField(TEXT("blendOut"), BlendOut);
|
|
1419
|
+
if (bHasBlendOut)
|
|
1380
1420
|
{
|
|
1381
|
-
|
|
1382
|
-
|
|
1421
|
+
float NewOut = static_cast<float>(BlendOut);
|
|
1422
|
+
if (!FMath::IsNearlyEqual(NewOut, PrevBlendOut))
|
|
1423
|
+
{
|
|
1424
|
+
Montage->BlendOut.SetBlendTime(NewOut);
|
|
1425
|
+
Modified.Add(TEXT("blendOut"));
|
|
1426
|
+
bAnyChanged = true;
|
|
1427
|
+
}
|
|
1383
1428
|
}
|
|
1384
1429
|
|
|
1385
|
-
if (
|
|
1430
|
+
if (!bHasSeqLen && !bHasRate && !bHasBlendIn && !bHasBlendOut)
|
|
1386
1431
|
{
|
|
1387
1432
|
return MCPError(TEXT("No properties to set. Provide at least one of: sequenceLength, rateScale, blendIn, blendOut"));
|
|
1388
1433
|
}
|
|
1389
1434
|
|
|
1435
|
+
// Idempotent: requested values match current state
|
|
1436
|
+
if (!bAnyChanged)
|
|
1437
|
+
{
|
|
1438
|
+
auto Noop = MCPSuccess();
|
|
1439
|
+
MCPSetExisted(Noop);
|
|
1440
|
+
Noop->SetStringField(TEXT("assetPath"), AssetPath);
|
|
1441
|
+
return MCPResult(Noop);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1390
1444
|
Montage->PostEditChange();
|
|
1391
1445
|
Montage->MarkPackageDirty();
|
|
1392
1446
|
UEditorAssetLibrary::SaveAsset(AssetPath);
|
|
1393
1447
|
|
|
1394
1448
|
// Return current state
|
|
1395
1449
|
auto Result = MCPSuccess();
|
|
1450
|
+
MCPSetUpdated(Result);
|
|
1396
1451
|
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
|
1397
1452
|
TArray<TSharedPtr<FJsonValue>> ModifiedArray;
|
|
1398
1453
|
for (const FString& M : Modified)
|
|
@@ -1405,6 +1460,15 @@ TSharedPtr<FJsonValue> FAnimationHandlers::SetMontageProperties(const TSharedPtr
|
|
|
1405
1460
|
Result->SetNumberField(TEXT("blendIn"), Montage->BlendIn.GetBlendTime());
|
|
1406
1461
|
Result->SetNumberField(TEXT("blendOut"), Montage->BlendOut.GetBlendTime());
|
|
1407
1462
|
|
|
1463
|
+
// Rollback: self-inverse with previous values
|
|
1464
|
+
TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
|
|
1465
|
+
Payload->SetStringField(TEXT("assetPath"), AssetPath);
|
|
1466
|
+
if (bHasSeqLen) Payload->SetNumberField(TEXT("sequenceLength"), PrevSeqLen);
|
|
1467
|
+
if (bHasRate) Payload->SetNumberField(TEXT("rateScale"), PrevRateScale);
|
|
1468
|
+
if (bHasBlendIn) Payload->SetNumberField(TEXT("blendIn"), PrevBlendIn);
|
|
1469
|
+
if (bHasBlendOut) Payload->SetNumberField(TEXT("blendOut"), PrevBlendOut);
|
|
1470
|
+
MCPSetRollback(Result, TEXT("set_montage_properties"), Payload);
|
|
1471
|
+
|
|
1408
1472
|
return MCPResult(Result);
|
|
1409
1473
|
}
|
|
1410
1474
|
|
|
@@ -1502,6 +1566,17 @@ TSharedPtr<FJsonValue> FAnimationHandlers::CreateStateMachine(const TSharedPtr<F
|
|
|
1502
1566
|
return MCPError(FString::Printf(TEXT("Graph not found: %s"), *GraphName));
|
|
1503
1567
|
}
|
|
1504
1568
|
|
|
1569
|
+
// Idempotency: check for existing state machine by name
|
|
1570
|
+
if (FindStateMachineNode(AnimBP, Name))
|
|
1571
|
+
{
|
|
1572
|
+
auto Existed = MCPSuccess();
|
|
1573
|
+
MCPSetExisted(Existed);
|
|
1574
|
+
Existed->SetStringField(TEXT("assetPath"), AssetPath);
|
|
1575
|
+
Existed->SetStringField(TEXT("name"), Name);
|
|
1576
|
+
Existed->SetStringField(TEXT("graphName"), GraphName);
|
|
1577
|
+
return MCPResult(Existed);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1505
1580
|
// Create the state machine container node in the AnimGraph
|
|
1506
1581
|
UAnimGraphNode_StateMachine* SMNode = NewObject<UAnimGraphNode_StateMachine>(TargetGraph);
|
|
1507
1582
|
TargetGraph->AddNode(SMNode, false, false);
|
|
@@ -1520,9 +1595,11 @@ TSharedPtr<FJsonValue> FAnimationHandlers::CreateStateMachine(const TSharedPtr<F
|
|
|
1520
1595
|
CompileAndSave(AnimBP);
|
|
1521
1596
|
|
|
1522
1597
|
auto Result = MCPSuccess();
|
|
1598
|
+
MCPSetCreated(Result);
|
|
1523
1599
|
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
|
1524
1600
|
Result->SetStringField(TEXT("name"), Name);
|
|
1525
1601
|
Result->SetStringField(TEXT("graphName"), GraphName);
|
|
1602
|
+
// No rollback: no paired remove_state_machine handler.
|
|
1526
1603
|
|
|
1527
1604
|
return MCPResult(Result);
|
|
1528
1605
|
}
|
|
@@ -1556,10 +1633,20 @@ TSharedPtr<FJsonValue> FAnimationHandlers::AddState(const TSharedPtr<FJsonObject
|
|
|
1556
1633
|
return MCPError(TEXT("State machine has no editor graph"));
|
|
1557
1634
|
}
|
|
1558
1635
|
|
|
1559
|
-
//
|
|
1636
|
+
// Idempotency: existing state with this name short-circuits
|
|
1637
|
+
const FString OnConflict = OptionalString(Params, TEXT("onConflict"), TEXT("skip"));
|
|
1560
1638
|
if (FindStateNode(SMGraph, StateName))
|
|
1561
1639
|
{
|
|
1562
|
-
|
|
1640
|
+
if (OnConflict == TEXT("error"))
|
|
1641
|
+
{
|
|
1642
|
+
return MCPError(FString::Printf(TEXT("State '%s' already exists"), *StateName));
|
|
1643
|
+
}
|
|
1644
|
+
auto Existed = MCPSuccess();
|
|
1645
|
+
MCPSetExisted(Existed);
|
|
1646
|
+
Existed->SetStringField(TEXT("assetPath"), AssetPath);
|
|
1647
|
+
Existed->SetStringField(TEXT("stateMachineName"), SMName);
|
|
1648
|
+
Existed->SetStringField(TEXT("stateName"), StateName);
|
|
1649
|
+
return MCPResult(Existed);
|
|
1563
1650
|
}
|
|
1564
1651
|
|
|
1565
1652
|
// Create state node
|
|
@@ -1584,9 +1671,11 @@ TSharedPtr<FJsonValue> FAnimationHandlers::AddState(const TSharedPtr<FJsonObject
|
|
|
1584
1671
|
CompileAndSave(AnimBP);
|
|
1585
1672
|
|
|
1586
1673
|
auto Result = MCPSuccess();
|
|
1674
|
+
MCPSetCreated(Result);
|
|
1587
1675
|
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
|
1588
1676
|
Result->SetStringField(TEXT("stateMachineName"), SMName);
|
|
1589
1677
|
Result->SetStringField(TEXT("stateName"), StateName);
|
|
1678
|
+
// No rollback: no paired remove_state handler.
|
|
1590
1679
|
|
|
1591
1680
|
return MCPResult(Result);
|
|
1592
1681
|
}
|
|
@@ -1629,6 +1718,33 @@ TSharedPtr<FJsonValue> FAnimationHandlers::AddTransition(const TSharedPtr<FJsonO
|
|
|
1629
1718
|
return MCPError(FString::Printf(TEXT("State '%s' not found"), *ToState));
|
|
1630
1719
|
}
|
|
1631
1720
|
|
|
1721
|
+
// Idempotency: check if a transition From→To already exists
|
|
1722
|
+
UEdGraphPin* FromOutPin = From->GetOutputPin();
|
|
1723
|
+
if (FromOutPin)
|
|
1724
|
+
{
|
|
1725
|
+
for (UEdGraphPin* Linked : FromOutPin->LinkedTo)
|
|
1726
|
+
{
|
|
1727
|
+
if (!Linked || !Linked->GetOwningNode()) continue;
|
|
1728
|
+
UAnimStateTransitionNode* ExistingTrans = Cast<UAnimStateTransitionNode>(Linked->GetOwningNode());
|
|
1729
|
+
if (!ExistingTrans) continue;
|
|
1730
|
+
UEdGraphPin* ExistingTransOut = ExistingTrans->GetOutputPin();
|
|
1731
|
+
if (!ExistingTransOut) continue;
|
|
1732
|
+
for (UEdGraphPin* ToLinked : ExistingTransOut->LinkedTo)
|
|
1733
|
+
{
|
|
1734
|
+
if (ToLinked && ToLinked->GetOwningNode() == To)
|
|
1735
|
+
{
|
|
1736
|
+
auto ExistedRes = MCPSuccess();
|
|
1737
|
+
MCPSetExisted(ExistedRes);
|
|
1738
|
+
ExistedRes->SetStringField(TEXT("assetPath"), AssetPath);
|
|
1739
|
+
ExistedRes->SetStringField(TEXT("stateMachineName"), SMName);
|
|
1740
|
+
ExistedRes->SetStringField(TEXT("fromState"), FromState);
|
|
1741
|
+
ExistedRes->SetStringField(TEXT("toState"), ToState);
|
|
1742
|
+
return MCPResult(ExistedRes);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1632
1748
|
// Create transition node
|
|
1633
1749
|
UAnimStateTransitionNode* TransNode = NewObject<UAnimStateTransitionNode>(SMGraph);
|
|
1634
1750
|
SMGraph->AddNode(TransNode, false, false);
|
|
@@ -1658,10 +1774,12 @@ TSharedPtr<FJsonValue> FAnimationHandlers::AddTransition(const TSharedPtr<FJsonO
|
|
|
1658
1774
|
CompileAndSave(AnimBP);
|
|
1659
1775
|
|
|
1660
1776
|
auto Result = MCPSuccess();
|
|
1777
|
+
MCPSetCreated(Result);
|
|
1661
1778
|
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
|
1662
1779
|
Result->SetStringField(TEXT("stateMachineName"), SMName);
|
|
1663
1780
|
Result->SetStringField(TEXT("fromState"), FromState);
|
|
1664
1781
|
Result->SetStringField(TEXT("toState"), ToState);
|
|
1782
|
+
// No rollback: no paired remove_transition handler.
|
|
1665
1783
|
|
|
1666
1784
|
return MCPResult(Result);
|
|
1667
1785
|
}
|
|
@@ -2053,15 +2171,21 @@ TSharedPtr<FJsonValue> FAnimationHandlers::AddCurve(const TSharedPtr<FJsonObject
|
|
|
2053
2171
|
|
|
2054
2172
|
if (!bAdded)
|
|
2055
2173
|
{
|
|
2056
|
-
// Curve
|
|
2057
|
-
Result
|
|
2174
|
+
// Curve already exists — idempotent replay
|
|
2175
|
+
MCPSetExisted(Result);
|
|
2176
|
+
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
|
2177
|
+
Result->SetStringField(TEXT("curveName"), CurveName);
|
|
2178
|
+
return MCPResult(Result);
|
|
2058
2179
|
}
|
|
2059
2180
|
|
|
2181
|
+
MCPSetCreated(Result);
|
|
2182
|
+
|
|
2060
2183
|
AnimSeq->MarkPackageDirty();
|
|
2061
2184
|
UEditorAssetLibrary::SaveAsset(AssetPath);
|
|
2062
2185
|
|
|
2063
2186
|
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
|
2064
2187
|
Result->SetStringField(TEXT("curveName"), CurveName);
|
|
2188
|
+
// No rollback: no paired remove_curve handler.
|
|
2065
2189
|
|
|
2066
2190
|
return MCPResult(Result);
|
|
2067
2191
|
}
|
|
@@ -2090,16 +2214,37 @@ TSharedPtr<FJsonValue> FAnimationHandlers::SetMontageSlot(const TSharedPtr<FJson
|
|
|
2090
2214
|
return MCPError(FString::Printf(TEXT("trackIndex %d out of range (0..%d)"), TrackIndex, Montage->SlotAnimTracks.Num() - 1));
|
|
2091
2215
|
}
|
|
2092
2216
|
|
|
2093
|
-
|
|
2217
|
+
// Capture previous slot name for rollback and idempotency
|
|
2218
|
+
const FName PrevSlot = Montage->SlotAnimTracks[TrackIndex].SlotName;
|
|
2219
|
+
const FName NewSlotFName(*SlotName);
|
|
2220
|
+
if (PrevSlot == NewSlotFName)
|
|
2221
|
+
{
|
|
2222
|
+
auto Noop = MCPSuccess();
|
|
2223
|
+
MCPSetExisted(Noop);
|
|
2224
|
+
Noop->SetStringField(TEXT("assetPath"), AssetPath);
|
|
2225
|
+
Noop->SetStringField(TEXT("slotName"), SlotName);
|
|
2226
|
+
Noop->SetNumberField(TEXT("trackIndex"), TrackIndex);
|
|
2227
|
+
return MCPResult(Noop);
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
Montage->SlotAnimTracks[TrackIndex].SlotName = NewSlotFName;
|
|
2094
2231
|
|
|
2095
2232
|
Montage->MarkPackageDirty();
|
|
2096
2233
|
UEditorAssetLibrary::SaveAsset(AssetPath);
|
|
2097
2234
|
|
|
2098
2235
|
auto Result = MCPSuccess();
|
|
2236
|
+
MCPSetUpdated(Result);
|
|
2099
2237
|
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
|
2100
2238
|
Result->SetStringField(TEXT("slotName"), SlotName);
|
|
2101
2239
|
Result->SetNumberField(TEXT("trackIndex"), TrackIndex);
|
|
2102
2240
|
|
|
2241
|
+
// Rollback: self-inverse with previous slot name
|
|
2242
|
+
TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
|
|
2243
|
+
Payload->SetStringField(TEXT("assetPath"), AssetPath);
|
|
2244
|
+
Payload->SetStringField(TEXT("slotName"), PrevSlot.ToString());
|
|
2245
|
+
Payload->SetNumberField(TEXT("trackIndex"), TrackIndex);
|
|
2246
|
+
MCPSetRollback(Result, TEXT("set_montage_slot"), Payload);
|
|
2247
|
+
|
|
2103
2248
|
return MCPResult(Result);
|
|
2104
2249
|
}
|
|
2105
2250
|
|
|
@@ -2123,11 +2268,21 @@ TSharedPtr<FJsonValue> FAnimationHandlers::AddMontageSection(const TSharedPtr<FJ
|
|
|
2123
2268
|
return MCPError(FString::Printf(TEXT("Failed to load AnimMontage at '%s'"), *AssetPath));
|
|
2124
2269
|
}
|
|
2125
2270
|
|
|
2126
|
-
//
|
|
2271
|
+
// Idempotency: existing section short-circuits
|
|
2127
2272
|
int32 ExistingIdx = Montage->GetSectionIndex(FName(*SectionName));
|
|
2128
2273
|
if (ExistingIdx != INDEX_NONE)
|
|
2129
2274
|
{
|
|
2130
|
-
|
|
2275
|
+
const FString OnConflict = OptionalString(Params, TEXT("onConflict"), TEXT("skip"));
|
|
2276
|
+
if (OnConflict == TEXT("error"))
|
|
2277
|
+
{
|
|
2278
|
+
return MCPError(FString::Printf(TEXT("Section '%s' already exists at index %d"), *SectionName, ExistingIdx));
|
|
2279
|
+
}
|
|
2280
|
+
auto Existed = MCPSuccess();
|
|
2281
|
+
MCPSetExisted(Existed);
|
|
2282
|
+
Existed->SetStringField(TEXT("assetPath"), AssetPath);
|
|
2283
|
+
Existed->SetStringField(TEXT("sectionName"), SectionName);
|
|
2284
|
+
Existed->SetNumberField(TEXT("sectionIndex"), ExistingIdx);
|
|
2285
|
+
return MCPResult(Existed);
|
|
2131
2286
|
}
|
|
2132
2287
|
|
|
2133
2288
|
// Add the composite section
|
|
@@ -2145,6 +2300,7 @@ TSharedPtr<FJsonValue> FAnimationHandlers::AddMontageSection(const TSharedPtr<FJ
|
|
|
2145
2300
|
UEditorAssetLibrary::SaveAsset(AssetPath);
|
|
2146
2301
|
|
|
2147
2302
|
auto Result = MCPSuccess();
|
|
2303
|
+
MCPSetCreated(Result);
|
|
2148
2304
|
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
|
2149
2305
|
Result->SetStringField(TEXT("sectionName"), SectionName);
|
|
2150
2306
|
Result->SetNumberField(TEXT("startTime"), StartTime);
|
|
@@ -2153,6 +2309,7 @@ TSharedPtr<FJsonValue> FAnimationHandlers::AddMontageSection(const TSharedPtr<FJ
|
|
|
2153
2309
|
Result->SetStringField(TEXT("linkedSection"), LinkedSection);
|
|
2154
2310
|
}
|
|
2155
2311
|
Result->SetNumberField(TEXT("totalSections"), Montage->CompositeSections.Num());
|
|
2312
|
+
// No rollback: no paired remove_montage_section handler.
|
|
2156
2313
|
|
|
2157
2314
|
return MCPResult(Result);
|
|
2158
2315
|
}
|
|
@@ -2168,6 +2325,12 @@ TSharedPtr<FJsonValue> FAnimationHandlers::CreateIKRig(const TSharedPtr<FJsonObj
|
|
|
2168
2325
|
if (auto Err = RequireString(Params, TEXT("skeletalMeshPath"), SkeletalMeshPath)) return Err;
|
|
2169
2326
|
|
|
2170
2327
|
FString PackagePath = OptionalString(Params, TEXT("packagePath"), TEXT("/Game"));
|
|
2328
|
+
const FString OnConflict = OptionalString(Params, TEXT("onConflict"), TEXT("skip"));
|
|
2329
|
+
|
|
2330
|
+
if (auto Hit = MCPCheckAssetExists(PackagePath, Name, OnConflict, TEXT("IKRigDefinition")))
|
|
2331
|
+
{
|
|
2332
|
+
return Hit;
|
|
2333
|
+
}
|
|
2171
2334
|
|
|
2172
2335
|
// Load the skeletal mesh to get the skeleton
|
|
2173
2336
|
USkeletalMesh* SkelMesh = LoadObject<USkeletalMesh>(nullptr, *SkeletalMeshPath);
|
|
@@ -2198,9 +2361,11 @@ TSharedPtr<FJsonValue> FAnimationHandlers::CreateIKRig(const TSharedPtr<FJsonObj
|
|
|
2198
2361
|
UEditorAssetLibrary::SaveAsset(IKRig->GetPathName());
|
|
2199
2362
|
|
|
2200
2363
|
auto Result = MCPSuccess();
|
|
2364
|
+
MCPSetCreated(Result);
|
|
2201
2365
|
Result->SetStringField(TEXT("assetPath"), IKRig->GetPathName());
|
|
2202
2366
|
Result->SetStringField(TEXT("name"), Name);
|
|
2203
2367
|
Result->SetStringField(TEXT("skeletalMeshPath"), SkeletalMeshPath);
|
|
2368
|
+
MCPSetDeleteAssetRollback(Result, IKRig->GetPathName());
|
|
2204
2369
|
|
|
2205
2370
|
return MCPResult(Result);
|
|
2206
2371
|
}
|
|
@@ -2432,16 +2597,33 @@ TSharedPtr<FJsonValue> FAnimationHandlers::RemoveVirtualBone(const TSharedPtr<FJ
|
|
|
2432
2597
|
USkeleton* Skeleton = LoadAssetByPath<USkeleton>(SkeletonPath);
|
|
2433
2598
|
if (!Skeleton) return MCPError(FString::Printf(TEXT("Skeleton not found: %s"), *SkeletonPath));
|
|
2434
2599
|
|
|
2600
|
+
// Idempotency: check if virtual bone exists
|
|
2601
|
+
const FName BoneFName(*BoneName);
|
|
2602
|
+
bool bFound = false;
|
|
2603
|
+
for (const FVirtualBone& VB : Skeleton->GetVirtualBones())
|
|
2604
|
+
{
|
|
2605
|
+
if (VB.VirtualBoneName == BoneFName) { bFound = true; break; }
|
|
2606
|
+
}
|
|
2607
|
+
if (!bFound)
|
|
2608
|
+
{
|
|
2609
|
+
auto Noop = MCPSuccess();
|
|
2610
|
+
Noop->SetStringField(TEXT("skeletonPath"), SkeletonPath);
|
|
2611
|
+
Noop->SetStringField(TEXT("virtualBoneName"), BoneName);
|
|
2612
|
+
Noop->SetBoolField(TEXT("alreadyDeleted"), true);
|
|
2613
|
+
return MCPResult(Noop);
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2435
2616
|
Skeleton->Modify();
|
|
2436
|
-
TArray<FName> ToRemove = {
|
|
2617
|
+
TArray<FName> ToRemove = { BoneFName };
|
|
2437
2618
|
Skeleton->RemoveVirtualBones(ToRemove);
|
|
2438
2619
|
Skeleton->PostEditChange();
|
|
2439
2620
|
UEditorAssetLibrary::SaveLoadedAsset(Skeleton);
|
|
2440
2621
|
|
|
2441
2622
|
TSharedPtr<FJsonObject> Result = MCPSuccess();
|
|
2442
|
-
MCPSetUpdated(Result);
|
|
2443
2623
|
Result->SetStringField(TEXT("skeletonPath"), SkeletonPath);
|
|
2444
2624
|
Result->SetStringField(TEXT("removed"), BoneName);
|
|
2625
|
+
Result->SetBoolField(TEXT("deleted"), true);
|
|
2626
|
+
// No rollback: removal of a virtual bone is not reversible without source/target capture.
|
|
2445
2627
|
return MCPResult(Result);
|
|
2446
2628
|
}
|
|
2447
2629
|
|
|
@@ -813,9 +813,11 @@ TSharedPtr<FJsonValue> FAssetHandlers::ImportDataTableJson(const TSharedPtr<FJso
|
|
|
813
813
|
DataTable->MarkPackageDirty();
|
|
814
814
|
|
|
815
815
|
auto Result = MCPSuccess();
|
|
816
|
+
MCPSetUpdated(Result);
|
|
816
817
|
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
|
817
818
|
Result->SetNumberField(TEXT("rowCount"), DataTable->GetRowMap().Num());
|
|
818
819
|
Result->SetStringField(TEXT("message"), TEXT("DataTable imported successfully from JSON"));
|
|
820
|
+
// No rollback: destructive — import replaces table contents.
|
|
819
821
|
|
|
820
822
|
return MCPResult(Result);
|
|
821
823
|
}
|
|
@@ -1682,9 +1684,12 @@ TSharedPtr<FJsonValue> FAssetHandlers::RecenterPivot(const TSharedPtr<FJsonObjec
|
|
|
1682
1684
|
}
|
|
1683
1685
|
|
|
1684
1686
|
auto Result = MCPSuccess();
|
|
1687
|
+
MCPSetUpdated(Result);
|
|
1685
1688
|
Result->SetArrayField(TEXT("meshes"), ResultArray);
|
|
1686
1689
|
Result->SetStringField(TEXT("offsetApplied"), FString::Printf(TEXT("(%.2f, %.2f, %.2f)"), Center.X, Center.Y, Center.Z));
|
|
1687
1690
|
Result->SetNumberField(TEXT("meshCount"), Meshes.Num());
|
|
1691
|
+
// No rollback: destructive/external — vertex offsets applied non-idempotently;
|
|
1692
|
+
// re-running shifts the pivot again. Not natural-key idempotent.
|
|
1688
1693
|
|
|
1689
1694
|
return MCPResult(Result);
|
|
1690
1695
|
}
|
|
@@ -1917,9 +1922,11 @@ TSharedPtr<FJsonValue> FAssetHandlers::ReimportDataTable(const TSharedPtr<FJsonO
|
|
|
1917
1922
|
DataTable->MarkPackageDirty();
|
|
1918
1923
|
|
|
1919
1924
|
auto Result = MCPSuccess();
|
|
1925
|
+
MCPSetUpdated(Result);
|
|
1920
1926
|
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
|
1921
1927
|
Result->SetNumberField(TEXT("rowCount"), DataTable->GetRowMap().Num());
|
|
1922
1928
|
Result->SetStringField(TEXT("message"), TEXT("DataTable reimported successfully from JSON"));
|
|
1929
|
+
// No rollback: destructive/external — reimport replaces table contents.
|
|
1923
1930
|
|
|
1924
1931
|
return MCPResult(Result);
|
|
1925
1932
|
}
|
|
@@ -1976,6 +1983,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::ReimportAsset(const TSharedPtr<FJsonObjec
|
|
|
1976
1983
|
bool bSuccess = FReimportManager::Instance()->Reimport(Asset, /*bAskForNewFileIfMissing=*/false, /*bShowNotification=*/false);
|
|
1977
1984
|
|
|
1978
1985
|
auto Result = MCPSuccess();
|
|
1986
|
+
if (bSuccess) MCPSetUpdated(Result);
|
|
1979
1987
|
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
|
1980
1988
|
Result->SetStringField(TEXT("assetClass"), Asset->GetClass()->GetName());
|
|
1981
1989
|
Result->SetBoolField(TEXT("success"), bSuccess);
|
|
@@ -1983,6 +1991,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::ReimportAsset(const TSharedPtr<FJsonObjec
|
|
|
1983
1991
|
{
|
|
1984
1992
|
Result->SetStringField(TEXT("error"), TEXT("Reimport failed -- check that the asset has a valid source file"));
|
|
1985
1993
|
}
|
|
1994
|
+
// No rollback: destructive/external — reimport pulls fresh from source file.
|
|
1986
1995
|
|
|
1987
1996
|
return MCPResult(Result);
|
|
1988
1997
|
}
|
|
@@ -199,10 +199,12 @@ TSharedPtr<FJsonValue> FAudioHandlers::PlaySoundAtLocation(const TSharedPtr<FJso
|
|
|
199
199
|
Params->TryGetNumberField(TEXT("pitchMultiplier"), Pitch);
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
//
|
|
202
|
+
// No rollback: destructive/external — playing a one-shot sound has no inverse.
|
|
203
|
+
// Replays produce a new audible event; not natural-key idempotent.
|
|
203
204
|
UGameplayStatics::PlaySoundAtLocation(World, Sound, Location, static_cast<float>(Volume), static_cast<float>(Pitch));
|
|
204
205
|
|
|
205
206
|
auto Result = MCPSuccess();
|
|
207
|
+
MCPSetUpdated(Result);
|
|
206
208
|
Result->SetStringField(TEXT("assetPath"), SoundPath);
|
|
207
209
|
|
|
208
210
|
return MCPResult(Result);
|