ue-mcp 1.2.3 → 1.3.0
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 +4 -3
- package/dist/action-class.js +56 -3
- package/dist/action-class.js.map +1 -1
- package/dist/bridge-timeouts.d.ts +64 -0
- package/dist/bridge-timeouts.js +118 -0
- package/dist/bridge-timeouts.js.map +1 -0
- package/dist/bridge.js +10 -2
- package/dist/bridge.js.map +1 -1
- package/dist/editor-control.d.ts +72 -5
- package/dist/editor-control.js +164 -140
- package/dist/editor-control.js.map +1 -1
- package/dist/editor-target.d.ts +55 -1
- package/dist/editor-target.js +107 -10
- package/dist/editor-target.js.map +1 -1
- package/dist/engine-observer.d.ts +59 -5
- package/dist/engine-observer.js +249 -29
- package/dist/engine-observer.js.map +1 -1
- package/dist/engine-root.d.ts +91 -0
- package/dist/engine-root.js +448 -0
- package/dist/engine-root.js.map +1 -0
- package/dist/epic-enrich.js +16 -1
- package/dist/epic-enrich.js.map +1 -1
- package/dist/feedback-cli.js +4 -0
- package/dist/feedback-cli.js.map +1 -1
- package/dist/feedback-deferred.d.ts +9 -0
- package/dist/feedback-deferred.js +2 -0
- package/dist/feedback-deferred.js.map +1 -1
- package/dist/feedback-fallback.d.ts +99 -0
- package/dist/feedback-fallback.js +173 -0
- package/dist/feedback-fallback.js.map +1 -0
- package/dist/flow/write-methods.js +20 -0
- package/dist/flow/write-methods.js.map +1 -1
- package/dist/index.js +41 -2
- package/dist/index.js.map +1 -1
- package/dist/lean-context.js +8 -3
- package/dist/lean-context.js.map +1 -1
- package/dist/locking.js +11 -2
- package/dist/locking.js.map +1 -1
- package/dist/plugin/loader.d.ts +4 -0
- package/dist/plugin/loader.js +22 -8
- package/dist/plugin/loader.js.map +1 -1
- package/dist/plugin/manifest.d.ts +72 -52
- package/dist/plugin/manifest.js +118 -21
- package/dist/plugin/manifest.js.map +1 -1
- package/dist/plugin-cli.js +16 -3
- package/dist/plugin-cli.js.map +1 -1
- package/dist/project.d.ts +11 -0
- package/dist/project.js +23 -2
- package/dist/project.js.map +1 -1
- package/dist/python-gate.d.ts +95 -0
- package/dist/python-gate.js +175 -0
- package/dist/python-gate.js.map +1 -0
- package/dist/tool-counts.json +16 -16
- package/dist/tools/animation.js +288 -24
- package/dist/tools/animation.js.map +1 -1
- package/dist/tools/asset.js +79 -14
- package/dist/tools/asset.js.map +1 -1
- package/dist/tools/audio.js +59 -5
- package/dist/tools/audio.js.map +1 -1
- package/dist/tools/blueprint.js +29 -12
- package/dist/tools/blueprint.js.map +1 -1
- package/dist/tools/chooser.js +4 -1
- package/dist/tools/chooser.js.map +1 -1
- package/dist/tools/editor.js +81 -43
- package/dist/tools/editor.js.map +1 -1
- package/dist/tools/feedback.js +214 -48
- package/dist/tools/feedback.js.map +1 -1
- package/dist/tools/foliage.js +27 -1
- package/dist/tools/foliage.js.map +1 -1
- package/dist/tools/gameplay.js +38 -11
- package/dist/tools/gameplay.js.map +1 -1
- package/dist/tools/gas.js +17 -5
- package/dist/tools/gas.js.map +1 -1
- package/dist/tools/landscape.js +22 -7
- package/dist/tools/landscape.js.map +1 -1
- package/dist/tools/level.js +322 -51
- package/dist/tools/level.js.map +1 -1
- package/dist/tools/material.js +10 -4
- package/dist/tools/material.js.map +1 -1
- package/dist/tools/niagara.js +3 -2
- package/dist/tools/niagara.js.map +1 -1
- package/dist/tools/pcg.js +6 -5
- package/dist/tools/pcg.js.map +1 -1
- package/dist/tools/plugins.js +2 -0
- package/dist/tools/plugins.js.map +1 -1
- package/dist/tools/project.js +47 -20
- package/dist/tools/project.js.map +1 -1
- package/dist/types.d.ts +24 -0
- package/dist/types.js +46 -3
- package/dist/types.js.map +1 -1
- package/dist/ue-mcp.default.yml +493 -125
- package/package.json +2 -2
- package/plugin/ue_mcp_bridge/NATIVE_RUNTIME_VISIBILITY.md +53 -0
- package/plugin/ue_mcp_bridge/NATIVE_STATIC_MESH_USAGE_PROGRESS.md +115 -0
- package/plugin/ue_mcp_bridge/README_MassHandlers.md +97 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/BridgeServer.cpp +14 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.cpp +21 -4
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.h +9 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerJsonProperty.h +395 -69
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.cpp +79 -3
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.h +25 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRig.cpp +3 -3
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRigSequencer.cpp +3061 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRetargeterAuthoring.cpp +961 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRigAuthoring.cpp +900 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Pose.cpp +769 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Sequence.cpp +44 -3
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_SkeletalLive.cpp +217 -11
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_StateMachine.cpp +551 -24
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Validation.cpp +735 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.cpp +352 -73
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.h +8 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkProperties.cpp +1 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.cpp +503 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.h +21 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkUpsert.cpp +2 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Enum.cpp +2 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.cpp +902 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.h +33 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Import.cpp +478 -134
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Mesh.cpp +5 -5
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.cpp +933 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.h +35 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Redirectors.cpp +478 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_RenderTarget.cpp +1 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Sockets.cpp +4 -4
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Struct.cpp +2 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Subobject.cpp +319 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AudioHandlers_SoundCue.cpp +3 -3
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.cpp +482 -35
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.h +10 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.cpp +502 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.h +43 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Functions.cpp +89 -7
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Graph.cpp +17 -51
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Internal.h +107 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Properties.cpp +10 -10
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Search.cpp +648 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DemoHandlers.cpp +5 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.cpp +252 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.h +10 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.cpp +252 -41
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.h +4 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_Build.cpp +250 -27
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIE.cpp +303 -125
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIERuntime.cpp +73 -50
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_RuntimeVisibility.cpp +757 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.cpp +11 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.h +3 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers_Batch.cpp +419 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.cpp +39 -161
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.h +66 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BTAuthoring.cpp +1323 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BehaviorTree.cpp +1211 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.cpp +2 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.h +60 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers_Runtime.cpp +363 -12
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/HandlerEditorState.h +84 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +1288 -36
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.h +3 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers_Sculpt.cpp +41 -16
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.cpp +959 -213
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.h +66 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_BatchWrite.cpp +1571 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Convert.cpp +316 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Inspection.cpp +280 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection.cpp +666 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection_Internal.h +63 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Lights.cpp +43 -12
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_MultiLevel.cpp +13 -47
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_PostProcess.cpp +527 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Query.cpp +1169 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Refresh.cpp +723 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Save.cpp +325 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Spatial.cpp +360 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Trace.cpp +178 -58
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Transient.cpp +337 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Volumes.cpp +25 -12
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartition.cpp +10 -9
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartitionSettings.cpp +686 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.cpp +467 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.h +22 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.cpp +452 -175
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.h +5 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Build.cpp +597 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NiagaraHandlers.cpp +17 -15
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PCGHandlers.cpp +38 -168
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +39 -30
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.cpp +392 -78
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.h +2 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers_Internal.h +83 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.cpp +200 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.h +15 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.cpp +19 -12
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/StateTreeHandlers.cpp +31 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.cpp +307 -69
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.h +84 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Extraction.cpp +8 -41
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Properties.cpp +33 -52
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/ActorSelectorTests.cpp +344 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationControlRigTimelineTests.cpp +60 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationPoseTests.cpp +115 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetBulkReadTests.cpp +177 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetDeleteGuardTests.cpp +276 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPathResolutionTests.cpp +243 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPropertyPersistTests.cpp +213 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetSubobjectTests.cpp +235 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AutomationRunnerTests.cpp +72 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeAuthoringTests.cpp +526 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeNodeTests.cpp +334 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BlueprintHandlerSurfaceTests.cpp +362 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BridgeProtocolTests.cpp +14 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/CollisionQueryTests.cpp +288 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/DataTableRowWriteTests.cpp +453 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/EditorFunctionCallTests.cpp +223 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/FoliageBatchTests.cpp +147 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GameThreadGateTests.cpp +82 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GasLiveAttributeTests.cpp +183 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LandscapeSampleTests.cpp +119 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelBatchWriteTests.cpp +176 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelComponentTreeInstanceCountTests.cpp +189 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelConvertBrushesTests.cpp +100 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelInstanceProjectionTests.cpp +265 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelPostProcessOverrideTests.cpp +258 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelQueryComponentsTests.cpp +195 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelRefreshTests.cpp +142 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelSaveReportTests.cpp +125 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelScriptHandlersTests.cpp +42 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelStaticMeshUsageTests.cpp +412 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTraceWorldScopeTests.cpp +133 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTransientActorTests.cpp +205 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelWorldPartitionSettingsTests.cpp +168 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTestTypes.h +51 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTests.cpp +379 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MaterialBuildTests.cpp +238 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshBooleanTests.cpp +170 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshGeometryTests.cpp +116 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageSaveExtensionTests.cpp +161 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageWriteGuardTests.cpp +253 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/RuntimeVisibilityTests.cpp +143 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerHandlerTests.cpp +660 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerScrubTests.cpp +50 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SkeletonDiffTests.cpp +685 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/WidgetBlueprintResolveTests.cpp +145 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/UE_MCP_Bridge.cpp +80 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerFunctionCall.h +509 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerQuery.h +407 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerUtils.h +1008 -77
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/UE_MCP_Bridge.Build.cs +87 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/MCPEngineStatus.cpp +152 -14
- package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/Tests/MCPEngineStatusTests.cpp +65 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Public/MCPEngineStatus.h +32 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/UE_MCP_BridgeStatus.Build.cs +2 -0
- package/skills/ue-mcp-animation/SKILL.md +154 -0
- package/skills/ue-mcp-blueprint/SKILL.md +71 -0
package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_BatchWrite.cpp
ADDED
|
@@ -0,0 +1,1571 @@
|
|
|
1
|
+
// Level-wide writes that used to be Python loops (#984, #941, #907, #987).
|
|
2
|
+
//
|
|
3
|
+
// The read side of this problem is level(query_components): a level-wide
|
|
4
|
+
// question had no editor-side answer. These are the same complaint on the
|
|
5
|
+
// write side. Every one of them was a loop over an editor-side selector that
|
|
6
|
+
// the client had to reproduce for itself:
|
|
7
|
+
//
|
|
8
|
+
// #984 190 SpotLights tagged LichtTest, moved, retinted and shadow-toggled
|
|
9
|
+
// one call at a time. batch_translate exists but wants an explicit
|
|
10
|
+
// label list, and auto-numbered labels make that list a generated
|
|
11
|
+
// payload rather than a selector.
|
|
12
|
+
// #941 NavModifierComponent.AreaClass across dozens of placed
|
|
13
|
+
// StaticMeshActors. set_component_property is one actor per call.
|
|
14
|
+
// #907 230 orphaned SplineMeshComponents across 23 actors. Their names are
|
|
15
|
+
// auto-generated (SplineMeshComponent_0, _1, ...) and remove_component
|
|
16
|
+
// takes one name at a time, so there was no way to name them all.
|
|
17
|
+
// #987 A SpotLight at every lamp post, positioned from each lamp
|
|
18
|
+
// component's own bounds. place_actors_batch is StaticMesh only and
|
|
19
|
+
// spawn_light takes a single position.
|
|
20
|
+
//
|
|
21
|
+
// The property writes deliberately delegate to FLevelHandlers::SetActorProperty
|
|
22
|
+
// and ::SetComponentProperty rather than reimplementing them. Those two carry
|
|
23
|
+
// years of accumulated behaviour (actor-label object refs, TArray-of-actor
|
|
24
|
+
// refs, instanced sub-object descent, the EditDefaultsOnly override), and a
|
|
25
|
+
// batch that quietly meant something narrower than the single-item action
|
|
26
|
+
// would be worse than no batch at all.
|
|
27
|
+
|
|
28
|
+
#include "LevelHandlers.h"
|
|
29
|
+
|
|
30
|
+
#include "Components/ActorComponent.h"
|
|
31
|
+
#include "Components/MeshComponent.h"
|
|
32
|
+
#include "Components/SceneComponent.h"
|
|
33
|
+
#include "Components/SplineComponent.h"
|
|
34
|
+
#include "Dom/JsonObject.h"
|
|
35
|
+
#include "Dom/JsonValue.h"
|
|
36
|
+
#include "Editor.h"
|
|
37
|
+
#include "Engine/World.h"
|
|
38
|
+
#include "EngineUtils.h"
|
|
39
|
+
#include "FileHelpers.h"
|
|
40
|
+
#include "GameFramework/Actor.h"
|
|
41
|
+
#include "HandlerQuery.h"
|
|
42
|
+
#include "HandlerRegistry.h"
|
|
43
|
+
#include "HandlerUtils.h"
|
|
44
|
+
#include "Materials/MaterialInterface.h"
|
|
45
|
+
#include "ScopedTransaction.h"
|
|
46
|
+
// #985: bulk HLOD layer assignment. UHLODLayer has to be a complete type for
|
|
47
|
+
// AActor::SetHLODLayer and for the asset load.
|
|
48
|
+
#include "WorldPartition/HLOD/HLODLayer.h"
|
|
49
|
+
|
|
50
|
+
namespace
|
|
51
|
+
{
|
|
52
|
+
// Bounds. Each of these actions replaced an unbounded loop, and an
|
|
53
|
+
// unbounded batch is the same failure with fewer round trips.
|
|
54
|
+
constexpr int32 MCPBatchMaxActors = 5000;
|
|
55
|
+
constexpr int32 MCPBatchMaxProperties = 32;
|
|
56
|
+
constexpr int32 MCPBatchMaxResultRows = 1000;
|
|
57
|
+
constexpr int32 MCPBatchDefaultMaxSpawn = 500;
|
|
58
|
+
constexpr int32 MCPBatchHardMaxSpawn = 5000;
|
|
59
|
+
constexpr double MCPBatchMinSplineSpacing = 1.0;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The selector every action in this file shares.
|
|
63
|
+
*
|
|
64
|
+
* Naming rule, and it is a rule rather than a style: a parameter that says
|
|
65
|
+
* `label` matches the EDITOR LABEL, a parameter that says `name` matches
|
|
66
|
+
* the internal object name, `Prefix` is a prefix and `Contains` is a
|
|
67
|
+
* substring. Two level actions that took similar-sounding filters and
|
|
68
|
+
* matched different things is exactly how #963 happened.
|
|
69
|
+
*/
|
|
70
|
+
struct FMCPBatchSelector
|
|
71
|
+
{
|
|
72
|
+
TArray<FString> ActorLabels;
|
|
73
|
+
FString LabelPrefix;
|
|
74
|
+
FString LabelContains;
|
|
75
|
+
FString Tag;
|
|
76
|
+
UClass* ActorClass = nullptr;
|
|
77
|
+
FString ActorClassFallback;
|
|
78
|
+
bool bMatchSubclasses = true;
|
|
79
|
+
FString FolderPath;
|
|
80
|
+
FString FolderPathPrefix;
|
|
81
|
+
bool bAny = false;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
TSharedPtr<FJsonValue> MCPBatchReadSelector(
|
|
85
|
+
const TSharedPtr<FJsonObject>& Params,
|
|
86
|
+
FMCPBatchSelector& OutSelector)
|
|
87
|
+
{
|
|
88
|
+
const TArray<TSharedPtr<FJsonValue>>* LabelValues = nullptr;
|
|
89
|
+
if (Params->TryGetArrayField(TEXT("actorLabels"), LabelValues) && LabelValues)
|
|
90
|
+
{
|
|
91
|
+
OutSelector.ActorLabels = JsonArrayToStringList(LabelValues);
|
|
92
|
+
}
|
|
93
|
+
OutSelector.LabelPrefix = OptionalString(Params, TEXT("labelPrefix"));
|
|
94
|
+
OutSelector.LabelContains = OptionalString(Params, TEXT("labelContains"));
|
|
95
|
+
OutSelector.Tag = OptionalString(Params, TEXT("tag"));
|
|
96
|
+
OutSelector.FolderPath = OptionalString(Params, TEXT("folderPath"));
|
|
97
|
+
OutSelector.FolderPathPrefix = OptionalString(Params, TEXT("folderPathPrefix"));
|
|
98
|
+
OutSelector.bMatchSubclasses = OptionalBool(Params, TEXT("matchSubclasses"), true);
|
|
99
|
+
|
|
100
|
+
const FString ClassSpec = OptionalString(Params, TEXT("classFilter"));
|
|
101
|
+
if (!ClassSpec.IsEmpty())
|
|
102
|
+
{
|
|
103
|
+
OutSelector.ActorClass = MCPResolveClassOfType(ClassSpec, AActor::StaticClass(), true);
|
|
104
|
+
if (!OutSelector.ActorClass)
|
|
105
|
+
{
|
|
106
|
+
// Substring on the class name is what the older level actions
|
|
107
|
+
// accept, so a call that used to work keeps working.
|
|
108
|
+
OutSelector.ActorClassFallback = ClassSpec;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
OutSelector.bAny =
|
|
113
|
+
!OutSelector.ActorLabels.IsEmpty() ||
|
|
114
|
+
!OutSelector.LabelPrefix.IsEmpty() ||
|
|
115
|
+
!OutSelector.LabelContains.IsEmpty() ||
|
|
116
|
+
!OutSelector.Tag.IsEmpty() ||
|
|
117
|
+
OutSelector.ActorClass != nullptr ||
|
|
118
|
+
!OutSelector.ActorClassFallback.IsEmpty() ||
|
|
119
|
+
!OutSelector.FolderPath.IsEmpty() ||
|
|
120
|
+
!OutSelector.FolderPathPrefix.IsEmpty();
|
|
121
|
+
|
|
122
|
+
return nullptr;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
bool MCPBatchActorMatches(const FMCPBatchSelector& Selector, AActor* Actor, const FString& Label)
|
|
126
|
+
{
|
|
127
|
+
if (!Selector.ActorLabels.IsEmpty() && !Selector.ActorLabels.Contains(Label)) return false;
|
|
128
|
+
if (!Selector.LabelPrefix.IsEmpty() && !Label.StartsWith(Selector.LabelPrefix, ESearchCase::CaseSensitive)) return false;
|
|
129
|
+
if (!Selector.LabelContains.IsEmpty() && !Label.Contains(Selector.LabelContains, ESearchCase::IgnoreCase)) return false;
|
|
130
|
+
if (!Selector.Tag.IsEmpty() && !Actor->ActorHasTag(FName(*Selector.Tag))) return false;
|
|
131
|
+
if (Selector.ActorClass)
|
|
132
|
+
{
|
|
133
|
+
const bool bMatches = Selector.bMatchSubclasses
|
|
134
|
+
? Actor->IsA(Selector.ActorClass)
|
|
135
|
+
: (Actor->GetClass() == Selector.ActorClass);
|
|
136
|
+
if (!bMatches) return false;
|
|
137
|
+
}
|
|
138
|
+
else if (!Selector.ActorClassFallback.IsEmpty() &&
|
|
139
|
+
!Actor->GetClass()->GetName().Contains(Selector.ActorClassFallback, ESearchCase::IgnoreCase))
|
|
140
|
+
{
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
if (!Selector.FolderPath.IsEmpty() &&
|
|
144
|
+
!Actor->GetFolderPath().ToString().Equals(Selector.FolderPath, ESearchCase::IgnoreCase))
|
|
145
|
+
{
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
if (!Selector.FolderPathPrefix.IsEmpty() &&
|
|
149
|
+
!Actor->GetFolderPath().ToString().StartsWith(Selector.FolderPathPrefix, ESearchCase::IgnoreCase))
|
|
150
|
+
{
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Every actor the selector picks, in a deterministic order. */
|
|
157
|
+
void MCPBatchCollectActors(UWorld* World, const FMCPBatchSelector& Selector, TArray<AActor*>& OutActors)
|
|
158
|
+
{
|
|
159
|
+
OutActors.Reset();
|
|
160
|
+
for (TActorIterator<AActor> It(World); It; ++It)
|
|
161
|
+
{
|
|
162
|
+
AActor* Actor = *It;
|
|
163
|
+
if (!Actor) continue;
|
|
164
|
+
if (MCPBatchActorMatches(Selector, Actor, Actor->GetActorLabel()))
|
|
165
|
+
{
|
|
166
|
+
OutActors.Add(Actor);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
OutActors.Sort([](const AActor& A, const AActor& B)
|
|
170
|
+
{
|
|
171
|
+
return A.GetPathName() < B.GetPathName();
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Labels the selector asked for by exact name and did not find.
|
|
177
|
+
*
|
|
178
|
+
* A destructive or sweeping call that matched nothing must say so with the
|
|
179
|
+
* reason, not return success and a zero. That is #963's actual harm.
|
|
180
|
+
*/
|
|
181
|
+
TArray<FString> MCPBatchMissingLabels(const FMCPBatchSelector& Selector, const TArray<AActor*>& Matched)
|
|
182
|
+
{
|
|
183
|
+
TArray<FString> Missing;
|
|
184
|
+
if (Selector.ActorLabels.IsEmpty())
|
|
185
|
+
{
|
|
186
|
+
return Missing;
|
|
187
|
+
}
|
|
188
|
+
TSet<FString> Found;
|
|
189
|
+
for (AActor* Actor : Matched)
|
|
190
|
+
{
|
|
191
|
+
Found.Add(Actor->GetActorLabel());
|
|
192
|
+
}
|
|
193
|
+
for (const FString& Label : Selector.ActorLabels)
|
|
194
|
+
{
|
|
195
|
+
if (!Found.Contains(Label))
|
|
196
|
+
{
|
|
197
|
+
Missing.Add(Label);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return Missing;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
FVector MCPBatchReadVector(const TSharedPtr<FJsonObject>& Obj, const FVector& Default)
|
|
204
|
+
{
|
|
205
|
+
if (!Obj.IsValid()) return Default;
|
|
206
|
+
FVector Value = Default;
|
|
207
|
+
double Component = 0.0;
|
|
208
|
+
if (Obj->TryGetNumberField(TEXT("x"), Component)) Value.X = Component;
|
|
209
|
+
if (Obj->TryGetNumberField(TEXT("y"), Component)) Value.Y = Component;
|
|
210
|
+
if (Obj->TryGetNumberField(TEXT("z"), Component)) Value.Z = Component;
|
|
211
|
+
return Value;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
FRotator MCPBatchReadRotator(const TSharedPtr<FJsonObject>& Obj, const FRotator& Default)
|
|
215
|
+
{
|
|
216
|
+
if (!Obj.IsValid()) return Default;
|
|
217
|
+
FRotator Value = Default;
|
|
218
|
+
double Component = 0.0;
|
|
219
|
+
if (Obj->TryGetNumberField(TEXT("pitch"), Component)) Value.Pitch = Component;
|
|
220
|
+
if (Obj->TryGetNumberField(TEXT("yaw"), Component)) Value.Yaw = Component;
|
|
221
|
+
if (Obj->TryGetNumberField(TEXT("roll"), Component)) Value.Roll = Component;
|
|
222
|
+
return Value;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Pull the { ok, error } verdict out of a delegated single-item handler. */
|
|
226
|
+
void MCPBatchReadVerdict(const TSharedPtr<FJsonValue>& Response, bool& bOutOk, FString& OutError, FString& OutPrevious)
|
|
227
|
+
{
|
|
228
|
+
bOutOk = false;
|
|
229
|
+
OutError.Reset();
|
|
230
|
+
OutPrevious.Reset();
|
|
231
|
+
if (!Response.IsValid() || Response->Type != EJson::Object)
|
|
232
|
+
{
|
|
233
|
+
OutError = TEXT("handler returned no result");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const TSharedPtr<FJsonObject> Object = Response->AsObject();
|
|
237
|
+
bOutOk = Object->HasField(TEXT("success")) ? Object->GetBoolField(TEXT("success")) : true;
|
|
238
|
+
Object->TryGetStringField(TEXT("error"), OutError);
|
|
239
|
+
Object->TryGetStringField(TEXT("previousValue"), OutPrevious);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Resolve one named component on an actor, matching the case-insensitive
|
|
243
|
+
* rule set_component_property already uses for SCS components. */
|
|
244
|
+
UActorComponent* MCPBatchFindComponent(AActor* Actor, const FString& ComponentName)
|
|
245
|
+
{
|
|
246
|
+
if (!Actor) return nullptr;
|
|
247
|
+
for (UActorComponent* Component : Actor->GetComponents())
|
|
248
|
+
{
|
|
249
|
+
if (Component && Component->GetName().Equals(ComponentName, ESearchCase::IgnoreCase))
|
|
250
|
+
{
|
|
251
|
+
return Component;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return nullptr;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// batch_set_actor_properties (#984)
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
TSharedPtr<FJsonValue> FLevelHandlers::BatchSetActorProperties(const TSharedPtr<FJsonObject>& Params)
|
|
262
|
+
{
|
|
263
|
+
MCP_CHECK_GAME_THREAD();
|
|
264
|
+
REQUIRE_EDITOR_WORLD(World);
|
|
265
|
+
|
|
266
|
+
const TSharedPtr<FJsonObject>* PropertiesObject = nullptr;
|
|
267
|
+
if (!Params->TryGetObjectField(TEXT("properties"), PropertiesObject) ||
|
|
268
|
+
!PropertiesObject || !(*PropertiesObject).IsValid() || (*PropertiesObject)->Values.Num() == 0)
|
|
269
|
+
{
|
|
270
|
+
return MCPError(TEXT("Missing 'properties' (an object of propertyName -> value; dotted paths are supported)"));
|
|
271
|
+
}
|
|
272
|
+
if ((*PropertiesObject)->Values.Num() > MCPBatchMaxProperties)
|
|
273
|
+
{
|
|
274
|
+
return MCPError(FString::Printf(
|
|
275
|
+
TEXT("'properties' exceeds the maximum of %d entries"), MCPBatchMaxProperties));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
FMCPBatchSelector Selector;
|
|
279
|
+
if (auto Err = MCPBatchReadSelector(Params, Selector)) return Err;
|
|
280
|
+
if (!Selector.bAny)
|
|
281
|
+
{
|
|
282
|
+
return MCPError(TEXT("Pass at least one selector: actorLabels, labelPrefix, labelContains, tag, classFilter, folderPath or folderPathPrefix"));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const bool bDryRun = OptionalBool(Params, TEXT("dryRun"), false);
|
|
286
|
+
const bool bForce = OptionalBool(Params, TEXT("force"), false);
|
|
287
|
+
const FString TransactionLabel = OptionalString(
|
|
288
|
+
Params, TEXT("transactionLabel"), TEXT("MCP batch set actor properties"));
|
|
289
|
+
|
|
290
|
+
TArray<AActor*> Actors;
|
|
291
|
+
MCPBatchCollectActors(World, Selector, Actors);
|
|
292
|
+
if (Actors.Num() > MCPBatchMaxActors)
|
|
293
|
+
{
|
|
294
|
+
return MCPError(FString::Printf(
|
|
295
|
+
TEXT("The selector matched %d actors, over the maximum of %d. Narrow it."),
|
|
296
|
+
Actors.Num(), MCPBatchMaxActors));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
auto Result = MCPSuccess();
|
|
300
|
+
Result->SetBoolField(TEXT("dryRun"), bDryRun);
|
|
301
|
+
Result->SetNumberField(TEXT("matched"), Actors.Num());
|
|
302
|
+
MCPNoteLoadedOnlyEnumeration(World, Result);
|
|
303
|
+
{
|
|
304
|
+
const TArray<FString> Missing = MCPBatchMissingLabels(Selector, Actors);
|
|
305
|
+
if (Missing.Num() > 0)
|
|
306
|
+
{
|
|
307
|
+
Result->SetArrayField(TEXT("missingLabels"), MCPStringListToJson(Missing));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (Actors.IsEmpty())
|
|
311
|
+
{
|
|
312
|
+
const FString Needle = !Selector.LabelPrefix.IsEmpty()
|
|
313
|
+
? Selector.LabelPrefix
|
|
314
|
+
: Selector.LabelContains;
|
|
315
|
+
if (const TSharedPtr<FJsonObject> Hint = MCPDescribeZeroActorMatch(World, Needle))
|
|
316
|
+
{
|
|
317
|
+
Result->SetObjectField(TEXT("zeroMatchHint"), Hint);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
TArray<TSharedPtr<FJsonValue>> Rows;
|
|
322
|
+
int32 Updated = 0;
|
|
323
|
+
int32 Failed = 0;
|
|
324
|
+
int32 PropertyWrites = 0;
|
|
325
|
+
int32 PropertyFailures = 0;
|
|
326
|
+
|
|
327
|
+
// One transaction for the whole batch, so 190 lights is one undo rather
|
|
328
|
+
// than 190. Skipped entirely on a dry run, which writes nothing.
|
|
329
|
+
TUniquePtr<FScopedTransaction> Transaction;
|
|
330
|
+
if (!bDryRun && !Actors.IsEmpty())
|
|
331
|
+
{
|
|
332
|
+
Transaction = MakeUnique<FScopedTransaction>(FText::FromString(TransactionLabel));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
for (AActor* Actor : Actors)
|
|
336
|
+
{
|
|
337
|
+
const FString Label = Actor->GetActorLabel();
|
|
338
|
+
TSharedPtr<FJsonObject> Row = MakeShared<FJsonObject>();
|
|
339
|
+
Row->SetStringField(TEXT("actorLabel"), Label);
|
|
340
|
+
Row->SetStringField(TEXT("actorClass"), Actor->GetClass()->GetName());
|
|
341
|
+
|
|
342
|
+
TArray<TSharedPtr<FJsonValue>> PropertyRows;
|
|
343
|
+
bool bActorOk = true;
|
|
344
|
+
for (const TPair<FString, TSharedPtr<FJsonValue>>& Pair : (*PropertiesObject)->Values)
|
|
345
|
+
{
|
|
346
|
+
TSharedPtr<FJsonObject> PropertyRow = MakeShared<FJsonObject>();
|
|
347
|
+
PropertyRow->SetStringField(TEXT("propertyName"), Pair.Key);
|
|
348
|
+
|
|
349
|
+
if (bDryRun)
|
|
350
|
+
{
|
|
351
|
+
// A preview has to be a real check, or it is not a preview.
|
|
352
|
+
// Resolve the path against this actor's class and say whether
|
|
353
|
+
// the write would land.
|
|
354
|
+
FString ResolvedType;
|
|
355
|
+
const TSharedPtr<FJsonValue> Current =
|
|
356
|
+
MCPQuery::ReadDottedProperty(Actor, Pair.Key, ResolvedType);
|
|
357
|
+
const bool bResolvable = Current.IsValid();
|
|
358
|
+
PropertyRow->SetBoolField(TEXT("ok"), bResolvable);
|
|
359
|
+
if (bResolvable)
|
|
360
|
+
{
|
|
361
|
+
PropertyRow->SetField(TEXT("currentValue"), Current);
|
|
362
|
+
PropertyRow->SetStringField(TEXT("propertyType"), ResolvedType);
|
|
363
|
+
}
|
|
364
|
+
else
|
|
365
|
+
{
|
|
366
|
+
PropertyRow->SetStringField(TEXT("error"), FString::Printf(
|
|
367
|
+
TEXT("Property '%s' does not resolve on %s"), *Pair.Key, *Actor->GetClass()->GetName()));
|
|
368
|
+
bActorOk = false;
|
|
369
|
+
++PropertyFailures;
|
|
370
|
+
}
|
|
371
|
+
PropertyRows.Add(MakeShared<FJsonValueObject>(PropertyRow));
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Delegate to the single-actor writer. Reimplementing it here
|
|
376
|
+
// would quietly support fewer value shapes than set_actor_property
|
|
377
|
+
// does, which is the worse failure: the call succeeds and means
|
|
378
|
+
// something narrower than the caller asked for.
|
|
379
|
+
TSharedPtr<FJsonObject> SubParams = MakeShared<FJsonObject>();
|
|
380
|
+
SubParams->SetStringField(TEXT("actorLabel"), Label);
|
|
381
|
+
SubParams->SetStringField(TEXT("propertyName"), Pair.Key);
|
|
382
|
+
SubParams->SetField(TEXT("value"), Pair.Value);
|
|
383
|
+
if (bForce) SubParams->SetBoolField(TEXT("force"), true);
|
|
384
|
+
|
|
385
|
+
bool bOk = false;
|
|
386
|
+
FString Error;
|
|
387
|
+
FString Previous;
|
|
388
|
+
MCPBatchReadVerdict(FLevelHandlers::SetActorProperty(SubParams), bOk, Error, Previous);
|
|
389
|
+
PropertyRow->SetBoolField(TEXT("ok"), bOk);
|
|
390
|
+
if (!Previous.IsEmpty()) PropertyRow->SetStringField(TEXT("previousValue"), Previous);
|
|
391
|
+
if (!bOk)
|
|
392
|
+
{
|
|
393
|
+
PropertyRow->SetStringField(TEXT("error"), Error);
|
|
394
|
+
bActorOk = false;
|
|
395
|
+
++PropertyFailures;
|
|
396
|
+
}
|
|
397
|
+
else
|
|
398
|
+
{
|
|
399
|
+
++PropertyWrites;
|
|
400
|
+
}
|
|
401
|
+
PropertyRows.Add(MakeShared<FJsonValueObject>(PropertyRow));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
Row->SetBoolField(TEXT("ok"), bActorOk);
|
|
405
|
+
Row->SetArrayField(TEXT("properties"), PropertyRows);
|
|
406
|
+
if (bActorOk) ++Updated; else ++Failed;
|
|
407
|
+
if (Rows.Num() < MCPBatchMaxResultRows)
|
|
408
|
+
{
|
|
409
|
+
Rows.Add(MakeShared<FJsonValueObject>(Row));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
Result->SetBoolField(TEXT("success"), Failed == 0);
|
|
414
|
+
if (Failed > 0)
|
|
415
|
+
{
|
|
416
|
+
Result->SetStringField(TEXT("error"), FString::Printf(
|
|
417
|
+
TEXT("%d of %d actors had at least one property that did not write; see results[]."),
|
|
418
|
+
Failed, Actors.Num()));
|
|
419
|
+
}
|
|
420
|
+
Result->SetNumberField(bDryRun ? TEXT("wouldUpdate") : TEXT("updated"), Updated);
|
|
421
|
+
Result->SetNumberField(TEXT("failed"), Failed);
|
|
422
|
+
Result->SetNumberField(TEXT("propertyWrites"), PropertyWrites);
|
|
423
|
+
Result->SetNumberField(TEXT("propertyFailures"), PropertyFailures);
|
|
424
|
+
Result->SetNumberField(TEXT("returnedResults"), Rows.Num());
|
|
425
|
+
Result->SetBoolField(TEXT("resultsTruncated"), Rows.Num() < Actors.Num());
|
|
426
|
+
Result->SetArrayField(TEXT("results"), Rows);
|
|
427
|
+
if (!bDryRun && Updated > 0)
|
|
428
|
+
{
|
|
429
|
+
MCPSetUpdated(Result);
|
|
430
|
+
Result->SetStringField(TEXT("saveNote"),
|
|
431
|
+
TEXT("The level is left dirty and is NOT saved. Call level(save) when you are done."));
|
|
432
|
+
}
|
|
433
|
+
return MCPResult(Result);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ---------------------------------------------------------------------------
|
|
437
|
+
// bulk_set_component_property (#941)
|
|
438
|
+
// ---------------------------------------------------------------------------
|
|
439
|
+
TSharedPtr<FJsonValue> FLevelHandlers::BulkSetComponentProperty(const TSharedPtr<FJsonObject>& Params)
|
|
440
|
+
{
|
|
441
|
+
MCP_CHECK_GAME_THREAD();
|
|
442
|
+
REQUIRE_EDITOR_WORLD(World);
|
|
443
|
+
|
|
444
|
+
FString ComponentName;
|
|
445
|
+
if (auto Err = RequireString(Params, TEXT("componentName"), ComponentName)) return Err;
|
|
446
|
+
FString PropertyName;
|
|
447
|
+
if (auto Err = RequireString(Params, TEXT("propertyName"), PropertyName)) return Err;
|
|
448
|
+
|
|
449
|
+
const TSharedPtr<FJsonValue>* ValueField = Params->Values.Find(TEXT("value"));
|
|
450
|
+
if (!ValueField)
|
|
451
|
+
{
|
|
452
|
+
return MCPError(TEXT("Missing 'value' parameter (pass JSON null to clear an object reference)"));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
FMCPBatchSelector Selector;
|
|
456
|
+
if (auto Err = MCPBatchReadSelector(Params, Selector)) return Err;
|
|
457
|
+
if (!Selector.bAny)
|
|
458
|
+
{
|
|
459
|
+
return MCPError(TEXT("Pass at least one selector: actorLabels, labelPrefix, labelContains, tag, classFilter, folderPath or folderPathPrefix"));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const bool bDryRun = OptionalBool(Params, TEXT("dryRun"), false);
|
|
463
|
+
const FString TransactionLabel = OptionalString(
|
|
464
|
+
Params, TEXT("transactionLabel"), TEXT("MCP bulk set component property"));
|
|
465
|
+
|
|
466
|
+
TArray<AActor*> Actors;
|
|
467
|
+
MCPBatchCollectActors(World, Selector, Actors);
|
|
468
|
+
if (Actors.Num() > MCPBatchMaxActors)
|
|
469
|
+
{
|
|
470
|
+
return MCPError(FString::Printf(
|
|
471
|
+
TEXT("The selector matched %d actors, over the maximum of %d. Narrow it."),
|
|
472
|
+
Actors.Num(), MCPBatchMaxActors));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
auto Result = MCPSuccess();
|
|
476
|
+
Result->SetBoolField(TEXT("dryRun"), bDryRun);
|
|
477
|
+
Result->SetStringField(TEXT("componentName"), ComponentName);
|
|
478
|
+
Result->SetStringField(TEXT("propertyName"), PropertyName);
|
|
479
|
+
Result->SetNumberField(TEXT("matchedActors"), Actors.Num());
|
|
480
|
+
MCPNoteLoadedOnlyEnumeration(World, Result);
|
|
481
|
+
{
|
|
482
|
+
const TArray<FString> Missing = MCPBatchMissingLabels(Selector, Actors);
|
|
483
|
+
if (Missing.Num() > 0)
|
|
484
|
+
{
|
|
485
|
+
Result->SetArrayField(TEXT("missingLabels"), MCPStringListToJson(Missing));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
TUniquePtr<FScopedTransaction> Transaction;
|
|
490
|
+
if (!bDryRun && !Actors.IsEmpty())
|
|
491
|
+
{
|
|
492
|
+
Transaction = MakeUnique<FScopedTransaction>(FText::FromString(TransactionLabel));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
TArray<TSharedPtr<FJsonValue>> Rows;
|
|
496
|
+
int32 Updated = 0;
|
|
497
|
+
int32 Failed = 0;
|
|
498
|
+
int32 WithoutComponent = 0;
|
|
499
|
+
|
|
500
|
+
for (AActor* Actor : Actors)
|
|
501
|
+
{
|
|
502
|
+
const FString Label = Actor->GetActorLabel();
|
|
503
|
+
TSharedPtr<FJsonObject> Row = MakeShared<FJsonObject>();
|
|
504
|
+
Row->SetStringField(TEXT("actorLabel"), Label);
|
|
505
|
+
|
|
506
|
+
UActorComponent* Component = MCPBatchFindComponent(Actor, ComponentName);
|
|
507
|
+
if (!Component)
|
|
508
|
+
{
|
|
509
|
+
// An actor that has no such component is neither a success nor an
|
|
510
|
+
// error: it is a fact the caller has to be told, because a
|
|
511
|
+
// selector wide enough to be useful will pick some up.
|
|
512
|
+
++WithoutComponent;
|
|
513
|
+
Row->SetBoolField(TEXT("ok"), false);
|
|
514
|
+
Row->SetStringField(TEXT("status"), TEXT("no_such_component"));
|
|
515
|
+
if (Rows.Num() < MCPBatchMaxResultRows) Rows.Add(MakeShared<FJsonValueObject>(Row));
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
Row->SetStringField(TEXT("componentClass"), Component->GetClass()->GetName());
|
|
519
|
+
|
|
520
|
+
if (bDryRun)
|
|
521
|
+
{
|
|
522
|
+
FString ResolvedType;
|
|
523
|
+
const TSharedPtr<FJsonValue> Current =
|
|
524
|
+
MCPQuery::ReadDottedProperty(Component, PropertyName, ResolvedType);
|
|
525
|
+
const bool bResolvable = Current.IsValid();
|
|
526
|
+
Row->SetBoolField(TEXT("ok"), bResolvable);
|
|
527
|
+
Row->SetStringField(TEXT("status"), bResolvable ? TEXT("would_write") : TEXT("property_not_found"));
|
|
528
|
+
if (bResolvable)
|
|
529
|
+
{
|
|
530
|
+
Row->SetField(TEXT("currentValue"), Current);
|
|
531
|
+
Row->SetStringField(TEXT("propertyType"), ResolvedType);
|
|
532
|
+
++Updated;
|
|
533
|
+
}
|
|
534
|
+
else
|
|
535
|
+
{
|
|
536
|
+
++Failed;
|
|
537
|
+
}
|
|
538
|
+
if (Rows.Num() < MCPBatchMaxResultRows) Rows.Add(MakeShared<FJsonValueObject>(Row));
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
TSharedPtr<FJsonObject> SubParams = MakeShared<FJsonObject>();
|
|
543
|
+
SubParams->SetStringField(TEXT("actorLabel"), Label);
|
|
544
|
+
SubParams->SetStringField(TEXT("componentName"), ComponentName);
|
|
545
|
+
SubParams->SetStringField(TEXT("propertyName"), PropertyName);
|
|
546
|
+
SubParams->SetField(TEXT("value"), *ValueField);
|
|
547
|
+
|
|
548
|
+
bool bOk = false;
|
|
549
|
+
FString Error;
|
|
550
|
+
FString Previous;
|
|
551
|
+
MCPBatchReadVerdict(FLevelHandlers::SetComponentProperty(SubParams), bOk, Error, Previous);
|
|
552
|
+
Row->SetBoolField(TEXT("ok"), bOk);
|
|
553
|
+
Row->SetStringField(TEXT("status"), bOk ? TEXT("written") : TEXT("failed"));
|
|
554
|
+
if (!Previous.IsEmpty()) Row->SetStringField(TEXT("previousValue"), Previous);
|
|
555
|
+
if (!bOk)
|
|
556
|
+
{
|
|
557
|
+
Row->SetStringField(TEXT("error"), Error);
|
|
558
|
+
++Failed;
|
|
559
|
+
}
|
|
560
|
+
else
|
|
561
|
+
{
|
|
562
|
+
++Updated;
|
|
563
|
+
}
|
|
564
|
+
if (Rows.Num() < MCPBatchMaxResultRows) Rows.Add(MakeShared<FJsonValueObject>(Row));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
Result->SetBoolField(TEXT("success"), Failed == 0);
|
|
568
|
+
if (Failed > 0)
|
|
569
|
+
{
|
|
570
|
+
Result->SetStringField(TEXT("error"), FString::Printf(
|
|
571
|
+
TEXT("%d of %d matched actors did not accept the write; see results[]."), Failed, Actors.Num()));
|
|
572
|
+
}
|
|
573
|
+
Result->SetNumberField(bDryRun ? TEXT("wouldUpdate") : TEXT("updated"), Updated);
|
|
574
|
+
Result->SetNumberField(TEXT("failed"), Failed);
|
|
575
|
+
Result->SetNumberField(TEXT("actorsWithoutComponent"), WithoutComponent);
|
|
576
|
+
Result->SetNumberField(TEXT("returnedResults"), Rows.Num());
|
|
577
|
+
Result->SetBoolField(TEXT("resultsTruncated"), Rows.Num() < Actors.Num());
|
|
578
|
+
Result->SetArrayField(TEXT("results"), Rows);
|
|
579
|
+
if (!bDryRun && Updated > 0)
|
|
580
|
+
{
|
|
581
|
+
MCPSetUpdated(Result);
|
|
582
|
+
Result->SetStringField(TEXT("saveNote"),
|
|
583
|
+
TEXT("The level is left dirty and is NOT saved. Call level(save) when you are done."));
|
|
584
|
+
}
|
|
585
|
+
return MCPResult(Result);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// ---------------------------------------------------------------------------
|
|
589
|
+
// remove_components_by_class (#907)
|
|
590
|
+
// ---------------------------------------------------------------------------
|
|
591
|
+
TSharedPtr<FJsonValue> FLevelHandlers::RemoveComponentsByClass(const TSharedPtr<FJsonObject>& Params)
|
|
592
|
+
{
|
|
593
|
+
MCP_CHECK_GAME_THREAD();
|
|
594
|
+
REQUIRE_EDITOR_WORLD(World);
|
|
595
|
+
|
|
596
|
+
FString ComponentClassSpec;
|
|
597
|
+
if (auto Err = RequireString(Params, TEXT("componentClass"), ComponentClassSpec)) return Err;
|
|
598
|
+
|
|
599
|
+
UClass* ComponentClass = MCPResolveClassOfType(ComponentClassSpec, UActorComponent::StaticClass(), true);
|
|
600
|
+
if (!ComponentClass)
|
|
601
|
+
{
|
|
602
|
+
return MCPClassNotFoundError(ComponentClassSpec, TEXT("componentClass"));
|
|
603
|
+
}
|
|
604
|
+
const bool bMatchComponentSubclasses = OptionalBool(Params, TEXT("matchComponentSubclasses"), true);
|
|
605
|
+
const FString ComponentNameContains = OptionalString(Params, TEXT("componentNameContains"));
|
|
606
|
+
|
|
607
|
+
FMCPBatchSelector Selector;
|
|
608
|
+
if (auto Err = MCPBatchReadSelector(Params, Selector)) return Err;
|
|
609
|
+
// actorClassFilter is the name #907 asks for; classFilter is what every
|
|
610
|
+
// other level action calls it. Accept both rather than make the caller
|
|
611
|
+
// remember which action they are in.
|
|
612
|
+
const FString ActorClassFilter = OptionalString(Params, TEXT("actorClassFilter"));
|
|
613
|
+
if (!ActorClassFilter.IsEmpty() && !Selector.ActorClass && Selector.ActorClassFallback.IsEmpty())
|
|
614
|
+
{
|
|
615
|
+
Selector.ActorClass = MCPResolveClassOfType(ActorClassFilter, AActor::StaticClass(), true);
|
|
616
|
+
if (!Selector.ActorClass) Selector.ActorClassFallback = ActorClassFilter;
|
|
617
|
+
Selector.bAny = true;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// dryRun DEFAULTS TO TRUE. This deletes components whose auto-generated
|
|
621
|
+
// names a caller cannot enumerate, which means it is the one level action
|
|
622
|
+
// where the caller most needs to see the list before it happens. Same
|
|
623
|
+
// default, and same reason, as delete_exact_labeled_actors_in_levels.
|
|
624
|
+
const bool bDryRun = OptionalBool(Params, TEXT("dryRun"), true);
|
|
625
|
+
const bool bSave = OptionalBool(Params, TEXT("save"), false);
|
|
626
|
+
const FString TransactionLabel = OptionalString(
|
|
627
|
+
Params, TEXT("transactionLabel"), TEXT("MCP remove components by class"));
|
|
628
|
+
|
|
629
|
+
TArray<AActor*> Actors;
|
|
630
|
+
if (Selector.bAny)
|
|
631
|
+
{
|
|
632
|
+
MCPBatchCollectActors(World, Selector, Actors);
|
|
633
|
+
}
|
|
634
|
+
else
|
|
635
|
+
{
|
|
636
|
+
// No actor selector means every loaded actor. That is a legitimate
|
|
637
|
+
// request (230 orphans across 23 actors nobody had named yet), which
|
|
638
|
+
// is exactly why dryRun defaults to true.
|
|
639
|
+
for (TActorIterator<AActor> It(World); It; ++It)
|
|
640
|
+
{
|
|
641
|
+
if (AActor* Actor = *It) Actors.Add(Actor);
|
|
642
|
+
}
|
|
643
|
+
Actors.Sort([](const AActor& A, const AActor& B) { return A.GetPathName() < B.GetPathName(); });
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
auto Result = MCPSuccess();
|
|
647
|
+
Result->SetBoolField(TEXT("dryRun"), bDryRun);
|
|
648
|
+
Result->SetStringField(TEXT("componentClass"), ComponentClass->GetName());
|
|
649
|
+
MCPNoteLoadedOnlyEnumeration(World, Result);
|
|
650
|
+
{
|
|
651
|
+
const TArray<FString> Missing = MCPBatchMissingLabels(Selector, Actors);
|
|
652
|
+
if (Missing.Num() > 0)
|
|
653
|
+
{
|
|
654
|
+
Result->SetArrayField(TEXT("missingLabels"), MCPStringListToJson(Missing));
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
TUniquePtr<FScopedTransaction> Transaction;
|
|
659
|
+
if (!bDryRun)
|
|
660
|
+
{
|
|
661
|
+
Transaction = MakeUnique<FScopedTransaction>(FText::FromString(TransactionLabel));
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
TArray<TSharedPtr<FJsonValue>> Rows;
|
|
665
|
+
int32 ScannedActors = 0;
|
|
666
|
+
int32 MatchedComponents = 0;
|
|
667
|
+
int32 RemovedComponents = 0;
|
|
668
|
+
int32 SkippedNonInstance = 0;
|
|
669
|
+
int32 TouchedActors = 0;
|
|
670
|
+
|
|
671
|
+
for (AActor* Actor : Actors)
|
|
672
|
+
{
|
|
673
|
+
++ScannedActors;
|
|
674
|
+
|
|
675
|
+
TArray<UActorComponent*> Targets;
|
|
676
|
+
for (UActorComponent* Component : Actor->GetComponents())
|
|
677
|
+
{
|
|
678
|
+
if (!Component) continue;
|
|
679
|
+
const bool bClassMatches = bMatchComponentSubclasses
|
|
680
|
+
? Component->IsA(ComponentClass)
|
|
681
|
+
: (Component->GetClass() == ComponentClass);
|
|
682
|
+
if (!bClassMatches) continue;
|
|
683
|
+
if (!ComponentNameContains.IsEmpty() &&
|
|
684
|
+
!Component->GetName().Contains(ComponentNameContains, ESearchCase::IgnoreCase))
|
|
685
|
+
{
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
Targets.Add(Component);
|
|
689
|
+
}
|
|
690
|
+
if (Targets.IsEmpty())
|
|
691
|
+
{
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Deterministic order so a dry run and the commit that follows report
|
|
696
|
+
// the same names.
|
|
697
|
+
Targets.Sort([](const UActorComponent& A, const UActorComponent& B)
|
|
698
|
+
{
|
|
699
|
+
return A.GetName() < B.GetName();
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
TArray<FString> RemovedNames;
|
|
703
|
+
TArray<FString> SkippedNames;
|
|
704
|
+
for (UActorComponent* Component : Targets)
|
|
705
|
+
{
|
|
706
|
+
++MatchedComponents;
|
|
707
|
+
// Only instance components can be removed. A native or SCS
|
|
708
|
+
// component belongs to the class, and destroying it here would
|
|
709
|
+
// come back on the next construction rerun looking like the
|
|
710
|
+
// removal silently failed.
|
|
711
|
+
const bool bIsInstanceComponent = Actor->GetInstanceComponents().Contains(Component);
|
|
712
|
+
if (!bIsInstanceComponent)
|
|
713
|
+
{
|
|
714
|
+
++SkippedNonInstance;
|
|
715
|
+
SkippedNames.Add(Component->GetName());
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
if (bDryRun)
|
|
719
|
+
{
|
|
720
|
+
RemovedNames.Add(Component->GetName());
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
const FString Name = Component->GetName();
|
|
724
|
+
Actor->Modify();
|
|
725
|
+
Component->Modify();
|
|
726
|
+
Actor->RemoveInstanceComponent(Component);
|
|
727
|
+
Component->DestroyComponent();
|
|
728
|
+
RemovedNames.Add(Name);
|
|
729
|
+
++RemovedComponents;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (RemovedNames.IsEmpty() && SkippedNames.IsEmpty())
|
|
733
|
+
{
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
++TouchedActors;
|
|
737
|
+
if (Rows.Num() < MCPBatchMaxResultRows)
|
|
738
|
+
{
|
|
739
|
+
TSharedPtr<FJsonObject> Row = MakeShared<FJsonObject>();
|
|
740
|
+
Row->SetStringField(TEXT("actorLabel"), Actor->GetActorLabel());
|
|
741
|
+
Row->SetStringField(TEXT("actorClass"), Actor->GetClass()->GetName());
|
|
742
|
+
Row->SetNumberField(bDryRun ? TEXT("wouldRemove") : TEXT("removed"), RemovedNames.Num());
|
|
743
|
+
Row->SetArrayField(TEXT("components"), MCPStringListToJson(RemovedNames));
|
|
744
|
+
if (SkippedNames.Num() > 0)
|
|
745
|
+
{
|
|
746
|
+
Row->SetArrayField(TEXT("skippedNonInstanceComponents"), MCPStringListToJson(SkippedNames));
|
|
747
|
+
}
|
|
748
|
+
Rows.Add(MakeShared<FJsonValueObject>(Row));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
Result->SetNumberField(TEXT("scannedActors"), ScannedActors);
|
|
753
|
+
Result->SetNumberField(TEXT("actorsAffected"), TouchedActors);
|
|
754
|
+
Result->SetNumberField(TEXT("matchedComponents"), MatchedComponents);
|
|
755
|
+
Result->SetNumberField(
|
|
756
|
+
bDryRun ? TEXT("wouldRemoveComponents") : TEXT("removedComponents"),
|
|
757
|
+
bDryRun ? MatchedComponents - SkippedNonInstance : RemovedComponents);
|
|
758
|
+
Result->SetNumberField(TEXT("skippedNonInstanceComponents"), SkippedNonInstance);
|
|
759
|
+
Result->SetNumberField(TEXT("returnedResults"), Rows.Num());
|
|
760
|
+
Result->SetBoolField(TEXT("resultsTruncated"), Rows.Num() < TouchedActors);
|
|
761
|
+
Result->SetArrayField(TEXT("results"), Rows);
|
|
762
|
+
|
|
763
|
+
if (MatchedComponents == 0)
|
|
764
|
+
{
|
|
765
|
+
Result->SetStringField(TEXT("zeroMatchNote"), FString::Printf(
|
|
766
|
+
TEXT("No component of class '%s' was found on any of the %d actors enumerated. Check the class name with level(query_components, componentClass:'%s', countOnly:true) before assuming there is nothing to remove."),
|
|
767
|
+
*ComponentClass->GetName(), ScannedActors, *ComponentClassSpec));
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (!bDryRun && RemovedComponents > 0)
|
|
771
|
+
{
|
|
772
|
+
MCPSetUpdated(Result);
|
|
773
|
+
if (bSave)
|
|
774
|
+
{
|
|
775
|
+
const bool bSaved = FEditorFileUtils::SaveCurrentLevel();
|
|
776
|
+
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
777
|
+
if (!bSaved)
|
|
778
|
+
{
|
|
779
|
+
Result->SetStringField(TEXT("saveError"),
|
|
780
|
+
TEXT("The removal landed but saving the level failed. The change is in memory and undoable; save from the editor or call level(save)."));
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
else
|
|
784
|
+
{
|
|
785
|
+
Result->SetBoolField(TEXT("saved"), false);
|
|
786
|
+
Result->SetStringField(TEXT("saveNote"),
|
|
787
|
+
TEXT("The level is left dirty and is NOT saved. Pass save=true, or call level(save), to commit it."));
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
else if (bDryRun)
|
|
791
|
+
{
|
|
792
|
+
Result->SetBoolField(TEXT("saved"), false);
|
|
793
|
+
Result->SetStringField(TEXT("dryRunNote"),
|
|
794
|
+
TEXT("dryRun defaults to TRUE for this action because it deletes components whose auto-generated names you cannot enumerate. Pass dryRun=false to commit."));
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return MCPResult(Result);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// ---------------------------------------------------------------------------
|
|
801
|
+
// spawn_actors_batch (#987)
|
|
802
|
+
// ---------------------------------------------------------------------------
|
|
803
|
+
TSharedPtr<FJsonValue> FLevelHandlers::SpawnActorsBatch(const TSharedPtr<FJsonObject>& Params)
|
|
804
|
+
{
|
|
805
|
+
MCP_CHECK_GAME_THREAD();
|
|
806
|
+
REQUIRE_EDITOR_WORLD(World);
|
|
807
|
+
|
|
808
|
+
FString ActorClassSpec;
|
|
809
|
+
if (auto Err = RequireString(Params, TEXT("actorClass"), ActorClassSpec)) return Err;
|
|
810
|
+
UClass* ActorClass = MCPResolveClassOfType(ActorClassSpec, AActor::StaticClass(), true);
|
|
811
|
+
if (!ActorClass)
|
|
812
|
+
{
|
|
813
|
+
return MCPClassNotFoundError(ActorClassSpec, TEXT("actorClass"));
|
|
814
|
+
}
|
|
815
|
+
if (auto Err = MCPCheckClassUsable(ActorClassSpec, ActorClass, AActor::StaticClass()))
|
|
816
|
+
{
|
|
817
|
+
return Err;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const bool bDryRun = OptionalBool(Params, TEXT("dryRun"), false);
|
|
821
|
+
const FString LabelPrefix = OptionalString(Params, TEXT("labelPrefix"));
|
|
822
|
+
const int32 MaxSpawn = FMath::Clamp(
|
|
823
|
+
OptionalInt(Params, TEXT("maxSpawn"), MCPBatchDefaultMaxSpawn), 1, MCPBatchHardMaxSpawn);
|
|
824
|
+
const FString TransactionLabel = OptionalString(
|
|
825
|
+
Params, TEXT("transactionLabel"), TEXT("MCP spawn actors batch"));
|
|
826
|
+
|
|
827
|
+
const TSharedPtr<FJsonObject>* SharedProperties = nullptr;
|
|
828
|
+
Params->TryGetObjectField(TEXT("properties"), SharedProperties);
|
|
829
|
+
|
|
830
|
+
// ── Build the transform list ────────────────────────────────────────────
|
|
831
|
+
//
|
|
832
|
+
// Three sources, and exactly one of them per call. The derived ones exist
|
|
833
|
+
// because the transforms in #987 were a function of geometry already in
|
|
834
|
+
// the editor: round-tripping 190 component bounds out and 190 transforms
|
|
835
|
+
// back in is what made it a Python job.
|
|
836
|
+
struct FPlannedSpawn
|
|
837
|
+
{
|
|
838
|
+
FTransform Transform;
|
|
839
|
+
FString Label;
|
|
840
|
+
FString Source;
|
|
841
|
+
TSharedPtr<FJsonObject> Properties;
|
|
842
|
+
};
|
|
843
|
+
TArray<FPlannedSpawn> Planned;
|
|
844
|
+
FString PlanSource;
|
|
845
|
+
|
|
846
|
+
const TArray<TSharedPtr<FJsonValue>>* InstanceValues = nullptr;
|
|
847
|
+
const TSharedPtr<FJsonObject>* FromComponents = nullptr;
|
|
848
|
+
const TSharedPtr<FJsonObject>* AlongSpline = nullptr;
|
|
849
|
+
const bool bHasInstances = Params->TryGetArrayField(TEXT("instances"), InstanceValues) && InstanceValues;
|
|
850
|
+
const bool bHasFromComponents = Params->TryGetObjectField(TEXT("fromComponents"), FromComponents) && FromComponents;
|
|
851
|
+
const bool bHasAlongSpline = Params->TryGetObjectField(TEXT("alongSpline"), AlongSpline) && AlongSpline;
|
|
852
|
+
|
|
853
|
+
const int32 SourceCount = (bHasInstances ? 1 : 0) + (bHasFromComponents ? 1 : 0) + (bHasAlongSpline ? 1 : 0);
|
|
854
|
+
if (SourceCount == 0)
|
|
855
|
+
{
|
|
856
|
+
return MCPError(TEXT("Pass exactly one of 'instances' (explicit transforms), 'fromComponents' (one actor per matched component's bounds) or 'alongSpline' (scatter by distance)"));
|
|
857
|
+
}
|
|
858
|
+
if (SourceCount > 1)
|
|
859
|
+
{
|
|
860
|
+
return MCPError(TEXT("Pass only ONE of 'instances', 'fromComponents' or 'alongSpline'; combining them makes the spawn count ambiguous"));
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (bHasInstances)
|
|
864
|
+
{
|
|
865
|
+
PlanSource = TEXT("instances");
|
|
866
|
+
for (int32 Index = 0; Index < InstanceValues->Num(); ++Index)
|
|
867
|
+
{
|
|
868
|
+
const TSharedPtr<FJsonValue>& Entry = (*InstanceValues)[Index];
|
|
869
|
+
if (!Entry.IsValid() || Entry->Type != EJson::Object)
|
|
870
|
+
{
|
|
871
|
+
return MCPError(FString::Printf(TEXT("'instances[%d]' must be an object"), Index));
|
|
872
|
+
}
|
|
873
|
+
const TSharedPtr<FJsonObject> Row = Entry->AsObject();
|
|
874
|
+
const TSharedPtr<FJsonObject>* LocationObject = nullptr;
|
|
875
|
+
const TSharedPtr<FJsonObject>* RotationObject = nullptr;
|
|
876
|
+
const TSharedPtr<FJsonObject>* ScaleObject = nullptr;
|
|
877
|
+
Row->TryGetObjectField(TEXT("location"), LocationObject);
|
|
878
|
+
Row->TryGetObjectField(TEXT("rotation"), RotationObject);
|
|
879
|
+
Row->TryGetObjectField(TEXT("scale"), ScaleObject);
|
|
880
|
+
|
|
881
|
+
FPlannedSpawn Spawn;
|
|
882
|
+
Spawn.Transform = FTransform(
|
|
883
|
+
RotationObject ? MCPBatchReadRotator(*RotationObject, FRotator::ZeroRotator) : FRotator::ZeroRotator,
|
|
884
|
+
LocationObject ? MCPBatchReadVector(*LocationObject, FVector::ZeroVector) : FVector::ZeroVector,
|
|
885
|
+
ScaleObject ? MCPBatchReadVector(*ScaleObject, FVector::OneVector) : FVector::OneVector);
|
|
886
|
+
Row->TryGetStringField(TEXT("label"), Spawn.Label);
|
|
887
|
+
Spawn.Source = TEXT("instance");
|
|
888
|
+
const TSharedPtr<FJsonObject>* RowProperties = nullptr;
|
|
889
|
+
if (Row->TryGetObjectField(TEXT("properties"), RowProperties) && RowProperties)
|
|
890
|
+
{
|
|
891
|
+
Spawn.Properties = *RowProperties;
|
|
892
|
+
}
|
|
893
|
+
Planned.Add(MoveTemp(Spawn));
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
else if (bHasFromComponents)
|
|
897
|
+
{
|
|
898
|
+
PlanSource = TEXT("fromComponents");
|
|
899
|
+
const TSharedPtr<FJsonObject> Source = *FromComponents;
|
|
900
|
+
|
|
901
|
+
const FString ComponentClassSpec = OptionalString(Source, TEXT("componentClass"));
|
|
902
|
+
UClass* ComponentClass = ComponentClassSpec.IsEmpty()
|
|
903
|
+
? nullptr
|
|
904
|
+
: MCPResolveClassOfType(ComponentClassSpec, UActorComponent::StaticClass(), true);
|
|
905
|
+
if (!ComponentClassSpec.IsEmpty() && !ComponentClass)
|
|
906
|
+
{
|
|
907
|
+
return MCPClassNotFoundError(ComponentClassSpec, TEXT("fromComponents.componentClass"));
|
|
908
|
+
}
|
|
909
|
+
const FString ComponentNameContains = OptionalString(Source, TEXT("componentNameContains"));
|
|
910
|
+
|
|
911
|
+
FMCPBatchSelector Selector;
|
|
912
|
+
if (auto Err = MCPBatchReadSelector(Source, Selector)) return Err;
|
|
913
|
+
|
|
914
|
+
// Local by default, and this is the whole point of the mode. A
|
|
915
|
+
// component's WORLD bounds are axis-aligned, so on a rotated lamp post
|
|
916
|
+
// "the top of the mesh" computed from world bounds is not on the lamp
|
|
917
|
+
// post. The local bounds carry the component's own orientation, and
|
|
918
|
+
// transforming the offset by the component transform puts the spawn
|
|
919
|
+
// where a human would point.
|
|
920
|
+
const FString Space = OptionalString(Source, TEXT("space"), TEXT("local")).ToLower();
|
|
921
|
+
if (Space != TEXT("local") && Space != TEXT("world"))
|
|
922
|
+
{
|
|
923
|
+
return MCPError(TEXT("'fromComponents.space' must be either 'local' (default) or 'world'"));
|
|
924
|
+
}
|
|
925
|
+
const bool bLocal = Space == TEXT("local");
|
|
926
|
+
|
|
927
|
+
const TSharedPtr<FJsonObject>* OffsetObject = nullptr;
|
|
928
|
+
Source->TryGetObjectField(TEXT("offset"), OffsetObject);
|
|
929
|
+
const FVector Offset = OffsetObject ? MCPBatchReadVector(*OffsetObject, FVector::ZeroVector) : FVector::ZeroVector;
|
|
930
|
+
|
|
931
|
+
const TSharedPtr<FJsonObject>* FractionObject = nullptr;
|
|
932
|
+
Source->TryGetObjectField(TEXT("extentFraction"), FractionObject);
|
|
933
|
+
const FVector ExtentFraction = FractionObject
|
|
934
|
+
? MCPBatchReadVector(*FractionObject, FVector::ZeroVector)
|
|
935
|
+
: FVector::ZeroVector;
|
|
936
|
+
|
|
937
|
+
const bool bInheritRotation = OptionalBool(Source, TEXT("inheritRotation"), false);
|
|
938
|
+
|
|
939
|
+
for (TActorIterator<AActor> It(World); It; ++It)
|
|
940
|
+
{
|
|
941
|
+
AActor* Actor = *It;
|
|
942
|
+
if (!Actor) continue;
|
|
943
|
+
if (Selector.bAny && !MCPBatchActorMatches(Selector, Actor, Actor->GetActorLabel())) continue;
|
|
944
|
+
|
|
945
|
+
for (UActorComponent* Component : Actor->GetComponents())
|
|
946
|
+
{
|
|
947
|
+
USceneComponent* SceneComponent = Cast<USceneComponent>(Component);
|
|
948
|
+
if (!SceneComponent) continue;
|
|
949
|
+
if (ComponentClass && !SceneComponent->IsA(ComponentClass)) continue;
|
|
950
|
+
if (!ComponentNameContains.IsEmpty() &&
|
|
951
|
+
!SceneComponent->GetName().Contains(ComponentNameContains, ESearchCase::IgnoreCase))
|
|
952
|
+
{
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const FTransform ComponentTransform = SceneComponent->GetComponentTransform();
|
|
957
|
+
FVector Location;
|
|
958
|
+
if (bLocal)
|
|
959
|
+
{
|
|
960
|
+
const FBoxSphereBounds LocalBounds = SceneComponent->GetLocalBounds();
|
|
961
|
+
const FVector LocalPoint =
|
|
962
|
+
LocalBounds.Origin + LocalBounds.BoxExtent * ExtentFraction + Offset;
|
|
963
|
+
Location = ComponentTransform.TransformPosition(LocalPoint);
|
|
964
|
+
}
|
|
965
|
+
else
|
|
966
|
+
{
|
|
967
|
+
const FBoxSphereBounds WorldBounds = SceneComponent->GetBounds();
|
|
968
|
+
Location = WorldBounds.Origin + WorldBounds.BoxExtent * ExtentFraction + Offset;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
FPlannedSpawn Spawn;
|
|
972
|
+
Spawn.Transform = FTransform(
|
|
973
|
+
bInheritRotation ? ComponentTransform.Rotator() : FRotator::ZeroRotator,
|
|
974
|
+
Location,
|
|
975
|
+
FVector::OneVector);
|
|
976
|
+
Spawn.Source = FString::Printf(TEXT("%s.%s"), *Actor->GetActorLabel(), *SceneComponent->GetName());
|
|
977
|
+
Planned.Add(MoveTemp(Spawn));
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
Planned.Sort([](const FPlannedSpawn& A, const FPlannedSpawn& B) { return A.Source < B.Source; });
|
|
981
|
+
}
|
|
982
|
+
else
|
|
983
|
+
{
|
|
984
|
+
PlanSource = TEXT("alongSpline");
|
|
985
|
+
const TSharedPtr<FJsonObject> Source = *AlongSpline;
|
|
986
|
+
|
|
987
|
+
FString SplineActorLabel;
|
|
988
|
+
if (auto Err = RequireStringAlt(Source, TEXT("actorLabel"), TEXT("actorPath"), SplineActorLabel)) return Err;
|
|
989
|
+
// #983: the spline the batch runs along is selected the same way as
|
|
990
|
+
// any other actor, and a duplicated label refuses rather than picking
|
|
991
|
+
// a spline at the other end of the map to scatter along.
|
|
992
|
+
TSharedPtr<FJsonValue> SplineErr;
|
|
993
|
+
AActor* SplineActor = MCPResolveActor(World, Source, SplineErr);
|
|
994
|
+
if (!SplineActor) return SplineErr;
|
|
995
|
+
SplineActorLabel = SplineActor->GetActorLabel();
|
|
996
|
+
const FString SplineComponentName = OptionalString(Source, TEXT("componentName"));
|
|
997
|
+
USplineComponent* Spline = nullptr;
|
|
998
|
+
for (UActorComponent* Component : SplineActor->GetComponents())
|
|
999
|
+
{
|
|
1000
|
+
USplineComponent* Candidate = Cast<USplineComponent>(Component);
|
|
1001
|
+
if (!Candidate) continue;
|
|
1002
|
+
if (SplineComponentName.IsEmpty() ||
|
|
1003
|
+
Candidate->GetName().Equals(SplineComponentName, ESearchCase::IgnoreCase))
|
|
1004
|
+
{
|
|
1005
|
+
Spline = Candidate;
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (!Spline)
|
|
1010
|
+
{
|
|
1011
|
+
return MCPError(FString::Printf(
|
|
1012
|
+
TEXT("No SplineComponent%s on actor '%s'"),
|
|
1013
|
+
SplineComponentName.IsEmpty() ? TEXT("") : *FString::Printf(TEXT(" named '%s'"), *SplineComponentName),
|
|
1014
|
+
*SplineActorLabel));
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const double Spacing = OptionalNumber(Source, TEXT("spacing"), 0.0);
|
|
1018
|
+
if (Spacing < MCPBatchMinSplineSpacing)
|
|
1019
|
+
{
|
|
1020
|
+
return MCPError(FString::Printf(
|
|
1021
|
+
TEXT("'alongSpline.spacing' must be at least %.0f cm; a smaller value would spawn an unbounded number of actors"),
|
|
1022
|
+
MCPBatchMinSplineSpacing));
|
|
1023
|
+
}
|
|
1024
|
+
const double SplineLength = Spline->GetSplineLength();
|
|
1025
|
+
const double StartDistance = FMath::Clamp(
|
|
1026
|
+
OptionalNumber(Source, TEXT("startDistance"), 0.0), 0.0, SplineLength);
|
|
1027
|
+
const double EndDistance = FMath::Clamp(
|
|
1028
|
+
OptionalNumber(Source, TEXT("endDistance"), SplineLength), StartDistance, SplineLength);
|
|
1029
|
+
const bool bAlignToTangent = OptionalBool(Source, TEXT("alignToTangent"), true);
|
|
1030
|
+
|
|
1031
|
+
const TSharedPtr<FJsonObject>* OffsetObject = nullptr;
|
|
1032
|
+
Source->TryGetObjectField(TEXT("offset"), OffsetObject);
|
|
1033
|
+
const FVector Offset = OffsetObject ? MCPBatchReadVector(*OffsetObject, FVector::ZeroVector) : FVector::ZeroVector;
|
|
1034
|
+
|
|
1035
|
+
for (double Distance = StartDistance; Distance <= EndDistance; Distance += Spacing)
|
|
1036
|
+
{
|
|
1037
|
+
const FRotator Rotation = Spline->GetRotationAtDistanceAlongSpline(
|
|
1038
|
+
Distance, ESplineCoordinateSpace::World);
|
|
1039
|
+
FVector Location = Spline->GetLocationAtDistanceAlongSpline(
|
|
1040
|
+
Distance, ESplineCoordinateSpace::World);
|
|
1041
|
+
// The offset follows the spline's frame when aligning to it, so
|
|
1042
|
+
// "20cm to the left of the path" stays to the left round a corner.
|
|
1043
|
+
Location += bAlignToTangent ? Rotation.RotateVector(Offset) : Offset;
|
|
1044
|
+
|
|
1045
|
+
FPlannedSpawn Spawn;
|
|
1046
|
+
Spawn.Transform = FTransform(
|
|
1047
|
+
bAlignToTangent ? Rotation : FRotator::ZeroRotator, Location, FVector::OneVector);
|
|
1048
|
+
Spawn.Source = FString::Printf(TEXT("%s@%.0f"), *Spline->GetName(), Distance);
|
|
1049
|
+
Planned.Add(MoveTemp(Spawn));
|
|
1050
|
+
|
|
1051
|
+
if (Planned.Num() > MCPBatchHardMaxSpawn)
|
|
1052
|
+
{
|
|
1053
|
+
break;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
auto Result = MCPSuccess();
|
|
1059
|
+
Result->SetBoolField(TEXT("dryRun"), bDryRun);
|
|
1060
|
+
Result->SetStringField(TEXT("actorClass"), ActorClass->GetPathName());
|
|
1061
|
+
Result->SetStringField(TEXT("source"), PlanSource);
|
|
1062
|
+
Result->SetNumberField(TEXT("planned"), Planned.Num());
|
|
1063
|
+
|
|
1064
|
+
if (Planned.Num() > MaxSpawn)
|
|
1065
|
+
{
|
|
1066
|
+
Result->SetBoolField(TEXT("success"), false);
|
|
1067
|
+
Result->SetStringField(TEXT("error"), FString::Printf(
|
|
1068
|
+
TEXT("The plan produced %d spawns, over maxSpawn (%d). Narrow the selector or raise maxSpawn deliberately."),
|
|
1069
|
+
Planned.Num(), MaxSpawn));
|
|
1070
|
+
Result->SetNumberField(TEXT("maxSpawn"), MaxSpawn);
|
|
1071
|
+
return MCPResult(Result);
|
|
1072
|
+
}
|
|
1073
|
+
if (Planned.IsEmpty())
|
|
1074
|
+
{
|
|
1075
|
+
Result->SetNumberField(TEXT("spawned"), 0);
|
|
1076
|
+
Result->SetArrayField(TEXT("results"), TArray<TSharedPtr<FJsonValue>>());
|
|
1077
|
+
Result->SetStringField(TEXT("zeroMatchNote"),
|
|
1078
|
+
TEXT("The plan produced no positions. For fromComponents, check componentClass and the actor selector with level(query_components, countOnly:true); for alongSpline, check the spline length against startDistance/endDistance."));
|
|
1079
|
+
return MCPResult(Result);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
TUniquePtr<FScopedTransaction> Transaction;
|
|
1083
|
+
if (!bDryRun)
|
|
1084
|
+
{
|
|
1085
|
+
Transaction = MakeUnique<FScopedTransaction>(FText::FromString(TransactionLabel));
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
TArray<TSharedPtr<FJsonValue>> Rows;
|
|
1089
|
+
int32 Spawned = 0;
|
|
1090
|
+
int32 Failed = 0;
|
|
1091
|
+
int32 PropertyFailures = 0;
|
|
1092
|
+
|
|
1093
|
+
for (int32 Index = 0; Index < Planned.Num(); ++Index)
|
|
1094
|
+
{
|
|
1095
|
+
const FPlannedSpawn& Plan = Planned[Index];
|
|
1096
|
+
TSharedPtr<FJsonObject> Row = MakeShared<FJsonObject>();
|
|
1097
|
+
Row->SetNumberField(TEXT("index"), Index);
|
|
1098
|
+
Row->SetStringField(TEXT("from"), Plan.Source);
|
|
1099
|
+
Row->SetObjectField(TEXT("location"), MCPVec3ToJsonObject(Plan.Transform.GetLocation()));
|
|
1100
|
+
Row->SetObjectField(TEXT("rotation"), MCPRotatorToJsonObject(Plan.Transform.Rotator()));
|
|
1101
|
+
Row->SetObjectField(TEXT("scale"), MCPVec3ToJsonObject(Plan.Transform.GetScale3D()));
|
|
1102
|
+
|
|
1103
|
+
if (bDryRun)
|
|
1104
|
+
{
|
|
1105
|
+
Row->SetBoolField(TEXT("ok"), true);
|
|
1106
|
+
if (Rows.Num() < MCPBatchMaxResultRows) Rows.Add(MakeShared<FJsonValueObject>(Row));
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
FActorSpawnParameters SpawnParameters;
|
|
1111
|
+
SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
|
1112
|
+
AActor* Actor = World->SpawnActor<AActor>(ActorClass, Plan.Transform, SpawnParameters);
|
|
1113
|
+
if (!Actor)
|
|
1114
|
+
{
|
|
1115
|
+
++Failed;
|
|
1116
|
+
Row->SetBoolField(TEXT("ok"), false);
|
|
1117
|
+
Row->SetStringField(TEXT("error"), TEXT("SpawnActor returned null"));
|
|
1118
|
+
if (Rows.Num() < MCPBatchMaxResultRows) Rows.Add(MakeShared<FJsonValueObject>(Row));
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
++Spawned;
|
|
1122
|
+
|
|
1123
|
+
FString Label = Plan.Label;
|
|
1124
|
+
if (Label.IsEmpty() && !LabelPrefix.IsEmpty())
|
|
1125
|
+
{
|
|
1126
|
+
Label = FString::Printf(TEXT("%s%d"), *LabelPrefix, Index);
|
|
1127
|
+
}
|
|
1128
|
+
if (!Label.IsEmpty())
|
|
1129
|
+
{
|
|
1130
|
+
Actor->SetActorLabel(Label);
|
|
1131
|
+
}
|
|
1132
|
+
Row->SetStringField(TEXT("actorLabel"), Actor->GetActorLabel());
|
|
1133
|
+
Row->SetBoolField(TEXT("ok"), true);
|
|
1134
|
+
|
|
1135
|
+
// Per-instance properties override the shared ones, so a caller can
|
|
1136
|
+
// set one field per spawn without repeating the rest.
|
|
1137
|
+
TArray<TSharedPtr<FJsonValue>> PropertyRows;
|
|
1138
|
+
auto ApplyProperties = [&](const TSharedPtr<FJsonObject>& Properties)
|
|
1139
|
+
{
|
|
1140
|
+
if (!Properties.IsValid()) return;
|
|
1141
|
+
for (const TPair<FString, TSharedPtr<FJsonValue>>& Pair : Properties->Values)
|
|
1142
|
+
{
|
|
1143
|
+
TSharedPtr<FJsonObject> SubParams = MakeShared<FJsonObject>();
|
|
1144
|
+
SubParams->SetStringField(TEXT("actorLabel"), Actor->GetActorLabel());
|
|
1145
|
+
SubParams->SetStringField(TEXT("propertyName"), Pair.Key);
|
|
1146
|
+
SubParams->SetField(TEXT("value"), Pair.Value);
|
|
1147
|
+
SubParams->SetBoolField(TEXT("force"), true);
|
|
1148
|
+
|
|
1149
|
+
bool bOk = false;
|
|
1150
|
+
FString Error;
|
|
1151
|
+
FString Previous;
|
|
1152
|
+
MCPBatchReadVerdict(FLevelHandlers::SetActorProperty(SubParams), bOk, Error, Previous);
|
|
1153
|
+
TSharedPtr<FJsonObject> PropertyRow = MakeShared<FJsonObject>();
|
|
1154
|
+
PropertyRow->SetStringField(TEXT("propertyName"), Pair.Key);
|
|
1155
|
+
PropertyRow->SetBoolField(TEXT("ok"), bOk);
|
|
1156
|
+
if (!bOk)
|
|
1157
|
+
{
|
|
1158
|
+
PropertyRow->SetStringField(TEXT("error"), Error);
|
|
1159
|
+
++PropertyFailures;
|
|
1160
|
+
}
|
|
1161
|
+
PropertyRows.Add(MakeShared<FJsonValueObject>(PropertyRow));
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
ApplyProperties(SharedProperties ? *SharedProperties : nullptr);
|
|
1165
|
+
ApplyProperties(Plan.Properties);
|
|
1166
|
+
if (PropertyRows.Num() > 0)
|
|
1167
|
+
{
|
|
1168
|
+
Row->SetArrayField(TEXT("properties"), PropertyRows);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
if (Rows.Num() < MCPBatchMaxResultRows) Rows.Add(MakeShared<FJsonValueObject>(Row));
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
Result->SetBoolField(TEXT("success"), Failed == 0);
|
|
1175
|
+
if (Failed > 0)
|
|
1176
|
+
{
|
|
1177
|
+
Result->SetStringField(TEXT("error"), FString::Printf(
|
|
1178
|
+
TEXT("%d of %d planned spawns failed; see results[]."), Failed, Planned.Num()));
|
|
1179
|
+
}
|
|
1180
|
+
Result->SetNumberField(bDryRun ? TEXT("wouldSpawn") : TEXT("spawned"), bDryRun ? Planned.Num() : Spawned);
|
|
1181
|
+
Result->SetNumberField(TEXT("failed"), Failed);
|
|
1182
|
+
Result->SetNumberField(TEXT("propertyFailures"), PropertyFailures);
|
|
1183
|
+
Result->SetNumberField(TEXT("returnedResults"), Rows.Num());
|
|
1184
|
+
Result->SetBoolField(TEXT("resultsTruncated"), Rows.Num() < Planned.Num());
|
|
1185
|
+
Result->SetArrayField(TEXT("results"), Rows);
|
|
1186
|
+
if (!bDryRun && Spawned > 0)
|
|
1187
|
+
{
|
|
1188
|
+
MCPSetCreated(Result);
|
|
1189
|
+
Result->SetStringField(TEXT("saveNote"),
|
|
1190
|
+
TEXT("The level is left dirty and is NOT saved. Call level(save) when you are done."));
|
|
1191
|
+
}
|
|
1192
|
+
return MCPResult(Result);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// ---------------------------------------------------------------------------
|
|
1196
|
+
// set_component_materials (#946)
|
|
1197
|
+
//
|
|
1198
|
+
// material(build_material) writes the ASSET's material slots. A placed actor
|
|
1199
|
+
// usually needs the other thing: a per-slot COMPONENT override, which is what
|
|
1200
|
+
// the details panel edits and what SetMaterial writes into OverrideMaterials.
|
|
1201
|
+
// There was no action for that. set_actor_material does one slot, on whichever
|
|
1202
|
+
// primitive component it happens to find first, so dressing a placed skeletal
|
|
1203
|
+
// mesh meant a Python loop over get_num_materials().
|
|
1204
|
+
// ---------------------------------------------------------------------------
|
|
1205
|
+
TSharedPtr<FJsonValue> FLevelHandlers::SetComponentMaterials(const TSharedPtr<FJsonObject>& Params)
|
|
1206
|
+
{
|
|
1207
|
+
MCP_CHECK_GAME_THREAD();
|
|
1208
|
+
REQUIRE_EDITOR_WORLD(World);
|
|
1209
|
+
|
|
1210
|
+
FMCPBatchSelector Selector;
|
|
1211
|
+
if (auto Err = MCPBatchReadSelector(Params, Selector)) return Err;
|
|
1212
|
+
if (!Selector.bAny)
|
|
1213
|
+
{
|
|
1214
|
+
return MCPError(TEXT("Pass at least one selector: actorLabels, labelPrefix, labelContains, tag, classFilter, folderPath or folderPathPrefix"));
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
const FString ComponentName = OptionalString(Params, TEXT("componentName"));
|
|
1218
|
+
const bool bDryRun = OptionalBool(Params, TEXT("dryRun"), false);
|
|
1219
|
+
const bool bClearOverrides = OptionalBool(Params, TEXT("clearOverrides"), false);
|
|
1220
|
+
const FString SingleMaterialPath = OptionalString(Params, TEXT("material"));
|
|
1221
|
+
|
|
1222
|
+
const TArray<TSharedPtr<FJsonValue>>* MaterialValues = nullptr;
|
|
1223
|
+
const bool bHasMaterialList = Params->TryGetArrayField(TEXT("materials"), MaterialValues) && MaterialValues;
|
|
1224
|
+
|
|
1225
|
+
const int32 ModeCount = (bHasMaterialList ? 1 : 0) + (SingleMaterialPath.IsEmpty() ? 0 : 1) + (bClearOverrides ? 1 : 0);
|
|
1226
|
+
if (ModeCount == 0)
|
|
1227
|
+
{
|
|
1228
|
+
return MCPError(TEXT("Pass 'materials' (per-slot paths), 'material' (one path applied to every slot) or clearOverrides=true"));
|
|
1229
|
+
}
|
|
1230
|
+
if (ModeCount > 1)
|
|
1231
|
+
{
|
|
1232
|
+
return MCPError(TEXT("Pass only ONE of 'materials', 'material' or clearOverrides; together they would fight over the same slots"));
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// Resolve every requested material up front. A path that does not load is
|
|
1236
|
+
// a caller error, and finding that out after writing half the slots on
|
|
1237
|
+
// half the actors is not a useful place to find it out.
|
|
1238
|
+
TArray<UMaterialInterface*> SlotMaterials;
|
|
1239
|
+
if (bHasMaterialList)
|
|
1240
|
+
{
|
|
1241
|
+
for (int32 Index = 0; Index < MaterialValues->Num(); ++Index)
|
|
1242
|
+
{
|
|
1243
|
+
const TSharedPtr<FJsonValue>& Entry = (*MaterialValues)[Index];
|
|
1244
|
+
FString Path;
|
|
1245
|
+
const bool bIsClear = !Entry.IsValid() || Entry->Type == EJson::Null ||
|
|
1246
|
+
(Entry->TryGetString(Path) && Path.IsEmpty());
|
|
1247
|
+
if (bIsClear)
|
|
1248
|
+
{
|
|
1249
|
+
// An explicit null clears that one slot's override, so a caller
|
|
1250
|
+
// can reset slot 2 without touching the rest.
|
|
1251
|
+
SlotMaterials.Add(nullptr);
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
UMaterialInterface* Material = LoadObject<UMaterialInterface>(nullptr, *Path);
|
|
1255
|
+
if (!Material)
|
|
1256
|
+
{
|
|
1257
|
+
return MCPError(FString::Printf(
|
|
1258
|
+
TEXT("Material not found for 'materials[%d]': %s"), Index, *Path));
|
|
1259
|
+
}
|
|
1260
|
+
SlotMaterials.Add(Material);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
UMaterialInterface* SingleMaterial = nullptr;
|
|
1264
|
+
if (!SingleMaterialPath.IsEmpty())
|
|
1265
|
+
{
|
|
1266
|
+
SingleMaterial = LoadObject<UMaterialInterface>(nullptr, *SingleMaterialPath);
|
|
1267
|
+
if (!SingleMaterial)
|
|
1268
|
+
{
|
|
1269
|
+
return MCPError(FString::Printf(TEXT("Material not found: %s"), *SingleMaterialPath));
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
TArray<AActor*> Actors;
|
|
1274
|
+
MCPBatchCollectActors(World, Selector, Actors);
|
|
1275
|
+
if (Actors.Num() > MCPBatchMaxActors)
|
|
1276
|
+
{
|
|
1277
|
+
return MCPError(FString::Printf(
|
|
1278
|
+
TEXT("The selector matched %d actors, over the maximum of %d. Narrow it."),
|
|
1279
|
+
Actors.Num(), MCPBatchMaxActors));
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
auto Result = MCPSuccess();
|
|
1283
|
+
Result->SetBoolField(TEXT("dryRun"), bDryRun);
|
|
1284
|
+
Result->SetNumberField(TEXT("matchedActors"), Actors.Num());
|
|
1285
|
+
MCPNoteLoadedOnlyEnumeration(World, Result);
|
|
1286
|
+
{
|
|
1287
|
+
const TArray<FString> Missing = MCPBatchMissingLabels(Selector, Actors);
|
|
1288
|
+
if (Missing.Num() > 0)
|
|
1289
|
+
{
|
|
1290
|
+
Result->SetArrayField(TEXT("missingLabels"), MCPStringListToJson(Missing));
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
TUniquePtr<FScopedTransaction> Transaction;
|
|
1295
|
+
if (!bDryRun && !Actors.IsEmpty())
|
|
1296
|
+
{
|
|
1297
|
+
Transaction = MakeUnique<FScopedTransaction>(FText::FromString(
|
|
1298
|
+
OptionalString(Params, TEXT("transactionLabel"), TEXT("MCP set component materials"))));
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
TArray<TSharedPtr<FJsonValue>> Rows;
|
|
1302
|
+
int32 Updated = 0;
|
|
1303
|
+
int32 WithoutComponent = 0;
|
|
1304
|
+
int32 SlotWrites = 0;
|
|
1305
|
+
int32 OutOfRange = 0;
|
|
1306
|
+
|
|
1307
|
+
for (AActor* Actor : Actors)
|
|
1308
|
+
{
|
|
1309
|
+
TSharedPtr<FJsonObject> Row = MakeShared<FJsonObject>();
|
|
1310
|
+
Row->SetStringField(TEXT("actorLabel"), Actor->GetActorLabel());
|
|
1311
|
+
|
|
1312
|
+
UMeshComponent* MeshComponent = nullptr;
|
|
1313
|
+
if (ComponentName.IsEmpty())
|
|
1314
|
+
{
|
|
1315
|
+
MeshComponent = Actor->FindComponentByClass<UMeshComponent>();
|
|
1316
|
+
}
|
|
1317
|
+
else
|
|
1318
|
+
{
|
|
1319
|
+
MeshComponent = Cast<UMeshComponent>(MCPBatchFindComponent(Actor, ComponentName));
|
|
1320
|
+
}
|
|
1321
|
+
if (!MeshComponent)
|
|
1322
|
+
{
|
|
1323
|
+
++WithoutComponent;
|
|
1324
|
+
Row->SetBoolField(TEXT("ok"), false);
|
|
1325
|
+
Row->SetStringField(TEXT("status"), TEXT("no_mesh_component"));
|
|
1326
|
+
if (Rows.Num() < MCPBatchMaxResultRows) Rows.Add(MakeShared<FJsonValueObject>(Row));
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
Row->SetStringField(TEXT("componentName"), MeshComponent->GetName());
|
|
1330
|
+
Row->SetStringField(TEXT("componentClass"), MeshComponent->GetClass()->GetName());
|
|
1331
|
+
|
|
1332
|
+
const int32 SlotCount = MeshComponent->GetNumMaterials();
|
|
1333
|
+
Row->SetNumberField(TEXT("slotCount"), SlotCount);
|
|
1334
|
+
const TArray<FName> SlotNames = MeshComponent->GetMaterialSlotNames();
|
|
1335
|
+
|
|
1336
|
+
if (bHasMaterialList && SlotMaterials.Num() > SlotCount)
|
|
1337
|
+
{
|
|
1338
|
+
// Extra entries would silently do nothing, which reads as a
|
|
1339
|
+
// successful assignment that did not happen.
|
|
1340
|
+
++OutOfRange;
|
|
1341
|
+
Row->SetBoolField(TEXT("ok"), false);
|
|
1342
|
+
Row->SetStringField(TEXT("status"), TEXT("too_many_materials"));
|
|
1343
|
+
Row->SetStringField(TEXT("error"), FString::Printf(
|
|
1344
|
+
TEXT("%d materials given but the component has %d slots"), SlotMaterials.Num(), SlotCount));
|
|
1345
|
+
if (Rows.Num() < MCPBatchMaxResultRows) Rows.Add(MakeShared<FJsonValueObject>(Row));
|
|
1346
|
+
continue;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
TArray<TSharedPtr<FJsonValue>> SlotRows;
|
|
1350
|
+
const int32 WriteCount = bHasMaterialList ? SlotMaterials.Num() : SlotCount;
|
|
1351
|
+
for (int32 SlotIndex = 0; SlotIndex < WriteCount; ++SlotIndex)
|
|
1352
|
+
{
|
|
1353
|
+
UMaterialInterface* Before = MeshComponent->GetMaterial(SlotIndex);
|
|
1354
|
+
const bool bWasOverride =
|
|
1355
|
+
MeshComponent->OverrideMaterials.IsValidIndex(SlotIndex) &&
|
|
1356
|
+
MeshComponent->OverrideMaterials[SlotIndex] != nullptr;
|
|
1357
|
+
|
|
1358
|
+
UMaterialInterface* Desired = nullptr;
|
|
1359
|
+
if (bHasMaterialList) Desired = SlotMaterials[SlotIndex];
|
|
1360
|
+
else if (SingleMaterial) Desired = SingleMaterial;
|
|
1361
|
+
|
|
1362
|
+
if (!bDryRun)
|
|
1363
|
+
{
|
|
1364
|
+
MeshComponent->Modify();
|
|
1365
|
+
// A null override is how the asset's own slot shows through
|
|
1366
|
+
// again, which is the inverse of what this action does and is
|
|
1367
|
+
// otherwise unreachable.
|
|
1368
|
+
MeshComponent->SetMaterial(SlotIndex, bClearOverrides ? nullptr : Desired);
|
|
1369
|
+
++SlotWrites;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
UMaterialInterface* After = bDryRun ? Before : MeshComponent->GetMaterial(SlotIndex);
|
|
1373
|
+
const bool bIsOverride = bDryRun
|
|
1374
|
+
? bWasOverride
|
|
1375
|
+
: (MeshComponent->OverrideMaterials.IsValidIndex(SlotIndex) &&
|
|
1376
|
+
MeshComponent->OverrideMaterials[SlotIndex] != nullptr);
|
|
1377
|
+
|
|
1378
|
+
TSharedPtr<FJsonObject> SlotRow = MakeShared<FJsonObject>();
|
|
1379
|
+
SlotRow->SetNumberField(TEXT("slotIndex"), SlotIndex);
|
|
1380
|
+
SlotRow->SetStringField(TEXT("slotName"),
|
|
1381
|
+
SlotNames.IsValidIndex(SlotIndex) ? SlotNames[SlotIndex].ToString() : FString());
|
|
1382
|
+
SlotRow->SetStringField(TEXT("previousMaterialPath"), Before ? Before->GetPathName() : FString());
|
|
1383
|
+
SlotRow->SetStringField(TEXT("previousSource"),
|
|
1384
|
+
bWasOverride ? TEXT("componentOverride") : (Before ? TEXT("meshDefault") : TEXT("none")));
|
|
1385
|
+
SlotRow->SetStringField(TEXT("materialPath"), After ? After->GetPathName() : FString());
|
|
1386
|
+
// Read back rather than echoing: the difference between "the
|
|
1387
|
+
// component now overrides this slot" and "the asset happens to
|
|
1388
|
+
// have that material" is the whole distinction #946 is about.
|
|
1389
|
+
SlotRow->SetStringField(TEXT("source"),
|
|
1390
|
+
bIsOverride ? TEXT("componentOverride") : (After ? TEXT("meshDefault") : TEXT("none")));
|
|
1391
|
+
SlotRows.Add(MakeShared<FJsonValueObject>(SlotRow));
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
if (!bDryRun)
|
|
1395
|
+
{
|
|
1396
|
+
MeshComponent->MarkRenderStateDirty();
|
|
1397
|
+
Actor->MarkPackageDirty();
|
|
1398
|
+
++Updated;
|
|
1399
|
+
}
|
|
1400
|
+
Row->SetBoolField(TEXT("ok"), true);
|
|
1401
|
+
Row->SetStringField(TEXT("status"), bDryRun ? TEXT("would_write") : TEXT("written"));
|
|
1402
|
+
Row->SetArrayField(TEXT("slots"), SlotRows);
|
|
1403
|
+
if (Rows.Num() < MCPBatchMaxResultRows) Rows.Add(MakeShared<FJsonValueObject>(Row));
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
Result->SetBoolField(TEXT("success"), OutOfRange == 0);
|
|
1407
|
+
if (OutOfRange > 0)
|
|
1408
|
+
{
|
|
1409
|
+
Result->SetStringField(TEXT("error"), FString::Printf(
|
|
1410
|
+
TEXT("%d actor(s) were given more materials than their component has slots; see results[]."), OutOfRange));
|
|
1411
|
+
}
|
|
1412
|
+
Result->SetNumberField(bDryRun ? TEXT("wouldUpdate") : TEXT("updated"),
|
|
1413
|
+
bDryRun ? Actors.Num() - WithoutComponent : Updated);
|
|
1414
|
+
Result->SetNumberField(TEXT("slotWrites"), SlotWrites);
|
|
1415
|
+
Result->SetNumberField(TEXT("actorsWithoutMeshComponent"), WithoutComponent);
|
|
1416
|
+
Result->SetNumberField(TEXT("returnedResults"), Rows.Num());
|
|
1417
|
+
Result->SetBoolField(TEXT("resultsTruncated"), Rows.Num() < Actors.Num());
|
|
1418
|
+
Result->SetArrayField(TEXT("results"), Rows);
|
|
1419
|
+
if (!bDryRun && Updated > 0)
|
|
1420
|
+
{
|
|
1421
|
+
MCPSetUpdated(Result);
|
|
1422
|
+
Result->SetStringField(TEXT("saveNote"),
|
|
1423
|
+
TEXT("These are COMPONENT overrides on placed actors, so the LEVEL is dirty and is NOT saved. The mesh ASSET is untouched; use material(build_material) or asset(set_property) to write the asset's own slots."));
|
|
1424
|
+
}
|
|
1425
|
+
return MCPResult(Result);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// ---------------------------------------------------------------------------
|
|
1429
|
+
// set_actor_hlod_layer (#985)
|
|
1430
|
+
// ---------------------------------------------------------------------------
|
|
1431
|
+
//
|
|
1432
|
+
// Assigning an HLODLayer override across 295 InstancedFoliageActors was a
|
|
1433
|
+
// Python loop, for the same reason every other action in this file exists: the
|
|
1434
|
+
// selector vocabulary lived on the client and every actor cost a round trip.
|
|
1435
|
+
//
|
|
1436
|
+
// This lives beside the other selector-driven batch writes rather than with the
|
|
1437
|
+
// World Partition settings, because the selector, the bounds, the dry run and
|
|
1438
|
+
// the missing-label reporting are all shared with them, and a second copy of
|
|
1439
|
+
// that vocabulary would be one more thing to drift.
|
|
1440
|
+
//
|
|
1441
|
+
// The write goes through AActor::SetHLODLayer rather than through the reflected
|
|
1442
|
+
// property. HLODLayer is a private UPROPERTY and reflection would reach it, but
|
|
1443
|
+
// the engine's own setter is what the HLOD system expects to have been called,
|
|
1444
|
+
// and a bulk write that skipped it would be the kind of edit that looks applied
|
|
1445
|
+
// and behaves differently from one made in the details panel.
|
|
1446
|
+
TSharedPtr<FJsonValue> FLevelHandlers::SetActorHLODLayer(const TSharedPtr<FJsonObject>& Params)
|
|
1447
|
+
{
|
|
1448
|
+
MCP_CHECK_GAME_THREAD();
|
|
1449
|
+
REQUIRE_EDITOR_WORLD(World);
|
|
1450
|
+
|
|
1451
|
+
// null clears the override, which is a legitimate request and has to be
|
|
1452
|
+
// distinguishable from "the parameter was omitted".
|
|
1453
|
+
if (!Params->HasField(TEXT("hlodLayer")))
|
|
1454
|
+
{
|
|
1455
|
+
return MCPError(TEXT("Missing 'hlodLayer': an HLODLayer asset path, or null to clear the per-actor override"));
|
|
1456
|
+
}
|
|
1457
|
+
FString LayerPath;
|
|
1458
|
+
Params->TryGetStringField(TEXT("hlodLayer"), LayerPath);
|
|
1459
|
+
LayerPath.TrimStartAndEndInline();
|
|
1460
|
+
|
|
1461
|
+
UHLODLayer* Layer = nullptr;
|
|
1462
|
+
if (!LayerPath.IsEmpty())
|
|
1463
|
+
{
|
|
1464
|
+
Layer = LoadAssetByPath<UHLODLayer>(LayerPath);
|
|
1465
|
+
if (!Layer) return MCPAssetLoadError(LayerPath, TEXT("UHLODLayer"));
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
FMCPBatchSelector Selector;
|
|
1469
|
+
if (auto Err = MCPBatchReadSelector(Params, Selector)) return Err;
|
|
1470
|
+
if (!Selector.bAny)
|
|
1471
|
+
{
|
|
1472
|
+
return MCPError(TEXT("Pass at least one selector: actorLabels, labelPrefix, labelContains, tag, classFilter, folderPath or folderPathPrefix"));
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
const bool bDryRun = OptionalBool(Params, TEXT("dryRun"), false);
|
|
1476
|
+
const bool bHasAutoLODField = Params->HasField(TEXT("enableAutoLODGeneration"));
|
|
1477
|
+
const bool bEnableAutoLOD = OptionalBool(Params, TEXT("enableAutoLODGeneration"), true);
|
|
1478
|
+
const FString TransactionLabel = OptionalString(
|
|
1479
|
+
Params, TEXT("transactionLabel"), TEXT("MCP set actor HLOD layer"));
|
|
1480
|
+
|
|
1481
|
+
TArray<AActor*> Actors;
|
|
1482
|
+
MCPBatchCollectActors(World, Selector, Actors);
|
|
1483
|
+
if (Actors.Num() > MCPBatchMaxActors)
|
|
1484
|
+
{
|
|
1485
|
+
return MCPError(FString::Printf(
|
|
1486
|
+
TEXT("The selector matched %d actors, over the maximum of %d. Narrow it."),
|
|
1487
|
+
Actors.Num(), MCPBatchMaxActors));
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
auto Result = MCPSuccess();
|
|
1491
|
+
Result->SetBoolField(TEXT("dryRun"), bDryRun);
|
|
1492
|
+
Result->SetStringField(TEXT("hlodLayer"), Layer ? Layer->GetPathName() : FString());
|
|
1493
|
+
Result->SetBoolField(TEXT("clearing"), Layer == nullptr);
|
|
1494
|
+
Result->SetNumberField(TEXT("matched"), Actors.Num());
|
|
1495
|
+
MCPNoteLoadedOnlyEnumeration(World, Result);
|
|
1496
|
+
{
|
|
1497
|
+
const TArray<FString> Missing = MCPBatchMissingLabels(Selector, Actors);
|
|
1498
|
+
if (Missing.Num() > 0)
|
|
1499
|
+
{
|
|
1500
|
+
Result->SetArrayField(TEXT("missingLabels"), MCPStringListToJson(Missing));
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
TArray<TSharedPtr<FJsonValue>> Rows;
|
|
1505
|
+
int32 Updated = 0;
|
|
1506
|
+
int32 Unchanged = 0;
|
|
1507
|
+
{
|
|
1508
|
+
const FScopedTransaction Transaction(FText::FromString(TransactionLabel));
|
|
1509
|
+
for (AActor* Actor : Actors)
|
|
1510
|
+
{
|
|
1511
|
+
if (!Actor) continue;
|
|
1512
|
+
UHLODLayer* Previous = Actor->GetHLODLayer();
|
|
1513
|
+
const bool bWouldChange = Previous != Layer;
|
|
1514
|
+
|
|
1515
|
+
TSharedPtr<FJsonObject> Row = MakeShared<FJsonObject>();
|
|
1516
|
+
Row->SetStringField(TEXT("actorLabel"), Actor->GetActorLabel());
|
|
1517
|
+
Row->SetStringField(TEXT("actorClass"), Actor->GetClass()->GetName());
|
|
1518
|
+
Row->SetStringField(TEXT("previousHLODLayer"), Previous ? Previous->GetPathName() : FString());
|
|
1519
|
+
|
|
1520
|
+
if (!bWouldChange && !bHasAutoLODField)
|
|
1521
|
+
{
|
|
1522
|
+
// Already what was asked for. Reporting it as an update would
|
|
1523
|
+
// make a no-op look like work, which is what makes a rerun
|
|
1524
|
+
// impossible to reason about.
|
|
1525
|
+
++Unchanged;
|
|
1526
|
+
Row->SetStringField(TEXT("status"), TEXT("unchanged"));
|
|
1527
|
+
if (Rows.Num() < MCPBatchMaxResultRows) Rows.Add(MakeShared<FJsonValueObject>(Row));
|
|
1528
|
+
continue;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
if (bDryRun)
|
|
1532
|
+
{
|
|
1533
|
+
Row->SetStringField(TEXT("status"), TEXT("would_write"));
|
|
1534
|
+
if (Rows.Num() < MCPBatchMaxResultRows) Rows.Add(MakeShared<FJsonValueObject>(Row));
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
Actor->Modify();
|
|
1539
|
+
Actor->SetHLODLayer(Layer);
|
|
1540
|
+
if (bHasAutoLODField)
|
|
1541
|
+
{
|
|
1542
|
+
// The layer only matters when the actor is allowed to build
|
|
1543
|
+
// HLODs at all, so the two are settable in one call.
|
|
1544
|
+
Actor->bEnableAutoLODGeneration = bEnableAutoLOD;
|
|
1545
|
+
}
|
|
1546
|
+
Actor->PostEditChange();
|
|
1547
|
+
Actor->MarkPackageDirty();
|
|
1548
|
+
++Updated;
|
|
1549
|
+
|
|
1550
|
+
UHLODLayer* After = Actor->GetHLODLayer();
|
|
1551
|
+
Row->SetStringField(TEXT("hlodLayer"), After ? After->GetPathName() : FString());
|
|
1552
|
+
Row->SetBoolField(TEXT("enableAutoLODGeneration"), Actor->bEnableAutoLODGeneration);
|
|
1553
|
+
Row->SetStringField(TEXT("status"), TEXT("written"));
|
|
1554
|
+
if (Rows.Num() < MCPBatchMaxResultRows) Rows.Add(MakeShared<FJsonValueObject>(Row));
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
Result->SetNumberField(bDryRun ? TEXT("wouldUpdate") : TEXT("updated"),
|
|
1559
|
+
bDryRun ? Actors.Num() - Unchanged : Updated);
|
|
1560
|
+
Result->SetNumberField(TEXT("unchanged"), Unchanged);
|
|
1561
|
+
Result->SetNumberField(TEXT("returnedResults"), Rows.Num());
|
|
1562
|
+
Result->SetBoolField(TEXT("resultsTruncated"), Rows.Num() < Actors.Num());
|
|
1563
|
+
Result->SetArrayField(TEXT("results"), Rows);
|
|
1564
|
+
if (!bDryRun && Updated > 0)
|
|
1565
|
+
{
|
|
1566
|
+
MCPSetUpdated(Result);
|
|
1567
|
+
Result->SetStringField(TEXT("saveNote"),
|
|
1568
|
+
TEXT("On a World Partition map each actor lives in its own package, so this dirties one package per actor. Save them with level(save) or editor(save_dirty). HLOD assignment takes effect on the next HLOD build."));
|
|
1569
|
+
}
|
|
1570
|
+
return MCPResult(Result);
|
|
1571
|
+
}
|