ue-mcp 1.2.4 → 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/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/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/project.js +47 -20
- package/dist/tools/project.js.map +1 -1
- package/dist/types.d.ts +21 -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,769 @@
|
|
|
1
|
+
// Evaluated pose reads (#923, #926, #922).
|
|
2
|
+
//
|
|
3
|
+
// Translation-unit partition of FAnimationHandlers - registration stays in
|
|
4
|
+
// AnimationHandlers.cpp::RegisterHandlers.
|
|
5
|
+
//
|
|
6
|
+
// Everything the bridge could previously say about an animation was either raw
|
|
7
|
+
// or reference data. read_bone_track returns LOCAL space, so recovering where a
|
|
8
|
+
// foot actually is means composing the whole parent chain outside the engine,
|
|
9
|
+
// and get_bone_transforms has a component-space mode but reads the REFERENCE
|
|
10
|
+
// pose, which is the read that looks right while the evaluated instance is
|
|
11
|
+
// wrong - exactly the shape of #922, where a character lay flat and every
|
|
12
|
+
// transform the bridge could show read clean.
|
|
13
|
+
//
|
|
14
|
+
// These evaluate. sample_pose runs the engine's own pose evaluator over an
|
|
15
|
+
// AnimSequence (or a BlendSpace at a blend position) and hands back composed
|
|
16
|
+
// transforms; measure_natural_speed builds on it to answer the question that
|
|
17
|
+
// blocks every retarget-then-rebuild-the-BlendSpace loop (#923): how fast does
|
|
18
|
+
// THIS clip on THIS skeleton actually move.
|
|
19
|
+
|
|
20
|
+
#include "AnimationHandlers.h"
|
|
21
|
+
|
|
22
|
+
#include "HandlerRegistry.h"
|
|
23
|
+
#include "HandlerUtils.h"
|
|
24
|
+
|
|
25
|
+
#include "AnimPose.h"
|
|
26
|
+
#include "Animation/AnimSequence.h"
|
|
27
|
+
#include "Animation/AnimMontage.h"
|
|
28
|
+
#include "Animation/AnimationAsset.h"
|
|
29
|
+
#include "Animation/BlendSpace.h"
|
|
30
|
+
#include "Animation/Skeleton.h"
|
|
31
|
+
#include "Engine/SkeletalMesh.h"
|
|
32
|
+
#include "Misc/FrameRate.h"
|
|
33
|
+
#include "ReferenceSkeleton.h"
|
|
34
|
+
|
|
35
|
+
#include "Dom/JsonObject.h"
|
|
36
|
+
#include "Dom/JsonValue.h"
|
|
37
|
+
|
|
38
|
+
namespace
|
|
39
|
+
{
|
|
40
|
+
// A pose sample costs one object per bone. Sampling every frame of a long clip
|
|
41
|
+
// on a full humanoid skeleton is tens of thousands of transforms, which is a
|
|
42
|
+
// payload no caller wants by accident, so the request is refused with the two
|
|
43
|
+
// parameters that narrow it named in the message.
|
|
44
|
+
constexpr int32 PoseSampleTransformCap = 20000;
|
|
45
|
+
|
|
46
|
+
/** Resolved evaluation target: an AnimSequenceBase, or a BlendSpace. */
|
|
47
|
+
struct FPoseEvaluationTarget
|
|
48
|
+
{
|
|
49
|
+
UAnimSequenceBase* Sequence = nullptr;
|
|
50
|
+
UBlendSpace* BlendSpace = nullptr;
|
|
51
|
+
USkeleton* Skeleton = nullptr;
|
|
52
|
+
FString AssetType;
|
|
53
|
+
float PlayLength = 0.0f;
|
|
54
|
+
int32 NumberOfSampledKeys = 0;
|
|
55
|
+
FFrameRate SamplingFrameRate = FFrameRate(30, 1);
|
|
56
|
+
bool bHasRootMotion = false;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Load the addressed animation asset, rejecting the ones the evaluator cannot
|
|
60
|
+
* take. Returns an error value on failure, or an unset pointer on success. */
|
|
61
|
+
TSharedPtr<FJsonValue> ResolvePoseEvaluationTarget(const FString& AssetPath, FPoseEvaluationTarget& Out)
|
|
62
|
+
{
|
|
63
|
+
UObject* Asset = LoadAssetByPath<UObject>(AssetPath);
|
|
64
|
+
if (!Asset)
|
|
65
|
+
{
|
|
66
|
+
return MCPError(FString::Printf(TEXT("Animation asset not found: %s"), *AssetPath));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (UBlendSpace* BlendSpace = Cast<UBlendSpace>(Asset))
|
|
70
|
+
{
|
|
71
|
+
Out.BlendSpace = BlendSpace;
|
|
72
|
+
Out.Skeleton = BlendSpace->GetSkeleton();
|
|
73
|
+
Out.AssetType = BlendSpace->GetClass()->GetName();
|
|
74
|
+
// UBlendSpace::GetPlayLength() is the normalised 1.0 the runtime plays
|
|
75
|
+
// against, not a duration. The real one depends on which samples the
|
|
76
|
+
// blend position lands between, so it is filled in by
|
|
77
|
+
// ResolveBlendSpaceLength once that position is known.
|
|
78
|
+
Out.PlayLength = 0.0f;
|
|
79
|
+
Out.NumberOfSampledKeys = 0;
|
|
80
|
+
return nullptr;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (UAnimMontage* Montage = Cast<UAnimMontage>(Asset))
|
|
84
|
+
{
|
|
85
|
+
// UAnimMontage::GetAnimationPose is a hard check(false) in the engine: a
|
|
86
|
+
// montage is a schedule of other assets, so the thing to evaluate is the
|
|
87
|
+
// sequence in the slot, not the montage.
|
|
88
|
+
return MCPError(FString::Printf(
|
|
89
|
+
TEXT("'%s' is an AnimMontage, which has no pose of its own. Read its segments with animation(list_montage_segments) and sample the AnimSequence each one plays."),
|
|
90
|
+
*Montage->GetPathName()));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (UAnimSequenceBase* Sequence = Cast<UAnimSequenceBase>(Asset))
|
|
94
|
+
{
|
|
95
|
+
Out.Sequence = Sequence;
|
|
96
|
+
Out.Skeleton = Sequence->GetSkeleton();
|
|
97
|
+
Out.AssetType = Sequence->GetClass()->GetName();
|
|
98
|
+
Out.PlayLength = Sequence->GetPlayLength();
|
|
99
|
+
Out.NumberOfSampledKeys = Sequence->GetNumberOfSampledKeys();
|
|
100
|
+
Out.SamplingFrameRate = Sequence->GetSamplingFrameRate();
|
|
101
|
+
Out.bHasRootMotion = Sequence->HasRootMotion();
|
|
102
|
+
return nullptr;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return MCPError(FString::Printf(
|
|
106
|
+
TEXT("Asset '%s' is a %s, which is not an AnimSequence or BlendSpace"),
|
|
107
|
+
*AssetPath, *Asset->GetClass()->GetName()));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Space token to the engine's enum. EAnimPoseSpaces::World is component space
|
|
111
|
+
* in this API (the engine's own comment says so); an animation asset has no
|
|
112
|
+
* outer frame beyond its skeleton root, so 'world' resolves to the same pose. */
|
|
113
|
+
bool ParsePoseSpace(const FString& Token, EAnimPoseSpaces& OutSpace, FString& OutResolved)
|
|
114
|
+
{
|
|
115
|
+
if (Token == TEXT("local"))
|
|
116
|
+
{
|
|
117
|
+
OutSpace = EAnimPoseSpaces::Local;
|
|
118
|
+
OutResolved = TEXT("local");
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (Token == TEXT("component") || Token == TEXT("world"))
|
|
122
|
+
{
|
|
123
|
+
OutSpace = EAnimPoseSpaces::World;
|
|
124
|
+
OutResolved = TEXT("component");
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
FAnimPoseEvaluationOptions MakePoseEvaluationOptions(USkeletalMesh* OptionalMesh, bool bIncorporateRootMotion)
|
|
131
|
+
{
|
|
132
|
+
FAnimPoseEvaluationOptions Options;
|
|
133
|
+
Options.EvaluationType = EAnimDataEvalType::Raw;
|
|
134
|
+
Options.bShouldRetarget = true;
|
|
135
|
+
Options.bExtractRootMotion = false;
|
|
136
|
+
Options.bIncorporateRootMotionIntoPose = bIncorporateRootMotion;
|
|
137
|
+
Options.bRetrieveAdditiveAsFullPose = true;
|
|
138
|
+
Options.bEvaluateCurves = false;
|
|
139
|
+
Options.OptionalSkeletalMesh = OptionalMesh;
|
|
140
|
+
return Options;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
TSharedPtr<FJsonObject> PoseBoneTransformToJson(const FName& BoneName, const FTransform& Transform)
|
|
144
|
+
{
|
|
145
|
+
TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
|
|
146
|
+
Obj->SetStringField(TEXT("name"), BoneName.ToString());
|
|
147
|
+
Obj->SetObjectField(TEXT("location"), MCPVec3ToJsonObject(Transform.GetLocation()));
|
|
148
|
+
Obj->SetObjectField(TEXT("rotation"), MCPRotatorToJsonObject(Transform.GetRotation().Rotator()));
|
|
149
|
+
Obj->SetObjectField(TEXT("scale"), MCPVec3ToJsonObject(Transform.GetScale3D()));
|
|
150
|
+
return Obj;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Weighted blend of the contributing samples of a BlendSpace, evaluated at one
|
|
154
|
+
* normalised phase. Mirrors what the runtime does: every contributing sequence
|
|
155
|
+
* is sampled at the same phase and the results are averaged by weight. */
|
|
156
|
+
bool EvaluateBlendSpacePoseAtTime(
|
|
157
|
+
UBlendSpace* BlendSpace,
|
|
158
|
+
const FVector& BlendInput,
|
|
159
|
+
double Time,
|
|
160
|
+
const FAnimPoseEvaluationOptions& Options,
|
|
161
|
+
const TArray<FName>& RequestedBones,
|
|
162
|
+
EAnimPoseSpaces Space,
|
|
163
|
+
TArray<FName>& OutBoneNames,
|
|
164
|
+
TArray<FTransform>& OutTransforms,
|
|
165
|
+
FString& OutError)
|
|
166
|
+
{
|
|
167
|
+
TArray<FBlendSampleData> SampleData;
|
|
168
|
+
int32 CachedTriangulationIndex = INDEX_NONE;
|
|
169
|
+
if (!BlendSpace->GetSamplesFromBlendInput(BlendInput, SampleData, CachedTriangulationIndex, /*bCombineAnimations=*/true))
|
|
170
|
+
{
|
|
171
|
+
OutError = FString::Printf(
|
|
172
|
+
TEXT("BlendSpace '%s' produced no samples at blend position (%g, %g, %g)"),
|
|
173
|
+
*BlendSpace->GetPathName(), BlendInput.X, BlendInput.Y, BlendInput.Z);
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const double BlendedLength = static_cast<double>(BlendSpace->GetAnimationLengthFromSampleData(SampleData));
|
|
178
|
+
const double Phase = BlendedLength > KINDA_SMALL_NUMBER ? FMath::Clamp(Time / BlendedLength, 0.0, 1.0) : 0.0;
|
|
179
|
+
|
|
180
|
+
double TotalWeight = 0.0;
|
|
181
|
+
bool bSeeded = false;
|
|
182
|
+
TArray<FQuat> AccumulatedRotations;
|
|
183
|
+
TArray<FVector> AccumulatedTranslations;
|
|
184
|
+
TArray<FVector> AccumulatedScales;
|
|
185
|
+
|
|
186
|
+
for (const FBlendSampleData& Sample : SampleData)
|
|
187
|
+
{
|
|
188
|
+
UAnimSequence* Animation = Sample.Animation;
|
|
189
|
+
if (!Animation || Sample.TotalWeight <= KINDA_SMALL_NUMBER) continue;
|
|
190
|
+
|
|
191
|
+
FAnimPose Pose;
|
|
192
|
+
UAnimPoseExtensions::GetAnimPoseAtTime(Animation, Phase * Animation->GetPlayLength(), Options, Pose);
|
|
193
|
+
if (!UAnimPoseExtensions::IsValid(Pose)) continue;
|
|
194
|
+
|
|
195
|
+
TArray<FName> BoneNames;
|
|
196
|
+
if (RequestedBones.Num() > 0)
|
|
197
|
+
{
|
|
198
|
+
BoneNames = RequestedBones;
|
|
199
|
+
}
|
|
200
|
+
else
|
|
201
|
+
{
|
|
202
|
+
UAnimPoseExtensions::GetBoneNames(Pose, BoneNames);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!bSeeded)
|
|
206
|
+
{
|
|
207
|
+
OutBoneNames = BoneNames;
|
|
208
|
+
AccumulatedRotations.SetNum(BoneNames.Num());
|
|
209
|
+
AccumulatedTranslations.SetNumZeroed(BoneNames.Num());
|
|
210
|
+
AccumulatedScales.SetNumZeroed(BoneNames.Num());
|
|
211
|
+
for (FQuat& Rotation : AccumulatedRotations) Rotation = FQuat(0, 0, 0, 0);
|
|
212
|
+
bSeeded = true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const double Weight = static_cast<double>(Sample.TotalWeight);
|
|
216
|
+
TotalWeight += Weight;
|
|
217
|
+
for (int32 BoneIndex = 0; BoneIndex < OutBoneNames.Num(); ++BoneIndex)
|
|
218
|
+
{
|
|
219
|
+
const FTransform BoneTransform = UAnimPoseExtensions::GetBonePose(Pose, OutBoneNames[BoneIndex], Space);
|
|
220
|
+
FQuat Rotation = BoneTransform.GetRotation();
|
|
221
|
+
// Align to the accumulator's hemisphere so opposite-sign quaternions
|
|
222
|
+
// for the same orientation do not cancel each other out.
|
|
223
|
+
if ((AccumulatedRotations[BoneIndex] | Rotation) < 0.0)
|
|
224
|
+
{
|
|
225
|
+
Rotation = -Rotation;
|
|
226
|
+
}
|
|
227
|
+
AccumulatedRotations[BoneIndex] += Rotation * Weight;
|
|
228
|
+
AccumulatedTranslations[BoneIndex] += BoneTransform.GetLocation() * Weight;
|
|
229
|
+
AccumulatedScales[BoneIndex] += BoneTransform.GetScale3D() * Weight;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!bSeeded || TotalWeight <= KINDA_SMALL_NUMBER)
|
|
234
|
+
{
|
|
235
|
+
OutError = FString::Printf(
|
|
236
|
+
TEXT("BlendSpace '%s' has no evaluable animation at blend position (%g, %g, %g)"),
|
|
237
|
+
*BlendSpace->GetPathName(), BlendInput.X, BlendInput.Y, BlendInput.Z);
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
OutTransforms.Reset(OutBoneNames.Num());
|
|
242
|
+
for (int32 BoneIndex = 0; BoneIndex < OutBoneNames.Num(); ++BoneIndex)
|
|
243
|
+
{
|
|
244
|
+
FQuat Rotation = AccumulatedRotations[BoneIndex];
|
|
245
|
+
Rotation.Normalize();
|
|
246
|
+
OutTransforms.Add(FTransform(
|
|
247
|
+
Rotation,
|
|
248
|
+
AccumulatedTranslations[BoneIndex] / TotalWeight,
|
|
249
|
+
AccumulatedScales[BoneIndex] / TotalWeight));
|
|
250
|
+
}
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** One evaluated sample: the frame/time it came from and the bones it holds. */
|
|
255
|
+
struct FEvaluatedPoseSample
|
|
256
|
+
{
|
|
257
|
+
int32 Frame = INDEX_NONE;
|
|
258
|
+
double Time = 0.0;
|
|
259
|
+
TArray<FName> BoneNames;
|
|
260
|
+
TArray<FTransform> Transforms;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/** Shared driver for sample_pose and measure_natural_speed. */
|
|
264
|
+
TSharedPtr<FJsonValue> EvaluatePoseSamples(
|
|
265
|
+
const FPoseEvaluationTarget& Target,
|
|
266
|
+
const TArray<int32>& Frames,
|
|
267
|
+
const TArray<double>& Times,
|
|
268
|
+
const TArray<FName>& RequestedBones,
|
|
269
|
+
EAnimPoseSpaces Space,
|
|
270
|
+
const FAnimPoseEvaluationOptions& Options,
|
|
271
|
+
const FVector& BlendInput,
|
|
272
|
+
TArray<FEvaluatedPoseSample>& OutSamples)
|
|
273
|
+
{
|
|
274
|
+
const int32 SampleCount = Frames.Num() > 0 ? Frames.Num() : Times.Num();
|
|
275
|
+
OutSamples.Reset(SampleCount);
|
|
276
|
+
|
|
277
|
+
for (int32 SampleIndex = 0; SampleIndex < SampleCount; ++SampleIndex)
|
|
278
|
+
{
|
|
279
|
+
FEvaluatedPoseSample Sample;
|
|
280
|
+
Sample.Frame = Frames.IsValidIndex(SampleIndex) ? Frames[SampleIndex] : INDEX_NONE;
|
|
281
|
+
Sample.Time = Times.IsValidIndex(SampleIndex) ? Times[SampleIndex] : 0.0;
|
|
282
|
+
|
|
283
|
+
if (Target.BlendSpace)
|
|
284
|
+
{
|
|
285
|
+
FString Error;
|
|
286
|
+
if (!EvaluateBlendSpacePoseAtTime(
|
|
287
|
+
Target.BlendSpace, BlendInput, Sample.Time, Options, RequestedBones, Space,
|
|
288
|
+
Sample.BoneNames, Sample.Transforms, Error))
|
|
289
|
+
{
|
|
290
|
+
return MCPError(Error);
|
|
291
|
+
}
|
|
292
|
+
OutSamples.Add(MoveTemp(Sample));
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
FAnimPose Pose;
|
|
297
|
+
if (Sample.Frame != INDEX_NONE)
|
|
298
|
+
{
|
|
299
|
+
UAnimPoseExtensions::GetAnimPoseAtFrame(Target.Sequence, Sample.Frame, Options, Pose);
|
|
300
|
+
}
|
|
301
|
+
else
|
|
302
|
+
{
|
|
303
|
+
UAnimPoseExtensions::GetAnimPoseAtTime(Target.Sequence, Sample.Time, Options, Pose);
|
|
304
|
+
}
|
|
305
|
+
if (!UAnimPoseExtensions::IsValid(Pose))
|
|
306
|
+
{
|
|
307
|
+
return MCPError(FString::Printf(
|
|
308
|
+
TEXT("Evaluating '%s' produced no pose at %s. The asset may have no bone tracks for its skeleton."),
|
|
309
|
+
*Target.Sequence->GetPathName(),
|
|
310
|
+
Sample.Frame != INDEX_NONE
|
|
311
|
+
? *FString::Printf(TEXT("frame %d"), Sample.Frame)
|
|
312
|
+
: *FString::Printf(TEXT("time %gs"), Sample.Time)));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (RequestedBones.Num() > 0)
|
|
316
|
+
{
|
|
317
|
+
Sample.BoneNames = RequestedBones;
|
|
318
|
+
}
|
|
319
|
+
else
|
|
320
|
+
{
|
|
321
|
+
UAnimPoseExtensions::GetBoneNames(Pose, Sample.BoneNames);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
Sample.Transforms.Reset(Sample.BoneNames.Num());
|
|
325
|
+
for (const FName& BoneName : Sample.BoneNames)
|
|
326
|
+
{
|
|
327
|
+
Sample.Transforms.Add(UAnimPoseExtensions::GetBonePose(Pose, BoneName, Space));
|
|
328
|
+
}
|
|
329
|
+
OutSamples.Add(MoveTemp(Sample));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return nullptr;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Read the caller's frame list, time list, or neither (= every sampled key). */
|
|
336
|
+
TSharedPtr<FJsonValue> ResolvePoseSampleTimes(
|
|
337
|
+
const TSharedPtr<FJsonObject>& Params,
|
|
338
|
+
const FPoseEvaluationTarget& Target,
|
|
339
|
+
TArray<int32>& OutFrames,
|
|
340
|
+
TArray<double>& OutTimes)
|
|
341
|
+
{
|
|
342
|
+
const TArray<TSharedPtr<FJsonValue>>* FramesArray = nullptr;
|
|
343
|
+
const TArray<TSharedPtr<FJsonValue>>* TimesArray = nullptr;
|
|
344
|
+
const bool bHasFrames = Params->TryGetArrayField(TEXT("frames"), FramesArray) && FramesArray;
|
|
345
|
+
const bool bHasTimes = Params->TryGetArrayField(TEXT("times"), TimesArray) && TimesArray;
|
|
346
|
+
|
|
347
|
+
if (bHasFrames && bHasTimes)
|
|
348
|
+
{
|
|
349
|
+
return MCPError(TEXT("Pass 'frames' or 'times', not both: they name the same samples two different ways."));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const double FrameRate = Target.SamplingFrameRate.AsDecimal() > 0.0
|
|
353
|
+
? Target.SamplingFrameRate.AsDecimal()
|
|
354
|
+
: 30.0;
|
|
355
|
+
|
|
356
|
+
if (bHasFrames)
|
|
357
|
+
{
|
|
358
|
+
if (Target.BlendSpace)
|
|
359
|
+
{
|
|
360
|
+
return MCPError(TEXT("A BlendSpace has no frames of its own. Pass 'times' in seconds instead."));
|
|
361
|
+
}
|
|
362
|
+
for (const TSharedPtr<FJsonValue>& Entry : *FramesArray)
|
|
363
|
+
{
|
|
364
|
+
if (!Entry.IsValid()) continue;
|
|
365
|
+
const int32 Frame = static_cast<int32>(Entry->AsNumber());
|
|
366
|
+
if (Frame < 0 || (Target.NumberOfSampledKeys > 0 && Frame >= Target.NumberOfSampledKeys))
|
|
367
|
+
{
|
|
368
|
+
return MCPError(FString::Printf(
|
|
369
|
+
TEXT("frame %d out of range (asset has %d sampled keys)"), Frame, Target.NumberOfSampledKeys));
|
|
370
|
+
}
|
|
371
|
+
OutFrames.Add(Frame);
|
|
372
|
+
OutTimes.Add(static_cast<double>(Frame) / FrameRate);
|
|
373
|
+
}
|
|
374
|
+
if (OutFrames.Num() == 0)
|
|
375
|
+
{
|
|
376
|
+
return MCPError(TEXT("'frames' was empty"));
|
|
377
|
+
}
|
|
378
|
+
return nullptr;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (bHasTimes)
|
|
382
|
+
{
|
|
383
|
+
for (const TSharedPtr<FJsonValue>& Entry : *TimesArray)
|
|
384
|
+
{
|
|
385
|
+
if (!Entry.IsValid()) continue;
|
|
386
|
+
const double Time = FMath::Clamp(Entry->AsNumber(), 0.0, static_cast<double>(Target.PlayLength));
|
|
387
|
+
OutTimes.Add(Time);
|
|
388
|
+
}
|
|
389
|
+
if (OutTimes.Num() == 0)
|
|
390
|
+
{
|
|
391
|
+
return MCPError(TEXT("'times' was empty"));
|
|
392
|
+
}
|
|
393
|
+
return nullptr;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Neither given: every sampled key for a sequence, and a frame-rate walk of
|
|
397
|
+
// the play length for a BlendSpace, which has no keys of its own.
|
|
398
|
+
if (Target.BlendSpace)
|
|
399
|
+
{
|
|
400
|
+
const int32 StepCount = FMath::Max(1, FMath::CeilToInt(Target.PlayLength * 30.0f));
|
|
401
|
+
for (int32 Step = 0; Step < StepCount; ++Step)
|
|
402
|
+
{
|
|
403
|
+
OutTimes.Add(static_cast<double>(Step) / 30.0);
|
|
404
|
+
}
|
|
405
|
+
return nullptr;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const int32 KeyCount = FMath::Max(1, Target.NumberOfSampledKeys);
|
|
409
|
+
for (int32 Frame = 0; Frame < KeyCount; ++Frame)
|
|
410
|
+
{
|
|
411
|
+
OutFrames.Add(Frame);
|
|
412
|
+
OutTimes.Add(static_cast<double>(Frame) / FrameRate);
|
|
413
|
+
}
|
|
414
|
+
return nullptr;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Read boneNames, if given. */
|
|
418
|
+
void ReadRequestedBoneNames(const TSharedPtr<FJsonObject>& Params, TArray<FName>& OutBones)
|
|
419
|
+
{
|
|
420
|
+
const TArray<TSharedPtr<FJsonValue>>* BonesArray = nullptr;
|
|
421
|
+
if (!Params->TryGetArrayField(TEXT("boneNames"), BonesArray) || !BonesArray) return;
|
|
422
|
+
for (const TSharedPtr<FJsonValue>& Entry : *BonesArray)
|
|
423
|
+
{
|
|
424
|
+
if (!Entry.IsValid()) continue;
|
|
425
|
+
const FString Name = Entry->AsString();
|
|
426
|
+
if (!Name.IsEmpty()) OutBones.Add(FName(*Name));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Fill in a BlendSpace's real duration at the requested blend position, so
|
|
431
|
+
* `times` and the default frame walk are in seconds for every asset type. */
|
|
432
|
+
TSharedPtr<FJsonValue> ResolveBlendSpaceLength(FPoseEvaluationTarget& Target, const FVector& BlendInput)
|
|
433
|
+
{
|
|
434
|
+
if (!Target.BlendSpace) return nullptr;
|
|
435
|
+
|
|
436
|
+
TArray<FBlendSampleData> SampleData;
|
|
437
|
+
int32 CachedTriangulationIndex = INDEX_NONE;
|
|
438
|
+
if (!Target.BlendSpace->GetSamplesFromBlendInput(BlendInput, SampleData, CachedTriangulationIndex, /*bCombineAnimations=*/true)
|
|
439
|
+
|| SampleData.Num() == 0)
|
|
440
|
+
{
|
|
441
|
+
return MCPError(FString::Printf(
|
|
442
|
+
TEXT("BlendSpace '%s' produced no samples at blend position (%g, %g, %g). Check the axis ranges with animation(read_blendspace)."),
|
|
443
|
+
*Target.BlendSpace->GetPathName(), BlendInput.X, BlendInput.Y, BlendInput.Z));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
Target.PlayLength = Target.BlendSpace->GetAnimationLengthFromSampleData(SampleData);
|
|
447
|
+
if (Target.PlayLength <= KINDA_SMALL_NUMBER)
|
|
448
|
+
{
|
|
449
|
+
return MCPError(FString::Printf(
|
|
450
|
+
TEXT("BlendSpace '%s' blends to a zero-length animation at blend position (%g, %g, %g)"),
|
|
451
|
+
*Target.BlendSpace->GetPathName(), BlendInput.X, BlendInput.Y, BlendInput.Z));
|
|
452
|
+
}
|
|
453
|
+
return nullptr;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
FVector ReadBlendPosition(const TSharedPtr<FJsonObject>& Params)
|
|
457
|
+
{
|
|
458
|
+
const TSharedPtr<FJsonObject>* PositionObject = nullptr;
|
|
459
|
+
if (Params->TryGetObjectField(TEXT("blendPosition"), PositionObject) && PositionObject && (*PositionObject).IsValid())
|
|
460
|
+
{
|
|
461
|
+
double X = 0.0, Y = 0.0, Z = 0.0;
|
|
462
|
+
(*PositionObject)->TryGetNumberField(TEXT("x"), X);
|
|
463
|
+
(*PositionObject)->TryGetNumberField(TEXT("y"), Y);
|
|
464
|
+
(*PositionObject)->TryGetNumberField(TEXT("z"), Z);
|
|
465
|
+
return FVector(X, Y, Z);
|
|
466
|
+
}
|
|
467
|
+
return FVector(
|
|
468
|
+
OptionalNumber(Params, TEXT("blendX"), 0.0),
|
|
469
|
+
OptionalNumber(Params, TEXT("blendY"), 0.0),
|
|
470
|
+
OptionalNumber(Params, TEXT("blendZ"), 0.0));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
TSharedPtr<FJsonValue> FAnimationHandlers::SamplePose(const TSharedPtr<FJsonObject>& Params)
|
|
476
|
+
{
|
|
477
|
+
FString AssetPath;
|
|
478
|
+
if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), AssetPath)) return Err;
|
|
479
|
+
|
|
480
|
+
FPoseEvaluationTarget Target;
|
|
481
|
+
if (auto Err = ResolvePoseEvaluationTarget(AssetPath, Target)) return Err;
|
|
482
|
+
|
|
483
|
+
const FString SpaceToken = OptionalString(Params, TEXT("space"), TEXT("component")).ToLower();
|
|
484
|
+
EAnimPoseSpaces Space = EAnimPoseSpaces::World;
|
|
485
|
+
FString ResolvedSpace;
|
|
486
|
+
if (!ParsePoseSpace(SpaceToken, Space, ResolvedSpace))
|
|
487
|
+
{
|
|
488
|
+
return MCPError(TEXT("space must be 'component' (default), 'local', or 'world'"));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
USkeletalMesh* OptionalMesh = nullptr;
|
|
492
|
+
const FString SkeletalMeshPath = OptionalString(Params, TEXT("skeletalMeshPath"));
|
|
493
|
+
if (!SkeletalMeshPath.IsEmpty())
|
|
494
|
+
{
|
|
495
|
+
OptionalMesh = LoadAssetByPath<USkeletalMesh>(SkeletalMeshPath);
|
|
496
|
+
if (!OptionalMesh)
|
|
497
|
+
{
|
|
498
|
+
return MCPError(FString::Printf(TEXT("SkeletalMesh not found: %s"), *SkeletalMeshPath));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const bool bIncorporateRootMotion = OptionalBool(Params, TEXT("incorporateRootMotion"), true);
|
|
503
|
+
const FAnimPoseEvaluationOptions Options = MakePoseEvaluationOptions(OptionalMesh, bIncorporateRootMotion);
|
|
504
|
+
|
|
505
|
+
TArray<FName> RequestedBones;
|
|
506
|
+
ReadRequestedBoneNames(Params, RequestedBones);
|
|
507
|
+
|
|
508
|
+
const FVector BlendInput = ReadBlendPosition(Params);
|
|
509
|
+
if (auto Err = ResolveBlendSpaceLength(Target, BlendInput)) return Err;
|
|
510
|
+
|
|
511
|
+
TArray<int32> Frames;
|
|
512
|
+
TArray<double> Times;
|
|
513
|
+
if (auto Err = ResolvePoseSampleTimes(Params, Target, Frames, Times)) return Err;
|
|
514
|
+
|
|
515
|
+
// Cost gate. A bone count of zero here means "every bone", which is the
|
|
516
|
+
// expensive default, so estimate it from the skeleton before evaluating.
|
|
517
|
+
const int32 BoneCountEstimate = RequestedBones.Num() > 0
|
|
518
|
+
? RequestedBones.Num()
|
|
519
|
+
: (Target.Skeleton ? Target.Skeleton->GetReferenceSkeleton().GetNum() : 0);
|
|
520
|
+
const int64 SampleCount = Frames.Num() > 0 ? Frames.Num() : Times.Num();
|
|
521
|
+
if (BoneCountEstimate > 0 && SampleCount * BoneCountEstimate > PoseSampleTransformCap)
|
|
522
|
+
{
|
|
523
|
+
return MCPError(FString::Printf(
|
|
524
|
+
TEXT("This request would return %lld bone transforms (%lld samples x %d bones), over the %d limit. Narrow it with 'boneNames' or with 'frames' / 'times'."),
|
|
525
|
+
SampleCount * BoneCountEstimate, SampleCount, BoneCountEstimate, PoseSampleTransformCap));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
TArray<FEvaluatedPoseSample> Samples;
|
|
529
|
+
if (auto Err = EvaluatePoseSamples(Target, Frames, Times, RequestedBones, Space, Options, BlendInput, Samples))
|
|
530
|
+
{
|
|
531
|
+
return Err;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
TArray<TSharedPtr<FJsonValue>> SampleObjects;
|
|
535
|
+
SampleObjects.Reserve(Samples.Num());
|
|
536
|
+
for (const FEvaluatedPoseSample& Sample : Samples)
|
|
537
|
+
{
|
|
538
|
+
TSharedPtr<FJsonObject> SampleObj = MakeShared<FJsonObject>();
|
|
539
|
+
if (Sample.Frame != INDEX_NONE) SampleObj->SetNumberField(TEXT("frame"), Sample.Frame);
|
|
540
|
+
SampleObj->SetNumberField(TEXT("time"), Sample.Time);
|
|
541
|
+
|
|
542
|
+
TArray<TSharedPtr<FJsonValue>> Bones;
|
|
543
|
+
Bones.Reserve(Sample.BoneNames.Num());
|
|
544
|
+
for (int32 BoneIndex = 0; BoneIndex < Sample.BoneNames.Num(); ++BoneIndex)
|
|
545
|
+
{
|
|
546
|
+
Bones.Add(MakeShared<FJsonValueObject>(
|
|
547
|
+
PoseBoneTransformToJson(Sample.BoneNames[BoneIndex], Sample.Transforms[BoneIndex])));
|
|
548
|
+
}
|
|
549
|
+
SampleObj->SetArrayField(TEXT("bones"), Bones);
|
|
550
|
+
SampleObjects.Add(MakeShared<FJsonValueObject>(SampleObj));
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
auto Result = MCPSuccess();
|
|
554
|
+
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
|
555
|
+
Result->SetStringField(TEXT("assetType"), Target.AssetType);
|
|
556
|
+
if (Target.Skeleton) Result->SetStringField(TEXT("skeleton"), Target.Skeleton->GetPathName());
|
|
557
|
+
if (OptionalMesh) Result->SetStringField(TEXT("skeletalMeshPath"), OptionalMesh->GetPathName());
|
|
558
|
+
Result->SetNumberField(TEXT("playLength"), Target.PlayLength);
|
|
559
|
+
Result->SetNumberField(TEXT("sampledKeyCount"), Target.NumberOfSampledKeys);
|
|
560
|
+
Result->SetNumberField(TEXT("frameRate"), Target.SamplingFrameRate.AsDecimal());
|
|
561
|
+
Result->SetBoolField(TEXT("hasRootMotion"), Target.bHasRootMotion);
|
|
562
|
+
Result->SetBoolField(TEXT("incorporateRootMotion"), bIncorporateRootMotion);
|
|
563
|
+
Result->SetStringField(TEXT("space"), ResolvedSpace);
|
|
564
|
+
if (ResolvedSpace != SpaceToken)
|
|
565
|
+
{
|
|
566
|
+
Result->SetStringField(TEXT("requestedSpace"), SpaceToken);
|
|
567
|
+
Result->SetStringField(TEXT("spaceResolution"),
|
|
568
|
+
TEXT("An animation asset is authored relative to its skeleton root, so component space is the outermost frame it has and 'world' resolves to it. For a placed character use animation(get_live_bone_transforms)."));
|
|
569
|
+
}
|
|
570
|
+
if (Target.BlendSpace)
|
|
571
|
+
{
|
|
572
|
+
Result->SetObjectField(TEXT("blendPosition"), MCPVec3ToJsonObject(BlendInput));
|
|
573
|
+
}
|
|
574
|
+
Result->SetNumberField(TEXT("sampleCount"), SampleObjects.Num());
|
|
575
|
+
Result->SetNumberField(TEXT("boneCount"), Samples.Num() > 0 ? Samples[0].BoneNames.Num() : 0);
|
|
576
|
+
Result->SetArrayField(TEXT("samples"), SampleObjects);
|
|
577
|
+
return MCPResult(Result);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
TSharedPtr<FJsonValue> FAnimationHandlers::MeasureNaturalSpeed(const TSharedPtr<FJsonObject>& Params)
|
|
582
|
+
{
|
|
583
|
+
FString AssetPath;
|
|
584
|
+
if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), AssetPath)) return Err;
|
|
585
|
+
|
|
586
|
+
TArray<FName> FootBones;
|
|
587
|
+
{
|
|
588
|
+
const TArray<TSharedPtr<FJsonValue>>* FootArray = nullptr;
|
|
589
|
+
if (!Params->TryGetArrayField(TEXT("footBones"), FootArray) || !FootArray || FootArray->Num() == 0)
|
|
590
|
+
{
|
|
591
|
+
return MCPError(TEXT("Missing required parameter 'footBones' (array of bone names, e.g. [\"foot_l\",\"foot_r\"])"));
|
|
592
|
+
}
|
|
593
|
+
for (const TSharedPtr<FJsonValue>& Entry : *FootArray)
|
|
594
|
+
{
|
|
595
|
+
if (!Entry.IsValid()) continue;
|
|
596
|
+
const FString Name = Entry->AsString();
|
|
597
|
+
if (!Name.IsEmpty()) FootBones.Add(FName(*Name));
|
|
598
|
+
}
|
|
599
|
+
if (FootBones.Num() == 0)
|
|
600
|
+
{
|
|
601
|
+
return MCPError(TEXT("'footBones' held no usable bone names"));
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
FPoseEvaluationTarget Target;
|
|
606
|
+
if (auto Err = ResolvePoseEvaluationTarget(AssetPath, Target)) return Err;
|
|
607
|
+
|
|
608
|
+
if (Target.Skeleton)
|
|
609
|
+
{
|
|
610
|
+
const FReferenceSkeleton& RefSkeleton = Target.Skeleton->GetReferenceSkeleton();
|
|
611
|
+
for (const FName& Bone : FootBones)
|
|
612
|
+
{
|
|
613
|
+
if (RefSkeleton.FindBoneIndex(Bone) == INDEX_NONE)
|
|
614
|
+
{
|
|
615
|
+
return MCPError(FString::Printf(
|
|
616
|
+
TEXT("Bone '%s' is not on skeleton '%s'. Use animation(get_skeleton_info) for the bone list."),
|
|
617
|
+
*Bone.ToString(), *Target.Skeleton->GetPathName()));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
USkeletalMesh* OptionalMesh = nullptr;
|
|
623
|
+
const FString SkeletalMeshPath = OptionalString(Params, TEXT("skeletalMeshPath"));
|
|
624
|
+
if (!SkeletalMeshPath.IsEmpty())
|
|
625
|
+
{
|
|
626
|
+
OptionalMesh = LoadAssetByPath<USkeletalMesh>(SkeletalMeshPath);
|
|
627
|
+
if (!OptionalMesh)
|
|
628
|
+
{
|
|
629
|
+
return MCPError(FString::Printf(TEXT("SkeletalMesh not found: %s"), *SkeletalMeshPath));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Root motion stays out of the pose here on purpose. The measurement asks
|
|
634
|
+
// how far the body travels relative to the ground the foot is planted on,
|
|
635
|
+
// which is what an in-place locomotion clip encodes as backwards foot slide;
|
|
636
|
+
// folding root motion in would move the whole pose with the character and
|
|
637
|
+
// leave a planted foot reading as stationary.
|
|
638
|
+
const FAnimPoseEvaluationOptions Options = MakePoseEvaluationOptions(OptionalMesh, /*bIncorporateRootMotion=*/false);
|
|
639
|
+
|
|
640
|
+
const FVector BlendInput = ReadBlendPosition(Params);
|
|
641
|
+
if (auto Err = ResolveBlendSpaceLength(Target, BlendInput)) return Err;
|
|
642
|
+
|
|
643
|
+
TArray<int32> Frames;
|
|
644
|
+
TArray<double> Times;
|
|
645
|
+
if (auto Err = ResolvePoseSampleTimes(Params, Target, Frames, Times)) return Err;
|
|
646
|
+
if (Times.Num() < 2)
|
|
647
|
+
{
|
|
648
|
+
return MCPError(TEXT("Measuring speed needs at least two samples; this asset resolved to fewer."));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
TArray<FEvaluatedPoseSample> Samples;
|
|
652
|
+
if (auto Err = EvaluatePoseSamples(Target, Frames, Times, FootBones, EAnimPoseSpaces::World, Options, BlendInput, Samples))
|
|
653
|
+
{
|
|
654
|
+
return Err;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Contact threshold: an explicit height in Unreal units, or derived from the
|
|
658
|
+
// clip itself as the lowest foot height plus a margin, which is what makes
|
|
659
|
+
// this work across skeletons of different scale without being told the scale.
|
|
660
|
+
const bool bHasExplicitThreshold = Params->HasField(TEXT("contactThreshold"));
|
|
661
|
+
double LowestFootHeight = TNumericLimits<double>::Max();
|
|
662
|
+
double HighestFootHeight = -TNumericLimits<double>::Max();
|
|
663
|
+
for (const FEvaluatedPoseSample& Sample : Samples)
|
|
664
|
+
{
|
|
665
|
+
for (const FTransform& Transform : Sample.Transforms)
|
|
666
|
+
{
|
|
667
|
+
LowestFootHeight = FMath::Min(LowestFootHeight, Transform.GetLocation().Z);
|
|
668
|
+
HighestFootHeight = FMath::Max(HighestFootHeight, Transform.GetLocation().Z);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const double DerivedMargin = FMath::Max(1.0, (HighestFootHeight - LowestFootHeight) * 0.15);
|
|
672
|
+
const double ContactThreshold = bHasExplicitThreshold
|
|
673
|
+
? OptionalNumber(Params, TEXT("contactThreshold"), 0.0)
|
|
674
|
+
: LowestFootHeight + DerivedMargin;
|
|
675
|
+
|
|
676
|
+
double PlantedDistance = 0.0;
|
|
677
|
+
double PlantedTime = 0.0;
|
|
678
|
+
double TotalDistance = 0.0;
|
|
679
|
+
double TotalTime = 0.0;
|
|
680
|
+
int32 PlantedIntervalCount = 0;
|
|
681
|
+
|
|
682
|
+
TArray<double> PerBoneDistance;
|
|
683
|
+
TArray<double> PerBoneTime;
|
|
684
|
+
PerBoneDistance.SetNumZeroed(FootBones.Num());
|
|
685
|
+
PerBoneTime.SetNumZeroed(FootBones.Num());
|
|
686
|
+
|
|
687
|
+
for (int32 SampleIndex = 1; SampleIndex < Samples.Num(); ++SampleIndex)
|
|
688
|
+
{
|
|
689
|
+
const FEvaluatedPoseSample& Previous = Samples[SampleIndex - 1];
|
|
690
|
+
const FEvaluatedPoseSample& Current = Samples[SampleIndex];
|
|
691
|
+
const double DeltaTime = Current.Time - Previous.Time;
|
|
692
|
+
if (DeltaTime <= KINDA_SMALL_NUMBER) continue;
|
|
693
|
+
TotalTime += DeltaTime;
|
|
694
|
+
|
|
695
|
+
// The planted foot for this interval is the one lowest to the ground
|
|
696
|
+
// across both ends of it. Ties go to the first, which is stable.
|
|
697
|
+
int32 BestBone = INDEX_NONE;
|
|
698
|
+
double BestHeight = TNumericLimits<double>::Max();
|
|
699
|
+
for (int32 BoneIndex = 0; BoneIndex < FootBones.Num(); ++BoneIndex)
|
|
700
|
+
{
|
|
701
|
+
if (!Previous.Transforms.IsValidIndex(BoneIndex) || !Current.Transforms.IsValidIndex(BoneIndex)) continue;
|
|
702
|
+
const double Height = FMath::Max(
|
|
703
|
+
Previous.Transforms[BoneIndex].GetLocation().Z,
|
|
704
|
+
Current.Transforms[BoneIndex].GetLocation().Z);
|
|
705
|
+
if (Height < BestHeight)
|
|
706
|
+
{
|
|
707
|
+
BestHeight = Height;
|
|
708
|
+
BestBone = BoneIndex;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (BestBone == INDEX_NONE) continue;
|
|
712
|
+
|
|
713
|
+
const FVector From = Previous.Transforms[BestBone].GetLocation();
|
|
714
|
+
const FVector To = Current.Transforms[BestBone].GetLocation();
|
|
715
|
+
// Horizontal only: a foot rising and falling is not the body travelling.
|
|
716
|
+
const double Distance = FVector2D(To.X - From.X, To.Y - From.Y).Size();
|
|
717
|
+
TotalDistance += Distance;
|
|
718
|
+
|
|
719
|
+
if (BestHeight <= ContactThreshold)
|
|
720
|
+
{
|
|
721
|
+
PlantedDistance += Distance;
|
|
722
|
+
PlantedTime += DeltaTime;
|
|
723
|
+
PerBoneDistance[BestBone] += Distance;
|
|
724
|
+
PerBoneTime[BestBone] += DeltaTime;
|
|
725
|
+
++PlantedIntervalCount;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const double NaturalSpeed = PlantedTime > KINDA_SMALL_NUMBER ? PlantedDistance / PlantedTime : 0.0;
|
|
730
|
+
const double AverageSpeed = TotalTime > KINDA_SMALL_NUMBER ? TotalDistance / TotalTime : 0.0;
|
|
731
|
+
|
|
732
|
+
TArray<TSharedPtr<FJsonValue>> PerBone;
|
|
733
|
+
for (int32 BoneIndex = 0; BoneIndex < FootBones.Num(); ++BoneIndex)
|
|
734
|
+
{
|
|
735
|
+
TSharedPtr<FJsonObject> BoneObj = MakeShared<FJsonObject>();
|
|
736
|
+
BoneObj->SetStringField(TEXT("name"), FootBones[BoneIndex].ToString());
|
|
737
|
+
BoneObj->SetNumberField(TEXT("plantedDistance"), PerBoneDistance[BoneIndex]);
|
|
738
|
+
BoneObj->SetNumberField(TEXT("plantedTime"), PerBoneTime[BoneIndex]);
|
|
739
|
+
BoneObj->SetNumberField(TEXT("speed"),
|
|
740
|
+
PerBoneTime[BoneIndex] > KINDA_SMALL_NUMBER ? PerBoneDistance[BoneIndex] / PerBoneTime[BoneIndex] : 0.0);
|
|
741
|
+
PerBone.Add(MakeShared<FJsonValueObject>(BoneObj));
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
auto Result = MCPSuccess();
|
|
745
|
+
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
|
746
|
+
Result->SetStringField(TEXT("assetType"), Target.AssetType);
|
|
747
|
+
if (Target.Skeleton) Result->SetStringField(TEXT("skeleton"), Target.Skeleton->GetPathName());
|
|
748
|
+
if (OptionalMesh) Result->SetStringField(TEXT("skeletalMeshPath"), OptionalMesh->GetPathName());
|
|
749
|
+
Result->SetNumberField(TEXT("naturalSpeed"), NaturalSpeed);
|
|
750
|
+
Result->SetStringField(TEXT("naturalSpeedUnits"), TEXT("cm/s (Unreal units per second)"));
|
|
751
|
+
Result->SetNumberField(TEXT("averageFootSpeed"), AverageSpeed);
|
|
752
|
+
Result->SetNumberField(TEXT("plantedDistance"), PlantedDistance);
|
|
753
|
+
Result->SetNumberField(TEXT("plantedTime"), PlantedTime);
|
|
754
|
+
Result->SetNumberField(TEXT("plantedIntervalCount"), PlantedIntervalCount);
|
|
755
|
+
Result->SetNumberField(TEXT("intervalCount"), FMath::Max(0, Samples.Num() - 1));
|
|
756
|
+
Result->SetNumberField(TEXT("contactThreshold"), ContactThreshold);
|
|
757
|
+
Result->SetBoolField(TEXT("contactThresholdWasDerived"), !bHasExplicitThreshold);
|
|
758
|
+
Result->SetNumberField(TEXT("lowestFootHeight"), Samples.Num() > 0 ? LowestFootHeight : 0.0);
|
|
759
|
+
Result->SetNumberField(TEXT("highestFootHeight"), Samples.Num() > 0 ? HighestFootHeight : 0.0);
|
|
760
|
+
Result->SetNumberField(TEXT("playLength"), Target.PlayLength);
|
|
761
|
+
Result->SetNumberField(TEXT("sampleCount"), Samples.Num());
|
|
762
|
+
Result->SetBoolField(TEXT("hasRootMotion"), Target.bHasRootMotion);
|
|
763
|
+
Result->SetArrayField(TEXT("footBones"), PerBone);
|
|
764
|
+
if (Target.BlendSpace)
|
|
765
|
+
{
|
|
766
|
+
Result->SetObjectField(TEXT("blendPosition"), MCPVec3ToJsonObject(BlendInput));
|
|
767
|
+
}
|
|
768
|
+
return MCPResult(Result);
|
|
769
|
+
}
|