ue-mcp 1.2.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/action-class.js +56 -3
- package/dist/action-class.js.map +1 -1
- package/dist/bridge-timeouts.d.ts +64 -0
- package/dist/bridge-timeouts.js +118 -0
- package/dist/bridge-timeouts.js.map +1 -0
- package/dist/bridge.js +10 -2
- package/dist/bridge.js.map +1 -1
- package/dist/editor-control.d.ts +72 -5
- package/dist/editor-control.js +164 -140
- package/dist/editor-control.js.map +1 -1
- package/dist/editor-target.d.ts +55 -1
- package/dist/editor-target.js +107 -10
- package/dist/editor-target.js.map +1 -1
- package/dist/engine-observer.d.ts +59 -5
- package/dist/engine-observer.js +249 -29
- package/dist/engine-observer.js.map +1 -1
- package/dist/engine-root.d.ts +91 -0
- package/dist/engine-root.js +448 -0
- package/dist/engine-root.js.map +1 -0
- package/dist/epic-enrich.js +16 -1
- package/dist/epic-enrich.js.map +1 -1
- package/dist/feedback-cli.js +4 -0
- package/dist/feedback-cli.js.map +1 -1
- package/dist/feedback-deferred.d.ts +9 -0
- package/dist/feedback-deferred.js +2 -0
- package/dist/feedback-deferred.js.map +1 -1
- package/dist/feedback-fallback.d.ts +99 -0
- package/dist/feedback-fallback.js +173 -0
- package/dist/feedback-fallback.js.map +1 -0
- package/dist/flow/write-methods.js +20 -0
- package/dist/flow/write-methods.js.map +1 -1
- package/dist/index.js +41 -2
- package/dist/index.js.map +1 -1
- package/dist/lean-context.js +8 -3
- package/dist/lean-context.js.map +1 -1
- package/dist/locking.js +11 -2
- package/dist/locking.js.map +1 -1
- package/dist/plugin/loader.d.ts +4 -0
- package/dist/plugin/loader.js +22 -8
- package/dist/plugin/loader.js.map +1 -1
- package/dist/plugin/manifest.d.ts +72 -52
- package/dist/plugin/manifest.js +118 -21
- package/dist/plugin/manifest.js.map +1 -1
- package/dist/plugin-cli.js +16 -3
- package/dist/plugin-cli.js.map +1 -1
- package/dist/project.d.ts +11 -0
- package/dist/project.js +23 -2
- package/dist/project.js.map +1 -1
- package/dist/python-gate.d.ts +95 -0
- package/dist/python-gate.js +175 -0
- package/dist/python-gate.js.map +1 -0
- package/dist/tool-counts.json +16 -16
- package/dist/tools/animation.js +288 -24
- package/dist/tools/animation.js.map +1 -1
- package/dist/tools/asset.js +79 -14
- package/dist/tools/asset.js.map +1 -1
- package/dist/tools/audio.js +59 -5
- package/dist/tools/audio.js.map +1 -1
- package/dist/tools/blueprint.js +29 -12
- package/dist/tools/blueprint.js.map +1 -1
- package/dist/tools/chooser.js +4 -1
- package/dist/tools/chooser.js.map +1 -1
- package/dist/tools/editor.js +81 -43
- package/dist/tools/editor.js.map +1 -1
- package/dist/tools/feedback.js +214 -48
- package/dist/tools/feedback.js.map +1 -1
- package/dist/tools/foliage.js +27 -1
- package/dist/tools/foliage.js.map +1 -1
- package/dist/tools/gameplay.js +38 -11
- package/dist/tools/gameplay.js.map +1 -1
- package/dist/tools/gas.js +17 -5
- package/dist/tools/gas.js.map +1 -1
- package/dist/tools/landscape.js +22 -7
- package/dist/tools/landscape.js.map +1 -1
- package/dist/tools/level.js +322 -51
- package/dist/tools/level.js.map +1 -1
- package/dist/tools/material.js +10 -4
- package/dist/tools/material.js.map +1 -1
- package/dist/tools/niagara.js +3 -2
- package/dist/tools/niagara.js.map +1 -1
- package/dist/tools/pcg.js +6 -5
- package/dist/tools/pcg.js.map +1 -1
- package/dist/tools/plugins.js +2 -0
- package/dist/tools/plugins.js.map +1 -1
- package/dist/tools/project.js +47 -20
- package/dist/tools/project.js.map +1 -1
- package/dist/types.d.ts +24 -0
- package/dist/types.js +46 -3
- package/dist/types.js.map +1 -1
- package/dist/ue-mcp.default.yml +493 -125
- package/package.json +2 -2
- package/plugin/ue_mcp_bridge/NATIVE_RUNTIME_VISIBILITY.md +53 -0
- package/plugin/ue_mcp_bridge/NATIVE_STATIC_MESH_USAGE_PROGRESS.md +115 -0
- package/plugin/ue_mcp_bridge/README_MassHandlers.md +97 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/BridgeServer.cpp +14 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.cpp +21 -4
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.h +9 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerJsonProperty.h +395 -69
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.cpp +79 -3
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.h +25 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRig.cpp +3 -3
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRigSequencer.cpp +3061 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRetargeterAuthoring.cpp +961 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRigAuthoring.cpp +900 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Pose.cpp +769 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Sequence.cpp +44 -3
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_SkeletalLive.cpp +217 -11
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_StateMachine.cpp +551 -24
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Validation.cpp +735 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.cpp +352 -73
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.h +8 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkProperties.cpp +1 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.cpp +503 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.h +21 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkUpsert.cpp +2 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Enum.cpp +2 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.cpp +902 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.h +33 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Import.cpp +478 -134
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Mesh.cpp +5 -5
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.cpp +933 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.h +35 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Redirectors.cpp +478 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_RenderTarget.cpp +1 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Sockets.cpp +4 -4
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Struct.cpp +2 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Subobject.cpp +319 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AudioHandlers_SoundCue.cpp +3 -3
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.cpp +482 -35
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.h +10 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.cpp +502 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.h +43 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Functions.cpp +89 -7
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Graph.cpp +17 -51
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Internal.h +107 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Properties.cpp +10 -10
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Search.cpp +648 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DemoHandlers.cpp +5 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.cpp +252 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.h +10 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.cpp +252 -41
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.h +4 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_Build.cpp +250 -27
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIE.cpp +303 -125
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIERuntime.cpp +73 -50
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_RuntimeVisibility.cpp +757 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.cpp +11 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.h +3 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers_Batch.cpp +419 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.cpp +39 -161
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.h +66 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BTAuthoring.cpp +1323 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BehaviorTree.cpp +1211 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.cpp +2 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.h +60 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers_Runtime.cpp +363 -12
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/HandlerEditorState.h +84 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +1288 -36
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.h +3 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers_Sculpt.cpp +41 -16
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.cpp +959 -213
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.h +66 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_BatchWrite.cpp +1571 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Convert.cpp +316 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Inspection.cpp +280 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection.cpp +666 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection_Internal.h +63 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Lights.cpp +43 -12
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_MultiLevel.cpp +13 -47
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_PostProcess.cpp +527 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Query.cpp +1169 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Refresh.cpp +723 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Save.cpp +325 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Spatial.cpp +360 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Trace.cpp +178 -58
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Transient.cpp +337 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Volumes.cpp +25 -12
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartition.cpp +10 -9
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartitionSettings.cpp +686 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.cpp +467 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.h +22 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.cpp +452 -175
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.h +5 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Build.cpp +597 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NiagaraHandlers.cpp +17 -15
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PCGHandlers.cpp +38 -168
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +39 -30
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.cpp +392 -78
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.h +2 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers_Internal.h +83 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.cpp +200 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.h +15 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.cpp +19 -12
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/StateTreeHandlers.cpp +31 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.cpp +307 -69
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.h +84 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Extraction.cpp +8 -41
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Properties.cpp +33 -52
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/ActorSelectorTests.cpp +344 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationControlRigTimelineTests.cpp +60 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationPoseTests.cpp +115 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetBulkReadTests.cpp +177 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetDeleteGuardTests.cpp +276 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPathResolutionTests.cpp +243 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPropertyPersistTests.cpp +213 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetSubobjectTests.cpp +235 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AutomationRunnerTests.cpp +72 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeAuthoringTests.cpp +526 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeNodeTests.cpp +334 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BlueprintHandlerSurfaceTests.cpp +362 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BridgeProtocolTests.cpp +14 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/CollisionQueryTests.cpp +288 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/DataTableRowWriteTests.cpp +453 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/EditorFunctionCallTests.cpp +223 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/FoliageBatchTests.cpp +147 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GameThreadGateTests.cpp +82 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GasLiveAttributeTests.cpp +183 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LandscapeSampleTests.cpp +119 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelBatchWriteTests.cpp +176 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelComponentTreeInstanceCountTests.cpp +189 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelConvertBrushesTests.cpp +100 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelInstanceProjectionTests.cpp +265 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelPostProcessOverrideTests.cpp +258 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelQueryComponentsTests.cpp +195 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelRefreshTests.cpp +142 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelSaveReportTests.cpp +125 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelScriptHandlersTests.cpp +42 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelStaticMeshUsageTests.cpp +412 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTraceWorldScopeTests.cpp +133 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTransientActorTests.cpp +205 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelWorldPartitionSettingsTests.cpp +168 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTestTypes.h +51 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTests.cpp +379 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MaterialBuildTests.cpp +238 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshBooleanTests.cpp +170 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshGeometryTests.cpp +116 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageSaveExtensionTests.cpp +161 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageWriteGuardTests.cpp +253 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/RuntimeVisibilityTests.cpp +143 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerHandlerTests.cpp +660 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerScrubTests.cpp +50 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SkeletonDiffTests.cpp +685 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/WidgetBlueprintResolveTests.cpp +145 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/UE_MCP_Bridge.cpp +80 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerFunctionCall.h +509 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerQuery.h +407 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerUtils.h +1008 -77
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/UE_MCP_Bridge.Build.cs +87 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/MCPEngineStatus.cpp +152 -14
- package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/Tests/MCPEngineStatusTests.cpp +65 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Public/MCPEngineStatus.h +32 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/UE_MCP_BridgeStatus.Build.cs +2 -0
- package/skills/ue-mcp-animation/SKILL.md +154 -0
- package/skills/ue-mcp-blueprint/SKILL.md +71 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// Saving the current level, reconciled with editor(save_dirty) (#964).
|
|
2
|
+
//
|
|
3
|
+
// level(save) used to be ULevelEditorSubsystem::SaveCurrentLevel() and a bare
|
|
4
|
+
// boolean: on false it answered {"success": false, "error": "Failed to save
|
|
5
|
+
// current level"} and said nothing else. It returned that twice in a row on a
|
|
6
|
+
// genuinely dirty map that editor(save_dirty, includeMaps=true) then wrote
|
|
7
|
+
// seconds later without complaint, and the engine log showed SavePackage work
|
|
8
|
+
// running for the package the whole time.
|
|
9
|
+
//
|
|
10
|
+
// A false failure is the dangerous half. An agent that believes a save did not
|
|
11
|
+
// happen redoes the work that was in fact written, and the second pass can undo
|
|
12
|
+
// or duplicate it. So this action now takes the same path save_dirty takes -
|
|
13
|
+
// resolve the package's on-disk filename, call UPackage::Save, read the dirty
|
|
14
|
+
// flag back - and the two can no longer disagree about the same package.
|
|
15
|
+
//
|
|
16
|
+
// The second half is the diagnostic. The old error named neither the package it
|
|
17
|
+
// tried nor why it stopped, which is why the report could not say whether the
|
|
18
|
+
// cause was source control, a read-only file, an I/O error or something that
|
|
19
|
+
// was not the save at all. Every package now comes back with its name, its
|
|
20
|
+
// resolved file, whether that file exists and is read-only, the
|
|
21
|
+
// ESavePackageResult the engine returned, and the error and warning lines the
|
|
22
|
+
// save itself emitted.
|
|
23
|
+
//
|
|
24
|
+
// On a World Partition or one-file-per-actor map the level's own package is
|
|
25
|
+
// only part of the answer: the actors live in their own packages and are what
|
|
26
|
+
// is usually dirty. Those are saved too, each reported separately, which is
|
|
27
|
+
// also why a caller can see that the map package was already clean rather than
|
|
28
|
+
// reading that as a failure.
|
|
29
|
+
//
|
|
30
|
+
// Translation-unit partition of FLevelHandlers - registration lives in
|
|
31
|
+
// LevelHandlers.cpp::RegisterHandlers.
|
|
32
|
+
|
|
33
|
+
#include "LevelHandlers.h"
|
|
34
|
+
#include "HandlerRegistry.h"
|
|
35
|
+
#include "HandlerUtils.h"
|
|
36
|
+
|
|
37
|
+
#include "Editor.h"
|
|
38
|
+
#include "Engine/Level.h"
|
|
39
|
+
#include "Engine/World.h"
|
|
40
|
+
#include "HAL/FileManager.h"
|
|
41
|
+
#include "Misc/OutputDevice.h"
|
|
42
|
+
// GError and GLog are declared as pointers in CoreGlobals; forwarding a line to
|
|
43
|
+
// either one needs the complete type.
|
|
44
|
+
#include "Misc/OutputDeviceError.h"
|
|
45
|
+
#include "Misc/OutputDeviceRedirector.h"
|
|
46
|
+
#include "Misc/PackageName.h"
|
|
47
|
+
#include "UObject/Package.h"
|
|
48
|
+
#include "UObject/SavePackage.h"
|
|
49
|
+
#include "Dom/JsonObject.h"
|
|
50
|
+
#include "Dom/JsonValue.h"
|
|
51
|
+
|
|
52
|
+
namespace
|
|
53
|
+
{
|
|
54
|
+
/**
|
|
55
|
+
* Collects the error and warning lines one SavePackage emits, and passes
|
|
56
|
+
* them on to the normal log so nothing is swallowed. Passed as
|
|
57
|
+
* FSavePackageArgs::Error rather than hooked onto GLog, so only this save's
|
|
58
|
+
* own output is captured and unrelated engine chatter cannot be reported as
|
|
59
|
+
* the reason a level would not write.
|
|
60
|
+
*/
|
|
61
|
+
class FMCPSavePackageDiagnostics : public FOutputDevice
|
|
62
|
+
{
|
|
63
|
+
public:
|
|
64
|
+
virtual void Serialize(const TCHAR* Message, ELogVerbosity::Type Verbosity, const FName& Category) override
|
|
65
|
+
{
|
|
66
|
+
const ELogVerbosity::Type Masked =
|
|
67
|
+
static_cast<ELogVerbosity::Type>(Verbosity & ELogVerbosity::VerbosityMask);
|
|
68
|
+
// Pass everything on so capturing the save's output does not
|
|
69
|
+
// silence the log. Fatal keeps going to GError, which is where the
|
|
70
|
+
// engine's own error handling lives.
|
|
71
|
+
if (Masked == ELogVerbosity::Fatal)
|
|
72
|
+
{
|
|
73
|
+
if (GError) GError->Serialize(Message, Verbosity, Category);
|
|
74
|
+
}
|
|
75
|
+
else if (GLog)
|
|
76
|
+
{
|
|
77
|
+
GLog->Serialize(Message, Verbosity, Category);
|
|
78
|
+
}
|
|
79
|
+
if (Masked > ELogVerbosity::Warning) return;
|
|
80
|
+
if (Lines.Num() >= 16) return;
|
|
81
|
+
Lines.Add(FString::Printf(TEXT("%s: %s"), *Category.ToString(), Message));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
TArray<TSharedPtr<FJsonValue>> ToJson() const
|
|
85
|
+
{
|
|
86
|
+
TArray<TSharedPtr<FJsonValue>> Out;
|
|
87
|
+
for (const FString& Line : Lines) Out.Add(MakeShared<FJsonValueString>(Line));
|
|
88
|
+
return Out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
TArray<FString> Lines;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** ESavePackageResult as a name a caller can act on. */
|
|
95
|
+
FString MCPDescribeSavePackageResult(ESavePackageResult Result)
|
|
96
|
+
{
|
|
97
|
+
// Deliberately not an exhaustive switch: several enumerators are
|
|
98
|
+
// deprecated and naming them would compile with warnings for no gain.
|
|
99
|
+
switch (Result)
|
|
100
|
+
{
|
|
101
|
+
case ESavePackageResult::Success: return TEXT("Success");
|
|
102
|
+
case ESavePackageResult::Error: return TEXT("Error");
|
|
103
|
+
case ESavePackageResult::Canceled: return TEXT("Canceled");
|
|
104
|
+
case ESavePackageResult::ContainsEditorOnlyData: return TEXT("ContainsEditorOnlyData");
|
|
105
|
+
case ESavePackageResult::MissingFile: return TEXT("MissingFile");
|
|
106
|
+
case ESavePackageResult::ValidatorError: return TEXT("ValidatorError");
|
|
107
|
+
case ESavePackageResult::ValidatorSuppress: return TEXT("ValidatorSuppress");
|
|
108
|
+
case ESavePackageResult::Timeout: return TEXT("Timeout");
|
|
109
|
+
default: return TEXT("Other");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Plain-language cause for a failed save, from what can be observed. */
|
|
114
|
+
FString MCPExplainLevelSaveFailure(
|
|
115
|
+
const FString& FileName,
|
|
116
|
+
bool bFileExists,
|
|
117
|
+
bool bReadOnly,
|
|
118
|
+
ESavePackageResult Result)
|
|
119
|
+
{
|
|
120
|
+
if (bReadOnly)
|
|
121
|
+
{
|
|
122
|
+
return FString::Printf(
|
|
123
|
+
TEXT("'%s' is read-only on disk. Under source control this means the file is not checked out; otherwise clear the read-only attribute."),
|
|
124
|
+
*FileName);
|
|
125
|
+
}
|
|
126
|
+
if (Result == ESavePackageResult::Canceled)
|
|
127
|
+
{
|
|
128
|
+
return TEXT("The save was cancelled, which in the editor usually means a checkout or overwrite prompt was declined.");
|
|
129
|
+
}
|
|
130
|
+
if (Result == ESavePackageResult::ValidatorError || Result == ESavePackageResult::ValidatorSuppress)
|
|
131
|
+
{
|
|
132
|
+
return TEXT("A save validator rejected the package. Its reason is in the captured log lines.");
|
|
133
|
+
}
|
|
134
|
+
if (!bFileExists)
|
|
135
|
+
{
|
|
136
|
+
return FString::Printf(
|
|
137
|
+
TEXT("'%s' does not exist yet and could not be created. Check the directory exists and is writable."),
|
|
138
|
+
*FileName);
|
|
139
|
+
}
|
|
140
|
+
return TEXT("SavePackage reported a failure. The captured log lines carry the engine's own reason.");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Save one package and report exactly what happened to it. */
|
|
144
|
+
TSharedPtr<FJsonObject> MCPSaveLevelPackage(UPackage* Package, bool bForce, bool& bOutFailed, bool& bOutWrote)
|
|
145
|
+
{
|
|
146
|
+
bOutFailed = false;
|
|
147
|
+
bOutWrote = false;
|
|
148
|
+
|
|
149
|
+
TSharedPtr<FJsonObject> Entry = MakeShared<FJsonObject>();
|
|
150
|
+
if (!Package)
|
|
151
|
+
{
|
|
152
|
+
Entry->SetBoolField(TEXT("saved"), false);
|
|
153
|
+
Entry->SetStringField(TEXT("error"), TEXT("null package"));
|
|
154
|
+
bOutFailed = true;
|
|
155
|
+
return Entry;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const FString PackageName = Package->GetName();
|
|
159
|
+
Entry->SetStringField(TEXT("package"), PackageName);
|
|
160
|
+
Entry->SetBoolField(TEXT("isMap"), IsMapPackage(Package));
|
|
161
|
+
const bool bWasDirty = Package->IsDirty();
|
|
162
|
+
Entry->SetBoolField(TEXT("wasDirty"), bWasDirty);
|
|
163
|
+
|
|
164
|
+
// A clean package is not a failed save. Reporting it as one is how the
|
|
165
|
+
// old boolean turned "nothing to do" into "redo your work".
|
|
166
|
+
if (!bWasDirty && !bForce)
|
|
167
|
+
{
|
|
168
|
+
Entry->SetBoolField(TEXT("saved"), false);
|
|
169
|
+
Entry->SetBoolField(TEXT("skipped"), true);
|
|
170
|
+
Entry->SetStringField(TEXT("reason"), TEXT("already clean; pass force=true to write it anyway"));
|
|
171
|
+
return Entry;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
FString FileName;
|
|
175
|
+
if (!ResolvePackageFileName(Package, FileName))
|
|
176
|
+
{
|
|
177
|
+
Entry->SetBoolField(TEXT("saved"), false);
|
|
178
|
+
Entry->SetStringField(TEXT("error"), FString::Printf(
|
|
179
|
+
TEXT("'%s' has no mounted root, so no on-disk filename could be resolved for it."), *PackageName));
|
|
180
|
+
bOutFailed = true;
|
|
181
|
+
return Entry;
|
|
182
|
+
}
|
|
183
|
+
Entry->SetStringField(TEXT("file"), FileName);
|
|
184
|
+
|
|
185
|
+
IFileManager& FileManager = IFileManager::Get();
|
|
186
|
+
const bool bFileExists = FileManager.FileExists(*FileName);
|
|
187
|
+
const bool bReadOnly = bFileExists && FileManager.IsReadOnly(*FileName);
|
|
188
|
+
Entry->SetBoolField(TEXT("fileExists"), bFileExists);
|
|
189
|
+
Entry->SetBoolField(TEXT("readOnly"), bReadOnly);
|
|
190
|
+
|
|
191
|
+
FMCPSavePackageDiagnostics Diagnostics;
|
|
192
|
+
FSavePackageArgs SaveArgs;
|
|
193
|
+
// The same flags editor(save_dirty) passes, so the two paths cannot
|
|
194
|
+
// write the same package differently.
|
|
195
|
+
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
|
|
196
|
+
SaveArgs.Error = &Diagnostics;
|
|
197
|
+
const FSavePackageResultStruct SaveResult =
|
|
198
|
+
UPackage::Save(Package, nullptr, *FileName, SaveArgs);
|
|
199
|
+
|
|
200
|
+
const FString ResultName = MCPDescribeSavePackageResult(SaveResult.Result);
|
|
201
|
+
Entry->SetStringField(TEXT("result"), ResultName);
|
|
202
|
+
Entry->SetNumberField(TEXT("bytesWritten"), static_cast<double>(SaveResult.TotalFileSize));
|
|
203
|
+
// Read the dirty flag back rather than trusting the return value. This
|
|
204
|
+
// is the reconciliation the issue asked for: the response says what the
|
|
205
|
+
// package's state IS, not what the call claimed.
|
|
206
|
+
const bool bStillDirty = Package->IsDirty();
|
|
207
|
+
Entry->SetBoolField(TEXT("dirtyAfter"), bStillDirty);
|
|
208
|
+
|
|
209
|
+
const bool bOk = IsSuccessful(SaveResult.Result);
|
|
210
|
+
Entry->SetBoolField(TEXT("saved"), bOk);
|
|
211
|
+
if (Diagnostics.Lines.Num() > 0)
|
|
212
|
+
{
|
|
213
|
+
Entry->SetArrayField(TEXT("log"), Diagnostics.ToJson());
|
|
214
|
+
}
|
|
215
|
+
if (!bOk)
|
|
216
|
+
{
|
|
217
|
+
Entry->SetStringField(TEXT("error"), FString::Printf(
|
|
218
|
+
TEXT("Could not save '%s' to '%s' (%s). %s"),
|
|
219
|
+
*PackageName, *FileName, *ResultName,
|
|
220
|
+
*MCPExplainLevelSaveFailure(FileName, bFileExists, bReadOnly, SaveResult.Result)));
|
|
221
|
+
bOutFailed = true;
|
|
222
|
+
return Entry;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
bOutWrote = true;
|
|
226
|
+
if (bStillDirty)
|
|
227
|
+
{
|
|
228
|
+
// Written, but something dirtied it again during the save. Worth
|
|
229
|
+
// saying: it is not a failure and it is not a clean finish either.
|
|
230
|
+
Entry->SetStringField(TEXT("note"),
|
|
231
|
+
TEXT("The package was written but is dirty again; something modified it during the save."));
|
|
232
|
+
}
|
|
233
|
+
return Entry;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
// level(save): save the level currently being edited, and say what happened.
|
|
239
|
+
TSharedPtr<FJsonValue> FLevelHandlers::SaveLevel(const TSharedPtr<FJsonObject>& Params)
|
|
240
|
+
{
|
|
241
|
+
REQUIRE_EDITOR_WORLD(World);
|
|
242
|
+
|
|
243
|
+
// A save during PIE is one of the ways the old action produced a bare
|
|
244
|
+
// false. Naming it is the difference between "stop play and try again" and
|
|
245
|
+
// an agent redoing work it never lost.
|
|
246
|
+
if (GEditor && (GEditor->PlayWorld != nullptr || GEditor->bIsSimulatingInEditor))
|
|
247
|
+
{
|
|
248
|
+
return MCPError(TEXT("Play In Editor is running; stop it before saving the level. Saving during PIE is what produced an unexplained failure before."));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
ULevel* Level = World->GetCurrentLevel();
|
|
252
|
+
if (!Level) Level = World->PersistentLevel;
|
|
253
|
+
if (!Level) return MCPError(TEXT("The editor world has no current level to save"));
|
|
254
|
+
|
|
255
|
+
UPackage* LevelPackage = Level->GetOutermost();
|
|
256
|
+
if (!LevelPackage) return MCPError(TEXT("The current level has no package"));
|
|
257
|
+
|
|
258
|
+
const bool bForce = OptionalBool(Params, TEXT("force"), false);
|
|
259
|
+
const bool bIncludeExternalActors = OptionalBool(Params, TEXT("includeExternalActors"), true);
|
|
260
|
+
|
|
261
|
+
TArray<UPackage*> Packages;
|
|
262
|
+
Packages.Add(LevelPackage);
|
|
263
|
+
|
|
264
|
+
// One-file-per-actor and World Partition maps keep their actors in their own
|
|
265
|
+
// packages. Saving only the map package there writes almost nothing and
|
|
266
|
+
// leaves the actual edits unsaved, which reads as a successful save.
|
|
267
|
+
int32 ExternalConsidered = 0;
|
|
268
|
+
if (bIncludeExternalActors && Level->IsUsingExternalObjects())
|
|
269
|
+
{
|
|
270
|
+
for (UPackage* External : Level->GetLoadedExternalObjectPackages())
|
|
271
|
+
{
|
|
272
|
+
if (!External) continue;
|
|
273
|
+
++ExternalConsidered;
|
|
274
|
+
if (!External->IsDirty() && !bForce) continue;
|
|
275
|
+
Packages.AddUnique(External);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
TArray<TSharedPtr<FJsonValue>> Saved;
|
|
280
|
+
TArray<TSharedPtr<FJsonValue>> Failed;
|
|
281
|
+
TArray<TSharedPtr<FJsonValue>> Skipped;
|
|
282
|
+
for (UPackage* Package : Packages)
|
|
283
|
+
{
|
|
284
|
+
bool bFailed = false;
|
|
285
|
+
bool bWrote = false;
|
|
286
|
+
TSharedPtr<FJsonObject> Entry = MCPSaveLevelPackage(Package, bForce, bFailed, bWrote);
|
|
287
|
+
TSharedPtr<FJsonValue> Value = MakeShared<FJsonValueObject>(Entry);
|
|
288
|
+
if (bFailed) Failed.Add(Value);
|
|
289
|
+
else if (bWrote) Saved.Add(Value);
|
|
290
|
+
else Skipped.Add(Value);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
auto Result = MCPSuccess();
|
|
294
|
+
Result->SetStringField(TEXT("levelName"), World->GetName());
|
|
295
|
+
Result->SetStringField(TEXT("levelPath"), World->GetPathName());
|
|
296
|
+
Result->SetStringField(TEXT("levelPackage"), LevelPackage->GetName());
|
|
297
|
+
Result->SetBoolField(TEXT("usesExternalActors"), Level->IsUsingExternalObjects());
|
|
298
|
+
Result->SetNumberField(TEXT("externalPackagesLoaded"), ExternalConsidered);
|
|
299
|
+
Result->SetNumberField(TEXT("savedCount"), Saved.Num());
|
|
300
|
+
Result->SetNumberField(TEXT("failedCount"), Failed.Num());
|
|
301
|
+
Result->SetNumberField(TEXT("skippedCount"), Skipped.Num());
|
|
302
|
+
Result->SetArrayField(TEXT("saved"), Saved);
|
|
303
|
+
if (Skipped.Num() > 0) Result->SetArrayField(TEXT("skipped"), Skipped);
|
|
304
|
+
|
|
305
|
+
if (Failed.Num() > 0)
|
|
306
|
+
{
|
|
307
|
+
Result->SetBoolField(TEXT("success"), false);
|
|
308
|
+
Result->SetArrayField(TEXT("failed"), Failed);
|
|
309
|
+
// The top-level error names the first package that actually failed,
|
|
310
|
+
// rather than the generic sentence the old action returned.
|
|
311
|
+
const TSharedPtr<FJsonObject> First = Failed[0]->AsObject();
|
|
312
|
+
Result->SetStringField(TEXT("error"), First.IsValid()
|
|
313
|
+
? First->GetStringField(TEXT("error"))
|
|
314
|
+
: TEXT("One or more level packages could not be saved"));
|
|
315
|
+
return MCPResult(Result);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Nothing failed. Say plainly whether anything needed writing, because
|
|
319
|
+
// "already saved" and "just saved" are both successes and only one of them
|
|
320
|
+
// means work was done.
|
|
321
|
+
Result->SetStringField(TEXT("message"), Saved.Num() > 0
|
|
322
|
+
? FString::Printf(TEXT("Saved %d package(s) for level '%s'"), Saved.Num(), *World->GetName())
|
|
323
|
+
: FString::Printf(TEXT("Level '%s' was already saved; nothing needed writing"), *World->GetName()));
|
|
324
|
+
return MCPResult(Result);
|
|
325
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#include "LevelHandlers.h"
|
|
2
|
+
|
|
3
|
+
#include "Components/SceneComponent.h"
|
|
4
|
+
#include "GameFramework/Actor.h"
|
|
5
|
+
#include "HandlerUtils.h"
|
|
6
|
+
#include "Misc/AutomationTest.h"
|
|
7
|
+
#include "ScopedTransaction.h"
|
|
8
|
+
|
|
9
|
+
namespace
|
|
10
|
+
{
|
|
11
|
+
USceneComponent* FindExactSceneComponent(AActor* Actor, const FString& ComponentName)
|
|
12
|
+
{
|
|
13
|
+
if (!Actor || ComponentName.IsEmpty()) return nullptr;
|
|
14
|
+
for (UActorComponent* Component : Actor->GetComponents())
|
|
15
|
+
{
|
|
16
|
+
if (USceneComponent* Scene = Cast<USceneComponent>(Component);
|
|
17
|
+
Scene && Scene->GetName().Equals(ComponentName, ESearchCase::IgnoreCase))
|
|
18
|
+
{
|
|
19
|
+
return Scene;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return nullptr;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
TSharedPtr<FJsonObject> QuatToJson(const FQuat& Quat)
|
|
26
|
+
{
|
|
27
|
+
auto Json = MakeShared<FJsonObject>();
|
|
28
|
+
Json->SetNumberField(TEXT("x"), Quat.X);
|
|
29
|
+
Json->SetNumberField(TEXT("y"), Quat.Y);
|
|
30
|
+
Json->SetNumberField(TEXT("z"), Quat.Z);
|
|
31
|
+
Json->SetNumberField(TEXT("w"), Quat.W);
|
|
32
|
+
return Json;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
TSharedPtr<FJsonObject> TransformToJson(const FTransform& Transform)
|
|
36
|
+
{
|
|
37
|
+
auto Json = MakeShared<FJsonObject>();
|
|
38
|
+
Json->SetObjectField(TEXT("location"), MCPVec3ToJsonObject(Transform.GetLocation()));
|
|
39
|
+
Json->SetObjectField(TEXT("rotation"), MCPRotatorToJsonObject(Transform.Rotator()));
|
|
40
|
+
Json->SetObjectField(TEXT("quaternion"), QuatToJson(Transform.GetRotation()));
|
|
41
|
+
Json->SetObjectField(TEXT("scale"), MCPVec3ToJsonObject(Transform.GetScale3D()));
|
|
42
|
+
return Json;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
TSharedPtr<FJsonObject> ComponentTransformsToJson(USceneComponent* Component)
|
|
46
|
+
{
|
|
47
|
+
auto Json = MakeShared<FJsonObject>();
|
|
48
|
+
Json->SetObjectField(TEXT("relative"), TransformToJson(Component->GetRelativeTransform()));
|
|
49
|
+
Json->SetObjectField(TEXT("world"), TransformToJson(Component->GetComponentTransform()));
|
|
50
|
+
return Json;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
bool ReadRequiredNumber(const TSharedPtr<FJsonObject>& Json, const TCHAR* Field, double& Out)
|
|
54
|
+
{
|
|
55
|
+
return Json && Json->TryGetNumberField(Field, Out) && FMath::IsFinite(Out);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
bool ReadRollbackTransform(const TSharedPtr<FJsonObject>& Json, FTransform& Out)
|
|
59
|
+
{
|
|
60
|
+
if (!Json) return false;
|
|
61
|
+
|
|
62
|
+
const TSharedPtr<FJsonObject>* Location = nullptr;
|
|
63
|
+
const TSharedPtr<FJsonObject>* Quaternion = nullptr;
|
|
64
|
+
const TSharedPtr<FJsonObject>* Scale = nullptr;
|
|
65
|
+
if (!Json->TryGetObjectField(TEXT("location"), Location) || !*Location ||
|
|
66
|
+
!Json->TryGetObjectField(TEXT("quaternion"), Quaternion) || !*Quaternion ||
|
|
67
|
+
!Json->TryGetObjectField(TEXT("scale"), Scale) || !*Scale)
|
|
68
|
+
{
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
double LX, LY, LZ, QX, QY, QZ, QW, SX, SY, SZ;
|
|
73
|
+
if (!ReadRequiredNumber(*Location, TEXT("x"), LX) ||
|
|
74
|
+
!ReadRequiredNumber(*Location, TEXT("y"), LY) ||
|
|
75
|
+
!ReadRequiredNumber(*Location, TEXT("z"), LZ) ||
|
|
76
|
+
!ReadRequiredNumber(*Quaternion, TEXT("x"), QX) ||
|
|
77
|
+
!ReadRequiredNumber(*Quaternion, TEXT("y"), QY) ||
|
|
78
|
+
!ReadRequiredNumber(*Quaternion, TEXT("z"), QZ) ||
|
|
79
|
+
!ReadRequiredNumber(*Quaternion, TEXT("w"), QW) ||
|
|
80
|
+
!ReadRequiredNumber(*Scale, TEXT("x"), SX) ||
|
|
81
|
+
!ReadRequiredNumber(*Scale, TEXT("y"), SY) ||
|
|
82
|
+
!ReadRequiredNumber(*Scale, TEXT("z"), SZ))
|
|
83
|
+
{
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
FQuat Rotation(QX, QY, QZ, QW);
|
|
88
|
+
if (Rotation.SizeSquared() <= UE_SMALL_NUMBER) return false;
|
|
89
|
+
Rotation.Normalize();
|
|
90
|
+
Out = FTransform(Rotation, FVector(LX, LY, LZ), FVector(SX, SY, SZ));
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
FVector NamedAxis(const FString& Axis)
|
|
95
|
+
{
|
|
96
|
+
if (Axis == TEXT("forward")) return FVector::ForwardVector;
|
|
97
|
+
if (Axis == TEXT("right")) return FVector::RightVector;
|
|
98
|
+
return FVector::UpVector;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
FTransform ApplyWorldNudge(
|
|
102
|
+
const FTransform& Current,
|
|
103
|
+
const FVector& TranslationWorld,
|
|
104
|
+
bool bRotate,
|
|
105
|
+
const FVector& RotationAxisWorld,
|
|
106
|
+
double RotationDegrees)
|
|
107
|
+
{
|
|
108
|
+
FTransform Result = Current;
|
|
109
|
+
Result.AddToTranslation(TranslationWorld);
|
|
110
|
+
if (bRotate)
|
|
111
|
+
{
|
|
112
|
+
FQuat NewRotation = FQuat(
|
|
113
|
+
RotationAxisWorld.GetSafeNormal(),
|
|
114
|
+
FMath::DegreesToRadians(RotationDegrees)) * Current.GetRotation();
|
|
115
|
+
NewRotation.Normalize();
|
|
116
|
+
Result.SetRotation(NewRotation);
|
|
117
|
+
}
|
|
118
|
+
return Result;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
TSharedPtr<FJsonValue> FLevelHandlers::NudgeComponent(const TSharedPtr<FJsonObject>& Params)
|
|
123
|
+
{
|
|
124
|
+
FString ActorLabel;
|
|
125
|
+
if (auto Error = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), ActorLabel)) return Error;
|
|
126
|
+
FString ComponentName;
|
|
127
|
+
if (auto Error = RequireString(Params, TEXT("componentName"), ComponentName)) return Error;
|
|
128
|
+
|
|
129
|
+
const FString WorldScope = OptionalString(Params, TEXT("world"), TEXT("editor")).ToLower();
|
|
130
|
+
UWorld* World = ResolveWorldFromParams(Params, *WorldScope);
|
|
131
|
+
if (!World)
|
|
132
|
+
{
|
|
133
|
+
return MCPError(WorldScope == TEXT("pie")
|
|
134
|
+
? TEXT("PIE not running (or no such pieInstance). See editor(list_pie_instances).")
|
|
135
|
+
: TEXT("Editor world not available"));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
FMCPActorSelector ActorSel;
|
|
139
|
+
ActorSel.Match = EMCPActorMatch::LabelNameOrPath;
|
|
140
|
+
ActorSel.WorldLabel = World->IsGameWorld() ? TEXT("PIE") : TEXT("editor");
|
|
141
|
+
TSharedPtr<FJsonValue> ActorErr;
|
|
142
|
+
AActor* Actor = MCPResolveActor(World, Params, ActorErr, ActorSel);
|
|
143
|
+
if (!Actor) return ActorErr;
|
|
144
|
+
ActorLabel = Actor->GetActorLabel();
|
|
145
|
+
USceneComponent* Component = FindExactSceneComponent(Actor, ComponentName);
|
|
146
|
+
if (!Component)
|
|
147
|
+
{
|
|
148
|
+
return MCPError(FString::Printf(
|
|
149
|
+
TEXT("Exact SceneComponent '%s' not found on actor '%s'"), *ComponentName, *ActorLabel));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const FTransform PreviousRelative = Component->GetRelativeTransform();
|
|
153
|
+
const FTransform PreviousWorld = Component->GetComponentTransform();
|
|
154
|
+
const TSharedPtr<FJsonObject>* RestoreJson = nullptr;
|
|
155
|
+
const bool bRestore = Params->TryGetObjectField(TEXT("_restoreRelative"), RestoreJson) && *RestoreJson;
|
|
156
|
+
|
|
157
|
+
FString Frame = OptionalString(Params, TEXT("frame"), TEXT("actor")).ToLower();
|
|
158
|
+
FVector TranslationWorld = FVector::ZeroVector;
|
|
159
|
+
FVector RotationAxisWorld = FVector::ZeroVector;
|
|
160
|
+
FString RotationAxis;
|
|
161
|
+
double RotationDegrees = 0.0;
|
|
162
|
+
double ScaleMultiplier = 1.0;
|
|
163
|
+
bool bHasTranslation = false;
|
|
164
|
+
bool bHasRotation = false;
|
|
165
|
+
bool bHasScale = false;
|
|
166
|
+
FTransform RestoreRelative;
|
|
167
|
+
|
|
168
|
+
if (bRestore)
|
|
169
|
+
{
|
|
170
|
+
if (!ReadRollbackTransform(*RestoreJson, RestoreRelative))
|
|
171
|
+
{
|
|
172
|
+
return MCPError(TEXT("Invalid internal rollback transform"));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else
|
|
176
|
+
{
|
|
177
|
+
if (Frame != TEXT("world") && Frame != TEXT("actor") &&
|
|
178
|
+
Frame != TEXT("parent") && Frame != TEXT("component"))
|
|
179
|
+
{
|
|
180
|
+
return MCPError(TEXT("frame must be world, actor, parent, or component"));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
FQuat FrameRotation = FQuat::Identity;
|
|
184
|
+
if (Frame == TEXT("actor"))
|
|
185
|
+
{
|
|
186
|
+
FrameRotation = Actor->GetActorQuat();
|
|
187
|
+
}
|
|
188
|
+
else if (Frame == TEXT("parent"))
|
|
189
|
+
{
|
|
190
|
+
USceneComponent* Parent = Component->GetAttachParent();
|
|
191
|
+
if (!Parent) return MCPError(TEXT("frame=parent requires an attached component"));
|
|
192
|
+
FrameRotation = Parent->GetComponentQuat();
|
|
193
|
+
}
|
|
194
|
+
else if (Frame == TEXT("component"))
|
|
195
|
+
{
|
|
196
|
+
FrameRotation = Component->GetComponentQuat();
|
|
197
|
+
}
|
|
198
|
+
FrameRotation.Normalize();
|
|
199
|
+
|
|
200
|
+
const TSharedPtr<FJsonObject>* TranslationJson = nullptr;
|
|
201
|
+
if (Params->TryGetObjectField(TEXT("translationDelta"), TranslationJson) && *TranslationJson)
|
|
202
|
+
{
|
|
203
|
+
double Forward = 0.0, Right = 0.0, Up = 0.0;
|
|
204
|
+
if (((*TranslationJson)->HasField(TEXT("forwardCm")) &&
|
|
205
|
+
!ReadRequiredNumber(*TranslationJson, TEXT("forwardCm"), Forward)) ||
|
|
206
|
+
((*TranslationJson)->HasField(TEXT("rightCm")) &&
|
|
207
|
+
!ReadRequiredNumber(*TranslationJson, TEXT("rightCm"), Right)) ||
|
|
208
|
+
((*TranslationJson)->HasField(TEXT("upCm")) &&
|
|
209
|
+
!ReadRequiredNumber(*TranslationJson, TEXT("upCm"), Up)))
|
|
210
|
+
{
|
|
211
|
+
return MCPError(TEXT("translationDelta values must be finite numbers"));
|
|
212
|
+
}
|
|
213
|
+
TranslationWorld = FrameRotation.RotateVector(FVector(Forward, Right, Up));
|
|
214
|
+
bHasTranslation = !TranslationWorld.IsNearlyZero();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const TSharedPtr<FJsonObject>* RotationJson = nullptr;
|
|
218
|
+
if (Params->TryGetObjectField(TEXT("axisRotation"), RotationJson) && *RotationJson)
|
|
219
|
+
{
|
|
220
|
+
if (!(*RotationJson)->TryGetStringField(TEXT("axis"), RotationAxis) ||
|
|
221
|
+
(RotationAxis != TEXT("forward") && RotationAxis != TEXT("right") && RotationAxis != TEXT("up")))
|
|
222
|
+
{
|
|
223
|
+
return MCPError(TEXT("axisRotation.axis must be forward, right, or up"));
|
|
224
|
+
}
|
|
225
|
+
if (!ReadRequiredNumber(*RotationJson, TEXT("degrees"), RotationDegrees))
|
|
226
|
+
{
|
|
227
|
+
return MCPError(TEXT("axisRotation.degrees must be a finite number"));
|
|
228
|
+
}
|
|
229
|
+
RotationAxisWorld = FrameRotation.RotateVector(NamedAxis(RotationAxis)).GetSafeNormal();
|
|
230
|
+
bHasRotation = !FMath::IsNearlyZero(RotationDegrees);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (Params->HasField(TEXT("scaleMultiplier")))
|
|
234
|
+
{
|
|
235
|
+
if (!Params->TryGetNumberField(TEXT("scaleMultiplier"), ScaleMultiplier) ||
|
|
236
|
+
!FMath::IsFinite(ScaleMultiplier) || ScaleMultiplier <= 0.0)
|
|
237
|
+
{
|
|
238
|
+
return MCPError(TEXT("scaleMultiplier must be a finite number greater than zero"));
|
|
239
|
+
}
|
|
240
|
+
bHasScale = !FMath::IsNearlyEqual(ScaleMultiplier, 1.0);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!bHasTranslation && !bHasRotation && !bHasScale)
|
|
244
|
+
{
|
|
245
|
+
return MCPError(TEXT("Provide a non-zero translationDelta, axisRotation, or scaleMultiplier"));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const bool bRuntimeWorld = World->IsGameWorld();
|
|
250
|
+
const FScopedTransaction Transaction(FText::FromString(TEXT("Nudge component")), !bRuntimeWorld);
|
|
251
|
+
if (!bRuntimeWorld)
|
|
252
|
+
{
|
|
253
|
+
Actor->Modify();
|
|
254
|
+
Component->Modify();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (bRestore)
|
|
258
|
+
{
|
|
259
|
+
Component->SetRelativeTransform(RestoreRelative);
|
|
260
|
+
}
|
|
261
|
+
else
|
|
262
|
+
{
|
|
263
|
+
const FTransform AdjustedWorld = ApplyWorldNudge(
|
|
264
|
+
PreviousWorld, TranslationWorld, bHasRotation, RotationAxisWorld, RotationDegrees);
|
|
265
|
+
Component->SetWorldLocationAndRotation(
|
|
266
|
+
AdjustedWorld.GetLocation(), AdjustedWorld.GetRotation(), false, nullptr, ETeleportType::TeleportPhysics);
|
|
267
|
+
if (bHasScale)
|
|
268
|
+
{
|
|
269
|
+
Component->SetRelativeScale3D(PreviousRelative.GetScale3D() * ScaleMultiplier);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
Component->UpdateComponentToWorld();
|
|
274
|
+
Component->MarkRenderStateDirty();
|
|
275
|
+
if (!bRuntimeWorld)
|
|
276
|
+
{
|
|
277
|
+
Component->PostEditComponentMove(true);
|
|
278
|
+
Component->MarkPackageDirty();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
auto Result = MCPSuccess();
|
|
282
|
+
MCPSetUpdated(Result);
|
|
283
|
+
Result->SetStringField(TEXT("actorLabel"), ActorLabel);
|
|
284
|
+
Result->SetStringField(TEXT("actorPath"), Actor->GetPathName());
|
|
285
|
+
Result->SetStringField(TEXT("componentName"), Component->GetName());
|
|
286
|
+
Result->SetStringField(TEXT("frame"), bRestore ? TEXT("rollback") : Frame);
|
|
287
|
+
Result->SetStringField(TEXT("world"), WorldScope);
|
|
288
|
+
Result->SetObjectField(TEXT("before"), [&]()
|
|
289
|
+
{
|
|
290
|
+
auto Json = MakeShared<FJsonObject>();
|
|
291
|
+
Json->SetObjectField(TEXT("relative"), TransformToJson(PreviousRelative));
|
|
292
|
+
Json->SetObjectField(TEXT("world"), TransformToJson(PreviousWorld));
|
|
293
|
+
return Json;
|
|
294
|
+
}());
|
|
295
|
+
Result->SetObjectField(TEXT("after"), ComponentTransformsToJson(Component));
|
|
296
|
+
if (bHasTranslation) Result->SetObjectField(TEXT("resolvedTranslationWorld"), MCPVec3ToJsonObject(TranslationWorld));
|
|
297
|
+
if (bHasRotation)
|
|
298
|
+
{
|
|
299
|
+
Result->SetStringField(TEXT("rotationAxis"), RotationAxis);
|
|
300
|
+
Result->SetNumberField(TEXT("rotationDegrees"), RotationDegrees);
|
|
301
|
+
Result->SetObjectField(TEXT("resolvedRotationAxisWorld"), MCPVec3ToJsonObject(RotationAxisWorld));
|
|
302
|
+
}
|
|
303
|
+
if (bHasScale) Result->SetNumberField(TEXT("scaleMultiplier"), ScaleMultiplier);
|
|
304
|
+
|
|
305
|
+
auto RollbackPayload = MakeShared<FJsonObject>();
|
|
306
|
+
RollbackPayload->SetStringField(TEXT("actorLabel"), ActorLabel);
|
|
307
|
+
RollbackPayload->SetStringField(TEXT("actorPath"), Actor->GetPathName());
|
|
308
|
+
RollbackPayload->SetStringField(TEXT("componentName"), Component->GetName());
|
|
309
|
+
RollbackPayload->SetStringField(TEXT("world"), WorldScope);
|
|
310
|
+
double PieInstance = 0.0;
|
|
311
|
+
if (Params->TryGetNumberField(TEXT("pieInstance"), PieInstance))
|
|
312
|
+
{
|
|
313
|
+
RollbackPayload->SetNumberField(TEXT("pieInstance"), PieInstance);
|
|
314
|
+
}
|
|
315
|
+
RollbackPayload->SetObjectField(TEXT("_restoreRelative"), TransformToJson(PreviousRelative));
|
|
316
|
+
MCPSetRollback(Result, TEXT("nudge_component"), RollbackPayload);
|
|
317
|
+
return MCPResult(Result);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
#if WITH_DEV_AUTOMATION_TESTS
|
|
321
|
+
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
|
322
|
+
FLevelSpatialComponentNudgeTest,
|
|
323
|
+
"UE.MCP.Level.SpatialComponentNudge",
|
|
324
|
+
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
|
325
|
+
|
|
326
|
+
bool FLevelSpatialComponentNudgeTest::RunTest(const FString& Parameters)
|
|
327
|
+
{
|
|
328
|
+
const FTransform ParentWorld(
|
|
329
|
+
FRotator(18.0, -42.0, 27.0), FVector(400.0, -80.0, 120.0), FVector::OneVector);
|
|
330
|
+
const FTransform Relative(
|
|
331
|
+
FRotator(-147.657698, 83.868340, -89.500431), FVector(-9.0, 3.0, 0.0), FVector(1.35));
|
|
332
|
+
const FTransform ChildWorld = Relative * ParentWorld;
|
|
333
|
+
const FVector AxisWorld = FVector::UpVector;
|
|
334
|
+
const FQuat ExpectedRotation =
|
|
335
|
+
FQuat(AxisWorld, FMath::DegreesToRadians(12.0)) * ChildWorld.GetRotation();
|
|
336
|
+
const FTransform Adjusted = ApplyWorldNudge(
|
|
337
|
+
ChildWorld, FVector::ZeroVector, true, AxisWorld, 12.0);
|
|
338
|
+
|
|
339
|
+
TestTrue(TEXT("Axis-angle nudge applies the requested world rotation"),
|
|
340
|
+
Adjusted.GetRotation().AngularDistance(ExpectedRotation) < 1.e-6);
|
|
341
|
+
TestTrue(TEXT("Rotation preserves world location"),
|
|
342
|
+
Adjusted.GetLocation().Equals(ChildWorld.GetLocation(), 1.e-6));
|
|
343
|
+
TestTrue(TEXT("Rotation preserves scale"),
|
|
344
|
+
Adjusted.GetScale3D().Equals(ChildWorld.GetScale3D(), 1.e-6));
|
|
345
|
+
|
|
346
|
+
const FTransform SolvedRelative = Adjusted.GetRelativeTransform(ParentWorld);
|
|
347
|
+
const FTransform RoundTripWorld = SolvedRelative * ParentWorld;
|
|
348
|
+
TestTrue(TEXT("Attached relative transform round-trips to the desired world rotation"),
|
|
349
|
+
RoundTripWorld.GetRotation().AngularDistance(Adjusted.GetRotation()) < 1.e-6);
|
|
350
|
+
|
|
351
|
+
FRotator NaiveRelativeRotation = Relative.Rotator();
|
|
352
|
+
NaiveRelativeRotation.Yaw += 12.0;
|
|
353
|
+
const FTransform NaiveRelative(
|
|
354
|
+
NaiveRelativeRotation, Relative.GetLocation(), Relative.GetScale3D());
|
|
355
|
+
const FTransform NaiveWorld = NaiveRelative * ParentWorld;
|
|
356
|
+
TestTrue(TEXT("Raw relative Euler yaw is not the requested world-axis adjustment"),
|
|
357
|
+
FMath::RadiansToDegrees(NaiveWorld.GetRotation().AngularDistance(Adjusted.GetRotation())) > 1.0);
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
#endif
|