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
|
@@ -0,0 +1,1169 @@
|
|
|
1
|
+
// level(query_components) - one editor-side component query for whole-map
|
|
2
|
+
// questions (#910, #943, #912).
|
|
3
|
+
//
|
|
4
|
+
// The complaint these three issues share is not a missing field. It is that
|
|
5
|
+
// answering a level-wide question meant shipping every candidate out to the
|
|
6
|
+
// client and filtering there: get_actors_by_component_class returned over a
|
|
7
|
+
// megabyte on a real map, and get_nanite_info answers one mesh per call. So
|
|
8
|
+
// the selector, the projection, the predicates, the grouping and the counts
|
|
9
|
+
// all run here, and only the answer crosses the wire.
|
|
10
|
+
//
|
|
11
|
+
// Three things this deliberately does NOT do:
|
|
12
|
+
//
|
|
13
|
+
// - It never writes. No MarkPackageDirty, no SetX, no property assignment.
|
|
14
|
+
// A map-wide audit that dirties 3,900 actors is worse than no audit
|
|
15
|
+
// (#912). The only state it can touch is the open map, and only when the
|
|
16
|
+
// caller passes levelPath: then it refuses outright if anything is
|
|
17
|
+
// already dirty, and it puts the original map back afterwards.
|
|
18
|
+
// - It never returns an unbounded payload. Rows are capped, groups are
|
|
19
|
+
// capped, value histograms are capped, and every cap reports itself.
|
|
20
|
+
// - It never reports a raw flag where the flag is not the answer. CastShadow
|
|
21
|
+
// true on a hidden debug marker is not a shadow caster, and
|
|
22
|
+
// str(mobility) == "EComponentMobility.MOVABLE" is why a substring test
|
|
23
|
+
// for "Movable" silently matched nothing. Both are computed here.
|
|
24
|
+
|
|
25
|
+
#include "LevelHandlers.h"
|
|
26
|
+
|
|
27
|
+
#include "HandlerRegistry.h"
|
|
28
|
+
#include "HandlerQuery.h"
|
|
29
|
+
#include "HandlerUtils.h"
|
|
30
|
+
#include "HandlerEditorState.h"
|
|
31
|
+
|
|
32
|
+
#include "Components/ActorComponent.h"
|
|
33
|
+
#include "Components/BoxComponent.h"
|
|
34
|
+
#include "Components/DecalComponent.h"
|
|
35
|
+
#include "Components/HierarchicalInstancedStaticMeshComponent.h"
|
|
36
|
+
#include "Components/InstancedStaticMeshComponent.h"
|
|
37
|
+
#include "Components/MeshComponent.h"
|
|
38
|
+
#include "Components/PrimitiveComponent.h"
|
|
39
|
+
#include "Components/SceneComponent.h"
|
|
40
|
+
#include "Components/SkinnedMeshComponent.h"
|
|
41
|
+
#include "Components/StaticMeshComponent.h"
|
|
42
|
+
#include "Dom/JsonObject.h"
|
|
43
|
+
#include "Dom/JsonValue.h"
|
|
44
|
+
#include "Editor.h"
|
|
45
|
+
#include "Engine/EngineTypes.h"
|
|
46
|
+
#include "Engine/StaticMesh.h"
|
|
47
|
+
#include "Engine/World.h"
|
|
48
|
+
#include "EngineUtils.h"
|
|
49
|
+
#include "GameFramework/Actor.h"
|
|
50
|
+
#include "GameFramework/Pawn.h"
|
|
51
|
+
#include "Materials/MaterialInterface.h"
|
|
52
|
+
#include "UObject/EnumProperty.h"
|
|
53
|
+
#include "UObject/UnrealType.h"
|
|
54
|
+
|
|
55
|
+
namespace
|
|
56
|
+
{
|
|
57
|
+
// ── Bounds ──────────────────────────────────────────────────────────────
|
|
58
|
+
//
|
|
59
|
+
// Every one of these exists because an unbounded version of this query is
|
|
60
|
+
// what the issues are complaining about.
|
|
61
|
+
constexpr int32 MCPQueryDefaultRowLimit = 200;
|
|
62
|
+
constexpr int32 MCPQueryMaxRowLimit = 2000;
|
|
63
|
+
/** Rows kept in memory for sorting and paging. `matched` stays exact past
|
|
64
|
+
* this; only the retained window is capped. */
|
|
65
|
+
constexpr int32 MCPQueryMaxRetainedRows = 20000;
|
|
66
|
+
constexpr int32 MCPQueryMaxGroups = 200;
|
|
67
|
+
constexpr int32 MCPQueryDefaultGroupSamples = 5;
|
|
68
|
+
constexpr int32 MCPQueryMaxGroupSamples = 25;
|
|
69
|
+
constexpr int32 MCPQueryMaxCountByPaths = 8;
|
|
70
|
+
constexpr int32 MCPQueryMaxCountByValues = 64;
|
|
71
|
+
constexpr int32 MCPQueryMaxProjectedProperties = 32;
|
|
72
|
+
constexpr int32 MCPQueryMaxPredicates = 24;
|
|
73
|
+
constexpr int32 MCPQueryMaxMaterialSlots = 64;
|
|
74
|
+
constexpr int32 MCPQueryMaxDuplicateGroups = 100;
|
|
75
|
+
|
|
76
|
+
const TCHAR* const MCPQueryWorldGridMaterial = TEXT("/Engine/EngineMaterials/WorldGridMaterial");
|
|
77
|
+
|
|
78
|
+
// ── Clean enum names ────────────────────────────────────────────────────
|
|
79
|
+
//
|
|
80
|
+
// #943's reporter lost a whole result set to `str(mobility)` producing
|
|
81
|
+
// "EComponentMobility.MOVABLE" and a case-sensitive test for "Movable"
|
|
82
|
+
// matching none of it. These switches are what a caller compares against.
|
|
83
|
+
|
|
84
|
+
const TCHAR* MCPQueryMobilityName(EComponentMobility::Type Mobility)
|
|
85
|
+
{
|
|
86
|
+
switch (Mobility)
|
|
87
|
+
{
|
|
88
|
+
case EComponentMobility::Static: return TEXT("Static");
|
|
89
|
+
case EComponentMobility::Stationary: return TEXT("Stationary");
|
|
90
|
+
case EComponentMobility::Movable: return TEXT("Movable");
|
|
91
|
+
default: return TEXT("Unknown");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const TCHAR* MCPQueryCollisionEnabledName(ECollisionEnabled::Type Value)
|
|
96
|
+
{
|
|
97
|
+
switch (Value)
|
|
98
|
+
{
|
|
99
|
+
case ECollisionEnabled::NoCollision: return TEXT("NoCollision");
|
|
100
|
+
case ECollisionEnabled::QueryOnly: return TEXT("QueryOnly");
|
|
101
|
+
case ECollisionEnabled::PhysicsOnly: return TEXT("PhysicsOnly");
|
|
102
|
+
case ECollisionEnabled::QueryAndPhysics: return TEXT("QueryAndPhysics");
|
|
103
|
+
case ECollisionEnabled::ProbeOnly: return TEXT("ProbeOnly");
|
|
104
|
+
case ECollisionEnabled::QueryAndProbe: return TEXT("QueryAndProbe");
|
|
105
|
+
default: return TEXT("Unknown");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const TCHAR* MCPQueryNavGeometryName(EHasCustomNavigableGeometry::Type Value)
|
|
110
|
+
{
|
|
111
|
+
switch (Value)
|
|
112
|
+
{
|
|
113
|
+
case EHasCustomNavigableGeometry::No: return TEXT("No");
|
|
114
|
+
case EHasCustomNavigableGeometry::Yes: return TEXT("Yes");
|
|
115
|
+
case EHasCustomNavigableGeometry::EvenIfNotCollidable: return TEXT("EvenIfNotCollidable");
|
|
116
|
+
case EHasCustomNavigableGeometry::DontExport: return TEXT("DontExport");
|
|
117
|
+
default: return TEXT("Unknown");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── JSON shaping ────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
TSharedPtr<FJsonObject> MCPQueryVec(const FVector& V)
|
|
124
|
+
{
|
|
125
|
+
TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
|
|
126
|
+
Obj->SetNumberField(TEXT("x"), V.X);
|
|
127
|
+
Obj->SetNumberField(TEXT("y"), V.Y);
|
|
128
|
+
Obj->SetNumberField(TEXT("z"), V.Z);
|
|
129
|
+
return Obj;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
TSharedPtr<FJsonObject> MCPQueryRot(const FRotator& R)
|
|
133
|
+
{
|
|
134
|
+
TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
|
|
135
|
+
Obj->SetNumberField(TEXT("pitch"), R.Pitch);
|
|
136
|
+
Obj->SetNumberField(TEXT("yaw"), R.Yaw);
|
|
137
|
+
Obj->SetNumberField(TEXT("roll"), R.Roll);
|
|
138
|
+
return Obj;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Projection ──────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
struct FMCPQueryFields
|
|
144
|
+
{
|
|
145
|
+
bool bTransform = false;
|
|
146
|
+
bool bBounds = false;
|
|
147
|
+
bool bLocalBounds = false;
|
|
148
|
+
bool bShadow = false;
|
|
149
|
+
bool bNanite = false;
|
|
150
|
+
bool bNavigation = false;
|
|
151
|
+
bool bTick = false;
|
|
152
|
+
bool bMaterials = false;
|
|
153
|
+
bool bMesh = false;
|
|
154
|
+
bool bDecal = false;
|
|
155
|
+
bool bHealth = false;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/** Every field name this action understands, so an unknown one is an error
|
|
159
|
+
* the caller sees rather than a silently empty projection. */
|
|
160
|
+
bool MCPQueryApplyFieldName(const FString& Name, FMCPQueryFields& Fields)
|
|
161
|
+
{
|
|
162
|
+
if (Name == TEXT("transform")) { Fields.bTransform = true; return true; }
|
|
163
|
+
if (Name == TEXT("bounds")) { Fields.bBounds = true; return true; }
|
|
164
|
+
if (Name == TEXT("localBounds")) { Fields.bLocalBounds = true; return true; }
|
|
165
|
+
if (Name == TEXT("shadow")) { Fields.bShadow = true; return true; }
|
|
166
|
+
if (Name == TEXT("nanite")) { Fields.bNanite = true; return true; }
|
|
167
|
+
if (Name == TEXT("navigation")) { Fields.bNavigation = true; return true; }
|
|
168
|
+
if (Name == TEXT("tick")) { Fields.bTick = true; return true; }
|
|
169
|
+
if (Name == TEXT("materials")) { Fields.bMaterials = true; return true; }
|
|
170
|
+
if (Name == TEXT("mesh")) { Fields.bMesh = true; return true; }
|
|
171
|
+
if (Name == TEXT("decal")) { Fields.bDecal = true; return true; }
|
|
172
|
+
if (Name == TEXT("health"))
|
|
173
|
+
{
|
|
174
|
+
// health is derived from the others, so asking for it turns on
|
|
175
|
+
// everything it reads rather than reporting false negatives.
|
|
176
|
+
Fields.bHealth = true;
|
|
177
|
+
Fields.bBounds = true;
|
|
178
|
+
Fields.bTransform = true;
|
|
179
|
+
Fields.bMesh = true;
|
|
180
|
+
Fields.bDecal = true;
|
|
181
|
+
Fields.bMaterials = true;
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The asset a component renders, if it renders one. */
|
|
188
|
+
UObject* MCPQueryMeshAsset(UActorComponent* Component, FString& OutKind, bool& bOutIsMeshComponent)
|
|
189
|
+
{
|
|
190
|
+
bOutIsMeshComponent = false;
|
|
191
|
+
OutKind.Reset();
|
|
192
|
+
if (UStaticMeshComponent* StaticMeshComponent = Cast<UStaticMeshComponent>(Component))
|
|
193
|
+
{
|
|
194
|
+
bOutIsMeshComponent = true;
|
|
195
|
+
OutKind = TEXT("StaticMesh");
|
|
196
|
+
return StaticMeshComponent->GetStaticMesh();
|
|
197
|
+
}
|
|
198
|
+
if (USkinnedMeshComponent* SkinnedComponent = Cast<USkinnedMeshComponent>(Component))
|
|
199
|
+
{
|
|
200
|
+
bOutIsMeshComponent = true;
|
|
201
|
+
OutKind = TEXT("SkinnedAsset");
|
|
202
|
+
return SkinnedComponent->GetSkinnedAsset();
|
|
203
|
+
}
|
|
204
|
+
return nullptr;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Read a UPROPERTY off a component by name without linking its module.
|
|
208
|
+
* NavModifierComponent lives in NavigationSystem and its AreaClass is the
|
|
209
|
+
* one field #943 needs; reflection reaches it on any engine version and
|
|
210
|
+
* degrades to absent rather than to a link error. */
|
|
211
|
+
TSharedPtr<FJsonValue> MCPQueryReflectProperty(UObject* Object, const TCHAR* PropertyName)
|
|
212
|
+
{
|
|
213
|
+
if (!Object)
|
|
214
|
+
{
|
|
215
|
+
return nullptr;
|
|
216
|
+
}
|
|
217
|
+
FProperty* Property = Object->GetClass()->FindPropertyByName(FName(PropertyName));
|
|
218
|
+
if (!Property)
|
|
219
|
+
{
|
|
220
|
+
return nullptr;
|
|
221
|
+
}
|
|
222
|
+
return MCPQuery::PropertyToJson(Property, Property->ContainerPtrToValuePtr<void>(Object));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
struct FMCPQueryRow
|
|
226
|
+
{
|
|
227
|
+
FString SortKey;
|
|
228
|
+
TSharedPtr<FJsonObject> Object;
|
|
229
|
+
FIntVector QuantizedLocation = FIntVector::ZeroValue;
|
|
230
|
+
bool bHasTransform = false;
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// query_components
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
TSharedPtr<FJsonValue> FLevelHandlers::QueryComponents(const TSharedPtr<FJsonObject>& Params)
|
|
238
|
+
{
|
|
239
|
+
MCP_CHECK_GAME_THREAD();
|
|
240
|
+
|
|
241
|
+
// ── Parameters ──────────────────────────────────────────────────────────
|
|
242
|
+
const FString WorldScope = OptionalString(Params, TEXT("world"), TEXT("editor"));
|
|
243
|
+
const FString LevelPath = OptionalString(Params, TEXT("levelPath"));
|
|
244
|
+
|
|
245
|
+
const FString ComponentClassSpec = OptionalString(Params, TEXT("componentClass"));
|
|
246
|
+
const FString ActorClassSpec = OptionalString(Params, TEXT("actorClass"));
|
|
247
|
+
const bool bMatchSubclasses = OptionalBool(Params, TEXT("matchSubclasses"), true);
|
|
248
|
+
const FString ComponentNameContains = OptionalString(Params, TEXT("componentNameContains"));
|
|
249
|
+
const FString ActorLabelPrefix = OptionalString(Params, TEXT("actorLabelPrefix"));
|
|
250
|
+
const FString ActorLabelContains = OptionalString(Params, TEXT("actorLabelContains"));
|
|
251
|
+
const FString ActorTag = OptionalString(Params, TEXT("actorTag"));
|
|
252
|
+
const FString FolderPath = OptionalString(Params, TEXT("folderPath"));
|
|
253
|
+
const FString FolderPathPrefix = OptionalString(Params, TEXT("folderPathPrefix"));
|
|
254
|
+
|
|
255
|
+
const FString WhereMode = OptionalString(Params, TEXT("whereMode"), TEXT("all")).ToLower();
|
|
256
|
+
if (WhereMode != TEXT("all") && WhereMode != TEXT("any"))
|
|
257
|
+
{
|
|
258
|
+
return MCPError(TEXT("'whereMode' must be either 'all' or 'any'"));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const bool bCountOnly = OptionalBool(Params, TEXT("countOnly"), false);
|
|
262
|
+
const bool bSuspectOnly = OptionalBool(Params, TEXT("suspectOnly"), false);
|
|
263
|
+
const FString GroupBy = OptionalString(Params, TEXT("groupBy"));
|
|
264
|
+
const int32 GroupSamples = FMath::Clamp(
|
|
265
|
+
OptionalInt(Params, TEXT("sampleLimit"), MCPQueryDefaultGroupSamples), 0, MCPQueryMaxGroupSamples);
|
|
266
|
+
const int32 Limit = FMath::Clamp(
|
|
267
|
+
OptionalInt(Params, TEXT("limit"), MCPQueryDefaultRowLimit), 0, MCPQueryMaxRowLimit);
|
|
268
|
+
const int32 StartIndex = FMath::Max(0, OptionalInt(Params, TEXT("startIndex"), 0));
|
|
269
|
+
const double DuplicateTolerance = OptionalNumber(Params, TEXT("duplicateTransformTolerance"), 0.0);
|
|
270
|
+
|
|
271
|
+
// Projection.
|
|
272
|
+
FMCPQueryFields Fields;
|
|
273
|
+
bool bAnyFieldRequested = false;
|
|
274
|
+
{
|
|
275
|
+
const TArray<TSharedPtr<FJsonValue>>* FieldValues = nullptr;
|
|
276
|
+
if (Params->TryGetArrayField(TEXT("fields"), FieldValues) && FieldValues)
|
|
277
|
+
{
|
|
278
|
+
for (const FString& Name : JsonArrayToStringList(FieldValues))
|
|
279
|
+
{
|
|
280
|
+
if (!MCPQueryApplyFieldName(Name, Fields))
|
|
281
|
+
{
|
|
282
|
+
return MCPError(FString::Printf(
|
|
283
|
+
TEXT("Unknown 'fields' entry '%s'. Valid: transform, bounds, localBounds, shadow, nanite, navigation, tick, materials, mesh, decal, health"),
|
|
284
|
+
*Name));
|
|
285
|
+
}
|
|
286
|
+
bAnyFieldRequested = true;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (bSuspectOnly)
|
|
291
|
+
{
|
|
292
|
+
MCPQueryApplyFieldName(TEXT("health"), Fields);
|
|
293
|
+
bAnyFieldRequested = true;
|
|
294
|
+
}
|
|
295
|
+
if (!bAnyFieldRequested)
|
|
296
|
+
{
|
|
297
|
+
// A caller who names no fields gets identity plus transform: enough to
|
|
298
|
+
// act on a row, small enough that an unfiltered query is not the
|
|
299
|
+
// megabyte payload these issues are about.
|
|
300
|
+
Fields.bTransform = true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
TArray<FString> ProjectedProperties;
|
|
304
|
+
{
|
|
305
|
+
const TArray<TSharedPtr<FJsonValue>>* PropertyValues = nullptr;
|
|
306
|
+
if (Params->TryGetArrayField(TEXT("propertyNames"), PropertyValues) && PropertyValues)
|
|
307
|
+
{
|
|
308
|
+
ProjectedProperties = JsonArrayToStringList(PropertyValues);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (ProjectedProperties.Num() > MCPQueryMaxProjectedProperties)
|
|
312
|
+
{
|
|
313
|
+
return MCPError(FString::Printf(
|
|
314
|
+
TEXT("'propertyNames' exceeds the maximum of %d entries"), MCPQueryMaxProjectedProperties));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Predicates. Parsing and evaluation live in the shared query header, so
|
|
318
|
+
// this action and asset(bulk_read_properties) cannot drift apart on what
|
|
319
|
+
// an operator name means.
|
|
320
|
+
TArray<MCPQuery::FPredicate> Predicates;
|
|
321
|
+
{
|
|
322
|
+
const TArray<TSharedPtr<FJsonValue>>* WhereValues = nullptr;
|
|
323
|
+
Params->TryGetArrayField(TEXT("where"), WhereValues);
|
|
324
|
+
FString ParseError;
|
|
325
|
+
if (!MCPQuery::ParsePredicates(WhereValues, MCPQueryMaxPredicates, Predicates, ParseError))
|
|
326
|
+
{
|
|
327
|
+
return MCPError(ParseError);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (bSuspectOnly)
|
|
332
|
+
{
|
|
333
|
+
MCPQuery::FPredicate Suspect;
|
|
334
|
+
Suspect.Field = TEXT("health.suspect");
|
|
335
|
+
Suspect.Op = TEXT("isTrue");
|
|
336
|
+
Predicates.Add(MoveTemp(Suspect));
|
|
337
|
+
// suspectOnly is an AND on top of whatever else was asked for, so an
|
|
338
|
+
// `any` request keeps its own semantics only among its own predicates.
|
|
339
|
+
// Rather than silently changing that, refuse the ambiguous combination.
|
|
340
|
+
if (WhereMode == TEXT("any") && Predicates.Num() > 1)
|
|
341
|
+
{
|
|
342
|
+
return MCPError(TEXT("'suspectOnly' cannot be combined with whereMode 'any'; add {field:'health.suspect', op:'isTrue'} to 'where' instead"));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Value histograms.
|
|
347
|
+
TArray<FString> CountByPaths;
|
|
348
|
+
{
|
|
349
|
+
const TArray<TSharedPtr<FJsonValue>>* CountByValues = nullptr;
|
|
350
|
+
if (Params->TryGetArrayField(TEXT("countBy"), CountByValues) && CountByValues)
|
|
351
|
+
{
|
|
352
|
+
CountByPaths = JsonArrayToStringList(CountByValues);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (CountByPaths.Num() > MCPQueryMaxCountByPaths)
|
|
356
|
+
{
|
|
357
|
+
return MCPError(FString::Printf(
|
|
358
|
+
TEXT("'countBy' exceeds the maximum of %d paths"), MCPQueryMaxCountByPaths));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// A predicate on `shadow.effectiveCastShadow` with `shadow` left out of
|
|
362
|
+
// `fields` would resolve to an absent field and match nothing, and a
|
|
363
|
+
// confident zero is the worst answer this action can give. So a path that
|
|
364
|
+
// names a field group turns that group on.
|
|
365
|
+
{
|
|
366
|
+
auto EnableGroupNamedBy = [&Fields](const FString& Path)
|
|
367
|
+
{
|
|
368
|
+
FString Head;
|
|
369
|
+
FString Tail;
|
|
370
|
+
MCPQueryApplyFieldName(Path.Split(TEXT("."), &Head, &Tail) ? Head : Path, Fields);
|
|
371
|
+
};
|
|
372
|
+
for (const MCPQuery::FPredicate& Predicate : Predicates) EnableGroupNamedBy(Predicate.Field);
|
|
373
|
+
for (const FString& CountPath : CountByPaths) EnableGroupNamedBy(CountPath);
|
|
374
|
+
if (!GroupBy.IsEmpty()) EnableGroupNamedBy(GroupBy);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── Class resolution ────────────────────────────────────────────────────
|
|
378
|
+
UClass* ComponentClass = nullptr;
|
|
379
|
+
FString ComponentClassFallback;
|
|
380
|
+
if (!ComponentClassSpec.IsEmpty())
|
|
381
|
+
{
|
|
382
|
+
ComponentClass = MCPResolveClassOfType(ComponentClassSpec, UActorComponent::StaticClass(), true);
|
|
383
|
+
if (!ComponentClass)
|
|
384
|
+
{
|
|
385
|
+
// A substring is a legitimate way to ask this question and the
|
|
386
|
+
// existing get_actors_by_component_class accepts one, so keep it
|
|
387
|
+
// working rather than failing a call that used to succeed.
|
|
388
|
+
ComponentClassFallback = ComponentClassSpec;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
UClass* ActorClass = nullptr;
|
|
392
|
+
FString ActorClassFallback;
|
|
393
|
+
if (!ActorClassSpec.IsEmpty())
|
|
394
|
+
{
|
|
395
|
+
ActorClass = MCPResolveClassOfType(ActorClassSpec, AActor::StaticClass(), true);
|
|
396
|
+
if (!ActorClass)
|
|
397
|
+
{
|
|
398
|
+
ActorClassFallback = ActorClassSpec;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ── World selection, and the map-restore contract (#912) ────────────────
|
|
403
|
+
//
|
|
404
|
+
// Reading another map means opening it. Opening it while the editor has
|
|
405
|
+
// unsaved work either discards that work or raises a modal save prompt the
|
|
406
|
+
// bridge cannot answer, so this refuses instead. And whatever happens, the
|
|
407
|
+
// map that was open when the call arrived is the map that is open when it
|
|
408
|
+
// returns.
|
|
409
|
+
FString RestoreLevelPath;
|
|
410
|
+
TArray<FString> InitialDirtyPackages;
|
|
411
|
+
if (!LevelPath.IsEmpty())
|
|
412
|
+
{
|
|
413
|
+
if (!GEditor)
|
|
414
|
+
{
|
|
415
|
+
return MCPError(TEXT("GEditor is not available"));
|
|
416
|
+
}
|
|
417
|
+
if (GEditor->PlayWorld != nullptr || GEditor->bIsSimulatingInEditor)
|
|
418
|
+
{
|
|
419
|
+
return MCPError(TEXT("Stop PIE or SIE before querying another level"));
|
|
420
|
+
}
|
|
421
|
+
if (!WorldScope.Equals(TEXT("editor"), ESearchCase::IgnoreCase))
|
|
422
|
+
{
|
|
423
|
+
return MCPError(TEXT("'levelPath' queries the editor world; drop 'world' or set it to 'editor'"));
|
|
424
|
+
}
|
|
425
|
+
if (!MCPEditorState::IsExistingMapPackage(LevelPath))
|
|
426
|
+
{
|
|
427
|
+
return MCPError(FString::Printf(TEXT("Level package was not found as a .umap: %s"), *LevelPath));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
MCPEditorState::CollectDirtyEditorPackageNames(InitialDirtyPackages);
|
|
431
|
+
if (!InitialDirtyPackages.IsEmpty())
|
|
432
|
+
{
|
|
433
|
+
auto DirtyResult = MCPSuccess();
|
|
434
|
+
DirtyResult->SetBoolField(TEXT("success"), false);
|
|
435
|
+
DirtyResult->SetStringField(
|
|
436
|
+
TEXT("error"),
|
|
437
|
+
TEXT("Refusing to open another level while content or map packages are dirty"));
|
|
438
|
+
DirtyResult->SetArrayField(TEXT("dirtyPackages"), MCPStringListToJson(InitialDirtyPackages));
|
|
439
|
+
return MCPResult(DirtyResult);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const FString CurrentLevelPath = MCPEditorState::CurrentEditorLevelPackageName();
|
|
443
|
+
if (CurrentLevelPath != LevelPath)
|
|
444
|
+
{
|
|
445
|
+
if (!MCPEditorState::IsExistingMapPackage(CurrentLevelPath))
|
|
446
|
+
{
|
|
447
|
+
return MCPError(FString::Printf(
|
|
448
|
+
TEXT("The currently open level cannot be restored from package path '%s', so this refuses to leave it"),
|
|
449
|
+
*CurrentLevelPath));
|
|
450
|
+
}
|
|
451
|
+
TSharedPtr<FJsonObject> LoadParams = MakeShared<FJsonObject>();
|
|
452
|
+
LoadParams->SetStringField(TEXT("levelPath"), LevelPath);
|
|
453
|
+
const TSharedPtr<FJsonValue> LoadResult = FLevelHandlers::LoadLevel(LoadParams);
|
|
454
|
+
if (!LoadResult.IsValid() || LoadResult->Type != EJson::Object ||
|
|
455
|
+
!LoadResult->AsObject()->GetBoolField(TEXT("success")))
|
|
456
|
+
{
|
|
457
|
+
return MCPError(FString::Printf(TEXT("Failed to open level '%s'"), *LevelPath));
|
|
458
|
+
}
|
|
459
|
+
RestoreLevelPath = CurrentLevelPath;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Everything below runs against this world. RestoreLevelPath is put back
|
|
464
|
+
// on every exit path from here on.
|
|
465
|
+
UWorld* World = LevelPath.IsEmpty()
|
|
466
|
+
? ResolveWorldFromParams(Params, *WorldScope)
|
|
467
|
+
: GetEditorWorld();
|
|
468
|
+
|
|
469
|
+
auto RestoreLevel = [&RestoreLevelPath]()
|
|
470
|
+
{
|
|
471
|
+
if (RestoreLevelPath.IsEmpty())
|
|
472
|
+
{
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
TSharedPtr<FJsonObject> RestoreParams = MakeShared<FJsonObject>();
|
|
476
|
+
RestoreParams->SetStringField(TEXT("levelPath"), RestoreLevelPath);
|
|
477
|
+
FLevelHandlers::LoadLevel(RestoreParams);
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
if (!World)
|
|
481
|
+
{
|
|
482
|
+
RestoreLevel();
|
|
483
|
+
return MCPError(FString::Printf(TEXT("World not available for scope '%s'"), *WorldScope));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ── Scan ────────────────────────────────────────────────────────────────
|
|
487
|
+
int32 ScannedActors = 0;
|
|
488
|
+
int32 ScannedComponents = 0;
|
|
489
|
+
int32 Matched = 0;
|
|
490
|
+
TArray<FMCPQueryRow> Rows;
|
|
491
|
+
TMap<FString, int32> GroupCounts;
|
|
492
|
+
TMap<FString, TArray<FString>> GroupSamplesByKey;
|
|
493
|
+
TArray<FString> GroupOrder;
|
|
494
|
+
TMap<FString, TMap<FString, int32>> CountByHistograms;
|
|
495
|
+
TMap<FIntVector, int32> DuplicateCounts;
|
|
496
|
+
TMap<FIntVector, TArray<FString>> DuplicateSamples;
|
|
497
|
+
bool bGroupsTruncated = false;
|
|
498
|
+
|
|
499
|
+
const double DuplicateQuantum = DuplicateTolerance > 0.0 ? DuplicateTolerance : 0.0;
|
|
500
|
+
|
|
501
|
+
for (TActorIterator<AActor> ActorIt(World); ActorIt; ++ActorIt)
|
|
502
|
+
{
|
|
503
|
+
AActor* Actor = *ActorIt;
|
|
504
|
+
if (!Actor)
|
|
505
|
+
{
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
++ScannedActors;
|
|
509
|
+
|
|
510
|
+
if (ActorClass)
|
|
511
|
+
{
|
|
512
|
+
const bool bActorMatches = bMatchSubclasses ? Actor->IsA(ActorClass) : (Actor->GetClass() == ActorClass);
|
|
513
|
+
if (!bActorMatches) continue;
|
|
514
|
+
}
|
|
515
|
+
else if (!ActorClassFallback.IsEmpty() &&
|
|
516
|
+
!Actor->GetClass()->GetName().Contains(ActorClassFallback, ESearchCase::IgnoreCase))
|
|
517
|
+
{
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const FString ActorLabel = Actor->GetActorLabel();
|
|
522
|
+
if (!ActorLabelPrefix.IsEmpty() && !ActorLabel.StartsWith(ActorLabelPrefix, ESearchCase::CaseSensitive)) continue;
|
|
523
|
+
if (!ActorLabelContains.IsEmpty() && !ActorLabel.Contains(ActorLabelContains, ESearchCase::IgnoreCase)) continue;
|
|
524
|
+
if (!ActorTag.IsEmpty() && !Actor->ActorHasTag(FName(*ActorTag))) continue;
|
|
525
|
+
|
|
526
|
+
const FString ActorFolder = Actor->GetFolderPath().ToString();
|
|
527
|
+
if (!FolderPath.IsEmpty() && !ActorFolder.Equals(FolderPath, ESearchCase::IgnoreCase)) continue;
|
|
528
|
+
if (!FolderPathPrefix.IsEmpty() && !ActorFolder.StartsWith(FolderPathPrefix, ESearchCase::IgnoreCase)) continue;
|
|
529
|
+
|
|
530
|
+
const bool bOwnerHidden = Actor->IsHidden();
|
|
531
|
+
const APawn* OwnerPawn = Cast<APawn>(Actor);
|
|
532
|
+
|
|
533
|
+
TArray<UActorComponent*> Components;
|
|
534
|
+
Actor->GetComponents(Components);
|
|
535
|
+
for (UActorComponent* Component : Components)
|
|
536
|
+
{
|
|
537
|
+
if (!Component)
|
|
538
|
+
{
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
++ScannedComponents;
|
|
542
|
+
|
|
543
|
+
if (ComponentClass)
|
|
544
|
+
{
|
|
545
|
+
const bool bMatches = bMatchSubclasses
|
|
546
|
+
? Component->IsA(ComponentClass)
|
|
547
|
+
: (Component->GetClass() == ComponentClass);
|
|
548
|
+
if (!bMatches) continue;
|
|
549
|
+
}
|
|
550
|
+
else if (!ComponentClassFallback.IsEmpty() &&
|
|
551
|
+
!Component->GetClass()->GetName().Contains(ComponentClassFallback, ESearchCase::IgnoreCase))
|
|
552
|
+
{
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const FString ComponentName = Component->GetName();
|
|
557
|
+
if (!ComponentNameContains.IsEmpty() &&
|
|
558
|
+
!ComponentName.Contains(ComponentNameContains, ESearchCase::IgnoreCase))
|
|
559
|
+
{
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
USceneComponent* SceneComponent = Cast<USceneComponent>(Component);
|
|
564
|
+
UPrimitiveComponent* Primitive = Cast<UPrimitiveComponent>(Component);
|
|
565
|
+
|
|
566
|
+
TSharedPtr<FJsonObject> Row = MakeShared<FJsonObject>();
|
|
567
|
+
Row->SetStringField(TEXT("actorLabel"), ActorLabel);
|
|
568
|
+
Row->SetStringField(TEXT("actorName"), Actor->GetName());
|
|
569
|
+
Row->SetStringField(TEXT("actorClass"), Actor->GetClass()->GetName());
|
|
570
|
+
Row->SetStringField(TEXT("actorPath"), Actor->GetPathName());
|
|
571
|
+
Row->SetStringField(TEXT("folderPath"), ActorFolder);
|
|
572
|
+
Row->SetStringField(TEXT("componentName"), ComponentName);
|
|
573
|
+
Row->SetStringField(TEXT("componentClass"), Component->GetClass()->GetName());
|
|
574
|
+
|
|
575
|
+
FVector WorldScale = FVector::OneVector;
|
|
576
|
+
bool bHasTransform = false;
|
|
577
|
+
FVector WorldLocation = FVector::ZeroVector;
|
|
578
|
+
if (SceneComponent)
|
|
579
|
+
{
|
|
580
|
+
bHasTransform = true;
|
|
581
|
+
const FTransform ComponentTransform = SceneComponent->GetComponentTransform();
|
|
582
|
+
WorldLocation = ComponentTransform.GetLocation();
|
|
583
|
+
WorldScale = ComponentTransform.GetScale3D();
|
|
584
|
+
if (Fields.bTransform)
|
|
585
|
+
{
|
|
586
|
+
TSharedPtr<FJsonObject> TransformObject = MakeShared<FJsonObject>();
|
|
587
|
+
TransformObject->SetObjectField(TEXT("location"), MCPQueryVec(WorldLocation));
|
|
588
|
+
TransformObject->SetObjectField(TEXT("rotation"), MCPQueryRot(ComponentTransform.Rotator()));
|
|
589
|
+
TransformObject->SetObjectField(TEXT("scale"), MCPQueryVec(WorldScale));
|
|
590
|
+
TransformObject->SetNumberField(TEXT("minAbsScale"),
|
|
591
|
+
FMath::Min3(FMath::Abs(WorldScale.X), FMath::Abs(WorldScale.Y), FMath::Abs(WorldScale.Z)));
|
|
592
|
+
Row->SetObjectField(TEXT("transform"), TransformObject);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
bool bBoundsZero = false;
|
|
597
|
+
bool bBoundsInvalid = false;
|
|
598
|
+
if (SceneComponent && (Fields.bBounds || Fields.bHealth))
|
|
599
|
+
{
|
|
600
|
+
const FBoxSphereBounds ComponentBounds = SceneComponent->GetBounds();
|
|
601
|
+
const FVector Extent = ComponentBounds.BoxExtent;
|
|
602
|
+
bBoundsZero = Extent.IsNearlyZero() && FMath::IsNearlyZero(ComponentBounds.SphereRadius);
|
|
603
|
+
bBoundsInvalid =
|
|
604
|
+
Extent.ContainsNaN() || ComponentBounds.Origin.ContainsNaN() ||
|
|
605
|
+
!FMath::IsFinite(ComponentBounds.SphereRadius) ||
|
|
606
|
+
Extent.X < 0.0 || Extent.Y < 0.0 || Extent.Z < 0.0;
|
|
607
|
+
if (Fields.bBounds)
|
|
608
|
+
{
|
|
609
|
+
TSharedPtr<FJsonObject> BoundsObject = MakeShared<FJsonObject>();
|
|
610
|
+
BoundsObject->SetObjectField(TEXT("origin"), MCPQueryVec(ComponentBounds.Origin));
|
|
611
|
+
BoundsObject->SetObjectField(TEXT("boxExtent"), MCPQueryVec(Extent));
|
|
612
|
+
BoundsObject->SetNumberField(TEXT("sphereRadius"), ComponentBounds.SphereRadius);
|
|
613
|
+
BoundsObject->SetBoolField(TEXT("zero"), bBoundsZero);
|
|
614
|
+
BoundsObject->SetBoolField(TEXT("invalid"), bBoundsInvalid);
|
|
615
|
+
Row->SetObjectField(TEXT("bounds"), BoundsObject);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (SceneComponent && Fields.bLocalBounds)
|
|
620
|
+
{
|
|
621
|
+
// #914: the local, unscaled bounds. A caller comparing two
|
|
622
|
+
// shapes needs the authored extent, not the extent times the
|
|
623
|
+
// component scale, and the scaled one is all the existing
|
|
624
|
+
// reads expose.
|
|
625
|
+
const FBoxSphereBounds LocalBounds = SceneComponent->GetLocalBounds();
|
|
626
|
+
TSharedPtr<FJsonObject> LocalObject = MakeShared<FJsonObject>();
|
|
627
|
+
LocalObject->SetObjectField(TEXT("origin"), MCPQueryVec(LocalBounds.Origin));
|
|
628
|
+
LocalObject->SetObjectField(TEXT("boxExtent"), MCPQueryVec(LocalBounds.BoxExtent));
|
|
629
|
+
LocalObject->SetNumberField(TEXT("sphereRadius"), LocalBounds.SphereRadius);
|
|
630
|
+
if (const UBoxComponent* BoxComponent = Cast<UBoxComponent>(Component))
|
|
631
|
+
{
|
|
632
|
+
// #914: UBoxComponent.BoxExtent itself, which is the
|
|
633
|
+
// authored value the details panel shows and is not
|
|
634
|
+
// derivable from the scaled bounds.
|
|
635
|
+
LocalObject->SetObjectField(TEXT("boxExtent_unscaled"),
|
|
636
|
+
MCPQueryVec(BoxComponent->GetUnscaledBoxExtent()));
|
|
637
|
+
}
|
|
638
|
+
Row->SetObjectField(TEXT("localBounds"), LocalObject);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// ── #910: effective shadow casting ──────────────────────────────
|
|
642
|
+
if (Primitive && Fields.bShadow)
|
|
643
|
+
{
|
|
644
|
+
const bool bVisibleFlag = Primitive->GetVisibleFlag();
|
|
645
|
+
const bool bVisible = Primitive->IsVisible();
|
|
646
|
+
const bool bHiddenInGame = Primitive->bHiddenInGame;
|
|
647
|
+
const bool bRenderedInGame = bVisible && !bHiddenInGame && !bOwnerHidden;
|
|
648
|
+
const bool bCastShadowFlag = Primitive->CastShadow != 0;
|
|
649
|
+
const bool bCastHidden = Primitive->bCastHiddenShadow != 0;
|
|
650
|
+
const bool bEffective = bCastShadowFlag && (bRenderedInGame || bCastHidden);
|
|
651
|
+
|
|
652
|
+
FString Reason;
|
|
653
|
+
if (bEffective)
|
|
654
|
+
{
|
|
655
|
+
Reason = bRenderedInGame ? TEXT("rendered and CastShadow") : TEXT("hidden but bCastHiddenShadow");
|
|
656
|
+
}
|
|
657
|
+
else if (!bCastShadowFlag)
|
|
658
|
+
{
|
|
659
|
+
Reason = TEXT("CastShadow is false");
|
|
660
|
+
}
|
|
661
|
+
else if (!bVisible)
|
|
662
|
+
{
|
|
663
|
+
Reason = TEXT("component is not visible");
|
|
664
|
+
}
|
|
665
|
+
else if (bHiddenInGame)
|
|
666
|
+
{
|
|
667
|
+
Reason = TEXT("bHiddenInGame and not bCastHiddenShadow");
|
|
668
|
+
}
|
|
669
|
+
else
|
|
670
|
+
{
|
|
671
|
+
Reason = TEXT("owning actor is hidden and not bCastHiddenShadow");
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
TSharedPtr<FJsonObject> ShadowObject = MakeShared<FJsonObject>();
|
|
675
|
+
ShadowObject->SetBoolField(TEXT("castShadow"), bCastShadowFlag);
|
|
676
|
+
ShadowObject->SetBoolField(TEXT("castHiddenShadow"), bCastHidden);
|
|
677
|
+
ShadowObject->SetBoolField(TEXT("castDynamicShadow"), Primitive->bCastDynamicShadow != 0);
|
|
678
|
+
ShadowObject->SetBoolField(TEXT("castStaticShadow"), Primitive->bCastStaticShadow != 0);
|
|
679
|
+
ShadowObject->SetBoolField(TEXT("visibleFlag"), bVisibleFlag);
|
|
680
|
+
ShadowObject->SetBoolField(TEXT("visible"), bVisible);
|
|
681
|
+
ShadowObject->SetBoolField(TEXT("hiddenInGame"), bHiddenInGame);
|
|
682
|
+
ShadowObject->SetBoolField(TEXT("ownerHidden"), bOwnerHidden);
|
|
683
|
+
ShadowObject->SetBoolField(TEXT("renderedInGame"), bRenderedInGame);
|
|
684
|
+
ShadowObject->SetBoolField(TEXT("effectiveCastShadow"), bEffective);
|
|
685
|
+
ShadowObject->SetStringField(TEXT("reason"), Reason);
|
|
686
|
+
ShadowObject->SetStringField(TEXT("mobility"), MCPQueryMobilityName(Primitive->Mobility));
|
|
687
|
+
Row->SetObjectField(TEXT("shadow"), ShadowObject);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ── #910: Nanite state, without a call per mesh ─────────────────
|
|
691
|
+
if (Fields.bNanite)
|
|
692
|
+
{
|
|
693
|
+
TSharedPtr<FJsonObject> NaniteObject = MakeShared<FJsonObject>();
|
|
694
|
+
UStaticMeshComponent* StaticMeshComponent = Cast<UStaticMeshComponent>(Component);
|
|
695
|
+
UStaticMesh* StaticMesh = StaticMeshComponent ? StaticMeshComponent->GetStaticMesh() : nullptr;
|
|
696
|
+
const bool bMeshNanite = StaticMesh ? StaticMesh->IsNaniteEnabled() : false;
|
|
697
|
+
const bool bDisallow = StaticMeshComponent ? StaticMeshComponent->IsDisallowNanite() : false;
|
|
698
|
+
const bool bForceDisable = StaticMeshComponent ? StaticMeshComponent->IsForceDisableNanite() : false;
|
|
699
|
+
NaniteObject->SetStringField(TEXT("staticMeshPath"), StaticMesh ? StaticMesh->GetPathName() : FString());
|
|
700
|
+
NaniteObject->SetBoolField(TEXT("meshNaniteEnabled"), bMeshNanite);
|
|
701
|
+
NaniteObject->SetBoolField(TEXT("componentDisallowNanite"), bDisallow);
|
|
702
|
+
NaniteObject->SetBoolField(TEXT("componentForceDisableNanite"), bForceDisable);
|
|
703
|
+
NaniteObject->SetBoolField(TEXT("effectiveNanite"), bMeshNanite && !bDisallow && !bForceDisable);
|
|
704
|
+
Row->SetObjectField(TEXT("nanite"), NaniteObject);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ── #943: navigation relevance ──────────────────────────────────
|
|
708
|
+
if (Fields.bNavigation)
|
|
709
|
+
{
|
|
710
|
+
TSharedPtr<FJsonObject> NavObject = MakeShared<FJsonObject>();
|
|
711
|
+
const bool bCanAffectNav = Component->CanEverAffectNavigation();
|
|
712
|
+
NavObject->SetBoolField(TEXT("canEverAffectNavigation"), bCanAffectNav);
|
|
713
|
+
NavObject->SetStringField(TEXT("mobility"),
|
|
714
|
+
SceneComponent ? MCPQueryMobilityName(SceneComponent->Mobility) : TEXT("None"));
|
|
715
|
+
|
|
716
|
+
ECollisionEnabled::Type CollisionEnabled = ECollisionEnabled::NoCollision;
|
|
717
|
+
if (Primitive)
|
|
718
|
+
{
|
|
719
|
+
CollisionEnabled = Primitive->GetCollisionEnabled();
|
|
720
|
+
NavObject->SetStringField(TEXT("collisionProfile"),
|
|
721
|
+
Primitive->GetCollisionProfileName().ToString());
|
|
722
|
+
NavObject->SetStringField(TEXT("hasCustomNavigableGeometry"),
|
|
723
|
+
MCPQueryNavGeometryName(Primitive->HasCustomNavigableGeometry()));
|
|
724
|
+
}
|
|
725
|
+
NavObject->SetStringField(TEXT("collisionEnabled"), MCPQueryCollisionEnabledName(CollisionEnabled));
|
|
726
|
+
|
|
727
|
+
const bool bCollides =
|
|
728
|
+
CollisionEnabled == ECollisionEnabled::QueryOnly ||
|
|
729
|
+
CollisionEnabled == ECollisionEnabled::QueryAndPhysics ||
|
|
730
|
+
CollisionEnabled == ECollisionEnabled::QueryAndProbe;
|
|
731
|
+
|
|
732
|
+
// NavModifierComponent lives in a module this file does not
|
|
733
|
+
// link, so its AreaClass is read reflectively (#943).
|
|
734
|
+
if (const TSharedPtr<FJsonValue> AreaClass = MCPQueryReflectProperty(Component, TEXT("AreaClass")))
|
|
735
|
+
{
|
|
736
|
+
NavObject->SetField(TEXT("navAreaClass"), AreaClass);
|
|
737
|
+
}
|
|
738
|
+
if (const TSharedPtr<FJsonValue> AreaClassToReplace =
|
|
739
|
+
MCPQueryReflectProperty(Component, TEXT("AreaClassToReplace")))
|
|
740
|
+
{
|
|
741
|
+
NavObject->SetField(TEXT("navAreaClassToReplace"), AreaClassToReplace);
|
|
742
|
+
}
|
|
743
|
+
if (OwnerPawn)
|
|
744
|
+
{
|
|
745
|
+
NavObject->SetBoolField(TEXT("ownerCanAffectNavigationGeneration"),
|
|
746
|
+
OwnerPawn->bCanAffectNavigationGeneration != 0);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const EHasCustomNavigableGeometry::Type CustomGeometry = Primitive
|
|
750
|
+
? Primitive->HasCustomNavigableGeometry()
|
|
751
|
+
: EHasCustomNavigableGeometry::No;
|
|
752
|
+
// EvenIfNotCollidable is the case that makes a raw collision
|
|
753
|
+
// check the wrong answer: the component exports navigation
|
|
754
|
+
// geometry with no collision at all.
|
|
755
|
+
const bool bCustomExports =
|
|
756
|
+
CustomGeometry == EHasCustomNavigableGeometry::Yes ||
|
|
757
|
+
CustomGeometry == EHasCustomNavigableGeometry::EvenIfNotCollidable;
|
|
758
|
+
const bool bEffectiveNav = bCanAffectNav &&
|
|
759
|
+
CustomGeometry != EHasCustomNavigableGeometry::DontExport &&
|
|
760
|
+
(bCollides || bCustomExports);
|
|
761
|
+
NavObject->SetBoolField(TEXT("effectiveNavRelevant"), bEffectiveNav);
|
|
762
|
+
NavObject->SetStringField(TEXT("reason"),
|
|
763
|
+
!bCanAffectNav ? TEXT("bCanEverAffectNavigation is false")
|
|
764
|
+
: bEffectiveNav ? TEXT("navigation relevant")
|
|
765
|
+
: TEXT("no query collision and no custom navigable geometry"));
|
|
766
|
+
Row->SetObjectField(TEXT("navigation"), NavObject);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// ── #912: tick settings ─────────────────────────────────────────
|
|
770
|
+
if (Fields.bTick)
|
|
771
|
+
{
|
|
772
|
+
TSharedPtr<FJsonObject> TickObject = MakeShared<FJsonObject>();
|
|
773
|
+
TickObject->SetBoolField(TEXT("componentCanEverTick"), Component->PrimaryComponentTick.bCanEverTick);
|
|
774
|
+
TickObject->SetBoolField(TEXT("componentTickEnabled"), Component->PrimaryComponentTick.IsTickFunctionEnabled());
|
|
775
|
+
TickObject->SetNumberField(TEXT("componentTickInterval"), Component->PrimaryComponentTick.TickInterval);
|
|
776
|
+
TickObject->SetBoolField(TEXT("actorCanEverTick"), Actor->PrimaryActorTick.bCanEverTick);
|
|
777
|
+
TickObject->SetBoolField(TEXT("actorTickEnabled"), Actor->PrimaryActorTick.IsTickFunctionEnabled());
|
|
778
|
+
TickObject->SetNumberField(TEXT("actorTickInterval"), Actor->PrimaryActorTick.TickInterval);
|
|
779
|
+
Row->SetObjectField(TEXT("tick"), TickObject);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// ── #912: the rendered asset ────────────────────────────────────
|
|
783
|
+
bool bIsMeshComponent = false;
|
|
784
|
+
FString MeshKind;
|
|
785
|
+
UObject* MeshAsset = MCPQueryMeshAsset(Component, MeshKind, bIsMeshComponent);
|
|
786
|
+
const bool bMissingMesh = bIsMeshComponent && MeshAsset == nullptr;
|
|
787
|
+
if (Fields.bMesh && bIsMeshComponent)
|
|
788
|
+
{
|
|
789
|
+
TSharedPtr<FJsonObject> MeshObject = MakeShared<FJsonObject>();
|
|
790
|
+
MeshObject->SetStringField(TEXT("assetKind"), MeshKind);
|
|
791
|
+
if (MeshAsset)
|
|
792
|
+
{
|
|
793
|
+
MeshObject->SetStringField(TEXT("assetPath"), MeshAsset->GetPathName());
|
|
794
|
+
}
|
|
795
|
+
else
|
|
796
|
+
{
|
|
797
|
+
MeshObject->SetField(TEXT("assetPath"), MakeShared<FJsonValueNull>());
|
|
798
|
+
}
|
|
799
|
+
MeshObject->SetBoolField(TEXT("missing"), bMissingMesh);
|
|
800
|
+
// #986: how many instances an ISM/HISM holds. Without it a
|
|
801
|
+
// component holding three instances and one holding three
|
|
802
|
+
// hundred thousand are the same row, and the decision about
|
|
803
|
+
// whether to touch it is made blind. Note this is a projected
|
|
804
|
+
// value, not an aggregate: groupBy/countBy count COMPONENTS, so
|
|
805
|
+
// use level(summarize_static_mesh_usage) for placement totals.
|
|
806
|
+
if (const UInstancedStaticMeshComponent* ISMC = Cast<UInstancedStaticMeshComponent>(Component))
|
|
807
|
+
{
|
|
808
|
+
MeshObject->SetNumberField(TEXT("instanceCount"), ISMC->GetInstanceCount());
|
|
809
|
+
MeshObject->SetBoolField(TEXT("hierarchical"),
|
|
810
|
+
ISMC->IsA<UHierarchicalInstancedStaticMeshComponent>());
|
|
811
|
+
}
|
|
812
|
+
Row->SetObjectField(TEXT("mesh"), MeshObject);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// ── #912: decal material ────────────────────────────────────────
|
|
816
|
+
bool bMissingDecalMaterial = false;
|
|
817
|
+
if (UDecalComponent* Decal = Cast<UDecalComponent>(Component))
|
|
818
|
+
{
|
|
819
|
+
UMaterialInterface* DecalMaterial = Decal->GetDecalMaterial();
|
|
820
|
+
bMissingDecalMaterial = DecalMaterial == nullptr;
|
|
821
|
+
if (Fields.bDecal)
|
|
822
|
+
{
|
|
823
|
+
TSharedPtr<FJsonObject> DecalObject = MakeShared<FJsonObject>();
|
|
824
|
+
if (DecalMaterial)
|
|
825
|
+
{
|
|
826
|
+
DecalObject->SetStringField(TEXT("materialPath"), DecalMaterial->GetPathName());
|
|
827
|
+
}
|
|
828
|
+
else
|
|
829
|
+
{
|
|
830
|
+
DecalObject->SetField(TEXT("materialPath"), MakeShared<FJsonValueNull>());
|
|
831
|
+
}
|
|
832
|
+
DecalObject->SetBoolField(TEXT("missing"), bMissingDecalMaterial);
|
|
833
|
+
Row->SetObjectField(TEXT("decal"), DecalObject);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// ── #912: effective material slots ──────────────────────────────
|
|
838
|
+
int32 NullMaterialSlots = 0;
|
|
839
|
+
int32 WorldGridMaterialSlots = 0;
|
|
840
|
+
if (UMeshComponent* MeshComponent = Cast<UMeshComponent>(Component))
|
|
841
|
+
{
|
|
842
|
+
const int32 SlotCount = FMath::Min(MeshComponent->GetNumMaterials(), MCPQueryMaxMaterialSlots);
|
|
843
|
+
const TArray<FName> SlotNames = MeshComponent->GetMaterialSlotNames();
|
|
844
|
+
TArray<TSharedPtr<FJsonValue>> SlotArray;
|
|
845
|
+
for (int32 SlotIndex = 0; SlotIndex < SlotCount; ++SlotIndex)
|
|
846
|
+
{
|
|
847
|
+
UMaterialInterface* Effective = MeshComponent->GetMaterial(SlotIndex);
|
|
848
|
+
// UMeshComponent::GetMaterial consults OverrideMaterials
|
|
849
|
+
// first, so a non-null entry there is exactly what makes
|
|
850
|
+
// this slot an instance override rather than the asset's
|
|
851
|
+
// own assignment.
|
|
852
|
+
const bool bOverride =
|
|
853
|
+
MeshComponent->OverrideMaterials.IsValidIndex(SlotIndex) &&
|
|
854
|
+
MeshComponent->OverrideMaterials[SlotIndex] != nullptr;
|
|
855
|
+
const FString Path = Effective ? Effective->GetPathName() : FString();
|
|
856
|
+
const bool bIsNull = Effective == nullptr;
|
|
857
|
+
const bool bIsWorldGrid = !bIsNull && Path.StartsWith(MCPQueryWorldGridMaterial);
|
|
858
|
+
if (bIsNull) ++NullMaterialSlots;
|
|
859
|
+
if (bIsWorldGrid) ++WorldGridMaterialSlots;
|
|
860
|
+
|
|
861
|
+
if (Fields.bMaterials)
|
|
862
|
+
{
|
|
863
|
+
TSharedPtr<FJsonObject> SlotObject = MakeShared<FJsonObject>();
|
|
864
|
+
SlotObject->SetNumberField(TEXT("slotIndex"), SlotIndex);
|
|
865
|
+
SlotObject->SetStringField(TEXT("slotName"),
|
|
866
|
+
SlotNames.IsValidIndex(SlotIndex) ? SlotNames[SlotIndex].ToString() : FString());
|
|
867
|
+
if (bIsNull)
|
|
868
|
+
{
|
|
869
|
+
SlotObject->SetField(TEXT("materialPath"), MakeShared<FJsonValueNull>());
|
|
870
|
+
}
|
|
871
|
+
else
|
|
872
|
+
{
|
|
873
|
+
SlotObject->SetStringField(TEXT("materialPath"), Path);
|
|
874
|
+
}
|
|
875
|
+
SlotObject->SetStringField(TEXT("source"),
|
|
876
|
+
bOverride ? TEXT("componentOverride") : (bIsNull ? TEXT("none") : TEXT("meshDefault")));
|
|
877
|
+
SlotObject->SetBoolField(TEXT("isNull"), bIsNull);
|
|
878
|
+
SlotObject->SetBoolField(TEXT("isWorldGrid"), bIsWorldGrid);
|
|
879
|
+
SlotArray.Add(MakeShared<FJsonValueObject>(SlotObject));
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
if (Fields.bMaterials)
|
|
883
|
+
{
|
|
884
|
+
Row->SetArrayField(TEXT("materials"), SlotArray);
|
|
885
|
+
Row->SetNumberField(TEXT("materialSlotCount"), MeshComponent->GetNumMaterials());
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// ── #912: the health roll-up ────────────────────────────────────
|
|
890
|
+
const bool bZeroScale = bHasTransform && (
|
|
891
|
+
FMath::IsNearlyZero(WorldScale.X) ||
|
|
892
|
+
FMath::IsNearlyZero(WorldScale.Y) ||
|
|
893
|
+
FMath::IsNearlyZero(WorldScale.Z));
|
|
894
|
+
const bool bNegativeScale = bHasTransform &&
|
|
895
|
+
(WorldScale.X < 0.0 || WorldScale.Y < 0.0 || WorldScale.Z < 0.0);
|
|
896
|
+
if (Fields.bHealth)
|
|
897
|
+
{
|
|
898
|
+
const bool bSuspect =
|
|
899
|
+
bBoundsZero || bBoundsInvalid || bZeroScale || bMissingMesh ||
|
|
900
|
+
bMissingDecalMaterial || NullMaterialSlots > 0 || WorldGridMaterialSlots > 0;
|
|
901
|
+
TSharedPtr<FJsonObject> HealthObject = MakeShared<FJsonObject>();
|
|
902
|
+
HealthObject->SetBoolField(TEXT("boundsZero"), bBoundsZero);
|
|
903
|
+
HealthObject->SetBoolField(TEXT("boundsInvalid"), bBoundsInvalid);
|
|
904
|
+
HealthObject->SetBoolField(TEXT("zeroScale"), bZeroScale);
|
|
905
|
+
HealthObject->SetBoolField(TEXT("negativeScale"), bNegativeScale);
|
|
906
|
+
HealthObject->SetBoolField(TEXT("missingMesh"), bMissingMesh);
|
|
907
|
+
HealthObject->SetBoolField(TEXT("missingDecalMaterial"), bMissingDecalMaterial);
|
|
908
|
+
HealthObject->SetNumberField(TEXT("nullMaterialSlotCount"), NullMaterialSlots);
|
|
909
|
+
HealthObject->SetNumberField(TEXT("worldGridMaterialSlotCount"), WorldGridMaterialSlots);
|
|
910
|
+
HealthObject->SetBoolField(TEXT("suspect"), bSuspect);
|
|
911
|
+
Row->SetObjectField(TEXT("health"), HealthObject);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// ── Requested UPROPERTY projection ──────────────────────────────
|
|
915
|
+
if (ProjectedProperties.Num() > 0)
|
|
916
|
+
{
|
|
917
|
+
TSharedPtr<FJsonObject> PropsObject = MakeShared<FJsonObject>();
|
|
918
|
+
for (const FString& PropertyName : ProjectedProperties)
|
|
919
|
+
{
|
|
920
|
+
FProperty* Property = Component->GetClass()->FindPropertyByName(FName(*PropertyName));
|
|
921
|
+
if (!Property)
|
|
922
|
+
{
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
PropsObject->SetField(PropertyName,
|
|
926
|
+
MCPQuery::PropertyToJson(Property, Property->ContainerPtrToValuePtr<void>(Component)));
|
|
927
|
+
}
|
|
928
|
+
Row->SetObjectField(TEXT("props"), PropsObject);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// ── Predicates, evaluated here rather than in the client ────────
|
|
932
|
+
if (!MCPQuery::EvaluateAll(Predicates, Row, WhereMode == TEXT("all")))
|
|
933
|
+
{
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
++Matched;
|
|
938
|
+
|
|
939
|
+
// Grouping and histograms count every match, including the ones
|
|
940
|
+
// past the retention cap, so an aggregate is never a partial
|
|
941
|
+
// answer dressed up as a total.
|
|
942
|
+
if (!GroupBy.IsEmpty())
|
|
943
|
+
{
|
|
944
|
+
const FString Key = MCPQuery::ValueKey(MCPQuery::ResolvePath(Row, GroupBy));
|
|
945
|
+
if (int32* Existing = GroupCounts.Find(Key))
|
|
946
|
+
{
|
|
947
|
+
++(*Existing);
|
|
948
|
+
TArray<FString>& Samples = GroupSamplesByKey.FindOrAdd(Key);
|
|
949
|
+
if (Samples.Num() < GroupSamples)
|
|
950
|
+
{
|
|
951
|
+
Samples.Add(FString::Printf(TEXT("%s.%s"), *ActorLabel, *ComponentName));
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
else if (GroupCounts.Num() < MCPQueryMaxGroups)
|
|
955
|
+
{
|
|
956
|
+
GroupCounts.Add(Key, 1);
|
|
957
|
+
GroupOrder.Add(Key);
|
|
958
|
+
TArray<FString>& Samples = GroupSamplesByKey.FindOrAdd(Key);
|
|
959
|
+
if (Samples.Num() < GroupSamples)
|
|
960
|
+
{
|
|
961
|
+
Samples.Add(FString::Printf(TEXT("%s.%s"), *ActorLabel, *ComponentName));
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
else
|
|
965
|
+
{
|
|
966
|
+
bGroupsTruncated = true;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
for (const FString& CountPath : CountByPaths)
|
|
971
|
+
{
|
|
972
|
+
TMap<FString, int32>& Histogram = CountByHistograms.FindOrAdd(CountPath);
|
|
973
|
+
const FString Key = MCPQuery::ValueKey(MCPQuery::ResolvePath(Row, CountPath));
|
|
974
|
+
if (int32* Existing = Histogram.Find(Key))
|
|
975
|
+
{
|
|
976
|
+
++(*Existing);
|
|
977
|
+
}
|
|
978
|
+
else if (Histogram.Num() < MCPQueryMaxCountByValues)
|
|
979
|
+
{
|
|
980
|
+
Histogram.Add(Key, 1);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
FIntVector Quantized = FIntVector::ZeroValue;
|
|
985
|
+
if (DuplicateQuantum > 0.0 && bHasTransform)
|
|
986
|
+
{
|
|
987
|
+
Quantized = FIntVector(
|
|
988
|
+
FMath::RoundToInt(WorldLocation.X / DuplicateQuantum),
|
|
989
|
+
FMath::RoundToInt(WorldLocation.Y / DuplicateQuantum),
|
|
990
|
+
FMath::RoundToInt(WorldLocation.Z / DuplicateQuantum));
|
|
991
|
+
int32& Count = DuplicateCounts.FindOrAdd(Quantized);
|
|
992
|
+
++Count;
|
|
993
|
+
TArray<FString>& Samples = DuplicateSamples.FindOrAdd(Quantized);
|
|
994
|
+
if (Samples.Num() < FMath::Max(GroupSamples, 2))
|
|
995
|
+
{
|
|
996
|
+
Samples.Add(FString::Printf(TEXT("%s.%s"), *ActorLabel, *ComponentName));
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (!bCountOnly && Rows.Num() < MCPQueryMaxRetainedRows)
|
|
1001
|
+
{
|
|
1002
|
+
FMCPQueryRow NewRow;
|
|
1003
|
+
NewRow.SortKey = FString::Printf(TEXT("%s|%s"), *Actor->GetPathName(), *ComponentName);
|
|
1004
|
+
NewRow.Object = Row;
|
|
1005
|
+
NewRow.QuantizedLocation = Quantized;
|
|
1006
|
+
NewRow.bHasTransform = bHasTransform;
|
|
1007
|
+
Rows.Add(MoveTemp(NewRow));
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// ── Response ────────────────────────────────────────────────────────────
|
|
1013
|
+
const bool bRetentionTruncated = !bCountOnly && Matched > Rows.Num();
|
|
1014
|
+
|
|
1015
|
+
// Deterministic order, so `startIndex` means the same thing on the next call.
|
|
1016
|
+
// TActorIterator order is not a contract.
|
|
1017
|
+
Rows.Sort([](const FMCPQueryRow& A, const FMCPQueryRow& B) { return A.SortKey < B.SortKey; });
|
|
1018
|
+
|
|
1019
|
+
TArray<TSharedPtr<FJsonValue>> RowArray;
|
|
1020
|
+
if (!bCountOnly)
|
|
1021
|
+
{
|
|
1022
|
+
const int32 First = FMath::Min(StartIndex, Rows.Num());
|
|
1023
|
+
const int32 Last = FMath::Min(First + Limit, Rows.Num());
|
|
1024
|
+
for (int32 Index = First; Index < Last; ++Index)
|
|
1025
|
+
{
|
|
1026
|
+
FMCPQueryRow& Row = Rows[Index];
|
|
1027
|
+
if (DuplicateQuantum > 0.0 && Row.bHasTransform)
|
|
1028
|
+
{
|
|
1029
|
+
const int32* Count = DuplicateCounts.Find(Row.QuantizedLocation);
|
|
1030
|
+
const bool bDuplicate = Count && *Count > 1;
|
|
1031
|
+
if (Row.Object->HasTypedField<EJson::Object>(TEXT("health")))
|
|
1032
|
+
{
|
|
1033
|
+
Row.Object->GetObjectField(TEXT("health"))->SetBoolField(TEXT("duplicateTransform"), bDuplicate);
|
|
1034
|
+
}
|
|
1035
|
+
else
|
|
1036
|
+
{
|
|
1037
|
+
Row.Object->SetBoolField(TEXT("duplicateTransform"), bDuplicate);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
RowArray.Add(MakeShared<FJsonValueObject>(Row.Object));
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
auto Result = MCPSuccess();
|
|
1045
|
+
Result->SetStringField(TEXT("worldName"), World->GetName());
|
|
1046
|
+
Result->SetStringField(TEXT("levelPath"), MCPEditorState::CurrentEditorLevelPackageName());
|
|
1047
|
+
Result->SetNumberField(TEXT("scannedActors"), ScannedActors);
|
|
1048
|
+
Result->SetNumberField(TEXT("scannedComponents"), ScannedComponents);
|
|
1049
|
+
Result->SetNumberField(TEXT("matched"), Matched);
|
|
1050
|
+
Result->SetNumberField(TEXT("returned"), RowArray.Num());
|
|
1051
|
+
Result->SetNumberField(TEXT("startIndex"), StartIndex);
|
|
1052
|
+
Result->SetNumberField(TEXT("limit"), Limit);
|
|
1053
|
+
Result->SetBoolField(TEXT("countOnly"), bCountOnly);
|
|
1054
|
+
Result->SetBoolField(TEXT("retentionTruncated"), bRetentionTruncated);
|
|
1055
|
+
if (bRetentionTruncated)
|
|
1056
|
+
{
|
|
1057
|
+
Result->SetStringField(TEXT("retentionNote"), FString::Printf(
|
|
1058
|
+
TEXT("Only the first %d matches were kept for paging; 'matched' is exact. Narrow the filters or use countOnly/groupBy."),
|
|
1059
|
+
MCPQueryMaxRetainedRows));
|
|
1060
|
+
}
|
|
1061
|
+
const int32 NextStartIndex = StartIndex + RowArray.Num();
|
|
1062
|
+
Result->SetBoolField(TEXT("hasMore"), !bCountOnly && NextStartIndex < Rows.Num());
|
|
1063
|
+
if (!bCountOnly && NextStartIndex < Rows.Num())
|
|
1064
|
+
{
|
|
1065
|
+
Result->SetNumberField(TEXT("nextStartIndex"), NextStartIndex);
|
|
1066
|
+
}
|
|
1067
|
+
if (!bCountOnly)
|
|
1068
|
+
{
|
|
1069
|
+
Result->SetArrayField(TEXT("rows"), RowArray);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (!GroupBy.IsEmpty())
|
|
1073
|
+
{
|
|
1074
|
+
GroupOrder.Sort([&GroupCounts](const FString& A, const FString& B)
|
|
1075
|
+
{
|
|
1076
|
+
const int32 CountA = GroupCounts.FindChecked(A);
|
|
1077
|
+
const int32 CountB = GroupCounts.FindChecked(B);
|
|
1078
|
+
return CountA != CountB ? CountA > CountB : A < B;
|
|
1079
|
+
});
|
|
1080
|
+
TArray<TSharedPtr<FJsonValue>> GroupArray;
|
|
1081
|
+
for (const FString& Key : GroupOrder)
|
|
1082
|
+
{
|
|
1083
|
+
TSharedPtr<FJsonObject> GroupObject = MakeShared<FJsonObject>();
|
|
1084
|
+
GroupObject->SetStringField(TEXT("key"), Key);
|
|
1085
|
+
GroupObject->SetNumberField(TEXT("count"), GroupCounts.FindChecked(Key));
|
|
1086
|
+
if (const TArray<FString>* Samples = GroupSamplesByKey.Find(Key))
|
|
1087
|
+
{
|
|
1088
|
+
GroupObject->SetArrayField(TEXT("samples"), MCPStringListToJson(*Samples));
|
|
1089
|
+
}
|
|
1090
|
+
GroupArray.Add(MakeShared<FJsonValueObject>(GroupObject));
|
|
1091
|
+
}
|
|
1092
|
+
Result->SetStringField(TEXT("groupBy"), GroupBy);
|
|
1093
|
+
Result->SetArrayField(TEXT("groups"), GroupArray);
|
|
1094
|
+
Result->SetBoolField(TEXT("groupsTruncated"), bGroupsTruncated);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if (CountByPaths.Num() > 0)
|
|
1098
|
+
{
|
|
1099
|
+
TSharedPtr<FJsonObject> CountsObject = MakeShared<FJsonObject>();
|
|
1100
|
+
for (const FString& CountPath : CountByPaths)
|
|
1101
|
+
{
|
|
1102
|
+
TSharedPtr<FJsonObject> Histogram = MakeShared<FJsonObject>();
|
|
1103
|
+
if (const TMap<FString, int32>* Values = CountByHistograms.Find(CountPath))
|
|
1104
|
+
{
|
|
1105
|
+
for (const TPair<FString, int32>& Pair : *Values)
|
|
1106
|
+
{
|
|
1107
|
+
Histogram->SetNumberField(Pair.Key, Pair.Value);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
CountsObject->SetObjectField(CountPath, Histogram);
|
|
1111
|
+
}
|
|
1112
|
+
Result->SetObjectField(TEXT("counts"), CountsObject);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
if (DuplicateQuantum > 0.0)
|
|
1116
|
+
{
|
|
1117
|
+
TArray<TSharedPtr<FJsonValue>> DuplicateArray;
|
|
1118
|
+
int32 DuplicateComponentCount = 0;
|
|
1119
|
+
for (const TPair<FIntVector, int32>& Pair : DuplicateCounts)
|
|
1120
|
+
{
|
|
1121
|
+
if (Pair.Value < 2)
|
|
1122
|
+
{
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
DuplicateComponentCount += Pair.Value;
|
|
1126
|
+
if (DuplicateArray.Num() >= MCPQueryMaxDuplicateGroups)
|
|
1127
|
+
{
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
TSharedPtr<FJsonObject> DuplicateObject = MakeShared<FJsonObject>();
|
|
1131
|
+
DuplicateObject->SetObjectField(TEXT("location"), MCPQueryVec(FVector(
|
|
1132
|
+
Pair.Key.X * DuplicateQuantum,
|
|
1133
|
+
Pair.Key.Y * DuplicateQuantum,
|
|
1134
|
+
Pair.Key.Z * DuplicateQuantum)));
|
|
1135
|
+
DuplicateObject->SetNumberField(TEXT("count"), Pair.Value);
|
|
1136
|
+
if (const TArray<FString>* Samples = DuplicateSamples.Find(Pair.Key))
|
|
1137
|
+
{
|
|
1138
|
+
DuplicateObject->SetArrayField(TEXT("samples"), MCPStringListToJson(*Samples));
|
|
1139
|
+
}
|
|
1140
|
+
DuplicateArray.Add(MakeShared<FJsonValueObject>(DuplicateObject));
|
|
1141
|
+
}
|
|
1142
|
+
Result->SetNumberField(TEXT("duplicateTransformTolerance"), DuplicateQuantum);
|
|
1143
|
+
Result->SetNumberField(TEXT("duplicateTransformComponents"), DuplicateComponentCount);
|
|
1144
|
+
Result->SetArrayField(TEXT("duplicateTransformGroups"), DuplicateArray);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// The read half of the #912 contract, asserted rather than assumed: if
|
|
1148
|
+
// anything about this call dirtied a package, say so instead of leaving a
|
|
1149
|
+
// map-wide audit as an unexplained unsaved-changes marker.
|
|
1150
|
+
TArray<FString> DirtyAfterQuery;
|
|
1151
|
+
MCPEditorState::CollectDirtyEditorPackageNames(DirtyAfterQuery);
|
|
1152
|
+
for (const FString& Already : InitialDirtyPackages)
|
|
1153
|
+
{
|
|
1154
|
+
DirtyAfterQuery.Remove(Already);
|
|
1155
|
+
}
|
|
1156
|
+
Result->SetBoolField(TEXT("dirtiedPackages"), DirtyAfterQuery.Num() > 0);
|
|
1157
|
+
if (DirtyAfterQuery.Num() > 0)
|
|
1158
|
+
{
|
|
1159
|
+
Result->SetArrayField(TEXT("dirtyPackages"), MCPStringListToJson(DirtyAfterQuery));
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
if (!RestoreLevelPath.IsEmpty())
|
|
1163
|
+
{
|
|
1164
|
+
Result->SetStringField(TEXT("restoredLevelPath"), RestoreLevelPath);
|
|
1165
|
+
}
|
|
1166
|
+
RestoreLevel();
|
|
1167
|
+
|
|
1168
|
+
return MCPResult(Result);
|
|
1169
|
+
}
|