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
|
@@ -7,26 +7,393 @@
|
|
|
7
7
|
#include "Engine/World.h"
|
|
8
8
|
#include "EngineUtils.h"
|
|
9
9
|
#include "Landscape.h"
|
|
10
|
+
#include "LandscapeDataAccess.h"
|
|
11
|
+
// #939: FLandscapeEditDataInterface, the same height/weight accessor
|
|
12
|
+
// landscape(sculpt) and landscape(paint_layer) write through, so a sample reads
|
|
13
|
+
// back exactly what a paint wrote.
|
|
14
|
+
#include "LandscapeEdit.h"
|
|
10
15
|
#include "LandscapeEditTypes.h"
|
|
11
16
|
#include "LandscapeProxy.h"
|
|
12
17
|
#include "LandscapeStreamingProxy.h"
|
|
13
18
|
#include "LandscapeInfo.h"
|
|
14
19
|
#include "LandscapeComponent.h"
|
|
20
|
+
#include "LandscapeHeightfieldCollisionComponent.h"
|
|
15
21
|
#include "LandscapeSplineActor.h"
|
|
16
22
|
#include "LandscapeSplinesComponent.h"
|
|
17
23
|
#include "LandscapeSplineControlPoint.h"
|
|
18
24
|
#include "LandscapeSplineSegment.h"
|
|
25
|
+
#include "CollisionQueryParams.h"
|
|
26
|
+
#include "Engine/HitResult.h"
|
|
27
|
+
#include "PhysicalMaterials/PhysicalMaterial.h"
|
|
19
28
|
#include "Kismet/KismetSystemLibrary.h"
|
|
20
29
|
#include "Misc/FileHelper.h"
|
|
30
|
+
#include "Misc/SecureHash.h"
|
|
31
|
+
#include "HAL/IConsoleManager.h"
|
|
32
|
+
#include "HAL/PlatformProcess.h"
|
|
33
|
+
#include "HAL/PlatformTime.h"
|
|
21
34
|
#include "Materials/MaterialInterface.h"
|
|
22
35
|
#include "EditorScriptingUtilities/Public/EditorAssetLibrary.h"
|
|
36
|
+
#include "RenderingThread.h"
|
|
23
37
|
#include "Components/PrimitiveComponent.h"
|
|
38
|
+
#include "PhysicsEngine/BodyInstance.h"
|
|
24
39
|
#include "LandscapeLayerInfoObject.h"
|
|
25
40
|
#include "UObject/Package.h"
|
|
41
|
+
#include "UObject/UnrealType.h"
|
|
26
42
|
#include "AssetRegistry/AssetRegistryModule.h"
|
|
27
43
|
#include "AssetToolsModule.h"
|
|
28
44
|
#include "IAssetTools.h"
|
|
29
45
|
|
|
46
|
+
#if WITH_DEV_AUTOMATION_TESTS
|
|
47
|
+
#include "Misc/AutomationTest.h"
|
|
48
|
+
#endif
|
|
49
|
+
|
|
50
|
+
#if UE_MCP_HAS_5_8_API
|
|
51
|
+
|
|
52
|
+
namespace
|
|
53
|
+
{
|
|
54
|
+
constexpr float HeightCollisionToleranceCm = 0.25f;
|
|
55
|
+
|
|
56
|
+
struct FLandscapeHeightCollisionSignature
|
|
57
|
+
{
|
|
58
|
+
ULandscapeHeightfieldCollisionComponent* Collision = nullptr;
|
|
59
|
+
FString ComponentPath;
|
|
60
|
+
int64 RawElementCount = 0;
|
|
61
|
+
int32 ComplexSampleCount = 0;
|
|
62
|
+
int32 CollisionSizeVerts = 0;
|
|
63
|
+
int32 SimpleSampleCount = 0;
|
|
64
|
+
int32 SimpleCollisionSizeVerts = 0;
|
|
65
|
+
FSHAHash RawHash;
|
|
66
|
+
FSHAHash LiveHash;
|
|
67
|
+
FTransform ComponentTransform;
|
|
68
|
+
float RawMinWorldZ = 0.0f;
|
|
69
|
+
float RawMaxWorldZ = 0.0f;
|
|
70
|
+
float LiveMinWorldZ = 0.0f;
|
|
71
|
+
float LiveMaxWorldZ = 0.0f;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
bool ReadCollisionSampleInfo(
|
|
75
|
+
const ULandscapeHeightfieldCollisionComponent* Collision,
|
|
76
|
+
ULandscapeHeightfieldCollisionComponent::FCollisionSampleInfo& OutInfo,
|
|
77
|
+
FString& OutError)
|
|
78
|
+
{
|
|
79
|
+
// GetCollisionSampleInfo is public but not LANDSCAPE_API, so calling it
|
|
80
|
+
// from an external plugin compiles and then fails to link. Reproduce its
|
|
81
|
+
// four-line calculation from the reflected size properties instead.
|
|
82
|
+
static const FIntProperty* CollisionSizeQuadsProperty =
|
|
83
|
+
FindFProperty<FIntProperty>(
|
|
84
|
+
ULandscapeHeightfieldCollisionComponent::StaticClass(),
|
|
85
|
+
TEXT("CollisionSizeQuads"));
|
|
86
|
+
static const FIntProperty* SimpleCollisionSizeQuadsProperty =
|
|
87
|
+
FindFProperty<FIntProperty>(
|
|
88
|
+
ULandscapeHeightfieldCollisionComponent::StaticClass(),
|
|
89
|
+
TEXT("SimpleCollisionSizeQuads"));
|
|
90
|
+
if (!CollisionSizeQuadsProperty || !SimpleCollisionSizeQuadsProperty)
|
|
91
|
+
{
|
|
92
|
+
OutError = TEXT("Landscape collision size properties are unavailable");
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const int32 CollisionSizeQuads =
|
|
97
|
+
CollisionSizeQuadsProperty->GetPropertyValue_InContainer(Collision);
|
|
98
|
+
const int32 SimpleCollisionSizeQuads =
|
|
99
|
+
SimpleCollisionSizeQuadsProperty->GetPropertyValue_InContainer(Collision);
|
|
100
|
+
if (CollisionSizeQuads <= 0 || SimpleCollisionSizeQuads < 0)
|
|
101
|
+
{
|
|
102
|
+
OutError = FString::Printf(
|
|
103
|
+
TEXT("Invalid landscape collision quad dimensions: complex=%d, simple=%d"),
|
|
104
|
+
CollisionSizeQuads, SimpleCollisionSizeQuads);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
OutInfo.CollisionSizeVerts = CollisionSizeQuads + 1;
|
|
109
|
+
OutInfo.SimpleCollisionSizeVerts =
|
|
110
|
+
SimpleCollisionSizeQuads > 0 ? SimpleCollisionSizeQuads + 1 : 0;
|
|
111
|
+
OutInfo.NumSamples = FMath::Square(OutInfo.CollisionSizeVerts);
|
|
112
|
+
OutInfo.NumSimpleSamples = FMath::Square(OutInfo.SimpleCollisionSizeVerts);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
bool BuildHeightCollisionSignature(
|
|
117
|
+
TConstArrayView<uint16> RawHeights,
|
|
118
|
+
int32 ComplexSampleCount,
|
|
119
|
+
int32 CollisionSizeVerts,
|
|
120
|
+
int32 SimpleSampleCount,
|
|
121
|
+
int32 SimpleCollisionSizeVerts,
|
|
122
|
+
TConstArrayView<float> LiveWorldHeights,
|
|
123
|
+
const FTransform& ComponentTransform,
|
|
124
|
+
FLandscapeHeightCollisionSignature& OutSignature,
|
|
125
|
+
FString& OutError)
|
|
126
|
+
{
|
|
127
|
+
if (ComplexSampleCount <= 0 || CollisionSizeVerts <= 1 ||
|
|
128
|
+
ComplexSampleCount != CollisionSizeVerts * CollisionSizeVerts)
|
|
129
|
+
{
|
|
130
|
+
OutError = TEXT("Invalid landscape collision sample dimensions");
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
if (SimpleSampleCount < 0 || SimpleCollisionSizeVerts < 0 ||
|
|
134
|
+
(SimpleSampleCount == 0) != (SimpleCollisionSizeVerts == 0) ||
|
|
135
|
+
(SimpleSampleCount > 0 && SimpleSampleCount != SimpleCollisionSizeVerts * SimpleCollisionSizeVerts))
|
|
136
|
+
{
|
|
137
|
+
OutError = TEXT("Invalid simple landscape collision sample dimensions");
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
const int32 TotalSampleCount = ComplexSampleCount + SimpleSampleCount;
|
|
141
|
+
if (RawHeights.Num() != TotalSampleCount)
|
|
142
|
+
{
|
|
143
|
+
OutError = FString::Printf(
|
|
144
|
+
TEXT("Raw collision height data has %d element(s), expected %d complex + simple samples"),
|
|
145
|
+
RawHeights.Num(), TotalSampleCount);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
if (LiveWorldHeights.Num() != TotalSampleCount)
|
|
149
|
+
{
|
|
150
|
+
OutError = FString::Printf(
|
|
151
|
+
TEXT("Live collision heightfields returned %d sample(s), expected %d"),
|
|
152
|
+
LiveWorldHeights.Num(), TotalSampleCount);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
OutSignature.RawElementCount = RawHeights.Num();
|
|
157
|
+
OutSignature.ComplexSampleCount = ComplexSampleCount;
|
|
158
|
+
OutSignature.CollisionSizeVerts = CollisionSizeVerts;
|
|
159
|
+
OutSignature.SimpleSampleCount = SimpleSampleCount;
|
|
160
|
+
OutSignature.SimpleCollisionSizeVerts = SimpleCollisionSizeVerts;
|
|
161
|
+
OutSignature.ComponentTransform = ComponentTransform;
|
|
162
|
+
OutSignature.RawHash = FSHA1::HashBuffer(
|
|
163
|
+
RawHeights.GetData(), static_cast<uint64>(RawHeights.Num()) * sizeof(uint16));
|
|
164
|
+
|
|
165
|
+
FSHA1 LiveHasher;
|
|
166
|
+
OutSignature.RawMinWorldZ = TNumericLimits<float>::Max();
|
|
167
|
+
OutSignature.RawMaxWorldZ = TNumericLimits<float>::Lowest();
|
|
168
|
+
OutSignature.LiveMinWorldZ = TNumericLimits<float>::Max();
|
|
169
|
+
OutSignature.LiveMaxWorldZ = TNumericLimits<float>::Lowest();
|
|
170
|
+
for (int32 Index = 0; Index < TotalSampleCount; ++Index)
|
|
171
|
+
{
|
|
172
|
+
const float ExpectedWorldZ = static_cast<float>(ComponentTransform.TransformPosition(
|
|
173
|
+
FVector(0.0, 0.0, LandscapeDataAccess::GetLocalHeight(RawHeights[Index]))).Z);
|
|
174
|
+
const float LiveWorldZ = LiveWorldHeights[Index];
|
|
175
|
+
if (!FMath::IsFinite(ExpectedWorldZ) || !FMath::IsFinite(LiveWorldZ) ||
|
|
176
|
+
!FMath::IsNearlyEqual(ExpectedWorldZ, LiveWorldZ, HeightCollisionToleranceCm))
|
|
177
|
+
{
|
|
178
|
+
OutError = FString::Printf(
|
|
179
|
+
TEXT("Raw/live collision height mismatch at sample %d: raw predicts %.3f cm, live heightfield reports %.3f cm"),
|
|
180
|
+
Index, ExpectedWorldZ, LiveWorldZ);
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
OutSignature.RawMinWorldZ = FMath::Min(OutSignature.RawMinWorldZ, ExpectedWorldZ);
|
|
185
|
+
OutSignature.RawMaxWorldZ = FMath::Max(OutSignature.RawMaxWorldZ, ExpectedWorldZ);
|
|
186
|
+
OutSignature.LiveMinWorldZ = FMath::Min(OutSignature.LiveMinWorldZ, LiveWorldZ);
|
|
187
|
+
OutSignature.LiveMaxWorldZ = FMath::Max(OutSignature.LiveMaxWorldZ, LiveWorldZ);
|
|
188
|
+
|
|
189
|
+
// Hash at 0.1 cm precision. A recreated Chaos heightfield can differ by a
|
|
190
|
+
// tiny floating-point amount while still representing the same uint16
|
|
191
|
+
// source height, but a flattened or shifted field must never compare equal.
|
|
192
|
+
const int64 QuantizedHeight = FMath::RoundToInt64(static_cast<double>(LiveWorldZ) * 10.0);
|
|
193
|
+
LiveHasher.Update(QuantizedHeight);
|
|
194
|
+
}
|
|
195
|
+
OutSignature.LiveHash = LiveHasher.Finalize();
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
bool CaptureHeightCollisionSignature(
|
|
200
|
+
ULandscapeHeightfieldCollisionComponent* Collision,
|
|
201
|
+
FLandscapeHeightCollisionSignature& OutSignature,
|
|
202
|
+
FString& OutError)
|
|
203
|
+
{
|
|
204
|
+
if (!Collision)
|
|
205
|
+
{
|
|
206
|
+
OutError = TEXT("Landscape collision component is null");
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
const FBodyInstance* BodyInstance = Collision->GetBodyInstance();
|
|
210
|
+
if (!Collision->IsRegistered() || !Collision->IsPhysicsStateCreated() ||
|
|
211
|
+
!BodyInstance || !BodyInstance->IsValidBodyInstance())
|
|
212
|
+
{
|
|
213
|
+
OutError = FString::Printf(
|
|
214
|
+
TEXT("%s does not have a registered, valid physics body"), *Collision->GetPathName());
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
ULandscapeHeightfieldCollisionComponent::FCollisionSampleInfo SampleInfo;
|
|
219
|
+
if (!ReadCollisionSampleInfo(Collision, SampleInfo, OutError))
|
|
220
|
+
{
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
const int64 ExpectedRawElements = static_cast<int64>(SampleInfo.NumSamples) + SampleInfo.NumSimpleSamples;
|
|
224
|
+
const int64 RawElementCount = Collision->CollisionHeightData.GetElementCount();
|
|
225
|
+
if (ExpectedRawElements <= 0 || RawElementCount != ExpectedRawElements)
|
|
226
|
+
{
|
|
227
|
+
OutError = FString::Printf(
|
|
228
|
+
TEXT("%s has %lld raw collision height element(s), expected %lld"),
|
|
229
|
+
*Collision->GetPathName(), RawElementCount, ExpectedRawElements);
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
TArray<float> LiveWorldHeights;
|
|
234
|
+
LiveWorldHeights.SetNumUninitialized(static_cast<int32>(ExpectedRawElements));
|
|
235
|
+
if (!Collision->FillHeightTile(
|
|
236
|
+
MakeArrayView(LiveWorldHeights.GetData(), SampleInfo.NumSamples),
|
|
237
|
+
0,
|
|
238
|
+
SampleInfo.CollisionSizeVerts))
|
|
239
|
+
{
|
|
240
|
+
OutError = FString::Printf(
|
|
241
|
+
TEXT("%s has no readable live complex collision heightfield"), *Collision->GetPathName());
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
if (SampleInfo.NumSimpleSamples > 0)
|
|
245
|
+
{
|
|
246
|
+
const FTransform& WorldTransform = Collision->GetComponentTransform();
|
|
247
|
+
for (int32 Y = 0; Y < SampleInfo.SimpleCollisionSizeVerts; ++Y)
|
|
248
|
+
{
|
|
249
|
+
for (int32 X = 0; X < SampleInfo.SimpleCollisionSizeVerts; ++X)
|
|
250
|
+
{
|
|
251
|
+
const TOptional<float> SimpleLocalHeight =
|
|
252
|
+
Collision->GetHeight(X, Y, EHeightfieldSource::Simple);
|
|
253
|
+
if (!SimpleLocalHeight.IsSet())
|
|
254
|
+
{
|
|
255
|
+
OutError = FString::Printf(
|
|
256
|
+
TEXT("%s has no readable live simple collision heightfield"),
|
|
257
|
+
*Collision->GetPathName());
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
const int32 SampleIndex = SampleInfo.NumSamples +
|
|
261
|
+
Y * SampleInfo.SimpleCollisionSizeVerts + X;
|
|
262
|
+
LiveWorldHeights[SampleIndex] = static_cast<float>(
|
|
263
|
+
WorldTransform.TransformPositionNoScale(
|
|
264
|
+
FVector(0.0, 0.0, SimpleLocalHeight.GetValue())).Z);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const uint16* RawHeightData = static_cast<const uint16*>(Collision->CollisionHeightData.LockReadOnly());
|
|
270
|
+
if (!RawHeightData)
|
|
271
|
+
{
|
|
272
|
+
Collision->CollisionHeightData.Unlock();
|
|
273
|
+
OutError = FString::Printf(
|
|
274
|
+
TEXT("%s raw collision height data could not be locked"), *Collision->GetPathName());
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
OutSignature.Collision = Collision;
|
|
279
|
+
OutSignature.ComponentPath = Collision->GetPathName();
|
|
280
|
+
const bool bBuilt = BuildHeightCollisionSignature(
|
|
281
|
+
MakeArrayView(RawHeightData, static_cast<int32>(RawElementCount)),
|
|
282
|
+
SampleInfo.NumSamples,
|
|
283
|
+
SampleInfo.CollisionSizeVerts,
|
|
284
|
+
SampleInfo.NumSimpleSamples,
|
|
285
|
+
SampleInfo.SimpleCollisionSizeVerts,
|
|
286
|
+
LiveWorldHeights,
|
|
287
|
+
Collision->GetComponentTransform(),
|
|
288
|
+
OutSignature,
|
|
289
|
+
OutError);
|
|
290
|
+
Collision->CollisionHeightData.Unlock();
|
|
291
|
+
return bBuilt;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
bool MatchesHeightCollisionSignature(
|
|
295
|
+
const FLandscapeHeightCollisionSignature& Baseline,
|
|
296
|
+
const FLandscapeHeightCollisionSignature& Current,
|
|
297
|
+
FString& OutError)
|
|
298
|
+
{
|
|
299
|
+
if (Baseline.RawElementCount != Current.RawElementCount || Baseline.RawHash != Current.RawHash)
|
|
300
|
+
{
|
|
301
|
+
OutError = FString::Printf(
|
|
302
|
+
TEXT("%s raw collision height data changed"), *Baseline.ComponentPath);
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
if (Baseline.ComplexSampleCount != Current.ComplexSampleCount ||
|
|
306
|
+
Baseline.CollisionSizeVerts != Current.CollisionSizeVerts ||
|
|
307
|
+
Baseline.SimpleSampleCount != Current.SimpleSampleCount ||
|
|
308
|
+
Baseline.SimpleCollisionSizeVerts != Current.SimpleCollisionSizeVerts ||
|
|
309
|
+
!Baseline.ComponentTransform.Equals(Current.ComponentTransform, 1.e-6f))
|
|
310
|
+
{
|
|
311
|
+
OutError = FString::Printf(
|
|
312
|
+
TEXT("%s collision dimensions or transform changed"), *Baseline.ComponentPath);
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
if (Baseline.LiveHash != Current.LiveHash ||
|
|
316
|
+
!FMath::IsNearlyEqual(Baseline.LiveMinWorldZ, Current.LiveMinWorldZ, HeightCollisionToleranceCm) ||
|
|
317
|
+
!FMath::IsNearlyEqual(Baseline.LiveMaxWorldZ, Current.LiveMaxWorldZ, HeightCollisionToleranceCm))
|
|
318
|
+
{
|
|
319
|
+
OutError = FString::Printf(
|
|
320
|
+
TEXT("%s live collision heightfield changed (baseline %.3f..%.3f cm, current %.3f..%.3f cm)"),
|
|
321
|
+
*Baseline.ComponentPath,
|
|
322
|
+
Baseline.LiveMinWorldZ, Baseline.LiveMaxWorldZ,
|
|
323
|
+
Current.LiveMinWorldZ, Current.LiveMaxWorldZ);
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
bool ValidateLandscapeCollisionPreflight(
|
|
330
|
+
int32 LandscapeComponents,
|
|
331
|
+
int32 CollisionComponents,
|
|
332
|
+
int32 CapturedHeightComponents,
|
|
333
|
+
int32 PendingLayerUpdateComponents,
|
|
334
|
+
bool bLandscapeLayersUpToDate,
|
|
335
|
+
bool bTextureResourcesReady,
|
|
336
|
+
FString& OutError)
|
|
337
|
+
{
|
|
338
|
+
if (CollisionComponents == 0)
|
|
339
|
+
{
|
|
340
|
+
OutError = TEXT("No registered landscape collision components were available to refresh");
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
if (CollisionComponents != LandscapeComponents ||
|
|
344
|
+
CapturedHeightComponents != CollisionComponents)
|
|
345
|
+
{
|
|
346
|
+
OutError = FString::Printf(
|
|
347
|
+
TEXT("Loaded proxy coverage is incomplete: %d of %d landscape component(s) have registered, captured collision"),
|
|
348
|
+
CapturedHeightComponents, LandscapeComponents);
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
if (!bLandscapeLayersUpToDate || PendingLayerUpdateComponents > 0)
|
|
352
|
+
{
|
|
353
|
+
OutError = TEXT("The parent landscape has pending edit-layer updates. A later PreSave could replace collision heights; let the landscape finish updating and validate it before retrying.");
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
if (!bTextureResourcesReady)
|
|
357
|
+
{
|
|
358
|
+
OutError = TEXT("The parent landscape could not make its texture resources resident");
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
class FScopedLandscapePhysicalMaterialRecreateMode
|
|
365
|
+
{
|
|
366
|
+
public:
|
|
367
|
+
FScopedLandscapePhysicalMaterialRecreateMode()
|
|
368
|
+
{
|
|
369
|
+
Variable = IConsoleManager::Get().FindConsoleVariable(
|
|
370
|
+
TEXT("landscape.ApplyPhysicalMaterialChangesImmediately"));
|
|
371
|
+
if (Variable)
|
|
372
|
+
{
|
|
373
|
+
Variable->Set(0, ECVF_SetByTemp, OverrideTag);
|
|
374
|
+
bEffective = Variable->GetInt() == 0;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
~FScopedLandscapePhysicalMaterialRecreateMode()
|
|
379
|
+
{
|
|
380
|
+
if (Variable)
|
|
381
|
+
{
|
|
382
|
+
Variable->Unset(ECVF_SetByTemp, OverrideTag);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
bool IsValid() const { return Variable != nullptr && bEffective; }
|
|
387
|
+
|
|
388
|
+
private:
|
|
389
|
+
static inline const FName OverrideTag = TEXT("UE_MCP_LandscapePhysicalMaterialCollision");
|
|
390
|
+
IConsoleVariable* Variable = nullptr;
|
|
391
|
+
bool bEffective = false;
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
#endif // UE_MCP_HAS_5_8_API
|
|
396
|
+
|
|
30
397
|
void FLandscapeHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
|
|
31
398
|
{
|
|
32
399
|
Registry.RegisterHandler(TEXT("get_landscape_info"), &GetLandscapeInfo);
|
|
@@ -42,6 +409,7 @@ void FLandscapeHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
|
|
|
42
409
|
// #733: World Partition landscape streaming-proxy enumeration + spatial lookup.
|
|
43
410
|
Registry.RegisterHandler(TEXT("list_landscape_proxies"), &ListLandscapeProxies);
|
|
44
411
|
Registry.RegisterHandler(TEXT("find_landscape_proxy_at"), &FindLandscapeProxyAt);
|
|
412
|
+
Registry.RegisterHandlerWithTimeout(TEXT("refresh_landscape_physical_material_collision"), &RefreshPhysicalMaterialCollision, 600.0f);
|
|
45
413
|
Registry.RegisterHandlerWithTimeout(TEXT("sculpt_landscape"), &Sculpt, 120.0f);
|
|
46
414
|
Registry.RegisterHandlerWithTimeout(TEXT("paint_landscape_layer"), &PaintLayer, 120.0f);
|
|
47
415
|
}
|
|
@@ -140,61 +508,295 @@ TSharedPtr<FJsonValue> FLandscapeHandlers::ListLandscapeLayers(const TSharedPtr<
|
|
|
140
508
|
return MCPResult(Result);
|
|
141
509
|
}
|
|
142
510
|
|
|
511
|
+
// landscape(sample): height AND paint-layer weights at a world XY (#939).
|
|
512
|
+
//
|
|
513
|
+
// The action advertised "Params: x, y" and the handler required an object
|
|
514
|
+
// called `point`, so BOTH documented call shapes failed with "Missing 'point'
|
|
515
|
+
// parameter" and there was no working call at all. It also never returned a
|
|
516
|
+
// single layer weight despite advertising "height/layers", which left reading a
|
|
517
|
+
// weight to rendering the weightmap into a render target and reading that back.
|
|
518
|
+
//
|
|
519
|
+
// Both halves are fixed here. The position is accepted in every shape the
|
|
520
|
+
// category already uses (x/y, point{x,y}, worldX/worldY, all named in the
|
|
521
|
+
// schema), and the weights come from the same FLandscapeEditDataInterface that
|
|
522
|
+
// landscape(paint_layer) writes through, so a sample taken after a paint reads
|
|
523
|
+
// back the number that was painted. No edit layer is selected, which is what
|
|
524
|
+
// makes the read the MERGED result the renderer and the physics use, rather
|
|
525
|
+
// than one layer's contribution to it.
|
|
143
526
|
TSharedPtr<FJsonValue> FLandscapeHandlers::SampleLandscape(const TSharedPtr<FJsonObject>& Params)
|
|
144
527
|
{
|
|
528
|
+
// Position, in every shape the schema names. `point` may carry a z, which is
|
|
529
|
+
// only ever used as the origin of the confirmation trace: the surface height
|
|
530
|
+
// is what this action answers, so it is never an input to itself.
|
|
531
|
+
double WorldX = 0.0;
|
|
532
|
+
double WorldY = 0.0;
|
|
533
|
+
double TraceOriginZ = 0.0;
|
|
534
|
+
bool bHavePosition = false;
|
|
535
|
+
|
|
145
536
|
const TSharedPtr<FJsonObject>* PointObj = nullptr;
|
|
146
|
-
if (
|
|
537
|
+
if (Params->TryGetObjectField(TEXT("point"), PointObj) && PointObj && PointObj->IsValid())
|
|
147
538
|
{
|
|
148
|
-
|
|
539
|
+
(*PointObj)->TryGetNumberField(TEXT("x"), WorldX);
|
|
540
|
+
(*PointObj)->TryGetNumberField(TEXT("y"), WorldY);
|
|
541
|
+
(*PointObj)->TryGetNumberField(TEXT("z"), TraceOriginZ);
|
|
542
|
+
bHavePosition = true;
|
|
543
|
+
}
|
|
544
|
+
else if (Params->HasField(TEXT("x")) && Params->HasField(TEXT("y")))
|
|
545
|
+
{
|
|
546
|
+
WorldX = OptionalNumber(Params, TEXT("x"), 0.0);
|
|
547
|
+
WorldY = OptionalNumber(Params, TEXT("y"), 0.0);
|
|
548
|
+
bHavePosition = true;
|
|
549
|
+
}
|
|
550
|
+
else if (Params->HasField(TEXT("worldX")) && Params->HasField(TEXT("worldY")))
|
|
551
|
+
{
|
|
552
|
+
// The names landscape(find_proxy_at) takes. A caller moving between the
|
|
553
|
+
// two actions should not have to rename the same two numbers.
|
|
554
|
+
WorldX = OptionalNumber(Params, TEXT("worldX"), 0.0);
|
|
555
|
+
WorldY = OptionalNumber(Params, TEXT("worldY"), 0.0);
|
|
556
|
+
bHavePosition = true;
|
|
557
|
+
}
|
|
558
|
+
if (!bHavePosition)
|
|
559
|
+
{
|
|
560
|
+
return MCPError(TEXT("Missing sample position. Pass x and y, or point {x, y}, or worldX and worldY (all world space)."));
|
|
149
561
|
}
|
|
150
|
-
|
|
151
|
-
FVector Point;
|
|
152
|
-
Point.X = (*PointObj)->GetNumberField(TEXT("x"));
|
|
153
|
-
Point.Y = (*PointObj)->GetNumberField(TEXT("y"));
|
|
154
|
-
Point.Z = (*PointObj)->GetNumberField(TEXT("z"));
|
|
155
562
|
|
|
156
563
|
REQUIRE_EDITOR_WORLD(World);
|
|
157
564
|
|
|
158
|
-
//
|
|
565
|
+
// Which landscape. A label picks one explicitly; without one, every proxy in
|
|
566
|
+
// the world contributes its ULandscapeInfo and the point decides between
|
|
567
|
+
// them, because a map with two landscapes has no single right default.
|
|
568
|
+
// #983: actorPath narrows to exactly one proxy when several share a label,
|
|
569
|
+
// which is what a World Partition map full of streaming proxies looks like.
|
|
570
|
+
const FString ActorLabel = OptionalString(Params, TEXT("actorLabel"));
|
|
571
|
+
const FString ActorPath = OptionalString(Params, TEXT("actorPath"));
|
|
572
|
+
// Normalised the same way MCPFindActorByPath normalises, so the export-text
|
|
573
|
+
// form (Actor'/Game/...') resolves here exactly as it does on every other
|
|
574
|
+
// action rather than silently matching nothing (#983).
|
|
575
|
+
const FString WantedPath = ActorPath.IsEmpty()
|
|
576
|
+
? FString()
|
|
577
|
+
: FPackageName::ExportTextPathToObjectPath(ActorPath).TrimStartAndEnd();
|
|
578
|
+
TArray<ULandscapeInfo*> Candidates;
|
|
159
579
|
for (TActorIterator<ALandscapeProxy> It(World); It; ++It)
|
|
160
580
|
{
|
|
161
|
-
ALandscapeProxy*
|
|
162
|
-
if (!
|
|
581
|
+
ALandscapeProxy* Proxy = *It;
|
|
582
|
+
if (!Proxy) continue;
|
|
583
|
+
if (!WantedPath.IsEmpty() && !Proxy->GetPathName().Equals(WantedPath, ESearchCase::IgnoreCase)) continue;
|
|
584
|
+
if (!ActorLabel.IsEmpty() && !Proxy->GetActorLabel().Equals(ActorLabel, ESearchCase::IgnoreCase)) continue;
|
|
585
|
+
ULandscapeInfo* ProxyInfo = Proxy->GetLandscapeInfo();
|
|
586
|
+
if (!ProxyInfo) continue;
|
|
587
|
+
if (Candidates.Contains(ProxyInfo)) continue;
|
|
588
|
+
Candidates.Add(ProxyInfo);
|
|
589
|
+
}
|
|
590
|
+
if (Candidates.Num() == 0)
|
|
591
|
+
{
|
|
592
|
+
if (ActorLabel.IsEmpty() && ActorPath.IsEmpty())
|
|
593
|
+
{
|
|
594
|
+
return MCPError(TEXT("No landscape in the current level. Create one with landscape(create)."));
|
|
595
|
+
}
|
|
596
|
+
return MCPError(FString::Printf(
|
|
597
|
+
TEXT("No landscape actor matched '%s' (nothing with a registered LandscapeInfo matched)"),
|
|
598
|
+
ActorPath.IsEmpty() ? *ActorLabel : *ActorPath));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Pick the landscape whose quad extent actually covers the point. With one
|
|
602
|
+
// landscape this is the only candidate and the extent test still reports
|
|
603
|
+
// whether the point is on it, which is the difference between "weight is
|
|
604
|
+
// zero here" and "this position is not on the landscape at all".
|
|
605
|
+
ULandscapeInfo* Info = nullptr;
|
|
606
|
+
FTransform LandscapeToWorld;
|
|
607
|
+
FVector LocalPoint = FVector::ZeroVector;
|
|
608
|
+
FIntRect Extent(0, 0, 0, 0);
|
|
609
|
+
for (ULandscapeInfo* Candidate : Candidates)
|
|
610
|
+
{
|
|
611
|
+
ALandscapeProxy* Reference = Candidate->GetLandscapeProxy();
|
|
612
|
+
if (!Reference) continue;
|
|
613
|
+
FIntRect CandidateExtent;
|
|
614
|
+
if (!Candidate->GetLandscapeExtent(CandidateExtent)) continue;
|
|
615
|
+
|
|
616
|
+
const FTransform CandidateTransform = Reference->ActorToWorld();
|
|
617
|
+
const FVector CandidateLocal =
|
|
618
|
+
CandidateTransform.InverseTransformPosition(FVector(WorldX, WorldY, 0.0));
|
|
619
|
+
// RoundToInt32, not RoundToInt: the double overload of the latter
|
|
620
|
+
// returns int64 and would narrow on the way into these.
|
|
621
|
+
const int32 QuadX = FMath::RoundToInt32(CandidateLocal.X);
|
|
622
|
+
const int32 QuadY = FMath::RoundToInt32(CandidateLocal.Y);
|
|
623
|
+
const bool bCovers =
|
|
624
|
+
QuadX >= CandidateExtent.Min.X && QuadX <= CandidateExtent.Max.X &&
|
|
625
|
+
QuadY >= CandidateExtent.Min.Y && QuadY <= CandidateExtent.Max.Y;
|
|
626
|
+
|
|
627
|
+
// Remember the first candidate either way, so a point off every
|
|
628
|
+
// landscape still reports which one it was measured against.
|
|
629
|
+
if (!Info || bCovers)
|
|
630
|
+
{
|
|
631
|
+
Info = Candidate;
|
|
632
|
+
LandscapeToWorld = CandidateTransform;
|
|
633
|
+
LocalPoint = CandidateLocal;
|
|
634
|
+
Extent = CandidateExtent;
|
|
635
|
+
}
|
|
636
|
+
if (bCovers) break;
|
|
637
|
+
}
|
|
638
|
+
if (!Info)
|
|
639
|
+
{
|
|
640
|
+
return MCPError(TEXT("Landscape found but its LandscapeInfo has no registered extent yet (are its components loaded?)"));
|
|
641
|
+
}
|
|
163
642
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
643
|
+
ALandscapeProxy* ReferenceProxy = Info->GetLandscapeProxy();
|
|
644
|
+
const int32 QuadX = FMath::RoundToInt32(LocalPoint.X);
|
|
645
|
+
const int32 QuadY = FMath::RoundToInt32(LocalPoint.Y);
|
|
646
|
+
const bool bInBounds =
|
|
647
|
+
QuadX >= Extent.Min.X && QuadX <= Extent.Max.X &&
|
|
648
|
+
QuadY >= Extent.Min.Y && QuadY <= Extent.Max.Y;
|
|
167
649
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
650
|
+
auto Result = MCPSuccess();
|
|
651
|
+
Result->SetStringField(TEXT("landscape"), ReferenceProxy ? ReferenceProxy->GetActorLabel() : FString());
|
|
652
|
+
TSharedPtr<FJsonObject> QuadObj = MakeShared<FJsonObject>();
|
|
653
|
+
QuadObj->SetNumberField(TEXT("x"), QuadX);
|
|
654
|
+
QuadObj->SetNumberField(TEXT("y"), QuadY);
|
|
655
|
+
Result->SetObjectField(TEXT("quad"), QuadObj);
|
|
656
|
+
Result->SetBoolField(TEXT("inBounds"), bInBounds);
|
|
657
|
+
TSharedPtr<FJsonObject> ExtentObj = MakeShared<FJsonObject>();
|
|
658
|
+
ExtentObj->SetNumberField(TEXT("minX"), Extent.Min.X);
|
|
659
|
+
ExtentObj->SetNumberField(TEXT("minY"), Extent.Min.Y);
|
|
660
|
+
ExtentObj->SetNumberField(TEXT("maxX"), Extent.Max.X);
|
|
661
|
+
ExtentObj->SetNumberField(TEXT("maxY"), Extent.Max.Y);
|
|
662
|
+
Result->SetObjectField(TEXT("quadExtent"), ExtentObj);
|
|
663
|
+
|
|
664
|
+
if (!bInBounds)
|
|
665
|
+
{
|
|
666
|
+
Result->SetBoolField(TEXT("hit"), false);
|
|
667
|
+
Result->SetStringField(TEXT("reason"),
|
|
668
|
+
TEXT("The world position is outside this landscape's loaded quad extent, so height and weights would both read as zero rather than as measurements. On a World Partition map check landscape(find_proxy_at) first."));
|
|
669
|
+
return MCPResult(Result);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// One edit-data interface for the height and every layer, with no edit layer
|
|
673
|
+
// selected so the read is the merged result rather than one layer's own
|
|
674
|
+
// contribution to it.
|
|
675
|
+
FLandscapeEditDataInterface EditData(Info);
|
|
171
676
|
|
|
172
|
-
|
|
677
|
+
uint16 RawHeight = 0;
|
|
678
|
+
{
|
|
679
|
+
int32 X1 = QuadX, Y1 = QuadY, X2 = QuadX, Y2 = QuadY;
|
|
680
|
+
// GetHeightData rewrites the rect with the range it could actually
|
|
681
|
+
// serve, so an unloaded component comes back as an inverted rect rather
|
|
682
|
+
// than as a zero sample that reads like real data.
|
|
683
|
+
EditData.GetHeightData(X1, Y1, X2, Y2, &RawHeight, 0);
|
|
684
|
+
if (X2 < X1 || Y2 < Y1)
|
|
173
685
|
{
|
|
174
|
-
|
|
686
|
+
Result->SetBoolField(TEXT("hit"), false);
|
|
687
|
+
Result->SetStringField(TEXT("reason"),
|
|
688
|
+
TEXT("No landscape height data at this position (the covering component is not loaded). Pin it with level(load_actor_descs) first."));
|
|
689
|
+
return MCPResult(Result);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const double LocalHeight = LandscapeDataAccess::GetLocalHeight(RawHeight);
|
|
694
|
+
const FVector SurfaceWorld = LandscapeToWorld.TransformPosition(
|
|
695
|
+
FVector(static_cast<double>(QuadX), static_cast<double>(QuadY), static_cast<double>(LocalHeight)));
|
|
696
|
+
|
|
697
|
+
Result->SetBoolField(TEXT("hit"), true);
|
|
698
|
+
Result->SetNumberField(TEXT("height"), SurfaceWorld.Z);
|
|
699
|
+
Result->SetNumberField(TEXT("rawHeight"), RawHeight);
|
|
700
|
+
TSharedPtr<FJsonObject> LocationObj = MakeShared<FJsonObject>();
|
|
701
|
+
LocationObj->SetNumberField(TEXT("x"), SurfaceWorld.X);
|
|
702
|
+
LocationObj->SetNumberField(TEXT("y"), SurfaceWorld.Y);
|
|
703
|
+
LocationObj->SetNumberField(TEXT("z"), SurfaceWorld.Z);
|
|
704
|
+
Result->SetObjectField(TEXT("location"), LocationObj);
|
|
705
|
+
|
|
706
|
+
// Paint-layer weights, the half that had no read at all. Each layer is
|
|
707
|
+
// reported both as the raw 0..255 weightmap byte and as the 0..1 fraction,
|
|
708
|
+
// because the editor shows one and a material reads the other.
|
|
709
|
+
if (OptionalBool(Params, TEXT("includeLayers"), true))
|
|
710
|
+
{
|
|
711
|
+
const FString LayerFilter = OptionalString(Params, TEXT("layerName"));
|
|
712
|
+
TArray<TSharedPtr<FJsonValue>> LayerArray;
|
|
713
|
+
double TotalWeight = 0.0;
|
|
714
|
+
FString DominantLayer;
|
|
715
|
+
double DominantWeight = -1.0;
|
|
716
|
+
TArray<FString> KnownLayers;
|
|
717
|
+
|
|
718
|
+
for (const FLandscapeInfoLayerSettings& LayerSettings : Info->Layers)
|
|
719
|
+
{
|
|
720
|
+
ULandscapeLayerInfoObject* LayerInfo = LayerSettings.LayerInfoObj;
|
|
721
|
+
if (!LayerInfo) continue;
|
|
722
|
+
const FString LayerName = LayerSettings.GetLayerName().ToString();
|
|
723
|
+
KnownLayers.Add(LayerName);
|
|
724
|
+
if (!LayerFilter.IsEmpty() && !LayerName.Equals(LayerFilter, ESearchCase::IgnoreCase)) continue;
|
|
725
|
+
|
|
726
|
+
uint8 RawWeight = 0;
|
|
727
|
+
int32 X1 = QuadX, Y1 = QuadY, X2 = QuadX, Y2 = QuadY;
|
|
728
|
+
EditData.GetWeightData(LayerInfo, X1, Y1, X2, Y2, &RawWeight, 0);
|
|
729
|
+
if (X2 < X1 || Y2 < Y1) continue;
|
|
730
|
+
|
|
731
|
+
const double Weight = static_cast<double>(RawWeight) / 255.0;
|
|
732
|
+
TotalWeight += Weight;
|
|
733
|
+
if (Weight > DominantWeight)
|
|
734
|
+
{
|
|
735
|
+
DominantWeight = Weight;
|
|
736
|
+
DominantLayer = LayerName;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
TSharedPtr<FJsonObject> LayerObj = MakeShared<FJsonObject>();
|
|
740
|
+
LayerObj->SetStringField(TEXT("name"), LayerName);
|
|
741
|
+
LayerObj->SetNumberField(TEXT("weight"), Weight);
|
|
742
|
+
LayerObj->SetNumberField(TEXT("weight255"), RawWeight);
|
|
743
|
+
LayerObj->SetStringField(TEXT("layerInfo"), LayerInfo->GetPathName());
|
|
744
|
+
if (UPhysicalMaterial* PhysMat = LayerInfo->GetPhysicalMaterial())
|
|
175
745
|
{
|
|
176
|
-
|
|
177
|
-
Result->SetNumberField(TEXT("height"), HitResult.Location.Z);
|
|
178
|
-
TSharedPtr<FJsonObject> HitPoint = MakeShared<FJsonObject>();
|
|
179
|
-
HitPoint->SetNumberField(TEXT("x"), HitResult.Location.X);
|
|
180
|
-
HitPoint->SetNumberField(TEXT("y"), HitResult.Location.Y);
|
|
181
|
-
HitPoint->SetNumberField(TEXT("z"), HitResult.Location.Z);
|
|
182
|
-
Result->SetObjectField(TEXT("hitLocation"), HitPoint);
|
|
183
|
-
|
|
184
|
-
TSharedPtr<FJsonObject> Normal = MakeShared<FJsonObject>();
|
|
185
|
-
Normal->SetNumberField(TEXT("x"), HitResult.Normal.X);
|
|
186
|
-
Normal->SetNumberField(TEXT("y"), HitResult.Normal.Y);
|
|
187
|
-
Normal->SetNumberField(TEXT("z"), HitResult.Normal.Z);
|
|
188
|
-
Result->SetObjectField(TEXT("normal"), Normal);
|
|
189
|
-
|
|
190
|
-
Result->SetBoolField(TEXT("hit"), true);
|
|
191
|
-
return MCPResult(Result);
|
|
746
|
+
LayerObj->SetStringField(TEXT("physicalMaterial"), PhysMat->GetPathName());
|
|
192
747
|
}
|
|
748
|
+
LayerArray.Add(MakeShared<FJsonValueObject>(LayerObj));
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
Result->SetArrayField(TEXT("layers"), LayerArray);
|
|
752
|
+
Result->SetNumberField(TEXT("layerCount"), LayerArray.Num());
|
|
753
|
+
Result->SetNumberField(TEXT("totalWeight"), TotalWeight);
|
|
754
|
+
if (!DominantLayer.IsEmpty())
|
|
755
|
+
{
|
|
756
|
+
Result->SetStringField(TEXT("dominantLayer"), DominantLayer);
|
|
757
|
+
Result->SetNumberField(TEXT("dominantWeight"), DominantWeight);
|
|
758
|
+
}
|
|
759
|
+
if (!LayerFilter.IsEmpty() && LayerArray.Num() == 0)
|
|
760
|
+
{
|
|
761
|
+
Result->SetStringField(TEXT("layerNote"), FString::Printf(
|
|
762
|
+
TEXT("No layer named '%s' is registered on this landscape. Registered layers: [%s]. Add one with landscape(add_layer_info)."),
|
|
763
|
+
*LayerFilter, *FString::Join(KnownLayers, TEXT(", "))));
|
|
193
764
|
}
|
|
194
765
|
}
|
|
195
766
|
|
|
196
|
-
|
|
197
|
-
|
|
767
|
+
// A confirmation trace against the collision heightfield, kept because it is
|
|
768
|
+
// the only source of a surface normal and because a disagreement between it
|
|
769
|
+
// and the heightmap is itself the answer to "is my collision stale".
|
|
770
|
+
{
|
|
771
|
+
const double TraceZ = FMath::Max(TraceOriginZ, SurfaceWorld.Z);
|
|
772
|
+
const FVector TraceStart(SurfaceWorld.X, SurfaceWorld.Y, TraceZ + 100000.0);
|
|
773
|
+
const FVector TraceEnd(SurfaceWorld.X, SurfaceWorld.Y, TraceZ - 200000.0);
|
|
774
|
+
FHitResult HitResult;
|
|
775
|
+
FCollisionQueryParams QueryParams(SCENE_QUERY_STAT(MCPLandscapeSample), /*bTraceComplex*/ true);
|
|
776
|
+
const bool bTraceHit =
|
|
777
|
+
World->LineTraceSingleByChannel(HitResult, TraceStart, TraceEnd, ECC_WorldStatic, QueryParams) &&
|
|
778
|
+
HitResult.GetActor() != nullptr &&
|
|
779
|
+
HitResult.GetActor()->IsA(ALandscapeProxy::StaticClass());
|
|
780
|
+
Result->SetBoolField(TEXT("traceHit"), bTraceHit);
|
|
781
|
+
if (bTraceHit)
|
|
782
|
+
{
|
|
783
|
+
TSharedPtr<FJsonObject> HitPoint = MakeShared<FJsonObject>();
|
|
784
|
+
HitPoint->SetNumberField(TEXT("x"), HitResult.Location.X);
|
|
785
|
+
HitPoint->SetNumberField(TEXT("y"), HitResult.Location.Y);
|
|
786
|
+
HitPoint->SetNumberField(TEXT("z"), HitResult.Location.Z);
|
|
787
|
+
Result->SetObjectField(TEXT("hitLocation"), HitPoint);
|
|
788
|
+
Result->SetNumberField(TEXT("traceHeight"), HitResult.Location.Z);
|
|
789
|
+
|
|
790
|
+
TSharedPtr<FJsonObject> Normal = MakeShared<FJsonObject>();
|
|
791
|
+
Normal->SetNumberField(TEXT("x"), HitResult.ImpactNormal.X);
|
|
792
|
+
Normal->SetNumberField(TEXT("y"), HitResult.ImpactNormal.Y);
|
|
793
|
+
Normal->SetNumberField(TEXT("z"), HitResult.ImpactNormal.Z);
|
|
794
|
+
Result->SetObjectField(TEXT("normal"), Normal);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
Result->SetStringField(TEXT("note"),
|
|
799
|
+
TEXT("height and layer weights come from the landscape's own height and weight data (the merged result of every edit layer), so they are exact and do not depend on collision. traceHeight and normal come from a collision trace and are absent when collision is unbuilt or the proxy is streamed out."));
|
|
198
800
|
return MCPResult(Result);
|
|
199
801
|
}
|
|
200
802
|
|
|
@@ -876,3 +1478,653 @@ TSharedPtr<FJsonValue> FLandscapeHandlers::FindLandscapeProxyAt(const TSharedPtr
|
|
|
876
1478
|
Result->SetStringField(TEXT("note"), TEXT("No loaded proxy covers this position; the covering proxy is likely streamed out, so weight/height readbacks here are ambiguous."));
|
|
877
1479
|
return MCPResult(Result);
|
|
878
1480
|
}
|
|
1481
|
+
|
|
1482
|
+
// Refresh physical-material collision data after a LayerInfo PhysMaterial edit.
|
|
1483
|
+
// LayerInfo PostEditChange requests a full edit-layer update as well as an
|
|
1484
|
+
// immediate collision recreation. This batch path deliberately separates those
|
|
1485
|
+
// operations: it refuses pending edit-layer work, updates layer/material data,
|
|
1486
|
+
// waits for the material build, and recreates collision exactly once. Complete
|
|
1487
|
+
// raw, complex-live, and simple-live heightfield signatures guard the mutation.
|
|
1488
|
+
// Package saving is deliberately outside this action. Only loaded streaming
|
|
1489
|
+
// proxies can be acted on; unloaded World Partition actors do not exist here.
|
|
1490
|
+
TSharedPtr<FJsonValue> FLandscapeHandlers::RefreshPhysicalMaterialCollision(const TSharedPtr<FJsonObject>& Params)
|
|
1491
|
+
{
|
|
1492
|
+
#if !UE_MCP_HAS_5_8_API
|
|
1493
|
+
return MCPError(TEXT("Landscape physical-material collision refresh requires Unreal Engine 5.8 or newer"));
|
|
1494
|
+
#else
|
|
1495
|
+
const double StartedAt = FPlatformTime::Seconds();
|
|
1496
|
+
if (!GEditor)
|
|
1497
|
+
{
|
|
1498
|
+
return MCPError(TEXT("Editor not available"));
|
|
1499
|
+
}
|
|
1500
|
+
if (GEditor->PlayWorld != nullptr || GEditor->bIsSimulatingInEditor)
|
|
1501
|
+
{
|
|
1502
|
+
return MCPError(TEXT("Stop PIE or SIE before refreshing landscape physical-material collision"));
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
REQUIRE_EDITOR_WORLD(World);
|
|
1506
|
+
if (World->WorldType != EWorldType::Editor)
|
|
1507
|
+
{
|
|
1508
|
+
return MCPError(TEXT("Physical-material collision refresh requires the current editor world"));
|
|
1509
|
+
}
|
|
1510
|
+
if (!World->IsPartitionedWorld())
|
|
1511
|
+
{
|
|
1512
|
+
return MCPError(TEXT("The current editor world is not a World Partition map"));
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
const int32 MaxActors = OptionalInt(Params, TEXT("maxActors"), 256);
|
|
1516
|
+
if (MaxActors < 1 || MaxActors > 1024)
|
|
1517
|
+
{
|
|
1518
|
+
return MCPError(TEXT("'maxActors' must be between 1 and 1024"));
|
|
1519
|
+
}
|
|
1520
|
+
if (OptionalBool(Params, TEXT("save"), false))
|
|
1521
|
+
{
|
|
1522
|
+
return MCPError(TEXT("save_not_supported: this action is in-memory only. Unreal Landscape PreSave can mutate pending edit-layer collision data, so persistence requires a separate validated workflow."));
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
TSet<FString> WantedLabels;
|
|
1526
|
+
const bool bHasLabelFilter = Params->HasField(TEXT("actorLabels"));
|
|
1527
|
+
if (bHasLabelFilter)
|
|
1528
|
+
{
|
|
1529
|
+
const TArray<TSharedPtr<FJsonValue>>* Values = nullptr;
|
|
1530
|
+
if (!Params->TryGetArrayField(TEXT("actorLabels"), Values) || !Values || Values->IsEmpty() || Values->Num() > 256)
|
|
1531
|
+
{
|
|
1532
|
+
return MCPError(TEXT("'actorLabels' must be a non-empty array of at most 256 strings"));
|
|
1533
|
+
}
|
|
1534
|
+
for (const TSharedPtr<FJsonValue>& Value : *Values)
|
|
1535
|
+
{
|
|
1536
|
+
FString Label;
|
|
1537
|
+
if (!Value.IsValid() || !Value->TryGetString(Label) || Label.IsEmpty())
|
|
1538
|
+
{
|
|
1539
|
+
return MCPError(TEXT("Every 'actorLabels' entry must be a non-empty string"));
|
|
1540
|
+
}
|
|
1541
|
+
WantedLabels.Add(Label.ToLower());
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
TSet<FGuid> WantedGuids;
|
|
1546
|
+
const bool bHasGuidFilter = Params->HasField(TEXT("guids"));
|
|
1547
|
+
if (bHasGuidFilter)
|
|
1548
|
+
{
|
|
1549
|
+
const TArray<TSharedPtr<FJsonValue>>* Values = nullptr;
|
|
1550
|
+
if (!Params->TryGetArrayField(TEXT("guids"), Values) || !Values || Values->IsEmpty() || Values->Num() > 256)
|
|
1551
|
+
{
|
|
1552
|
+
return MCPError(TEXT("'guids' must be a non-empty array of at most 256 GUID strings"));
|
|
1553
|
+
}
|
|
1554
|
+
for (const TSharedPtr<FJsonValue>& Value : *Values)
|
|
1555
|
+
{
|
|
1556
|
+
FString GuidText;
|
|
1557
|
+
FGuid Guid;
|
|
1558
|
+
if (!Value.IsValid() || !Value->TryGetString(GuidText) || !FGuid::Parse(GuidText, Guid))
|
|
1559
|
+
{
|
|
1560
|
+
return MCPError(FString::Printf(TEXT("Invalid actor GUID: '%s'"), *GuidText));
|
|
1561
|
+
}
|
|
1562
|
+
WantedGuids.Add(Guid);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
FBox FilterBounds(ForceInit);
|
|
1567
|
+
const bool bHasBoundsFilter = Params->HasField(TEXT("bounds"));
|
|
1568
|
+
if (bHasBoundsFilter)
|
|
1569
|
+
{
|
|
1570
|
+
const TSharedPtr<FJsonObject>* BoundsObject = nullptr;
|
|
1571
|
+
const TSharedPtr<FJsonObject>* MinObject = nullptr;
|
|
1572
|
+
const TSharedPtr<FJsonObject>* MaxObject = nullptr;
|
|
1573
|
+
if (!Params->TryGetObjectField(TEXT("bounds"), BoundsObject) || !BoundsObject ||
|
|
1574
|
+
!(*BoundsObject)->TryGetObjectField(TEXT("min"), MinObject) || !MinObject ||
|
|
1575
|
+
!(*BoundsObject)->TryGetObjectField(TEXT("max"), MaxObject) || !MaxObject)
|
|
1576
|
+
{
|
|
1577
|
+
return MCPError(TEXT("'bounds' must be {min:{x,y,z}, max:{x,y,z}}"));
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
auto ReadVector = [](const TSharedPtr<FJsonObject>& Object, FVector& Out) -> bool
|
|
1581
|
+
{
|
|
1582
|
+
double X = 0.0, Y = 0.0, Z = 0.0;
|
|
1583
|
+
if (!Object->TryGetNumberField(TEXT("x"), X) ||
|
|
1584
|
+
!Object->TryGetNumberField(TEXT("y"), Y) ||
|
|
1585
|
+
!Object->TryGetNumberField(TEXT("z"), Z) ||
|
|
1586
|
+
!FMath::IsFinite(X) || !FMath::IsFinite(Y) || !FMath::IsFinite(Z))
|
|
1587
|
+
{
|
|
1588
|
+
return false;
|
|
1589
|
+
}
|
|
1590
|
+
Out = FVector(X, Y, Z);
|
|
1591
|
+
return true;
|
|
1592
|
+
};
|
|
1593
|
+
|
|
1594
|
+
FVector Min, Max;
|
|
1595
|
+
if (!ReadVector(*MinObject, Min) || !ReadVector(*MaxObject, Max))
|
|
1596
|
+
{
|
|
1597
|
+
return MCPError(TEXT("Every bounds min/max coordinate must be a finite number"));
|
|
1598
|
+
}
|
|
1599
|
+
FilterBounds = FBox(FVector::Min(Min, Max), FVector::Max(Min, Max));
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
TArray<ALandscapeStreamingProxy*> LoadedProxies;
|
|
1603
|
+
TArray<ALandscapeStreamingProxy*> Matches;
|
|
1604
|
+
for (TActorIterator<ALandscapeStreamingProxy> It(World); It; ++It)
|
|
1605
|
+
{
|
|
1606
|
+
ALandscapeStreamingProxy* Proxy = *It;
|
|
1607
|
+
if (!Proxy || Proxy->GetWorld() != World) continue;
|
|
1608
|
+
LoadedProxies.Add(Proxy);
|
|
1609
|
+
|
|
1610
|
+
if (bHasLabelFilter && !WantedLabels.Contains(Proxy->GetActorLabel().ToLower())) continue;
|
|
1611
|
+
if (bHasGuidFilter && !WantedGuids.Contains(Proxy->GetActorGuid())) continue;
|
|
1612
|
+
if (bHasBoundsFilter)
|
|
1613
|
+
{
|
|
1614
|
+
FVector Origin, Extent;
|
|
1615
|
+
Proxy->GetActorBounds(false, Origin, Extent);
|
|
1616
|
+
if (!FBox::BuildAABB(Origin, Extent).Intersect(FilterBounds)) continue;
|
|
1617
|
+
}
|
|
1618
|
+
Matches.Add(Proxy);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
if (Matches.Num() > MaxActors)
|
|
1622
|
+
{
|
|
1623
|
+
return MCPError(FString::Printf(
|
|
1624
|
+
TEXT("%d loaded LandscapeStreamingProxy actors matched, above the maxActors limit of %d. Narrow actorLabels/guids/bounds or raise maxActors deliberately."),
|
|
1625
|
+
Matches.Num(), MaxActors));
|
|
1626
|
+
}
|
|
1627
|
+
for (ALandscapeStreamingProxy* Proxy : Matches)
|
|
1628
|
+
{
|
|
1629
|
+
if (UPackage* Package = Proxy ? Proxy->GetPackage() : nullptr)
|
|
1630
|
+
{
|
|
1631
|
+
// A later external save would fully load the package first. Establish the
|
|
1632
|
+
// same object residency before any safety baseline or layer-state test.
|
|
1633
|
+
Package->FullyLoad();
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
struct FRefreshEntry
|
|
1638
|
+
{
|
|
1639
|
+
TSharedPtr<FJsonObject> Json;
|
|
1640
|
+
ALandscapeStreamingProxy* Proxy = nullptr;
|
|
1641
|
+
ALandscape* ParentLandscape = nullptr;
|
|
1642
|
+
FString PackagePath;
|
|
1643
|
+
FString Error;
|
|
1644
|
+
TArray<FLandscapeHeightCollisionSignature> HeightSignatures;
|
|
1645
|
+
int32 LandscapeComponents = 0;
|
|
1646
|
+
int32 CollisionComponents = 0;
|
|
1647
|
+
int32 IneligibleCollisionComponents = 0;
|
|
1648
|
+
int32 ComponentsWithPendingLayerUpdates = 0;
|
|
1649
|
+
int32 OutdatedBefore = 0;
|
|
1650
|
+
int32 OutdatedAfter = 0;
|
|
1651
|
+
bool bTextureResourcesReady = false;
|
|
1652
|
+
bool bLandscapeLayersUpToDate = false;
|
|
1653
|
+
bool bHeightCollisionVerified = false;
|
|
1654
|
+
bool bRefreshed = false;
|
|
1655
|
+
};
|
|
1656
|
+
|
|
1657
|
+
TArray<FRefreshEntry> Entries;
|
|
1658
|
+
TArray<FString> PackagePaths;
|
|
1659
|
+
TMap<ALandscape*, bool> TextureResourcesReady;
|
|
1660
|
+
TMap<ALandscape*, bool> LandscapeLayersUpToDate;
|
|
1661
|
+
for (ALandscapeStreamingProxy* Proxy : Matches)
|
|
1662
|
+
{
|
|
1663
|
+
ALandscape* ParentLandscape = Proxy ? Proxy->GetLandscapeActor() : nullptr;
|
|
1664
|
+
if (ParentLandscape && !LandscapeLayersUpToDate.Contains(ParentLandscape))
|
|
1665
|
+
{
|
|
1666
|
+
// A LayerInfo PostEditChange requests Update_All. PreSave forcibly
|
|
1667
|
+
// flushes that work; on a stale edit-layer stack this can replace valid
|
|
1668
|
+
// collision heights while the package is already being serialized.
|
|
1669
|
+
// Refuse that state instead of discovering it after overwriting the OFPA
|
|
1670
|
+
// package.
|
|
1671
|
+
LandscapeLayersUpToDate.Add(ParentLandscape, ParentLandscape->IsUpToDate());
|
|
1672
|
+
}
|
|
1673
|
+
if (ParentLandscape && LandscapeLayersUpToDate.FindRef(ParentLandscape) &&
|
|
1674
|
+
!TextureResourcesReady.Contains(ParentLandscape))
|
|
1675
|
+
{
|
|
1676
|
+
// BuildPhysicalMaterial depends on resident weightmaps. Do this once per
|
|
1677
|
+
// parent landscape and wait, instead of letting each proxy make a
|
|
1678
|
+
// best-effort non-blocking request in the same frame.
|
|
1679
|
+
TextureResourcesReady.Add(ParentLandscape, ParentLandscape->PrepareTextureResources(true));
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
for (TPair<ALandscape*, bool>& Pair : LandscapeLayersUpToDate)
|
|
1683
|
+
{
|
|
1684
|
+
// Texture preparation is allowed to service queued landscape work. Capture
|
|
1685
|
+
// the state that actually exists immediately before the height baseline.
|
|
1686
|
+
Pair.Value = Pair.Key && Pair.Key->IsUpToDate();
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
int32 Refreshed = 0;
|
|
1690
|
+
int32 CollisionComponentsRefreshed = 0;
|
|
1691
|
+
int32 OutdatedBefore = 0;
|
|
1692
|
+
|
|
1693
|
+
for (ALandscapeStreamingProxy* Proxy : Matches)
|
|
1694
|
+
{
|
|
1695
|
+
FRefreshEntry& Entry = Entries.AddDefaulted_GetRef();
|
|
1696
|
+
Entry.Proxy = Proxy;
|
|
1697
|
+
Entry.ParentLandscape = Proxy->GetLandscapeActor();
|
|
1698
|
+
Entry.bLandscapeLayersUpToDate = Entry.ParentLandscape && LandscapeLayersUpToDate.FindRef(Entry.ParentLandscape);
|
|
1699
|
+
Entry.bTextureResourcesReady = Entry.ParentLandscape && TextureResourcesReady.FindRef(Entry.ParentLandscape);
|
|
1700
|
+
Entry.Json = MakeShared<FJsonObject>();
|
|
1701
|
+
Entry.Json->SetStringField(TEXT("label"), Proxy->GetActorLabel());
|
|
1702
|
+
Entry.Json->SetStringField(TEXT("guid"), Proxy->GetActorGuid().ToString());
|
|
1703
|
+
Entry.Json->SetBoolField(TEXT("landscapeLayersUpToDate"), Entry.bLandscapeLayersUpToDate);
|
|
1704
|
+
Entry.Json->SetBoolField(TEXT("textureResourcesReady"), Entry.bTextureResourcesReady);
|
|
1705
|
+
|
|
1706
|
+
if (UPackage* Package = Proxy->GetPackage())
|
|
1707
|
+
{
|
|
1708
|
+
Entry.PackagePath = Package->GetName();
|
|
1709
|
+
Entry.Json->SetStringField(TEXT("package"), Entry.PackagePath);
|
|
1710
|
+
PackagePaths.AddUnique(Entry.PackagePath);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
TArray<ULandscapeComponent*> Components;
|
|
1714
|
+
Proxy->GetComponents<ULandscapeComponent>(Components);
|
|
1715
|
+
for (ULandscapeComponent* Component : Components)
|
|
1716
|
+
{
|
|
1717
|
+
if (!Component) continue;
|
|
1718
|
+
++Entry.LandscapeComponents;
|
|
1719
|
+
if (Component && Component->GetLayerUpdateFlagPerMode() != 0)
|
|
1720
|
+
{
|
|
1721
|
+
++Entry.ComponentsWithPendingLayerUpdates;
|
|
1722
|
+
}
|
|
1723
|
+
if (Component && Component->IsRegistered() && Component->GetCollisionComponent())
|
|
1724
|
+
{
|
|
1725
|
+
++Entry.CollisionComponents;
|
|
1726
|
+
FLandscapeHeightCollisionSignature Signature;
|
|
1727
|
+
FString SignatureError;
|
|
1728
|
+
if (CaptureHeightCollisionSignature(Component->GetCollisionComponent(), Signature, SignatureError))
|
|
1729
|
+
{
|
|
1730
|
+
Entry.HeightSignatures.Add(MoveTemp(Signature));
|
|
1731
|
+
}
|
|
1732
|
+
else if (Entry.Error.IsEmpty())
|
|
1733
|
+
{
|
|
1734
|
+
Entry.Error = FString::Printf(TEXT("Height collision preflight failed: %s"), *SignatureError);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
else
|
|
1738
|
+
{
|
|
1739
|
+
++Entry.IneligibleCollisionComponents;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
Entry.Json->SetNumberField(TEXT("landscapeComponents"), Entry.LandscapeComponents);
|
|
1743
|
+
Entry.Json->SetNumberField(TEXT("collisionComponents"), Entry.CollisionComponents);
|
|
1744
|
+
Entry.Json->SetNumberField(TEXT("ineligibleCollisionComponents"), Entry.IneligibleCollisionComponents);
|
|
1745
|
+
Entry.Json->SetNumberField(TEXT("componentsWithPendingLayerUpdates"), Entry.ComponentsWithPendingLayerUpdates);
|
|
1746
|
+
Entry.Json->SetNumberField(TEXT("heightCollisionComponentsCaptured"), Entry.HeightSignatures.Num());
|
|
1747
|
+
if (Entry.Error.IsEmpty())
|
|
1748
|
+
{
|
|
1749
|
+
ValidateLandscapeCollisionPreflight(
|
|
1750
|
+
Entry.LandscapeComponents,
|
|
1751
|
+
Entry.CollisionComponents,
|
|
1752
|
+
Entry.HeightSignatures.Num(),
|
|
1753
|
+
Entry.ComponentsWithPendingLayerUpdates,
|
|
1754
|
+
Entry.bLandscapeLayersUpToDate,
|
|
1755
|
+
Entry.bTextureResourcesReady,
|
|
1756
|
+
Entry.Error);
|
|
1757
|
+
}
|
|
1758
|
+
if (!Entry.Error.IsEmpty())
|
|
1759
|
+
{
|
|
1760
|
+
Entry.Json->SetBoolField(TEXT("refreshed"), false);
|
|
1761
|
+
continue;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
const bool bBatchHeightPreflightPassed =
|
|
1767
|
+
Entries.Num() == Matches.Num() &&
|
|
1768
|
+
!Entries.ContainsByPredicate([](const FRefreshEntry& Entry)
|
|
1769
|
+
{
|
|
1770
|
+
return !Entry.Error.IsEmpty();
|
|
1771
|
+
});
|
|
1772
|
+
if (!bBatchHeightPreflightPassed)
|
|
1773
|
+
{
|
|
1774
|
+
// The selected proxy set is one safety unit. Updating the apparently-good
|
|
1775
|
+
// subset would leave the loaded landscape internally inconsistent and can
|
|
1776
|
+
// reproduce the stale-cache failure on the next save/reload.
|
|
1777
|
+
for (FRefreshEntry& Entry : Entries)
|
|
1778
|
+
{
|
|
1779
|
+
if (Entry.Error.IsEmpty())
|
|
1780
|
+
{
|
|
1781
|
+
Entry.Error = TEXT("Another matched proxy failed loaded/registered height-collision preflight; no matched proxy was mutated");
|
|
1782
|
+
Entry.Json->SetBoolField(TEXT("refreshed"), false);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
FScopedLandscapePhysicalMaterialRecreateMode RecreateMode;
|
|
1788
|
+
if (!RecreateMode.IsValid())
|
|
1789
|
+
{
|
|
1790
|
+
return MCPError(TEXT("Required console variable landscape.ApplyPhysicalMaterialChangesImmediately was unavailable or could not be forced to zero; a higher-priority override may be active"));
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
for (FRefreshEntry& Entry : Entries)
|
|
1794
|
+
{
|
|
1795
|
+
if (!Entry.Error.IsEmpty()) continue;
|
|
1796
|
+
TArray<ULandscapeComponent*> Components;
|
|
1797
|
+
Entry.Proxy->GetComponents<ULandscapeComponent>(Components);
|
|
1798
|
+
for (ULandscapeComponent* Component : Components)
|
|
1799
|
+
{
|
|
1800
|
+
if (Component && Component->IsRegistered() && Component->GetCollisionComponent())
|
|
1801
|
+
{
|
|
1802
|
+
// UpdateCollisionLayerData invalidates the physical-material task but,
|
|
1803
|
+
// unlike ChangedPhysMaterial, intentionally does not tear down physics
|
|
1804
|
+
// before the asynchronous material output is ready.
|
|
1805
|
+
Component->UpdateCollisionLayerData();
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
Entry.OutdatedBefore = Entry.Proxy->GetOudatedPhysicalMaterialComponentsCount();
|
|
1809
|
+
Entry.Json->SetNumberField(TEXT("outdatedBefore"), Entry.OutdatedBefore);
|
|
1810
|
+
// The return type changed between supported UE versions; ignoring it is
|
|
1811
|
+
// intentional. The exported outdated count below is the stable readback.
|
|
1812
|
+
Entry.Proxy->BuildPhysicalMaterial();
|
|
1813
|
+
Entry.bRefreshed = true;
|
|
1814
|
+
Entry.Json->SetBoolField(TEXT("refreshed"), true);
|
|
1815
|
+
++Refreshed;
|
|
1816
|
+
CollisionComponentsRefreshed += Entry.CollisionComponents;
|
|
1817
|
+
OutdatedBefore += Entry.OutdatedBefore;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// BuildPhysicalMaterial advances an asynchronous GPU readback. Re-enter the
|
|
1821
|
+
// exported build method after flushing render commands until it finalizes
|
|
1822
|
+
// every matched proxy, or until there is enough time left to return a useful
|
|
1823
|
+
// failure report before the handler's 600-second bridge timeout. Time spent
|
|
1824
|
+
// preparing texture resources counts too.
|
|
1825
|
+
const double BuildDeadline = StartedAt + 420.0;
|
|
1826
|
+
bool bBuildTimedOut = false;
|
|
1827
|
+
while (Refreshed > 0)
|
|
1828
|
+
{
|
|
1829
|
+
int32 Remaining = 0;
|
|
1830
|
+
for (const FRefreshEntry& Entry : Entries)
|
|
1831
|
+
{
|
|
1832
|
+
if (Entry.bRefreshed) Remaining += Entry.Proxy->GetOudatedPhysicalMaterialComponentsCount();
|
|
1833
|
+
}
|
|
1834
|
+
if (Remaining == 0) break;
|
|
1835
|
+
if (FPlatformTime::Seconds() >= BuildDeadline)
|
|
1836
|
+
{
|
|
1837
|
+
bBuildTimedOut = true;
|
|
1838
|
+
break;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
FlushRenderingCommands();
|
|
1842
|
+
for (FRefreshEntry& Entry : Entries)
|
|
1843
|
+
{
|
|
1844
|
+
if (Entry.bRefreshed && Entry.Proxy->GetOudatedPhysicalMaterialComponentsCount() > 0)
|
|
1845
|
+
{
|
|
1846
|
+
Entry.Proxy->BuildPhysicalMaterial();
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
FPlatformProcess::Sleep(0.001f);
|
|
1850
|
+
}
|
|
1851
|
+
if (Refreshed > 0) FlushRenderingCommands();
|
|
1852
|
+
|
|
1853
|
+
int32 CollisionComponentsRecreateRequested = 0;
|
|
1854
|
+
int32 CollisionComponentsRecreatedAfterBuild = 0;
|
|
1855
|
+
int32 CollisionComponentsUnchangedAfterBuild = 0;
|
|
1856
|
+
int32 OutdatedAfterBuild = 0;
|
|
1857
|
+
auto VerifyEntryHeightCollision = [](
|
|
1858
|
+
FRefreshEntry& Entry,
|
|
1859
|
+
const TCHAR* Phase,
|
|
1860
|
+
bool bWriteJsonField) -> bool
|
|
1861
|
+
{
|
|
1862
|
+
for (const FLandscapeHeightCollisionSignature& Baseline : Entry.HeightSignatures)
|
|
1863
|
+
{
|
|
1864
|
+
if (!IsValid(Baseline.Collision))
|
|
1865
|
+
{
|
|
1866
|
+
Entry.Error = FString::Printf(
|
|
1867
|
+
TEXT("Height collision verification failed %s: %s is no longer valid"),
|
|
1868
|
+
Phase, *Baseline.ComponentPath);
|
|
1869
|
+
if (bWriteJsonField) Entry.Json->SetBoolField(TEXT("heightCollisionVerified"), false);
|
|
1870
|
+
return false;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
FLandscapeHeightCollisionSignature Current;
|
|
1874
|
+
FString SignatureError;
|
|
1875
|
+
if (!CaptureHeightCollisionSignature(Baseline.Collision, Current, SignatureError) ||
|
|
1876
|
+
!MatchesHeightCollisionSignature(Baseline, Current, SignatureError))
|
|
1877
|
+
{
|
|
1878
|
+
Entry.Error = FString::Printf(
|
|
1879
|
+
TEXT("Height collision verification failed %s: %s"), Phase, *SignatureError);
|
|
1880
|
+
if (bWriteJsonField) Entry.Json->SetBoolField(TEXT("heightCollisionVerified"), false);
|
|
1881
|
+
return false;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
if (bWriteJsonField) Entry.Json->SetBoolField(TEXT("heightCollisionVerified"), true);
|
|
1885
|
+
return true;
|
|
1886
|
+
};
|
|
1887
|
+
|
|
1888
|
+
for (FRefreshEntry& Entry : Entries)
|
|
1889
|
+
{
|
|
1890
|
+
if (!Entry.bRefreshed) continue;
|
|
1891
|
+
Entry.OutdatedAfter = Entry.Proxy->GetOudatedPhysicalMaterialComponentsCount();
|
|
1892
|
+
Entry.Json->SetNumberField(TEXT("outdatedAfterBuild"), Entry.OutdatedAfter);
|
|
1893
|
+
Entry.Json->SetBoolField(TEXT("physicalMaterialCurrentAfterBuild"), Entry.OutdatedAfter == 0);
|
|
1894
|
+
OutdatedAfterBuild += Entry.OutdatedAfter;
|
|
1895
|
+
if (Entry.OutdatedAfter == 0)
|
|
1896
|
+
{
|
|
1897
|
+
// Prove that the material build did not touch raw or live heights before
|
|
1898
|
+
// tearing down the valid heightfield. The scoped CVar above prevents the
|
|
1899
|
+
// engine finalizer from recreating it early.
|
|
1900
|
+
if (!VerifyEntryHeightCollision(Entry, TEXT("after the physical-material build"), false))
|
|
1901
|
+
{
|
|
1902
|
+
continue;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
// Recreate exactly once, after both dominant-layer and rendered physical
|
|
1906
|
+
// material data are current. RecreateCollision's return value only says
|
|
1907
|
+
// whether a request was made; the full live height tile below is proof
|
|
1908
|
+
// that a valid, unchanged geometry was produced.
|
|
1909
|
+
int32 EntryRecreateRequested = 0;
|
|
1910
|
+
int32 EntryRecreated = 0;
|
|
1911
|
+
int32 EntryUnchanged = 0;
|
|
1912
|
+
for (const FLandscapeHeightCollisionSignature& Baseline : Entry.HeightSignatures)
|
|
1913
|
+
{
|
|
1914
|
+
if (IsValid(Baseline.Collision) && Baseline.Collision->IsRegistered())
|
|
1915
|
+
{
|
|
1916
|
+
++EntryRecreateRequested;
|
|
1917
|
+
if (Baseline.Collision->RecreateCollision()) ++EntryRecreated;
|
|
1918
|
+
else ++EntryUnchanged;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
Entry.Json->SetNumberField(TEXT("collisionRecreateRequestedAfterBuild"), EntryRecreateRequested);
|
|
1922
|
+
Entry.Json->SetNumberField(TEXT("collisionRecreatedAfterBuild"), EntryRecreated);
|
|
1923
|
+
Entry.Json->SetNumberField(TEXT("collisionUnchangedAfterBuild"), EntryUnchanged);
|
|
1924
|
+
CollisionComponentsRecreateRequested += EntryRecreateRequested;
|
|
1925
|
+
CollisionComponentsRecreatedAfterBuild += EntryRecreated;
|
|
1926
|
+
CollisionComponentsUnchangedAfterBuild += EntryUnchanged;
|
|
1927
|
+
Entry.bHeightCollisionVerified =
|
|
1928
|
+
EntryRecreateRequested == Entry.HeightSignatures.Num() &&
|
|
1929
|
+
EntryRecreated == Entry.HeightSignatures.Num() &&
|
|
1930
|
+
VerifyEntryHeightCollision(Entry, TEXT("after the final collision recreation"), true);
|
|
1931
|
+
if (!Entry.bHeightCollisionVerified && Entry.Error.IsEmpty())
|
|
1932
|
+
{
|
|
1933
|
+
Entry.Error = TEXT("Not every captured collision component was explicitly recreated for the new physical-material mapping");
|
|
1934
|
+
Entry.Json->SetBoolField(TEXT("heightCollisionVerified"), false);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
else
|
|
1938
|
+
{
|
|
1939
|
+
Entry.Error = FString::Printf(
|
|
1940
|
+
TEXT("Physical-material build did not flush %d outdated component(s)%s"),
|
|
1941
|
+
Entry.OutdatedAfter, bBuildTimedOut ? TEXT(" before the timeout") : TEXT(""));
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// Read back after the build/recreation so the response reflects the final
|
|
1946
|
+
// in-memory derived state. Persistence is deliberately unsupported here:
|
|
1947
|
+
// ALandscapeProxy::PreSave can mutate edit-layer collision data.
|
|
1948
|
+
int32 OutdatedAfter = 0;
|
|
1949
|
+
int32 PhysicalMaterialsCurrent = 0;
|
|
1950
|
+
for (FRefreshEntry& Entry : Entries)
|
|
1951
|
+
{
|
|
1952
|
+
if (!Entry.bRefreshed) continue;
|
|
1953
|
+
Entry.OutdatedAfter = Entry.Proxy->GetOudatedPhysicalMaterialComponentsCount();
|
|
1954
|
+
Entry.Json->SetNumberField(TEXT("outdatedAfter"), Entry.OutdatedAfter);
|
|
1955
|
+
const bool bPhysicalMaterialCurrent = Entry.OutdatedAfter == 0;
|
|
1956
|
+
Entry.Json->SetBoolField(TEXT("physicalMaterialCurrent"), bPhysicalMaterialCurrent);
|
|
1957
|
+
OutdatedAfter += Entry.OutdatedAfter;
|
|
1958
|
+
if (bPhysicalMaterialCurrent)
|
|
1959
|
+
{
|
|
1960
|
+
++PhysicalMaterialsCurrent;
|
|
1961
|
+
}
|
|
1962
|
+
else if (Entry.Error.IsEmpty())
|
|
1963
|
+
{
|
|
1964
|
+
Entry.Error = FString::Printf(
|
|
1965
|
+
TEXT("Physical-material state has %d outdated component(s) after the build"),
|
|
1966
|
+
Entry.OutdatedAfter);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
const bool bBatchAccepted = !Matches.IsEmpty() && Entries.Num() == Matches.Num() &&
|
|
1970
|
+
!Entries.ContainsByPredicate([](const FRefreshEntry& Entry)
|
|
1971
|
+
{
|
|
1972
|
+
return !Entry.Error.IsEmpty();
|
|
1973
|
+
});
|
|
1974
|
+
if (!bBatchAccepted)
|
|
1975
|
+
{
|
|
1976
|
+
for (FRefreshEntry& Entry : Entries)
|
|
1977
|
+
{
|
|
1978
|
+
if (Entry.Error.IsEmpty())
|
|
1979
|
+
{
|
|
1980
|
+
Entry.Error = TEXT("Another matched proxy failed the post-build safety check; the matched batch was not accepted and must not be saved");
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
TArray<TSharedPtr<FJsonValue>> ActorResults;
|
|
1986
|
+
TArray<FString> FailedPackagePaths;
|
|
1987
|
+
int32 Failed = 0;
|
|
1988
|
+
int32 HeightCollisionComponentsVerified = 0;
|
|
1989
|
+
for (FRefreshEntry& Entry : Entries)
|
|
1990
|
+
{
|
|
1991
|
+
Entry.Json->SetBoolField(TEXT("saved"), false);
|
|
1992
|
+
if (Entry.bHeightCollisionVerified)
|
|
1993
|
+
{
|
|
1994
|
+
HeightCollisionComponentsVerified += Entry.HeightSignatures.Num();
|
|
1995
|
+
}
|
|
1996
|
+
if (!Entry.Error.IsEmpty())
|
|
1997
|
+
{
|
|
1998
|
+
++Failed;
|
|
1999
|
+
Entry.Json->SetStringField(TEXT("error"), Entry.Error);
|
|
2000
|
+
if (!Entry.PackagePath.IsEmpty()) FailedPackagePaths.AddUnique(Entry.PackagePath);
|
|
2001
|
+
}
|
|
2002
|
+
ActorResults.Add(MakeShared<FJsonValueObject>(Entry.Json));
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
auto ToJsonStrings = [](const TArray<FString>& Strings)
|
|
2006
|
+
{
|
|
2007
|
+
TArray<TSharedPtr<FJsonValue>> Values;
|
|
2008
|
+
Values.Reserve(Strings.Num());
|
|
2009
|
+
for (const FString& String : Strings) Values.Add(MakeShared<FJsonValueString>(String));
|
|
2010
|
+
return Values;
|
|
2011
|
+
};
|
|
2012
|
+
|
|
2013
|
+
auto Result = MCPSuccess();
|
|
2014
|
+
if (Failed > 0 || Matches.IsEmpty()) Result->SetBoolField(TEXT("success"), false);
|
|
2015
|
+
if (Refreshed > 0) MCPSetUpdated(Result);
|
|
2016
|
+
Result->SetStringField(TEXT("world"), World->GetPathName());
|
|
2017
|
+
Result->SetBoolField(TEXT("batchAccepted"), bBatchAccepted);
|
|
2018
|
+
Result->SetBoolField(TEXT("saveSupported"), false);
|
|
2019
|
+
Result->SetBoolField(TEXT("saveRequested"), false);
|
|
2020
|
+
Result->SetNumberField(TEXT("maxActors"), MaxActors);
|
|
2021
|
+
Result->SetNumberField(TEXT("loaded"), LoadedProxies.Num());
|
|
2022
|
+
Result->SetNumberField(TEXT("matched"), Matches.Num());
|
|
2023
|
+
Result->SetNumberField(TEXT("refreshed"), Refreshed);
|
|
2024
|
+
Result->SetNumberField(TEXT("collisionComponentsRefreshed"), CollisionComponentsRefreshed);
|
|
2025
|
+
Result->SetNumberField(TEXT("collisionComponentsRecreateRequested"), CollisionComponentsRecreateRequested);
|
|
2026
|
+
Result->SetNumberField(TEXT("collisionComponentsRecreatedAfterBuild"), CollisionComponentsRecreatedAfterBuild);
|
|
2027
|
+
Result->SetNumberField(TEXT("collisionComponentsUnchangedAfterBuild"), CollisionComponentsUnchangedAfterBuild);
|
|
2028
|
+
Result->SetNumberField(TEXT("heightCollisionComponentsVerified"), HeightCollisionComponentsVerified);
|
|
2029
|
+
Result->SetNumberField(TEXT("physicalMaterialsCurrent"), PhysicalMaterialsCurrent);
|
|
2030
|
+
Result->SetNumberField(TEXT("outdatedBefore"), OutdatedBefore);
|
|
2031
|
+
Result->SetNumberField(TEXT("outdatedAfterBuild"), OutdatedAfterBuild);
|
|
2032
|
+
Result->SetNumberField(TEXT("outdatedAfter"), OutdatedAfter);
|
|
2033
|
+
Result->SetBoolField(TEXT("buildTimedOut"), bBuildTimedOut);
|
|
2034
|
+
Result->SetNumberField(TEXT("textureResourceParents"), TextureResourcesReady.Num());
|
|
2035
|
+
int32 ReadyParents = 0;
|
|
2036
|
+
for (const TPair<ALandscape*, bool>& Pair : TextureResourcesReady) ReadyParents += Pair.Value ? 1 : 0;
|
|
2037
|
+
Result->SetNumberField(TEXT("textureResourceParentsReady"), ReadyParents);
|
|
2038
|
+
int32 UpToDateParents = 0;
|
|
2039
|
+
for (const TPair<ALandscape*, bool>& Pair : LandscapeLayersUpToDate) UpToDateParents += Pair.Value ? 1 : 0;
|
|
2040
|
+
Result->SetNumberField(TEXT("landscapeLayerParents"), LandscapeLayersUpToDate.Num());
|
|
2041
|
+
Result->SetNumberField(TEXT("landscapeLayerParentsUpToDate"), UpToDateParents);
|
|
2042
|
+
Result->SetNumberField(TEXT("saved"), 0);
|
|
2043
|
+
Result->SetNumberField(TEXT("failed"), Failed);
|
|
2044
|
+
Result->SetArrayField(TEXT("packagePaths"), ToJsonStrings(PackagePaths));
|
|
2045
|
+
Result->SetArrayField(TEXT("savedPackagePaths"), TArray<TSharedPtr<FJsonValue>>());
|
|
2046
|
+
Result->SetArrayField(TEXT("failedPackagePaths"), ToJsonStrings(FailedPackagePaths));
|
|
2047
|
+
Result->SetArrayField(TEXT("actors"), ActorResults);
|
|
2048
|
+
FString Note;
|
|
2049
|
+
if (Matches.IsEmpty())
|
|
2050
|
+
{
|
|
2051
|
+
Note = TEXT("No loaded LandscapeStreamingProxy actor matched. Unloaded proxies were not changed; pin them first with level(load_actor_descs), then rerun this action.");
|
|
2052
|
+
}
|
|
2053
|
+
else if (Failed > 0)
|
|
2054
|
+
{
|
|
2055
|
+
Note = FString::Printf(TEXT("%d of %d matched proxies failed; inspect the per-actor errors. Unloaded proxies were not changed."), Failed, Matches.Num());
|
|
2056
|
+
}
|
|
2057
|
+
else
|
|
2058
|
+
{
|
|
2059
|
+
Note = TEXT("Collision and physical-material data were rebuilt in memory and matched packages may now be dirty. No packages were saved. Unloaded proxies were not changed.");
|
|
2060
|
+
}
|
|
2061
|
+
Result->SetStringField(TEXT("note"), Note);
|
|
2062
|
+
return MCPResult(Result);
|
|
2063
|
+
#endif // UE_MCP_HAS_5_8_API
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
#if WITH_DEV_AUTOMATION_TESTS && UE_MCP_HAS_5_8_API
|
|
2067
|
+
|
|
2068
|
+
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
|
2069
|
+
FMCPLandscapePhysicalMaterialCollisionHeightSafetyTest,
|
|
2070
|
+
"UE.MCP.Landscape.PhysicalMaterialCollision.HeightSafety",
|
|
2071
|
+
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
|
2072
|
+
|
|
2073
|
+
bool FMCPLandscapePhysicalMaterialCollisionHeightSafetyTest::RunTest(const FString& Parameters)
|
|
2074
|
+
{
|
|
2075
|
+
const TArray<uint16> TerrainRaw = {
|
|
2076
|
+
32768, 32896, 32640, 33024,
|
|
2077
|
+
32768, 32960, 32576, 33152
|
|
2078
|
+
};
|
|
2079
|
+
const TArray<float> TerrainLive = {
|
|
2080
|
+
100.0f, 200.0f, 0.0f, 300.0f,
|
|
2081
|
+
100.0f, 250.0f, -50.0f, 400.0f
|
|
2082
|
+
};
|
|
2083
|
+
const FTransform Transform(FRotator::ZeroRotator, FVector(0.0, 0.0, 100.0), FVector(100.0));
|
|
2084
|
+
|
|
2085
|
+
FLandscapeHeightCollisionSignature Baseline;
|
|
2086
|
+
FString Error;
|
|
2087
|
+
TestTrue(TEXT("varied raw terrain agrees with its complex and simple live heightfields"),
|
|
2088
|
+
BuildHeightCollisionSignature(TerrainRaw, 4, 2, 4, 2, TerrainLive, Transform, Baseline, Error));
|
|
2089
|
+
TestTrue(TEXT("baseline reports a non-flat height range"),
|
|
2090
|
+
Baseline.LiveMaxWorldZ > Baseline.LiveMinWorldZ);
|
|
2091
|
+
|
|
2092
|
+
const TArray<uint16> FlatRaw = {
|
|
2093
|
+
32768, 32768, 32768, 32768,
|
|
2094
|
+
32768, 32768, 32768, 32768
|
|
2095
|
+
};
|
|
2096
|
+
FLandscapeHeightCollisionSignature Invalid;
|
|
2097
|
+
Error.Reset();
|
|
2098
|
+
TestFalse(TEXT("flat raw data cannot masquerade as the original live terrain"),
|
|
2099
|
+
BuildHeightCollisionSignature(FlatRaw, 4, 2, 4, 2, TerrainLive, Transform, Invalid, Error));
|
|
2100
|
+
TestTrue(TEXT("raw/live mismatch names the unsafe sample"), Error.Contains(TEXT("mismatch at sample")));
|
|
2101
|
+
|
|
2102
|
+
TArray<float> FlatSimpleLive = TerrainLive;
|
|
2103
|
+
for (int32 Index = 4; Index < FlatSimpleLive.Num(); ++Index) FlatSimpleLive[Index] = 100.0f;
|
|
2104
|
+
Error.Reset();
|
|
2105
|
+
TestFalse(TEXT("flattened simple live collision is rejected"),
|
|
2106
|
+
BuildHeightCollisionSignature(TerrainRaw, 4, 2, 4, 2, FlatSimpleLive, Transform, Invalid, Error));
|
|
2107
|
+
|
|
2108
|
+
const TArray<float> FlatLive = {
|
|
2109
|
+
100.0f, 100.0f, 100.0f, 100.0f,
|
|
2110
|
+
100.0f, 100.0f, 100.0f, 100.0f
|
|
2111
|
+
};
|
|
2112
|
+
FLandscapeHeightCollisionSignature Flattened;
|
|
2113
|
+
Error.Reset();
|
|
2114
|
+
TestTrue(TEXT("internally consistent flat collision can be signed"),
|
|
2115
|
+
BuildHeightCollisionSignature(FlatRaw, 4, 2, 4, 2, FlatLive, Transform, Flattened, Error));
|
|
2116
|
+
TestFalse(TEXT("a flattened raw/live pair cannot match the terrain baseline"),
|
|
2117
|
+
MatchesHeightCollisionSignature(Baseline, Flattened, Error));
|
|
2118
|
+
|
|
2119
|
+
FString PreflightError;
|
|
2120
|
+
TestFalse(TEXT("pending edit-layer work fails preflight"),
|
|
2121
|
+
ValidateLandscapeCollisionPreflight(1, 1, 1, 1, true, true, PreflightError));
|
|
2122
|
+
TestTrue(TEXT("pending-work failure explains PreSave risk"),
|
|
2123
|
+
PreflightError.Contains(TEXT("pending edit-layer updates")));
|
|
2124
|
+
PreflightError.Reset();
|
|
2125
|
+
TestTrue(TEXT("complete idle collision coverage passes preflight"),
|
|
2126
|
+
ValidateLandscapeCollisionPreflight(1, 1, 1, 0, true, true, PreflightError));
|
|
2127
|
+
return true;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
#endif // WITH_DEV_AUTOMATION_TESTS && UE_MCP_HAS_5_8_API
|