unreal-engine-mcp-server 0.4.7 → 0.5.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/.env.example +26 -0
- package/.env.production +38 -7
- package/.eslintrc.json +0 -54
- package/.eslintrc.override.json +8 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +94 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +56 -0
- package/.github/copilot-instructions.md +478 -45
- package/.github/dependabot.yml +19 -0
- package/.github/labeler.yml +24 -0
- package/.github/labels.yml +70 -0
- package/.github/pull_request_template.md +42 -0
- package/.github/release-drafter.yml +148 -0
- package/.github/workflows/auto-merge.yml +38 -0
- package/.github/workflows/ci.yml +38 -0
- package/.github/workflows/dependency-review.yml +17 -0
- package/.github/workflows/gemini-issue-triage.yml +172 -0
- package/.github/workflows/greetings.yml +23 -0
- package/.github/workflows/labeler.yml +16 -0
- package/.github/workflows/links.yml +80 -0
- package/.github/workflows/pr-size-labeler.yml +137 -0
- package/.github/workflows/publish-mcp.yml +12 -7
- package/.github/workflows/release-drafter.yml +23 -0
- package/.github/workflows/release.yml +112 -0
- package/.github/workflows/semantic-pull-request.yml +35 -0
- package/.github/workflows/smoke-test.yml +36 -0
- package/.github/workflows/stale.yml +28 -0
- package/CHANGELOG.md +267 -31
- package/CONTRIBUTING.md +140 -0
- package/README.md +166 -71
- package/claude_desktop_config_example.json +7 -6
- package/dist/automation/bridge.d.ts +50 -0
- package/dist/automation/bridge.js +452 -0
- package/dist/automation/connection-manager.d.ts +23 -0
- package/dist/automation/connection-manager.js +107 -0
- package/dist/automation/handshake.d.ts +11 -0
- package/dist/automation/handshake.js +89 -0
- package/dist/automation/index.d.ts +3 -0
- package/dist/automation/index.js +3 -0
- package/dist/automation/message-handler.d.ts +12 -0
- package/dist/automation/message-handler.js +149 -0
- package/dist/automation/request-tracker.d.ts +25 -0
- package/dist/automation/request-tracker.js +98 -0
- package/dist/automation/types.d.ts +130 -0
- package/dist/automation/types.js +2 -0
- package/dist/cli.js +32 -5
- package/dist/config.d.ts +27 -0
- package/dist/config.js +60 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.js +12 -0
- package/dist/graphql/resolvers.d.ts +268 -0
- package/dist/graphql/resolvers.js +743 -0
- package/dist/graphql/schema.d.ts +5 -0
- package/dist/graphql/schema.js +437 -0
- package/dist/graphql/server.d.ts +26 -0
- package/dist/graphql/server.js +115 -0
- package/dist/graphql/types.d.ts +7 -0
- package/dist/graphql/types.js +2 -0
- package/dist/handlers/resource-handlers.d.ts +20 -0
- package/dist/handlers/resource-handlers.js +180 -0
- package/dist/index.d.ts +31 -18
- package/dist/index.js +119 -619
- package/dist/prompts/index.js +4 -4
- package/dist/resources/actors.d.ts +17 -12
- package/dist/resources/actors.js +56 -76
- package/dist/resources/assets.d.ts +6 -14
- package/dist/resources/assets.js +115 -147
- package/dist/resources/levels.d.ts +13 -13
- package/dist/resources/levels.js +25 -34
- package/dist/server/resource-registry.d.ts +20 -0
- package/dist/server/resource-registry.js +37 -0
- package/dist/server/tool-registry.d.ts +23 -0
- package/dist/server/tool-registry.js +322 -0
- package/dist/server-setup.d.ts +21 -0
- package/dist/server-setup.js +111 -0
- package/dist/services/health-monitor.d.ts +34 -0
- package/dist/services/health-monitor.js +105 -0
- package/dist/services/metrics-server.d.ts +11 -0
- package/dist/services/metrics-server.js +105 -0
- package/dist/tools/actors.d.ts +147 -9
- package/dist/tools/actors.js +350 -311
- package/dist/tools/animation.d.ts +135 -4
- package/dist/tools/animation.js +510 -411
- package/dist/tools/assets.d.ts +117 -19
- package/dist/tools/assets.js +259 -284
- package/dist/tools/audio.d.ts +102 -42
- package/dist/tools/audio.js +272 -685
- package/dist/tools/base-tool.d.ts +17 -0
- package/dist/tools/base-tool.js +46 -0
- package/dist/tools/behavior-tree.d.ts +94 -0
- package/dist/tools/behavior-tree.js +39 -0
- package/dist/tools/blueprint/helpers.d.ts +29 -0
- package/dist/tools/blueprint/helpers.js +182 -0
- package/dist/tools/blueprint.d.ts +228 -118
- package/dist/tools/blueprint.js +685 -832
- package/dist/tools/consolidated-tool-definitions.d.ts +5462 -1781
- package/dist/tools/consolidated-tool-definitions.js +829 -496
- package/dist/tools/consolidated-tool-handlers.d.ts +2 -1
- package/dist/tools/consolidated-tool-handlers.js +211 -1026
- package/dist/tools/debug.d.ts +143 -85
- package/dist/tools/debug.js +234 -180
- package/dist/tools/dynamic-handler-registry.d.ts +11 -0
- package/dist/tools/dynamic-handler-registry.js +101 -0
- package/dist/tools/editor.d.ts +139 -18
- package/dist/tools/editor.js +239 -244
- package/dist/tools/engine.d.ts +10 -4
- package/dist/tools/engine.js +13 -5
- package/dist/tools/environment.d.ts +36 -0
- package/dist/tools/environment.js +267 -0
- package/dist/tools/foliage.d.ts +105 -14
- package/dist/tools/foliage.js +219 -331
- package/dist/tools/handlers/actor-handlers.d.ts +3 -0
- package/dist/tools/handlers/actor-handlers.js +232 -0
- package/dist/tools/handlers/animation-handlers.d.ts +3 -0
- package/dist/tools/handlers/animation-handlers.js +185 -0
- package/dist/tools/handlers/argument-helper.d.ts +16 -0
- package/dist/tools/handlers/argument-helper.js +80 -0
- package/dist/tools/handlers/asset-handlers.d.ts +3 -0
- package/dist/tools/handlers/asset-handlers.js +496 -0
- package/dist/tools/handlers/audio-handlers.d.ts +3 -0
- package/dist/tools/handlers/audio-handlers.js +166 -0
- package/dist/tools/handlers/blueprint-handlers.d.ts +4 -0
- package/dist/tools/handlers/blueprint-handlers.js +358 -0
- package/dist/tools/handlers/common-handlers.d.ts +14 -0
- package/dist/tools/handlers/common-handlers.js +56 -0
- package/dist/tools/handlers/editor-handlers.d.ts +3 -0
- package/dist/tools/handlers/editor-handlers.js +119 -0
- package/dist/tools/handlers/effect-handlers.d.ts +3 -0
- package/dist/tools/handlers/effect-handlers.js +171 -0
- package/dist/tools/handlers/environment-handlers.d.ts +3 -0
- package/dist/tools/handlers/environment-handlers.js +170 -0
- package/dist/tools/handlers/graph-handlers.d.ts +3 -0
- package/dist/tools/handlers/graph-handlers.js +90 -0
- package/dist/tools/handlers/input-handlers.d.ts +3 -0
- package/dist/tools/handlers/input-handlers.js +21 -0
- package/dist/tools/handlers/inspect-handlers.d.ts +3 -0
- package/dist/tools/handlers/inspect-handlers.js +383 -0
- package/dist/tools/handlers/level-handlers.d.ts +3 -0
- package/dist/tools/handlers/level-handlers.js +237 -0
- package/dist/tools/handlers/lighting-handlers.d.ts +3 -0
- package/dist/tools/handlers/lighting-handlers.js +144 -0
- package/dist/tools/handlers/performance-handlers.d.ts +3 -0
- package/dist/tools/handlers/performance-handlers.js +130 -0
- package/dist/tools/handlers/pipeline-handlers.d.ts +3 -0
- package/dist/tools/handlers/pipeline-handlers.js +110 -0
- package/dist/tools/handlers/sequence-handlers.d.ts +3 -0
- package/dist/tools/handlers/sequence-handlers.js +376 -0
- package/dist/tools/handlers/system-handlers.d.ts +4 -0
- package/dist/tools/handlers/system-handlers.js +506 -0
- package/dist/tools/input.d.ts +19 -0
- package/dist/tools/input.js +89 -0
- package/dist/tools/introspection.d.ts +103 -40
- package/dist/tools/introspection.js +425 -568
- package/dist/tools/landscape.d.ts +97 -36
- package/dist/tools/landscape.js +280 -409
- package/dist/tools/level.d.ts +130 -10
- package/dist/tools/level.js +639 -675
- package/dist/tools/lighting.d.ts +77 -38
- package/dist/tools/lighting.js +441 -943
- package/dist/tools/logs.d.ts +3 -3
- package/dist/tools/logs.js +5 -57
- package/dist/tools/materials.d.ts +91 -24
- package/dist/tools/materials.js +190 -118
- package/dist/tools/niagara.d.ts +149 -39
- package/dist/tools/niagara.js +232 -182
- package/dist/tools/performance.d.ts +27 -12
- package/dist/tools/performance.js +204 -122
- package/dist/tools/physics.d.ts +32 -77
- package/dist/tools/physics.js +171 -582
- package/dist/tools/property-dictionary.d.ts +13 -0
- package/dist/tools/property-dictionary.js +82 -0
- package/dist/tools/sequence.d.ts +73 -48
- package/dist/tools/sequence.js +196 -748
- package/dist/tools/tool-definition-utils.d.ts +59 -0
- package/dist/tools/tool-definition-utils.js +35 -0
- package/dist/tools/ui.d.ts +66 -34
- package/dist/tools/ui.js +134 -214
- package/dist/types/env.d.ts +0 -3
- package/dist/types/env.js +0 -7
- package/dist/types/tool-interfaces.d.ts +898 -0
- package/dist/types/tool-interfaces.js +2 -0
- package/dist/types/tool-types.d.ts +183 -19
- package/dist/types/tool-types.js +0 -4
- package/dist/unreal-bridge.d.ts +24 -131
- package/dist/unreal-bridge.js +364 -1506
- package/dist/utils/command-validator.d.ts +9 -0
- package/dist/utils/command-validator.js +67 -0
- package/dist/utils/elicitation.d.ts +1 -1
- package/dist/utils/elicitation.js +12 -15
- package/dist/utils/error-handler.d.ts +2 -51
- package/dist/utils/error-handler.js +11 -87
- package/dist/utils/ini-reader.d.ts +3 -0
- package/dist/utils/ini-reader.js +69 -0
- package/dist/utils/logger.js +9 -6
- package/dist/utils/normalize.d.ts +3 -0
- package/dist/utils/normalize.js +56 -0
- package/dist/utils/response-factory.d.ts +7 -0
- package/dist/utils/response-factory.js +33 -0
- package/dist/utils/response-validator.d.ts +3 -24
- package/dist/utils/response-validator.js +130 -81
- package/dist/utils/result-helpers.d.ts +4 -5
- package/dist/utils/result-helpers.js +15 -16
- package/dist/utils/safe-json.js +5 -11
- package/dist/utils/unreal-command-queue.d.ts +24 -0
- package/dist/utils/unreal-command-queue.js +120 -0
- package/dist/utils/validation.d.ts +0 -40
- package/dist/utils/validation.js +1 -78
- package/dist/wasm/index.d.ts +70 -0
- package/dist/wasm/index.js +535 -0
- package/docs/GraphQL-API.md +888 -0
- package/docs/Migration-Guide-v0.5.0.md +692 -0
- package/docs/Roadmap.md +53 -0
- package/docs/WebAssembly-Integration.md +628 -0
- package/docs/editor-plugin-extension.md +370 -0
- package/docs/handler-mapping.md +242 -0
- package/docs/native-automation-progress.md +128 -0
- package/docs/testing-guide.md +423 -0
- package/mcp-config-example.json +6 -6
- package/package.json +60 -27
- package/plugins/McpAutomationBridge/Config/FilterPlugin.ini +8 -0
- package/plugins/McpAutomationBridge/McpAutomationBridge.uplugin +64 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/McpAutomationBridge.Build.cs +189 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.cpp +22 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.h +30 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h +1983 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeModule.cpp +72 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSettings.cpp +46 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +581 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +2394 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetQueryHandlers.cpp +300 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetWorkflowHandlers.cpp +2807 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AudioHandlers.cpp +1087 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BehaviorTreeHandlers.cpp +488 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.cpp +643 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.h +31 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +1184 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +5652 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers_List.cpp +152 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ControlHandlers.cpp +2614 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_DebugHandlers.cpp +42 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EditorFunctionHandlers.cpp +1237 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +1701 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +2145 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_FoliageHandlers.cpp +954 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InputHandlers.cpp +209 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InsightsHandlers.cpp +41 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LandscapeHandlers.cpp +1164 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelHandlers.cpp +762 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +634 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LogHandlers.cpp +136 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_MaterialGraphHandlers.cpp +494 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraGraphHandlers.cpp +278 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraHandlers.cpp +625 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PerformanceHandlers.cpp +401 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PipelineHandlers.cpp +67 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +735 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PropertyHandlers.cpp +2634 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_RenderHandlers.cpp +189 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.cpp +917 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.h +39 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +2670 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequencerHandlers.cpp +519 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_TestHandlers.cpp +38 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_UiHandlers.cpp +668 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WorldPartitionHandlers.cpp +346 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +1330 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.h +149 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +783 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSettings.h +115 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSubsystem.h +796 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpConnectionManager.h +117 -0
- package/scripts/check-unreal-connection.mjs +19 -0
- package/scripts/clean-tmp.js +23 -0
- package/scripts/patch-wasm.js +26 -0
- package/scripts/run-all-tests.mjs +131 -0
- package/scripts/smoke-test.ts +94 -0
- package/scripts/sync-mcp-plugin.js +143 -0
- package/scripts/test-no-plugin-alternates.mjs +113 -0
- package/scripts/validate-server.js +46 -0
- package/scripts/verify-automation-bridge.js +200 -0
- package/server.json +57 -21
- package/src/automation/bridge.ts +558 -0
- package/src/automation/connection-manager.ts +130 -0
- package/src/automation/handshake.ts +99 -0
- package/src/automation/index.ts +2 -0
- package/src/automation/message-handler.ts +167 -0
- package/src/automation/request-tracker.ts +123 -0
- package/src/automation/types.ts +107 -0
- package/src/cli.ts +33 -6
- package/src/config.ts +73 -0
- package/src/constants.ts +12 -0
- package/src/graphql/resolvers.ts +1010 -0
- package/src/graphql/schema.ts +452 -0
- package/src/graphql/server.ts +154 -0
- package/src/graphql/types.ts +7 -0
- package/src/handlers/resource-handlers.ts +186 -0
- package/src/index.ts +152 -663
- package/src/prompts/index.ts +4 -4
- package/src/resources/actors.ts +58 -76
- package/src/resources/assets.ts +147 -134
- package/src/resources/levels.ts +28 -33
- package/src/server/resource-registry.ts +47 -0
- package/src/server/tool-registry.ts +354 -0
- package/src/server-setup.ts +148 -0
- package/src/services/health-monitor.ts +132 -0
- package/src/services/metrics-server.ts +142 -0
- package/src/tools/actors.ts +417 -322
- package/src/tools/animation.ts +671 -461
- package/src/tools/assets.ts +353 -289
- package/src/tools/audio.ts +323 -766
- package/src/tools/base-tool.ts +52 -0
- package/src/tools/behavior-tree.ts +45 -0
- package/src/tools/blueprint/helpers.ts +189 -0
- package/src/tools/blueprint.ts +787 -965
- package/src/tools/consolidated-tool-definitions.ts +993 -515
- package/src/tools/consolidated-tool-handlers.ts +272 -1139
- package/src/tools/debug.ts +292 -187
- package/src/tools/dynamic-handler-registry.ts +151 -0
- package/src/tools/editor.ts +309 -246
- package/src/tools/engine.ts +14 -3
- package/src/tools/environment.ts +287 -0
- package/src/tools/foliage.ts +314 -379
- package/src/tools/handlers/actor-handlers.ts +271 -0
- package/src/tools/handlers/animation-handlers.ts +237 -0
- package/src/tools/handlers/argument-helper.ts +142 -0
- package/src/tools/handlers/asset-handlers.ts +532 -0
- package/src/tools/handlers/audio-handlers.ts +194 -0
- package/src/tools/handlers/blueprint-handlers.ts +380 -0
- package/src/tools/handlers/common-handlers.ts +87 -0
- package/src/tools/handlers/editor-handlers.ts +123 -0
- package/src/tools/handlers/effect-handlers.ts +220 -0
- package/src/tools/handlers/environment-handlers.ts +183 -0
- package/src/tools/handlers/graph-handlers.ts +116 -0
- package/src/tools/handlers/input-handlers.ts +28 -0
- package/src/tools/handlers/inspect-handlers.ts +450 -0
- package/src/tools/handlers/level-handlers.ts +252 -0
- package/src/tools/handlers/lighting-handlers.ts +147 -0
- package/src/tools/handlers/performance-handlers.ts +132 -0
- package/src/tools/handlers/pipeline-handlers.ts +127 -0
- package/src/tools/handlers/sequence-handlers.ts +415 -0
- package/src/tools/handlers/system-handlers.ts +564 -0
- package/src/tools/input.ts +101 -0
- package/src/tools/introspection.ts +493 -584
- package/src/tools/landscape.ts +394 -489
- package/src/tools/level.ts +752 -694
- package/src/tools/lighting.ts +583 -984
- package/src/tools/logs.ts +9 -57
- package/src/tools/materials.ts +231 -121
- package/src/tools/niagara.ts +293 -168
- package/src/tools/performance.ts +320 -168
- package/src/tools/physics.ts +268 -613
- package/src/tools/property-dictionary.ts +98 -0
- package/src/tools/sequence.ts +255 -815
- package/src/tools/tool-definition-utils.ts +35 -0
- package/src/tools/ui.ts +207 -283
- package/src/types/env.ts +0 -10
- package/src/types/tool-interfaces.ts +250 -0
- package/src/types/tool-types.ts +243 -21
- package/src/unreal-bridge.ts +460 -1550
- package/src/utils/command-validator.ts +75 -0
- package/src/utils/elicitation.ts +10 -7
- package/src/utils/error-handler.ts +14 -90
- package/src/utils/ini-reader.ts +86 -0
- package/src/utils/logger.ts +8 -3
- package/src/utils/normalize.ts +60 -0
- package/src/utils/response-factory.ts +39 -0
- package/src/utils/response-validator.ts +176 -56
- package/src/utils/result-helpers.ts +21 -19
- package/src/utils/safe-json.ts +14 -11
- package/src/utils/unreal-command-queue.ts +152 -0
- package/src/utils/validation.ts +4 -1
- package/src/wasm/index.ts +838 -0
- package/test-server.mjs +100 -0
- package/tests/run-unreal-tool-tests.mjs +242 -14
- package/tests/test-animation.mjs +44 -0
- package/tests/test-asset-advanced.mjs +82 -0
- package/tests/test-asset-errors.mjs +35 -0
- package/tests/test-audio.mjs +219 -0
- package/tests/test-automation-timeouts.mjs +98 -0
- package/tests/test-behavior-tree.mjs +261 -0
- package/tests/test-blueprint-events.mjs +35 -0
- package/tests/test-blueprint-graph.mjs +79 -0
- package/tests/test-blueprint.mjs +577 -0
- package/tests/test-client-mode.mjs +86 -0
- package/tests/test-console-command.mjs +56 -0
- package/tests/test-control-actor.mjs +425 -0
- package/tests/test-control-editor.mjs +80 -0
- package/tests/test-extra-tools.mjs +38 -0
- package/tests/test-graphql.mjs +322 -0
- package/tests/test-inspect.mjs +72 -0
- package/tests/test-landscape.mjs +60 -0
- package/tests/test-manage-asset.mjs +438 -0
- package/tests/test-manage-level.mjs +70 -0
- package/tests/test-materials.mjs +356 -0
- package/tests/test-niagara.mjs +185 -0
- package/tests/test-no-inline-python.mjs +122 -0
- package/tests/test-plugin-handshake.mjs +82 -0
- package/tests/test-render.mjs +33 -0
- package/tests/test-runner.mjs +933 -0
- package/tests/test-search-assets.mjs +66 -0
- package/tests/test-sequence.mjs +68 -0
- package/tests/test-system.mjs +57 -0
- package/tests/test-wasm.mjs +193 -0
- package/tests/test-world-partition.mjs +215 -0
- package/tsconfig.json +3 -3
- package/wasm/Cargo.lock +363 -0
- package/wasm/Cargo.toml +42 -0
- package/wasm/LICENSE +21 -0
- package/wasm/README.md +253 -0
- package/wasm/src/dependency_resolver.rs +377 -0
- package/wasm/src/lib.rs +153 -0
- package/wasm/src/property_parser.rs +271 -0
- package/wasm/src/transform_math.rs +396 -0
- package/wasm/tests/integration.rs +109 -0
- package/.github/workflows/smithery-build.yml +0 -29
- package/dist/tools/build_environment_advanced.d.ts +0 -65
- package/dist/tools/build_environment_advanced.js +0 -633
- package/dist/tools/rc.d.ts +0 -110
- package/dist/tools/rc.js +0 -437
- package/dist/tools/visual.d.ts +0 -40
- package/dist/tools/visual.js +0 -282
- package/dist/utils/http.d.ts +0 -6
- package/dist/utils/http.js +0 -151
- package/dist/utils/python-output.d.ts +0 -18
- package/dist/utils/python-output.js +0 -290
- package/dist/utils/python.d.ts +0 -2
- package/dist/utils/python.js +0 -4
- package/dist/utils/stdio-redirect.d.ts +0 -2
- package/dist/utils/stdio-redirect.js +0 -20
- package/docs/unreal-tool-test-cases.md +0 -574
- package/smithery.yaml +0 -29
- package/src/tools/build_environment_advanced.ts +0 -732
- package/src/tools/rc.ts +0 -515
- package/src/tools/visual.ts +0 -281
- package/src/utils/http.ts +0 -187
- package/src/utils/python-output.ts +0 -351
- package/src/utils/python.ts +0 -3
- package/src/utils/stdio-redirect.ts +0 -18
package/dist/unreal-bridge.js
CHANGED
|
@@ -1,335 +1,97 @@
|
|
|
1
|
-
import WebSocket from 'ws';
|
|
2
|
-
import { createHttpClient } from './utils/http.js';
|
|
3
1
|
import { Logger } from './utils/logger.js';
|
|
4
|
-
import { loadEnv } from './types/env.js';
|
|
5
2
|
import { ErrorHandler } from './utils/error-handler.js';
|
|
3
|
+
import { DEFAULT_AUTOMATION_HOST, DEFAULT_AUTOMATION_PORT } from './constants.js';
|
|
4
|
+
import { UnrealCommandQueue } from './utils/unreal-command-queue.js';
|
|
5
|
+
import { CommandValidator } from './utils/command-validator.js';
|
|
6
6
|
export class UnrealBridge {
|
|
7
|
-
ws;
|
|
8
|
-
http = createHttpClient('');
|
|
9
|
-
env = loadEnv();
|
|
10
7
|
log = new Logger('UnrealBridge');
|
|
11
8
|
connected = false;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
BASE_RECONNECT_DELAY = 1000;
|
|
16
|
-
autoReconnectEnabled = false; // disabled by default to prevent looping retries
|
|
17
|
-
engineVersionCache;
|
|
18
|
-
ENGINE_VERSION_TTL_MS = 5 * 60 * 1000;
|
|
19
|
-
// WebSocket health monitoring (best practice from WebSocket optimization guides)
|
|
20
|
-
lastPongReceived = 0;
|
|
21
|
-
pingInterval;
|
|
22
|
-
PING_INTERVAL_MS = 30000; // 30 seconds
|
|
23
|
-
PONG_TIMEOUT_MS = 10000; // 10 seconds
|
|
24
|
-
// Command queue for throttling
|
|
25
|
-
commandQueue = [];
|
|
26
|
-
isProcessing = false;
|
|
27
|
-
MIN_COMMAND_DELAY = 100; // Increased to prevent console spam
|
|
28
|
-
MAX_COMMAND_DELAY = 500; // Maximum delay for heavy operations
|
|
29
|
-
STAT_COMMAND_DELAY = 300; // Special delay for stat commands to avoid warnings
|
|
30
|
-
lastCommandTime = 0;
|
|
31
|
-
lastStatCommandTime = 0; // Track stat commands separately
|
|
32
|
-
// Console object cache to reduce FindConsoleObject warnings
|
|
33
|
-
consoleObjectCache = new Map();
|
|
34
|
-
CONSOLE_CACHE_TTL = 300000; // 5 minutes TTL for cached objects
|
|
35
|
-
pluginStatusCache = new Map();
|
|
36
|
-
PLUGIN_CACHE_TTL = 5 * 60 * 1000;
|
|
37
|
-
// Unsafe viewmodes that can cause crashes or instability via visualizeBuffer
|
|
38
|
-
UNSAFE_VIEWMODES = [
|
|
39
|
-
'BaseColor', 'WorldNormal', 'Metallic', 'Specular',
|
|
40
|
-
'Roughness',
|
|
41
|
-
'SubsurfaceColor',
|
|
42
|
-
'Opacity',
|
|
43
|
-
'LightComplexity', 'LightmapDensity',
|
|
44
|
-
'StationaryLightOverlap', 'CollisionPawn', 'CollisionVisibility'
|
|
45
|
-
];
|
|
46
|
-
HARD_BLOCKED_VIEWMODES = new Set([
|
|
47
|
-
'BaseColor', 'WorldNormal', 'Metallic', 'Specular', 'Roughness', 'SubsurfaceColor', 'Opacity'
|
|
48
|
-
]);
|
|
49
|
-
VIEWMODE_ALIASES = new Map([
|
|
50
|
-
['lit', 'Lit'],
|
|
51
|
-
['unlit', 'Unlit'],
|
|
52
|
-
['wireframe', 'Wireframe'],
|
|
53
|
-
['brushwireframe', 'BrushWireframe'],
|
|
54
|
-
['brush_wireframe', 'BrushWireframe'],
|
|
55
|
-
['detaillighting', 'DetailLighting'],
|
|
56
|
-
['detail_lighting', 'DetailLighting'],
|
|
57
|
-
['lightingonly', 'LightingOnly'],
|
|
58
|
-
['lighting_only', 'LightingOnly'],
|
|
59
|
-
['lightonly', 'LightingOnly'],
|
|
60
|
-
['light_only', 'LightingOnly'],
|
|
61
|
-
['lightcomplexity', 'LightComplexity'],
|
|
62
|
-
['light_complexity', 'LightComplexity'],
|
|
63
|
-
['shadercomplexity', 'ShaderComplexity'],
|
|
64
|
-
['shader_complexity', 'ShaderComplexity'],
|
|
65
|
-
['lightmapdensity', 'LightmapDensity'],
|
|
66
|
-
['lightmap_density', 'LightmapDensity'],
|
|
67
|
-
['stationarylightoverlap', 'StationaryLightOverlap'],
|
|
68
|
-
['stationary_light_overlap', 'StationaryLightOverlap'],
|
|
69
|
-
['reflectionoverride', 'ReflectionOverride'],
|
|
70
|
-
['reflection_override', 'ReflectionOverride'],
|
|
71
|
-
['texeldensity', 'TexelDensity'],
|
|
72
|
-
['texel_density', 'TexelDensity'],
|
|
73
|
-
['vertexcolor', 'VertexColor'],
|
|
74
|
-
['vertex_color', 'VertexColor'],
|
|
75
|
-
['litdetail', 'DetailLighting'],
|
|
76
|
-
['lit_only', 'LightingOnly']
|
|
77
|
-
]);
|
|
78
|
-
// Python script templates for EditorLevelLibrary access
|
|
79
|
-
PYTHON_TEMPLATES = {
|
|
80
|
-
GET_ALL_ACTORS: {
|
|
81
|
-
name: 'get_all_actors',
|
|
82
|
-
script: `
|
|
83
|
-
import unreal
|
|
84
|
-
import json
|
|
85
|
-
|
|
86
|
-
# Use EditorActorSubsystem instead of deprecated EditorLevelLibrary
|
|
87
|
-
try:
|
|
88
|
-
subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
89
|
-
if subsys:
|
|
90
|
-
actors = subsys.get_all_level_actors()
|
|
91
|
-
result = [{'name': a.get_name(), 'class': a.get_class().get_name(), 'path': a.get_path_name()} for a in actors]
|
|
92
|
-
print(f"RESULT:{json.dumps(result)}")
|
|
93
|
-
else:
|
|
94
|
-
print("RESULT:[]")
|
|
95
|
-
except Exception as e:
|
|
96
|
-
print(f"RESULT:{json.dumps({'error': str(e)})}")
|
|
97
|
-
`.trim()
|
|
98
|
-
},
|
|
99
|
-
SPAWN_ACTOR_AT_LOCATION: {
|
|
100
|
-
name: 'spawn_actor',
|
|
101
|
-
script: `
|
|
102
|
-
import unreal
|
|
103
|
-
import json
|
|
104
|
-
|
|
105
|
-
location = unreal.Vector({x}, {y}, {z})
|
|
106
|
-
rotation = unreal.Rotator({pitch}, {yaw}, {roll})
|
|
107
|
-
|
|
108
|
-
try:
|
|
109
|
-
# Use EditorActorSubsystem instead of deprecated EditorLevelLibrary
|
|
110
|
-
subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
111
|
-
if subsys:
|
|
112
|
-
# Try to load asset class
|
|
113
|
-
actor_class = unreal.EditorAssetLibrary.load_asset("{class_path}")
|
|
114
|
-
if actor_class:
|
|
115
|
-
spawned = subsys.spawn_actor_from_object(actor_class, location, rotation)
|
|
116
|
-
if spawned:
|
|
117
|
-
print(f"RESULT:{json.dumps({'success': True, 'actor': spawned.get_name(), 'location': [{x}, {y}, {z}]}})}")
|
|
118
|
-
else:
|
|
119
|
-
print(f"RESULT:{json.dumps({'success': False, 'error': 'Failed to spawn actor'})}")
|
|
120
|
-
else:
|
|
121
|
-
print(f"RESULT:{json.dumps({'success': False, 'error': 'Failed to load actor class: {class_path}'})}")
|
|
122
|
-
else:
|
|
123
|
-
print(f"RESULT:{json.dumps({'success': False, 'error': 'EditorActorSubsystem not available'})}")
|
|
124
|
-
except Exception as e:
|
|
125
|
-
print(f"RESULT:{json.dumps({'success': False, 'error': str(e)})}")
|
|
126
|
-
`.trim()
|
|
127
|
-
},
|
|
128
|
-
DELETE_ACTOR: {
|
|
129
|
-
name: 'delete_actor',
|
|
130
|
-
script: `
|
|
131
|
-
import unreal
|
|
132
|
-
import json
|
|
133
|
-
|
|
134
|
-
try:
|
|
135
|
-
subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
136
|
-
if subsys:
|
|
137
|
-
actors = subsys.get_all_level_actors()
|
|
138
|
-
found = False
|
|
139
|
-
for actor in actors:
|
|
140
|
-
if not actor:
|
|
141
|
-
continue
|
|
142
|
-
label = actor.get_actor_label()
|
|
143
|
-
name = actor.get_name()
|
|
144
|
-
if label == "{actor_name}" or name == "{actor_name}" or label.lower().startswith("{actor_name}".lower()+"_"):
|
|
145
|
-
success = subsys.destroy_actor(actor)
|
|
146
|
-
print(f"RESULT:{json.dumps({'success': success, 'deleted': label})}")
|
|
147
|
-
found = True
|
|
148
|
-
break
|
|
149
|
-
if not found:
|
|
150
|
-
print(f"RESULT:{json.dumps({'success': False, 'error': 'Actor not found: {actor_name}'})}")
|
|
151
|
-
else:
|
|
152
|
-
print(f"RESULT:{json.dumps({'success': False, 'error': 'EditorActorSubsystem not available'})}")
|
|
153
|
-
except Exception as e:
|
|
154
|
-
print(f"RESULT:{json.dumps({'success': False, 'error': str(e)})}")
|
|
155
|
-
`.trim()
|
|
156
|
-
},
|
|
157
|
-
CREATE_ASSET: {
|
|
158
|
-
name: 'create_asset',
|
|
159
|
-
script: `
|
|
160
|
-
import unreal
|
|
161
|
-
import json
|
|
162
|
-
|
|
163
|
-
try:
|
|
164
|
-
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
|
165
|
-
if asset_tools:
|
|
166
|
-
# Create factory based on asset type
|
|
167
|
-
factory_class = getattr(unreal, '{factory_class}', None)
|
|
168
|
-
asset_class = getattr(unreal, '{asset_class}', None)
|
|
169
|
-
|
|
170
|
-
if factory_class and asset_class:
|
|
171
|
-
factory = factory_class()
|
|
172
|
-
# Clean up the path - remove trailing slashes and normalize
|
|
173
|
-
package_path = "{package_path}".rstrip('/').replace('//', '/')
|
|
174
|
-
|
|
175
|
-
# Ensure package path is valid (starts with /Game or /Engine)
|
|
176
|
-
if not package_path.startswith('/Game') and not package_path.startswith('/Engine'):
|
|
177
|
-
if not package_path.startswith('/'):
|
|
178
|
-
package_path = f"/Game/{package_path}"
|
|
179
|
-
else:
|
|
180
|
-
package_path = f"/Game{package_path}"
|
|
181
|
-
|
|
182
|
-
# Create full asset path for verification
|
|
183
|
-
full_asset_path = f"{package_path}/{asset_name}" if package_path != "/Game" else f"/Game/{asset_name}"
|
|
184
|
-
|
|
185
|
-
# Create the asset with cleaned path
|
|
186
|
-
asset = asset_tools.create_asset("{asset_name}", package_path, asset_class, factory)
|
|
187
|
-
if asset:
|
|
188
|
-
# Save the asset
|
|
189
|
-
saved = unreal.EditorAssetLibrary.save_asset(asset.get_path_name())
|
|
190
|
-
# Enhanced verification with retry logic
|
|
191
|
-
asset_path = asset.get_path_name()
|
|
192
|
-
verification_attempts = 0
|
|
193
|
-
max_verification_attempts = 5
|
|
194
|
-
asset_verified = False
|
|
195
|
-
|
|
196
|
-
while verification_attempts < max_verification_attempts and not asset_verified:
|
|
197
|
-
verification_attempts += 1
|
|
198
|
-
# Wait a bit for the asset to be fully saved
|
|
199
|
-
import time
|
|
200
|
-
time.sleep(0.1)
|
|
201
|
-
|
|
202
|
-
# Check if asset exists
|
|
203
|
-
asset_exists = unreal.EditorAssetLibrary.does_asset_exist(asset_path)
|
|
204
|
-
|
|
205
|
-
if asset_exists:
|
|
206
|
-
asset_verified = True
|
|
207
|
-
elif verification_attempts < max_verification_attempts:
|
|
208
|
-
# Try to reload the asset registry
|
|
209
|
-
try:
|
|
210
|
-
unreal.AssetRegistryHelpers.get_asset_registry().scan_modified_asset_files([asset_path])
|
|
211
|
-
except:
|
|
212
|
-
pass
|
|
213
|
-
|
|
214
|
-
if asset_verified:
|
|
215
|
-
print(f"RESULT:{json.dumps({'success': saved, 'path': asset_path, 'verified': True})}")
|
|
216
|
-
else:
|
|
217
|
-
print(f"RESULT:{json.dumps({'success': saved, 'path': asset_path, 'warning': 'Asset created but verification pending'})}")
|
|
218
|
-
else:
|
|
219
|
-
print(f"RESULT:{json.dumps({'success': False, 'error': 'Failed to create asset'})}")
|
|
220
|
-
else:
|
|
221
|
-
print(f"RESULT:{json.dumps({'success': False, 'error': 'Invalid factory or asset class'})}")
|
|
222
|
-
else:
|
|
223
|
-
print(f"RESULT:{json.dumps({'success': False, 'error': 'AssetToolsHelpers not available'})}")
|
|
224
|
-
except Exception as e:
|
|
225
|
-
print(f"RESULT:{json.dumps({'success': False, 'error': str(e)})}")
|
|
226
|
-
`.trim()
|
|
227
|
-
},
|
|
228
|
-
SET_VIEWPORT_CAMERA: {
|
|
229
|
-
name: 'set_viewport_camera',
|
|
230
|
-
script: `
|
|
231
|
-
import unreal
|
|
232
|
-
import json
|
|
233
|
-
|
|
234
|
-
location = unreal.Vector({x}, {y}, {z})
|
|
235
|
-
rotation = unreal.Rotator({pitch}, {yaw}, {roll})
|
|
236
|
-
|
|
237
|
-
try:
|
|
238
|
-
# Use UnrealEditorSubsystem for viewport operations (UE5.1+)
|
|
239
|
-
ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
240
|
-
les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
|
|
241
|
-
|
|
242
|
-
if ues:
|
|
243
|
-
ues.set_level_viewport_camera_info(location, rotation)
|
|
244
|
-
try:
|
|
245
|
-
if les:
|
|
246
|
-
les.editor_invalidate_viewports()
|
|
247
|
-
except Exception:
|
|
248
|
-
pass
|
|
249
|
-
print(f"RESULT:{json.dumps({'success': True, 'location': [{x}, {y}, {z}], 'rotation': [{pitch}, {yaw}, {roll}]}})}")
|
|
250
|
-
else:
|
|
251
|
-
print(f"RESULT:{json.dumps({'success': False, 'error': 'UnrealEditorSubsystem not available'})}")
|
|
252
|
-
except Exception as e:
|
|
253
|
-
print(f"RESULT:{json.dumps({'success': False, 'error': str(e)})}")
|
|
254
|
-
`.trim()
|
|
255
|
-
},
|
|
256
|
-
BUILD_LIGHTING: {
|
|
257
|
-
name: 'build_lighting',
|
|
258
|
-
script: `
|
|
259
|
-
import unreal
|
|
260
|
-
import json
|
|
261
|
-
|
|
262
|
-
try:
|
|
263
|
-
les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
|
|
264
|
-
if les:
|
|
265
|
-
# Use UE 5.6 enhanced lighting quality settings
|
|
266
|
-
quality_map = {
|
|
267
|
-
'Preview': unreal.LightingBuildQuality.PREVIEW,
|
|
268
|
-
'Medium': unreal.LightingBuildQuality.MEDIUM,
|
|
269
|
-
'High': unreal.LightingBuildQuality.HIGH,
|
|
270
|
-
'Production': unreal.LightingBuildQuality.PRODUCTION
|
|
271
|
-
}
|
|
272
|
-
q = quality_map.get('{quality}', unreal.LightingBuildQuality.PREVIEW)
|
|
273
|
-
les.build_light_maps(q, True)
|
|
274
|
-
print(f"RESULT:{json.dumps({'success': True, 'quality': '{quality}', 'method': 'LevelEditorSubsystem'})}")
|
|
275
|
-
else:
|
|
276
|
-
print(f"RESULT:{json.dumps({'success': False, 'error': 'LevelEditorSubsystem not available'})}")
|
|
277
|
-
except Exception as e:
|
|
278
|
-
print(f"RESULT:{json.dumps({'success': False, 'error': str(e)})}")
|
|
279
|
-
`.trim()
|
|
280
|
-
},
|
|
281
|
-
SAVE_ALL_DIRTY_PACKAGES: {
|
|
282
|
-
name: 'save_dirty_packages',
|
|
283
|
-
script: `
|
|
284
|
-
import unreal
|
|
285
|
-
import json
|
|
286
|
-
|
|
287
|
-
try:
|
|
288
|
-
# Use UE 5.6 enhanced saving with better error handling
|
|
289
|
-
saved = unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True)
|
|
290
|
-
print(f"RESULT:{json.dumps({'success': bool(saved), 'saved_count': saved if isinstance(saved, int) else 0, 'message': 'All dirty packages saved'})}")
|
|
291
|
-
except Exception as e:
|
|
292
|
-
print(f"RESULT:{json.dumps({'success': False, 'error': str(e), 'message': 'Failed to save dirty packages'})}")
|
|
293
|
-
`.trim()
|
|
294
|
-
}
|
|
295
|
-
};
|
|
9
|
+
automationBridge;
|
|
10
|
+
automationBridgeListeners;
|
|
11
|
+
commandQueue = new UnrealCommandQueue();
|
|
296
12
|
get isConnected() { return this.connected; }
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
13
|
+
setAutomationBridge(automationBridge) {
|
|
14
|
+
if (this.automationBridge && this.automationBridgeListeners) {
|
|
15
|
+
this.automationBridge.off('connected', this.automationBridgeListeners.connected);
|
|
16
|
+
this.automationBridge.off('disconnected', this.automationBridgeListeners.disconnected);
|
|
17
|
+
this.automationBridge.off('handshakeFailed', this.automationBridgeListeners.handshakeFailed);
|
|
18
|
+
}
|
|
19
|
+
this.automationBridge = automationBridge;
|
|
20
|
+
this.automationBridgeListeners = undefined;
|
|
21
|
+
if (!automationBridge) {
|
|
22
|
+
this.connected = false;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const onConnected = (info) => {
|
|
26
|
+
this.connected = true;
|
|
27
|
+
this.log.debug('Automation bridge connected', info);
|
|
28
|
+
};
|
|
29
|
+
const onDisconnected = (info) => {
|
|
30
|
+
this.connected = false;
|
|
31
|
+
this.log.debug('Automation bridge disconnected', info);
|
|
32
|
+
};
|
|
33
|
+
const onHandshakeFailed = (info) => {
|
|
34
|
+
this.connected = false;
|
|
35
|
+
this.log.warn('Automation bridge handshake failed', info);
|
|
36
|
+
};
|
|
37
|
+
automationBridge.on('connected', onConnected);
|
|
38
|
+
automationBridge.on('disconnected', onDisconnected);
|
|
39
|
+
automationBridge.on('handshakeFailed', onHandshakeFailed);
|
|
40
|
+
this.automationBridgeListeners = {
|
|
41
|
+
connected: onConnected,
|
|
42
|
+
disconnected: onDisconnected,
|
|
43
|
+
handshakeFailed: onHandshakeFailed
|
|
44
|
+
};
|
|
45
|
+
this.connected = automationBridge.isConnected();
|
|
46
|
+
}
|
|
47
|
+
getAutomationBridge() {
|
|
48
|
+
if (!this.automationBridge) {
|
|
49
|
+
throw new Error('Automation bridge is not configured');
|
|
50
|
+
}
|
|
51
|
+
return this.automationBridge;
|
|
52
|
+
}
|
|
305
53
|
connectPromise;
|
|
306
|
-
async tryConnect(maxAttempts = 3, timeoutMs =
|
|
307
|
-
if (
|
|
54
|
+
async tryConnect(maxAttempts = 3, timeoutMs = 15000, retryDelayMs = 3000) {
|
|
55
|
+
if (process.env.MOCK_UNREAL_CONNECTION === 'true') {
|
|
56
|
+
this.log.info('🔌 MOCK MODE: Simulating active connection');
|
|
57
|
+
this.connected = true;
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (this.connected && this.automationBridge?.isConnected()) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (!this.automationBridge) {
|
|
64
|
+
this.log.warn('Automation bridge is not configured; cannot establish connection.');
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (this.automationBridge.isConnected()) {
|
|
68
|
+
this.connected = true;
|
|
308
69
|
return true;
|
|
70
|
+
}
|
|
309
71
|
if (this.connectPromise) {
|
|
310
72
|
try {
|
|
311
73
|
await this.connectPromise;
|
|
312
74
|
}
|
|
313
|
-
catch {
|
|
314
|
-
// swallow, we'll return connected flag
|
|
315
|
-
}
|
|
75
|
+
catch { }
|
|
316
76
|
return this.connected;
|
|
317
77
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
78
|
+
this.connectPromise = ErrorHandler.retryWithBackoff(() => {
|
|
79
|
+
const envTimeout = process.env.UNREAL_CONNECTION_TIMEOUT ? parseInt(process.env.UNREAL_CONNECTION_TIMEOUT, 10) : 30000;
|
|
80
|
+
const actualTimeout = envTimeout > 0 ? envTimeout : timeoutMs;
|
|
81
|
+
return this.connect(actualTimeout);
|
|
82
|
+
}, {
|
|
83
|
+
maxRetries: Math.max(0, maxAttempts - 1),
|
|
321
84
|
initialDelay: retryDelayMs,
|
|
322
85
|
maxDelay: 10000,
|
|
323
86
|
backoffMultiplier: 1.5,
|
|
324
87
|
shouldRetry: (error) => {
|
|
325
|
-
// Only retry on connection-related errors
|
|
326
88
|
const msg = error?.message?.toLowerCase() || '';
|
|
327
|
-
return msg.includes('timeout') || msg.includes('
|
|
89
|
+
return msg.includes('timeout') || msg.includes('connect') || msg.includes('automation');
|
|
328
90
|
}
|
|
329
|
-
}).then(() => {
|
|
330
|
-
// Success
|
|
331
91
|
}).catch((err) => {
|
|
332
|
-
this.log.warn(`
|
|
92
|
+
this.log.warn(`Automation bridge connection failed after ${maxAttempts} attempts:`, err.message);
|
|
93
|
+
this.log.warn('⚠️ Ensure Unreal Editor is running with MCP Automation Bridge plugin enabled');
|
|
94
|
+
this.log.warn(`⚠️ Plugin should listen on ws://${DEFAULT_AUTOMATION_HOST}:${DEFAULT_AUTOMATION_PORT} for MCP server connections`);
|
|
333
95
|
});
|
|
334
96
|
try {
|
|
335
97
|
await this.connectPromise;
|
|
@@ -337,530 +99,275 @@ except Exception as e:
|
|
|
337
99
|
finally {
|
|
338
100
|
this.connectPromise = undefined;
|
|
339
101
|
}
|
|
102
|
+
this.connected = this.automationBridge?.isConnected() ?? false;
|
|
340
103
|
return this.connected;
|
|
341
104
|
}
|
|
342
|
-
async connect(timeoutMs =
|
|
343
|
-
|
|
344
|
-
if (
|
|
345
|
-
|
|
105
|
+
async connect(timeoutMs = 15000) {
|
|
106
|
+
const automationBridge = this.automationBridge;
|
|
107
|
+
if (!automationBridge) {
|
|
108
|
+
throw new Error('Automation bridge not configured');
|
|
109
|
+
}
|
|
110
|
+
if (automationBridge.isConnected()) {
|
|
111
|
+
this.connected = true;
|
|
346
112
|
return;
|
|
347
113
|
}
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
114
|
+
automationBridge.start();
|
|
115
|
+
const success = await this.waitForAutomationConnection(timeoutMs);
|
|
116
|
+
if (!success) {
|
|
117
|
+
throw new Error('Automation bridge connection timeout');
|
|
118
|
+
}
|
|
119
|
+
this.connected = true;
|
|
120
|
+
}
|
|
121
|
+
async waitForAutomationConnection(timeoutMs) {
|
|
122
|
+
const automationBridge = this.automationBridge;
|
|
123
|
+
if (!automationBridge) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
if (automationBridge.isConnected()) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return new Promise((resolve) => {
|
|
357
130
|
let settled = false;
|
|
358
|
-
const
|
|
359
|
-
settled
|
|
360
|
-
|
|
361
|
-
} };
|
|
362
|
-
const safeReject = (err) => { if (!settled) {
|
|
363
|
-
settled = true;
|
|
364
|
-
reject(err);
|
|
365
|
-
} };
|
|
366
|
-
// Setup timeout
|
|
367
|
-
const timeout = setTimeout(() => {
|
|
368
|
-
this.log.warn(`Connection timeout after ${timeoutMs}ms`);
|
|
369
|
-
if (this.ws) {
|
|
370
|
-
try {
|
|
371
|
-
// Attach a temporary error handler to avoid unhandled 'error' events on abort
|
|
372
|
-
this.ws.on('error', () => { });
|
|
373
|
-
// Prefer graceful close; terminate as a fallback
|
|
374
|
-
if (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN) {
|
|
375
|
-
try {
|
|
376
|
-
this.ws.close();
|
|
377
|
-
}
|
|
378
|
-
catch { }
|
|
379
|
-
try {
|
|
380
|
-
this.ws.terminate();
|
|
381
|
-
}
|
|
382
|
-
catch { }
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
finally {
|
|
386
|
-
try {
|
|
387
|
-
this.ws.removeAllListeners();
|
|
388
|
-
}
|
|
389
|
-
catch { }
|
|
390
|
-
this.ws = undefined;
|
|
391
|
-
}
|
|
131
|
+
const cleanup = () => {
|
|
132
|
+
if (settled) {
|
|
133
|
+
return;
|
|
392
134
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
this.log.info('Connected to Unreal Remote Control');
|
|
400
|
-
this.startCommandProcessor(); // Start command processor on connect
|
|
401
|
-
safeResolve();
|
|
135
|
+
settled = true;
|
|
136
|
+
automationBridge.off('connected', onConnected);
|
|
137
|
+
automationBridge.off('handshakeFailed', onHandshakeFailed);
|
|
138
|
+
automationBridge.off('error', onError);
|
|
139
|
+
automationBridge.off('disconnected', onDisconnected);
|
|
140
|
+
clearTimeout(timer);
|
|
402
141
|
};
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
this.log.debug(`WebSocket error during connect: ${(err && err.code) || ''} ${err.message}`);
|
|
408
|
-
if (this.ws) {
|
|
409
|
-
try {
|
|
410
|
-
// Attach a temporary error handler to avoid unhandled 'error' events while aborting
|
|
411
|
-
this.ws.on('error', () => { });
|
|
412
|
-
if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
|
|
413
|
-
try {
|
|
414
|
-
this.ws.close();
|
|
415
|
-
}
|
|
416
|
-
catch { }
|
|
417
|
-
try {
|
|
418
|
-
this.ws.terminate();
|
|
419
|
-
}
|
|
420
|
-
catch { }
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
finally {
|
|
424
|
-
try {
|
|
425
|
-
this.ws.removeAllListeners();
|
|
426
|
-
}
|
|
427
|
-
catch { }
|
|
428
|
-
this.ws = undefined;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
safeReject(new Error(`Failed to connect: ${err.message}`));
|
|
142
|
+
const onConnected = (info) => {
|
|
143
|
+
cleanup();
|
|
144
|
+
this.log.debug('Automation bridge connected while waiting', info);
|
|
145
|
+
resolve(true);
|
|
432
146
|
};
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
safeReject(new Error('Connection closed before establishing'));
|
|
438
|
-
}
|
|
439
|
-
else {
|
|
440
|
-
// Normal close after connection was established
|
|
441
|
-
this.connected = false;
|
|
442
|
-
this.ws = undefined;
|
|
443
|
-
this.log.warn('WebSocket closed');
|
|
444
|
-
if (this.autoReconnectEnabled) {
|
|
445
|
-
this.scheduleReconnect();
|
|
446
|
-
}
|
|
447
|
-
}
|
|
147
|
+
const onHandshakeFailed = (info) => {
|
|
148
|
+
this.log.warn('Automation bridge handshake failed while waiting', info);
|
|
149
|
+
cleanup();
|
|
150
|
+
resolve(false);
|
|
448
151
|
};
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
this.log.debug('WS message', msg);
|
|
454
|
-
}
|
|
455
|
-
catch (_e) {
|
|
456
|
-
// Noise reduction: keep at debug and do nothing on parse errors
|
|
457
|
-
}
|
|
152
|
+
const onError = (err) => {
|
|
153
|
+
this.log.warn('Automation bridge error while waiting', err);
|
|
154
|
+
cleanup();
|
|
155
|
+
resolve(false);
|
|
458
156
|
};
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
157
|
+
const onDisconnected = (info) => {
|
|
158
|
+
this.log.warn('Automation bridge disconnected while waiting', info);
|
|
159
|
+
cleanup();
|
|
160
|
+
resolve(false);
|
|
161
|
+
};
|
|
162
|
+
const timer = setTimeout(() => {
|
|
163
|
+
cleanup();
|
|
164
|
+
resolve(false);
|
|
165
|
+
}, Math.max(0, timeoutMs));
|
|
166
|
+
automationBridge.on('connected', onConnected);
|
|
167
|
+
automationBridge.on('handshakeFailed', onHandshakeFailed);
|
|
168
|
+
automationBridge.on('error', onError);
|
|
169
|
+
automationBridge.on('disconnected', onDisconnected);
|
|
464
170
|
});
|
|
465
171
|
}
|
|
466
|
-
async
|
|
467
|
-
|
|
468
|
-
if (!
|
|
469
|
-
throw new Error('
|
|
470
|
-
}
|
|
471
|
-
const url = path.startsWith('/') ? path : `/${path}`;
|
|
472
|
-
const started = Date.now();
|
|
473
|
-
// Fix Content-Length header issue - ensure body is properly handled
|
|
474
|
-
let payload = body;
|
|
475
|
-
if ((payload === undefined || payload === null) && method !== 'GET') {
|
|
476
|
-
payload = {};
|
|
477
|
-
}
|
|
478
|
-
// Add timeout wrapper to prevent hanging - adjust based on operation type
|
|
479
|
-
let CALL_TIMEOUT = 10000; // Default 10 seconds timeout
|
|
480
|
-
const longRunningTimeout = 10 * 60 * 1000; // 10 minutes for heavy editor jobs
|
|
481
|
-
// Use payload contents to detect long-running editor operations
|
|
482
|
-
let payloadSignature = '';
|
|
483
|
-
if (typeof payload === 'string') {
|
|
484
|
-
payloadSignature = payload;
|
|
485
|
-
}
|
|
486
|
-
else if (payload && typeof payload === 'object') {
|
|
487
|
-
try {
|
|
488
|
-
payloadSignature = JSON.stringify(payload);
|
|
489
|
-
}
|
|
490
|
-
catch {
|
|
491
|
-
payloadSignature = '';
|
|
492
|
-
}
|
|
172
|
+
async getObjectProperty(params) {
|
|
173
|
+
const { objectPath, propertyName, timeoutMs } = params;
|
|
174
|
+
if (!objectPath || typeof objectPath !== 'string') {
|
|
175
|
+
throw new Error('Invalid objectPath: must be a non-empty string');
|
|
493
176
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if (payload && typeof payload === 'object' && '__callTimeoutMs' in payload) {
|
|
497
|
-
const overrideRaw = payload.__callTimeoutMs;
|
|
498
|
-
const overrideMs = typeof overrideRaw === 'number'
|
|
499
|
-
? overrideRaw
|
|
500
|
-
: Number.parseInt(String(overrideRaw), 10);
|
|
501
|
-
if (Number.isFinite(overrideMs) && overrideMs > 0) {
|
|
502
|
-
CALL_TIMEOUT = Math.max(CALL_TIMEOUT, overrideMs);
|
|
503
|
-
}
|
|
504
|
-
sanitizedPayload = { ...payload };
|
|
505
|
-
delete sanitizedPayload.__callTimeoutMs;
|
|
177
|
+
if (!propertyName || typeof propertyName !== 'string') {
|
|
178
|
+
throw new Error('Invalid propertyName: must be a non-empty string');
|
|
506
179
|
}
|
|
507
|
-
|
|
508
|
-
if (
|
|
509
|
-
|
|
180
|
+
const bridge = this.automationBridge;
|
|
181
|
+
if (process.env.MOCK_UNREAL_CONNECTION === 'true') {
|
|
182
|
+
return {
|
|
183
|
+
success: true,
|
|
184
|
+
objectPath,
|
|
185
|
+
propertyName,
|
|
186
|
+
value: 'MockValue',
|
|
187
|
+
propertyValue: 'MockValue',
|
|
188
|
+
transport: 'mock_bridge',
|
|
189
|
+
message: 'Mock property read successful'
|
|
190
|
+
};
|
|
510
191
|
}
|
|
511
|
-
if (
|
|
512
|
-
|
|
192
|
+
if (!bridge || typeof bridge.sendAutomationRequest !== 'function') {
|
|
193
|
+
return {
|
|
194
|
+
success: false,
|
|
195
|
+
objectPath,
|
|
196
|
+
propertyName,
|
|
197
|
+
error: 'Automation bridge not connected',
|
|
198
|
+
transport: 'automation_bridge'
|
|
199
|
+
};
|
|
513
200
|
}
|
|
514
|
-
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
201
|
+
try {
|
|
202
|
+
const response = await bridge.sendAutomationRequest('get_object_property', {
|
|
203
|
+
objectPath,
|
|
204
|
+
propertyName
|
|
205
|
+
}, timeoutMs ? { timeoutMs } : undefined);
|
|
206
|
+
const success = response.success !== false;
|
|
207
|
+
const rawResult = response.result && typeof response.result === 'object'
|
|
208
|
+
? { ...response.result }
|
|
209
|
+
: response.result;
|
|
210
|
+
const value = rawResult?.value ??
|
|
211
|
+
rawResult?.propertyValue ??
|
|
212
|
+
(success ? rawResult : undefined);
|
|
213
|
+
if (success) {
|
|
214
|
+
return {
|
|
215
|
+
success: true,
|
|
216
|
+
objectPath,
|
|
217
|
+
propertyName,
|
|
218
|
+
value,
|
|
219
|
+
propertyValue: value,
|
|
220
|
+
transport: 'automation_bridge',
|
|
221
|
+
message: response.message,
|
|
222
|
+
warnings: Array.isArray(rawResult?.warnings)
|
|
223
|
+
? rawResult.warnings
|
|
224
|
+
: undefined,
|
|
225
|
+
raw: rawResult,
|
|
226
|
+
bridge: {
|
|
227
|
+
requestId: response.requestId,
|
|
228
|
+
success: true,
|
|
229
|
+
error: response.error
|
|
230
|
+
}
|
|
231
|
+
};
|
|
527
232
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
'buildlandscapeinfo', // Can crash without landscape
|
|
540
|
-
'rebuildselectednavigation' // Nav-related crash
|
|
541
|
-
];
|
|
542
|
-
// Check if this is a crash-inducing command
|
|
543
|
-
if (crashCommands.some(dangerous => cmdLower === dangerous || cmdLower.startsWith(dangerous + ' '))) {
|
|
544
|
-
this.log.warn(`BLOCKED dangerous command that causes crashes: ${command}`);
|
|
545
|
-
// Return a safe error response instead of executing
|
|
546
|
-
return {
|
|
547
|
-
success: false,
|
|
548
|
-
error: `Command '${command}' blocked: This command can cause Unreal Engine to crash. Use the Python API alternatives instead.`
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
// Also block other dangerous commands
|
|
552
|
-
const dangerousPatterns = [
|
|
553
|
-
'quit', 'exit', 'r.gpucrash', 'debug crash',
|
|
554
|
-
'viewmode visualizebuffer' // These can crash in certain states
|
|
555
|
-
];
|
|
556
|
-
if (dangerousPatterns.some(pattern => cmdLower.includes(pattern))) {
|
|
557
|
-
this.log.warn(`BLOCKED potentially dangerous command: ${command}`);
|
|
558
|
-
return {
|
|
559
|
-
success: false,
|
|
560
|
-
error: `Command '${command}' blocked for safety.`
|
|
561
|
-
};
|
|
233
|
+
return {
|
|
234
|
+
success: false,
|
|
235
|
+
objectPath,
|
|
236
|
+
propertyName,
|
|
237
|
+
error: response.error || response.message || 'AUTOMATION_BRIDGE_FAILURE',
|
|
238
|
+
transport: 'automation_bridge',
|
|
239
|
+
raw: rawResult,
|
|
240
|
+
bridge: {
|
|
241
|
+
requestId: response.requestId,
|
|
242
|
+
success: false,
|
|
243
|
+
error: response.error
|
|
562
244
|
}
|
|
563
|
-
}
|
|
245
|
+
};
|
|
564
246
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
else if (sanitizedPayload !== undefined) {
|
|
575
|
-
config.data = sanitizedPayload;
|
|
576
|
-
}
|
|
577
|
-
// Wrap with timeout promise to ensure we don't hang
|
|
578
|
-
const requestPromise = this.http.request(config);
|
|
579
|
-
const resp = await new Promise((resolve, reject) => {
|
|
580
|
-
const timer = setTimeout(() => {
|
|
581
|
-
const err = new Error(`Request timeout after ${CALL_TIMEOUT}ms`);
|
|
582
|
-
err.code = 'UE_HTTP_TIMEOUT';
|
|
583
|
-
reject(err);
|
|
584
|
-
}, CALL_TIMEOUT);
|
|
585
|
-
requestPromise.then(result => {
|
|
586
|
-
clearTimeout(timer);
|
|
587
|
-
resolve(result);
|
|
588
|
-
}).catch(err => {
|
|
589
|
-
clearTimeout(timer);
|
|
590
|
-
reject(err);
|
|
591
|
-
});
|
|
592
|
-
});
|
|
593
|
-
const ms = Date.now() - started;
|
|
594
|
-
// Add connection health check for long-running requests
|
|
595
|
-
if (ms > 5000) {
|
|
596
|
-
this.log.debug(`[HTTP ${method}] ${url} -> ${ms}ms (long request)`);
|
|
597
|
-
}
|
|
598
|
-
else {
|
|
599
|
-
this.log.debug(`[HTTP ${method}] ${url} -> ${ms}ms`);
|
|
600
|
-
}
|
|
601
|
-
return resp.data;
|
|
602
|
-
}
|
|
603
|
-
catch (error) {
|
|
604
|
-
lastError = error;
|
|
605
|
-
const delay = Math.min(1000 * Math.pow(2, attempt), 5000); // Exponential backoff with 5s max
|
|
606
|
-
// Log timeout errors specifically
|
|
607
|
-
if (error.message?.includes('timeout')) {
|
|
608
|
-
this.log.debug(`HTTP request timed out (attempt ${attempt + 1}/3): ${url}`);
|
|
609
|
-
}
|
|
610
|
-
if (attempt < 2) {
|
|
611
|
-
this.log.debug(`HTTP request failed (attempt ${attempt + 1}/3), retrying in ${delay}ms...`);
|
|
612
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
613
|
-
// If connection error, try to reconnect
|
|
614
|
-
if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.message?.includes('timeout')) {
|
|
615
|
-
if (this.autoReconnectEnabled) {
|
|
616
|
-
this.scheduleReconnect();
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
249
|
+
return {
|
|
250
|
+
success: false,
|
|
251
|
+
objectPath,
|
|
252
|
+
propertyName,
|
|
253
|
+
error: message,
|
|
254
|
+
transport: 'automation_bridge'
|
|
255
|
+
};
|
|
621
256
|
}
|
|
622
|
-
throw lastError;
|
|
623
257
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
258
|
+
async setObjectProperty(params) {
|
|
259
|
+
const { objectPath, propertyName, value, markDirty, timeoutMs } = params;
|
|
260
|
+
if (!objectPath || typeof objectPath !== 'string') {
|
|
261
|
+
throw new Error('Invalid objectPath: must be a non-empty string');
|
|
627
262
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
fragments.push(raw);
|
|
263
|
+
if (!propertyName || typeof propertyName !== 'string') {
|
|
264
|
+
throw new Error('Invalid propertyName: must be a non-empty string');
|
|
631
265
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
266
|
+
const bridge = this.automationBridge;
|
|
267
|
+
if (process.env.MOCK_UNREAL_CONNECTION === 'true') {
|
|
268
|
+
return {
|
|
269
|
+
success: true,
|
|
270
|
+
objectPath,
|
|
271
|
+
propertyName,
|
|
272
|
+
message: 'Mock property set successful',
|
|
273
|
+
transport: 'mock_bridge'
|
|
274
|
+
};
|
|
637
275
|
}
|
|
638
|
-
if (
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
fragments.push(entry.Output);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
276
|
+
if (!bridge || typeof bridge.sendAutomationRequest !== 'function') {
|
|
277
|
+
return {
|
|
278
|
+
success: false,
|
|
279
|
+
objectPath,
|
|
280
|
+
propertyName,
|
|
281
|
+
error: 'Automation bridge not connected',
|
|
282
|
+
transport: 'automation_bridge'
|
|
283
|
+
};
|
|
649
284
|
}
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
285
|
+
const payload = {
|
|
286
|
+
objectPath,
|
|
287
|
+
propertyName,
|
|
288
|
+
value
|
|
289
|
+
};
|
|
290
|
+
if (markDirty !== undefined) {
|
|
291
|
+
payload.markDirty = Boolean(markDirty);
|
|
654
292
|
}
|
|
655
293
|
try {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
}
|
|
675
|
-
return false;
|
|
676
|
-
});
|
|
677
|
-
if (pluginsToCheck.length > 0) {
|
|
678
|
-
const python = `
|
|
679
|
-
import unreal
|
|
680
|
-
import json
|
|
681
|
-
|
|
682
|
-
plugins = ${JSON.stringify(pluginsToCheck)}
|
|
683
|
-
status = {}
|
|
684
|
-
|
|
685
|
-
def get_plugin_manager():
|
|
686
|
-
try:
|
|
687
|
-
return unreal.PluginManager.get()
|
|
688
|
-
except AttributeError:
|
|
689
|
-
return None
|
|
690
|
-
except Exception:
|
|
691
|
-
return None
|
|
692
|
-
|
|
693
|
-
def get_plugins_subsystem():
|
|
694
|
-
try:
|
|
695
|
-
return unreal.get_editor_subsystem(unreal.PluginsEditorSubsystem)
|
|
696
|
-
except AttributeError:
|
|
697
|
-
pass
|
|
698
|
-
except Exception:
|
|
699
|
-
pass
|
|
700
|
-
try:
|
|
701
|
-
return unreal.PluginsSubsystem()
|
|
702
|
-
except Exception:
|
|
703
|
-
return None
|
|
704
|
-
|
|
705
|
-
pm = get_plugin_manager()
|
|
706
|
-
ps = get_plugins_subsystem()
|
|
707
|
-
|
|
708
|
-
def is_enabled(plugin_name):
|
|
709
|
-
if pm:
|
|
710
|
-
try:
|
|
711
|
-
if pm.is_plugin_enabled(plugin_name):
|
|
712
|
-
return True
|
|
713
|
-
except Exception:
|
|
714
|
-
try:
|
|
715
|
-
plugin = pm.find_plugin(plugin_name)
|
|
716
|
-
if plugin and plugin.is_enabled():
|
|
717
|
-
return True
|
|
718
|
-
except Exception:
|
|
719
|
-
pass
|
|
720
|
-
if ps:
|
|
721
|
-
try:
|
|
722
|
-
return bool(ps.is_plugin_enabled(plugin_name))
|
|
723
|
-
except Exception:
|
|
724
|
-
try:
|
|
725
|
-
plugin = ps.find_plugin(plugin_name)
|
|
726
|
-
if plugin and plugin.is_enabled():
|
|
727
|
-
return True
|
|
728
|
-
except Exception:
|
|
729
|
-
pass
|
|
730
|
-
return False
|
|
731
|
-
|
|
732
|
-
for plugin_name in plugins:
|
|
733
|
-
enabled = False
|
|
734
|
-
try:
|
|
735
|
-
enabled = is_enabled(plugin_name)
|
|
736
|
-
except Exception:
|
|
737
|
-
enabled = False
|
|
738
|
-
status[plugin_name] = bool(enabled)
|
|
739
|
-
|
|
740
|
-
print('RESULT:' + json.dumps(status))
|
|
741
|
-
`.trim();
|
|
742
|
-
try {
|
|
743
|
-
const response = await this.executePython(python);
|
|
744
|
-
const parsed = this.parsePythonJsonResult(response);
|
|
745
|
-
if (parsed) {
|
|
746
|
-
for (const [name, enabled] of Object.entries(parsed)) {
|
|
747
|
-
this.pluginStatusCache.set(name, { enabled: Boolean(enabled), timestamp: now });
|
|
294
|
+
const response = await bridge.sendAutomationRequest('set_object_property', payload, timeoutMs ? { timeoutMs } : undefined);
|
|
295
|
+
const success = response.success !== false;
|
|
296
|
+
const rawResult = response.result && typeof response.result === 'object'
|
|
297
|
+
? { ...response.result }
|
|
298
|
+
: response.result;
|
|
299
|
+
if (success) {
|
|
300
|
+
return {
|
|
301
|
+
success: true,
|
|
302
|
+
objectPath,
|
|
303
|
+
propertyName,
|
|
304
|
+
message: response.message ||
|
|
305
|
+
(typeof rawResult?.message === 'string' ? rawResult.message : undefined),
|
|
306
|
+
transport: 'automation_bridge',
|
|
307
|
+
raw: rawResult,
|
|
308
|
+
bridge: {
|
|
309
|
+
requestId: response.requestId,
|
|
310
|
+
success: true,
|
|
311
|
+
error: response.error
|
|
748
312
|
}
|
|
749
|
-
}
|
|
750
|
-
else {
|
|
751
|
-
this.log.warn('Failed to parse plugin status response', { context, pluginsToCheck });
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
catch (error) {
|
|
755
|
-
this.log.warn('Plugin status check failed', { context, pluginsToCheck, error: error?.message ?? error });
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
for (const name of pluginNames) {
|
|
759
|
-
if (!this.pluginStatusCache.has(name)) {
|
|
760
|
-
this.pluginStatusCache.set(name, { enabled: false, timestamp: now });
|
|
313
|
+
};
|
|
761
314
|
}
|
|
315
|
+
return {
|
|
316
|
+
success: false,
|
|
317
|
+
objectPath,
|
|
318
|
+
propertyName,
|
|
319
|
+
error: response.error || response.message || 'AUTOMATION_BRIDGE_FAILURE',
|
|
320
|
+
transport: 'automation_bridge',
|
|
321
|
+
raw: rawResult,
|
|
322
|
+
bridge: {
|
|
323
|
+
requestId: response.requestId,
|
|
324
|
+
success: false,
|
|
325
|
+
error: response.error
|
|
326
|
+
}
|
|
327
|
+
};
|
|
762
328
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
329
|
+
catch (err) {
|
|
330
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
331
|
+
return {
|
|
332
|
+
success: false,
|
|
333
|
+
objectPath,
|
|
334
|
+
propertyName,
|
|
335
|
+
error: message,
|
|
336
|
+
transport: 'automation_bridge'
|
|
337
|
+
};
|
|
766
338
|
}
|
|
767
|
-
return missing;
|
|
768
|
-
}
|
|
769
|
-
// Generic function call via Remote Control HTTP API
|
|
770
|
-
async call(body) {
|
|
771
|
-
if (!this.connected)
|
|
772
|
-
throw new Error('Not connected to Unreal Engine');
|
|
773
|
-
// Using HTTP endpoint /remote/object/call
|
|
774
|
-
const result = await this.httpCall('/remote/object/call', 'PUT', {
|
|
775
|
-
generateTransaction: false,
|
|
776
|
-
...body
|
|
777
|
-
});
|
|
778
|
-
return result;
|
|
779
|
-
}
|
|
780
|
-
async getExposed() {
|
|
781
|
-
if (!this.connected)
|
|
782
|
-
throw new Error('Not connected to Unreal Engine');
|
|
783
|
-
return this.httpCall('/remote/preset', 'GET');
|
|
784
339
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
if (!
|
|
788
|
-
throw new Error('
|
|
789
|
-
}
|
|
790
|
-
const { allowPython = false } = options;
|
|
791
|
-
// Validate command is not empty
|
|
792
|
-
if (!command || typeof command !== 'string') {
|
|
793
|
-
throw new Error('Invalid command: must be a non-empty string');
|
|
340
|
+
async executeConsoleCommand(command, _options = {}) {
|
|
341
|
+
const automationAvailable = Boolean(this.automationBridge && typeof this.automationBridge.sendAutomationRequest === 'function');
|
|
342
|
+
if (!automationAvailable) {
|
|
343
|
+
throw new Error('Automation bridge not connected');
|
|
794
344
|
}
|
|
345
|
+
CommandValidator.validate(command);
|
|
795
346
|
const cmdTrimmed = command.trim();
|
|
796
347
|
if (cmdTrimmed.length === 0) {
|
|
797
|
-
// Return success for empty commands to match UE behavior
|
|
798
348
|
return { success: true, message: 'Empty command ignored' };
|
|
799
349
|
}
|
|
800
|
-
if (
|
|
801
|
-
throw new Error('Multi-line console commands are not allowed. Send one command per call.');
|
|
802
|
-
}
|
|
803
|
-
const cmdLower = cmdTrimmed.toLowerCase();
|
|
804
|
-
if (!allowPython && (cmdLower === 'py' || cmdLower.startsWith('py '))) {
|
|
805
|
-
throw new Error('Python console commands are blocked from external calls for safety.');
|
|
806
|
-
}
|
|
807
|
-
// Check for dangerous commands
|
|
808
|
-
const dangerousCommands = [
|
|
809
|
-
'quit', 'exit', 'delete', 'destroy', 'kill', 'crash',
|
|
810
|
-
'viewmode visualizebuffer basecolor',
|
|
811
|
-
'viewmode visualizebuffer worldnormal',
|
|
812
|
-
'r.gpucrash',
|
|
813
|
-
'buildpaths', // Can cause access violation if nav system not initialized
|
|
814
|
-
'rebuildnavigation' // Can also crash without proper nav setup
|
|
815
|
-
];
|
|
816
|
-
if (dangerousCommands.some(dangerous => cmdLower.includes(dangerous))) {
|
|
817
|
-
throw new Error(`Dangerous command blocked: ${command}`);
|
|
818
|
-
}
|
|
819
|
-
const forbiddenTokens = [
|
|
820
|
-
'rm ', 'rm-', 'del ', 'format ', 'shutdown', 'reboot',
|
|
821
|
-
'rmdir', 'mklink', 'copy ', 'move ', 'start "', 'system(',
|
|
822
|
-
'import os', 'import subprocess', 'subprocess.', 'os.system',
|
|
823
|
-
'exec(', 'eval(', '__import__', 'import sys', 'import importlib',
|
|
824
|
-
'with open', 'open('
|
|
825
|
-
];
|
|
826
|
-
if (cmdLower.includes('&&') || cmdLower.includes('||')) {
|
|
827
|
-
throw new Error('Command chaining with && or || is blocked for safety.');
|
|
828
|
-
}
|
|
829
|
-
if (forbiddenTokens.some(token => cmdLower.includes(token))) {
|
|
830
|
-
throw new Error(`Command contains unsafe token and was blocked: ${command}`);
|
|
831
|
-
}
|
|
832
|
-
// Determine priority based on command type
|
|
833
|
-
let priority = 7; // Default priority
|
|
834
|
-
if (command.includes('BuildLighting') || command.includes('BuildPaths')) {
|
|
835
|
-
priority = 1; // Heavy operation
|
|
836
|
-
}
|
|
837
|
-
else if (command.includes('summon') || command.includes('spawn')) {
|
|
838
|
-
priority = 5; // Medium operation
|
|
839
|
-
}
|
|
840
|
-
else if (command.startsWith('stat') || command.startsWith('show')) {
|
|
841
|
-
priority = 9; // Light operation
|
|
842
|
-
}
|
|
843
|
-
// Known invalid command patterns
|
|
844
|
-
const invalidPatterns = [
|
|
845
|
-
/^\d+$/, // Just numbers
|
|
846
|
-
/^invalid_command/i,
|
|
847
|
-
/^this_is_not_a_valid/i,
|
|
848
|
-
];
|
|
849
|
-
const isLikelyInvalid = invalidPatterns.some(pattern => pattern.test(cmdTrimmed));
|
|
850
|
-
if (isLikelyInvalid) {
|
|
350
|
+
if (CommandValidator.isLikelyInvalid(cmdTrimmed)) {
|
|
851
351
|
this.log.warn(`Command appears invalid: ${cmdTrimmed}`);
|
|
852
352
|
}
|
|
353
|
+
const priority = CommandValidator.getPriority(cmdTrimmed);
|
|
354
|
+
const executeCommand = async () => {
|
|
355
|
+
if (process.env.MOCK_UNREAL_CONNECTION === 'true') {
|
|
356
|
+
this.log.info(`[MOCK] Executing console command: ${cmdTrimmed}`);
|
|
357
|
+
return { success: true, message: `Mock execution of '${cmdTrimmed}' successful`, transport: 'mock_bridge' };
|
|
358
|
+
}
|
|
359
|
+
if (!this.automationBridge || !this.automationBridge.isConnected()) {
|
|
360
|
+
throw new Error('Automation bridge not connected');
|
|
361
|
+
}
|
|
362
|
+
const pluginResp = await this.automationBridge.sendAutomationRequest('console_command', { command: cmdTrimmed }, { timeoutMs: 30000 });
|
|
363
|
+
if (pluginResp && pluginResp.success) {
|
|
364
|
+
return { ...pluginResp, transport: 'automation_bridge' };
|
|
365
|
+
}
|
|
366
|
+
const errMsg = pluginResp?.message || pluginResp?.error || 'Plugin execution failed';
|
|
367
|
+
throw new Error(errMsg);
|
|
368
|
+
};
|
|
853
369
|
try {
|
|
854
|
-
const result = await this.executeThrottledCommand(
|
|
855
|
-
objectPath: '/Script/Engine.Default__KismetSystemLibrary',
|
|
856
|
-
functionName: 'ExecuteConsoleCommand',
|
|
857
|
-
parameters: {
|
|
858
|
-
WorldContextObject: null,
|
|
859
|
-
Command: cmdTrimmed,
|
|
860
|
-
SpecificPlayer: null
|
|
861
|
-
},
|
|
862
|
-
generateTransaction: false
|
|
863
|
-
}), priority);
|
|
370
|
+
const result = await this.executeThrottledCommand(executeCommand, priority);
|
|
864
371
|
return result;
|
|
865
372
|
}
|
|
866
373
|
catch (error) {
|
|
@@ -868,47 +375,6 @@ print('RESULT:' + json.dumps(status))
|
|
|
868
375
|
throw error;
|
|
869
376
|
}
|
|
870
377
|
}
|
|
871
|
-
summarizeConsoleCommand(command, response) {
|
|
872
|
-
const trimmedCommand = command.trim();
|
|
873
|
-
const logLines = Array.isArray(response?.LogOutput)
|
|
874
|
-
? response.LogOutput.map(entry => {
|
|
875
|
-
if (entry === null || entry === undefined) {
|
|
876
|
-
return '';
|
|
877
|
-
}
|
|
878
|
-
if (typeof entry === 'string') {
|
|
879
|
-
return entry;
|
|
880
|
-
}
|
|
881
|
-
return typeof entry.Output === 'string' ? entry.Output : '';
|
|
882
|
-
}).filter(Boolean)
|
|
883
|
-
: [];
|
|
884
|
-
let output = logLines.join('\n').trim();
|
|
885
|
-
if (!output) {
|
|
886
|
-
if (typeof response === 'string') {
|
|
887
|
-
output = response.trim();
|
|
888
|
-
}
|
|
889
|
-
else if (response && typeof response === 'object') {
|
|
890
|
-
if (typeof response.Output === 'string') {
|
|
891
|
-
output = response.Output.trim();
|
|
892
|
-
}
|
|
893
|
-
else if ('result' in response && response.result !== undefined) {
|
|
894
|
-
output = String(response.result).trim();
|
|
895
|
-
}
|
|
896
|
-
else if ('ReturnValue' in response && typeof response.ReturnValue === 'string') {
|
|
897
|
-
output = response.ReturnValue.trim();
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
const returnValue = response && typeof response === 'object' && 'ReturnValue' in response
|
|
902
|
-
? response.ReturnValue
|
|
903
|
-
: undefined;
|
|
904
|
-
return {
|
|
905
|
-
command: trimmedCommand,
|
|
906
|
-
output,
|
|
907
|
-
logLines,
|
|
908
|
-
returnValue,
|
|
909
|
-
raw: response
|
|
910
|
-
};
|
|
911
|
-
}
|
|
912
378
|
async executeConsoleCommands(commands, options = {}) {
|
|
913
379
|
const { continueOnError = false, delayMs = 0 } = options;
|
|
914
380
|
const results = [];
|
|
@@ -919,9 +385,7 @@ print('RESULT:' + json.dumps(status))
|
|
|
919
385
|
continue;
|
|
920
386
|
}
|
|
921
387
|
try {
|
|
922
|
-
const result = await this.executeConsoleCommand(command
|
|
923
|
-
allowPython: Boolean(descriptor.allowPython)
|
|
924
|
-
});
|
|
388
|
+
const result = await this.executeConsoleCommand(command);
|
|
925
389
|
results.push(result);
|
|
926
390
|
}
|
|
927
391
|
catch (error) {
|
|
@@ -937,698 +401,92 @@ print('RESULT:' + json.dumps(status))
|
|
|
937
401
|
}
|
|
938
402
|
return results;
|
|
939
403
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
PythonCommand: command,
|
|
953
|
-
ExecutionMode: isMultiLine ? 'ExecuteFile' : 'ExecuteStatement',
|
|
954
|
-
FileExecutionScope: 'Private'
|
|
955
|
-
},
|
|
956
|
-
generateTransaction: false
|
|
957
|
-
});
|
|
958
|
-
}
|
|
959
|
-
catch {
|
|
960
|
-
try {
|
|
961
|
-
// Fallback to ExecutePythonCommand (more tolerant for multi-line)
|
|
962
|
-
return await this.httpCall('/remote/object/call', 'PUT', {
|
|
963
|
-
objectPath: '/Script/PythonScriptPlugin.Default__PythonScriptLibrary',
|
|
964
|
-
functionName: 'ExecutePythonCommand',
|
|
965
|
-
parameters: {
|
|
966
|
-
Command: command
|
|
967
|
-
},
|
|
968
|
-
generateTransaction: false
|
|
969
|
-
});
|
|
970
|
-
}
|
|
971
|
-
catch {
|
|
972
|
-
// Final fallback: execute via console py command
|
|
973
|
-
this.log.warn('PythonScriptLibrary not available or failed, falling back to console `py` command');
|
|
974
|
-
// For simple single-line commands
|
|
975
|
-
if (!isMultiLine) {
|
|
976
|
-
return await this.executeConsoleCommand(`py ${command}`, { allowPython: true });
|
|
977
|
-
}
|
|
978
|
-
// For multi-line scripts, try to execute as a block
|
|
979
|
-
try {
|
|
980
|
-
// Try executing as a single exec block
|
|
981
|
-
// Properly escape the script for Python exec
|
|
982
|
-
const escapedScript = command
|
|
983
|
-
.replace(/\\/g, '\\\\')
|
|
984
|
-
.replace(/"/g, '\\"')
|
|
985
|
-
.replace(/\n/g, '\\n')
|
|
986
|
-
.replace(/\r/g, '');
|
|
987
|
-
return await this.executeConsoleCommand(`py exec("${escapedScript}")`, { allowPython: true });
|
|
988
|
-
}
|
|
989
|
-
catch {
|
|
990
|
-
// If that fails, break into smaller chunks
|
|
991
|
-
try {
|
|
992
|
-
// First ensure unreal is imported
|
|
993
|
-
await this.executeConsoleCommand('py import unreal');
|
|
994
|
-
// For complex multi-line scripts, execute in logical chunks
|
|
995
|
-
const commandWithoutImport = command.replace(/^\s*import\s+unreal\s*;?\s*/m, '');
|
|
996
|
-
// Split by semicolons first, then by newlines
|
|
997
|
-
const statements = commandWithoutImport
|
|
998
|
-
.split(/[;\n]/)
|
|
999
|
-
.map(s => s.trim())
|
|
1000
|
-
.filter(s => s.length > 0 && !s.startsWith('#'));
|
|
1001
|
-
let result = null;
|
|
1002
|
-
for (const stmt of statements) {
|
|
1003
|
-
// Skip if statement is too long for console
|
|
1004
|
-
if (stmt.length > 200) {
|
|
1005
|
-
// Try to execute as a single exec block
|
|
1006
|
-
const miniScript = `exec("""${stmt.replace(/"/g, '\\"')}""")`;
|
|
1007
|
-
result = await this.executeConsoleCommand(`py ${miniScript}`, { allowPython: true });
|
|
1008
|
-
}
|
|
1009
|
-
else {
|
|
1010
|
-
result = await this.executeConsoleCommand(`py ${stmt}`, { allowPython: true });
|
|
1011
|
-
}
|
|
1012
|
-
// Small delay between commands
|
|
1013
|
-
await new Promise(resolve => setTimeout(resolve, 30));
|
|
1014
|
-
}
|
|
1015
|
-
return result;
|
|
1016
|
-
}
|
|
1017
|
-
catch {
|
|
1018
|
-
// Final fallback: execute line by line
|
|
1019
|
-
const lines = command.split('\n').filter(line => line.trim().length > 0);
|
|
1020
|
-
let result = null;
|
|
1021
|
-
for (const line of lines) {
|
|
1022
|
-
// Skip comments
|
|
1023
|
-
if (line.trim().startsWith('#')) {
|
|
1024
|
-
continue;
|
|
1025
|
-
}
|
|
1026
|
-
result = await this.executeConsoleCommand(`py ${line.trim()}`, { allowPython: true });
|
|
1027
|
-
// Small delay between commands to ensure execution order
|
|
1028
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1029
|
-
}
|
|
1030
|
-
return result;
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
// Allow callers to enable/disable auto-reconnect behavior
|
|
1037
|
-
setAutoReconnectEnabled(enabled) {
|
|
1038
|
-
this.autoReconnectEnabled = enabled;
|
|
1039
|
-
}
|
|
1040
|
-
// Connection recovery
|
|
1041
|
-
scheduleReconnect() {
|
|
1042
|
-
if (!this.autoReconnectEnabled) {
|
|
1043
|
-
this.log.info('Auto-reconnect disabled; not scheduling reconnection');
|
|
1044
|
-
return;
|
|
1045
|
-
}
|
|
1046
|
-
if (this.reconnectTimer || this.connected) {
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
if (this.reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) {
|
|
1050
|
-
this.log.error('Max reconnection attempts reached. Please check Unreal Engine.');
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
// Exponential backoff with jitter
|
|
1054
|
-
const delay = Math.min(this.BASE_RECONNECT_DELAY * Math.pow(2, this.reconnectAttempts) + Math.random() * 1000, 30000 // Max 30 seconds
|
|
1055
|
-
);
|
|
1056
|
-
this.log.debug(`Scheduling reconnection attempt ${this.reconnectAttempts + 1}/${this.MAX_RECONNECT_ATTEMPTS} in ${Math.round(delay)}ms`);
|
|
1057
|
-
this.reconnectTimer = setTimeout(async () => {
|
|
1058
|
-
this.reconnectTimer = undefined;
|
|
1059
|
-
this.reconnectAttempts++;
|
|
1060
|
-
try {
|
|
1061
|
-
await this.connect();
|
|
1062
|
-
this.reconnectAttempts = 0;
|
|
1063
|
-
this.log.info('Successfully reconnected to Unreal Engine');
|
|
1064
|
-
}
|
|
1065
|
-
catch (err) {
|
|
1066
|
-
this.log.warn('Reconnection attempt failed:', err);
|
|
1067
|
-
this.scheduleReconnect();
|
|
1068
|
-
}
|
|
1069
|
-
}, delay);
|
|
1070
|
-
}
|
|
1071
|
-
// Graceful shutdown
|
|
1072
|
-
async disconnect() {
|
|
1073
|
-
if (this.reconnectTimer) {
|
|
1074
|
-
clearTimeout(this.reconnectTimer);
|
|
1075
|
-
this.reconnectTimer = undefined;
|
|
1076
|
-
}
|
|
1077
|
-
if (this.ws) {
|
|
1078
|
-
try {
|
|
1079
|
-
// Avoid unhandled error during shutdown
|
|
1080
|
-
this.ws.on('error', () => { });
|
|
1081
|
-
try {
|
|
1082
|
-
this.ws.close();
|
|
1083
|
-
}
|
|
1084
|
-
catch { }
|
|
1085
|
-
try {
|
|
1086
|
-
this.ws.terminate();
|
|
1087
|
-
}
|
|
1088
|
-
catch { }
|
|
1089
|
-
}
|
|
1090
|
-
finally {
|
|
1091
|
-
try {
|
|
1092
|
-
this.ws.removeAllListeners();
|
|
1093
|
-
}
|
|
1094
|
-
catch { }
|
|
1095
|
-
this.ws = undefined;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
this.connected = false;
|
|
1099
|
-
}
|
|
1100
|
-
/**
|
|
1101
|
-
* Enhanced Editor Function Access
|
|
1102
|
-
* Use Python scripting as a bridge to access modern Editor Subsystem functions
|
|
1103
|
-
*/
|
|
1104
|
-
async executeEditorFunction(functionName, params) {
|
|
1105
|
-
const template = this.PYTHON_TEMPLATES[functionName];
|
|
1106
|
-
if (!template) {
|
|
1107
|
-
throw new Error(`Unknown editor function: ${functionName}`);
|
|
1108
|
-
}
|
|
1109
|
-
let script = template.script;
|
|
1110
|
-
// Replace parameters in the script
|
|
1111
|
-
if (params) {
|
|
1112
|
-
for (const [key, value] of Object.entries(params)) {
|
|
1113
|
-
const placeholder = `{${key}}`;
|
|
1114
|
-
script = script.replace(new RegExp(placeholder, 'g'), String(value));
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
try {
|
|
1118
|
-
// Execute Python script with result parsing
|
|
1119
|
-
const result = await this.executePythonWithResult(script);
|
|
1120
|
-
return result;
|
|
1121
|
-
}
|
|
1122
|
-
catch (error) {
|
|
1123
|
-
this.log.error(`Failed to execute editor function ${functionName}:`, error);
|
|
1124
|
-
// Fallback to console command if Python fails
|
|
1125
|
-
return this.executeFallbackCommand(functionName, params);
|
|
1126
|
-
}
|
|
404
|
+
async executeEditorFunction(functionName, params, _options) {
|
|
405
|
+
if (process.env.MOCK_UNREAL_CONNECTION === 'true') {
|
|
406
|
+
return { success: true, result: { status: 'mock_success', function: functionName } };
|
|
407
|
+
}
|
|
408
|
+
if (!this.automationBridge || typeof this.automationBridge.sendAutomationRequest !== 'function') {
|
|
409
|
+
return { success: false, error: 'AUTOMATION_BRIDGE_UNAVAILABLE' };
|
|
410
|
+
}
|
|
411
|
+
const resp = await this.automationBridge.sendAutomationRequest('execute_editor_function', {
|
|
412
|
+
functionName,
|
|
413
|
+
params: params ?? {}
|
|
414
|
+
}, _options?.timeoutMs ? { timeoutMs: _options.timeoutMs } : undefined);
|
|
415
|
+
return resp && resp.success !== false ? (resp.result ?? resp) : resp;
|
|
1127
416
|
}
|
|
1128
|
-
/**
|
|
1129
|
-
* Execute Python script and parse the result
|
|
1130
|
-
*/
|
|
1131
|
-
// Expose for internal consumers (resources) that want parsed RESULT blocks
|
|
1132
|
-
async executePythonWithResult(script) {
|
|
1133
|
-
try {
|
|
1134
|
-
// Wrap script to capture output so we can parse RESULT: lines reliably
|
|
1135
|
-
const wrappedScript = `
|
|
1136
|
-
import sys
|
|
1137
|
-
import io
|
|
1138
|
-
old_stdout = sys.stdout
|
|
1139
|
-
sys.stdout = buffer = io.StringIO()
|
|
1140
|
-
try:
|
|
1141
|
-
${script.split('\n').join('\n ')}
|
|
1142
|
-
finally:
|
|
1143
|
-
output = buffer.getvalue()
|
|
1144
|
-
sys.stdout = old_stdout
|
|
1145
|
-
if output:
|
|
1146
|
-
print(output)
|
|
1147
|
-
`.trim()
|
|
1148
|
-
.replace(/\r?\n/g, '\n');
|
|
1149
|
-
const response = await this.executePython(wrappedScript);
|
|
1150
|
-
// Extract textual output from various response shapes
|
|
1151
|
-
let out = '';
|
|
1152
|
-
try {
|
|
1153
|
-
if (response && typeof response === 'string') {
|
|
1154
|
-
out = response;
|
|
1155
|
-
}
|
|
1156
|
-
else if (response && typeof response === 'object') {
|
|
1157
|
-
if (Array.isArray(response.LogOutput)) {
|
|
1158
|
-
out = response.LogOutput.map((l) => l.Output || '').join('');
|
|
1159
|
-
}
|
|
1160
|
-
else if (typeof response.Output === 'string') {
|
|
1161
|
-
out = response.Output;
|
|
1162
|
-
}
|
|
1163
|
-
else if (typeof response.result === 'string') {
|
|
1164
|
-
out = response.result;
|
|
1165
|
-
}
|
|
1166
|
-
else {
|
|
1167
|
-
out = JSON.stringify(response);
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
catch {
|
|
1172
|
-
out = String(response || '');
|
|
1173
|
-
}
|
|
1174
|
-
// Robust RESULT parsing with bracket matching (handles nested objects)
|
|
1175
|
-
const marker = 'RESULT:';
|
|
1176
|
-
const idx = out.lastIndexOf(marker);
|
|
1177
|
-
if (idx !== -1) {
|
|
1178
|
-
// Find first '{' after the marker
|
|
1179
|
-
let i = idx + marker.length;
|
|
1180
|
-
while (i < out.length && out[i] !== '{')
|
|
1181
|
-
i++;
|
|
1182
|
-
if (i < out.length && out[i] === '{') {
|
|
1183
|
-
let depth = 0;
|
|
1184
|
-
let inStr = false;
|
|
1185
|
-
let esc = false;
|
|
1186
|
-
let j = i;
|
|
1187
|
-
for (; j < out.length; j++) {
|
|
1188
|
-
const ch = out[j];
|
|
1189
|
-
if (esc) {
|
|
1190
|
-
esc = false;
|
|
1191
|
-
continue;
|
|
1192
|
-
}
|
|
1193
|
-
if (ch === '\\') {
|
|
1194
|
-
esc = true;
|
|
1195
|
-
continue;
|
|
1196
|
-
}
|
|
1197
|
-
if (ch === '"') {
|
|
1198
|
-
inStr = !inStr;
|
|
1199
|
-
continue;
|
|
1200
|
-
}
|
|
1201
|
-
if (!inStr) {
|
|
1202
|
-
if (ch === '{')
|
|
1203
|
-
depth++;
|
|
1204
|
-
else if (ch === '}') {
|
|
1205
|
-
depth--;
|
|
1206
|
-
if (depth === 0) {
|
|
1207
|
-
j++;
|
|
1208
|
-
break;
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
const jsonStr = out.slice(i, j);
|
|
1214
|
-
try {
|
|
1215
|
-
return JSON.parse(jsonStr);
|
|
1216
|
-
}
|
|
1217
|
-
catch { }
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
// Fallback to previous regex approach (best-effort)
|
|
1221
|
-
const matches = Array.from(out.matchAll(/RESULT:({[\s\S]*})/g));
|
|
1222
|
-
if (matches.length > 0) {
|
|
1223
|
-
const last = matches[matches.length - 1][1];
|
|
1224
|
-
try {
|
|
1225
|
-
return JSON.parse(last);
|
|
1226
|
-
}
|
|
1227
|
-
catch {
|
|
1228
|
-
return { raw: last };
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
// If no RESULT: marker, return the best-effort textual output or original response
|
|
1232
|
-
return typeof response !== 'undefined' ? response : out;
|
|
1233
|
-
}
|
|
1234
|
-
catch {
|
|
1235
|
-
this.log.warn('Python execution failed, trying direct execution');
|
|
1236
|
-
return this.executePython(script);
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
/**
|
|
1240
|
-
* Get the Unreal Engine version via Python and parse major/minor/patch.
|
|
1241
|
-
*/
|
|
1242
417
|
async getEngineVersion() {
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
return this.engineVersionCache.value;
|
|
418
|
+
if (process.env.MOCK_UNREAL_CONNECTION === 'true') {
|
|
419
|
+
return { version: '5.6.0-Mock', major: 5, minor: 6, patch: 0, isUE56OrAbove: true };
|
|
1246
420
|
}
|
|
421
|
+
const bridge = this.getAutomationBridge();
|
|
1247
422
|
try {
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
const minor = Number(result?.minor ?? 0) || 0;
|
|
1261
|
-
const patch = Number(result?.patch ?? 0) || 0;
|
|
1262
|
-
const isUE56OrAbove = major > 5 || (major === 5 && minor >= 6);
|
|
1263
|
-
const value = { version, major, minor, patch, isUE56OrAbove };
|
|
1264
|
-
this.engineVersionCache = { value, timestamp: now };
|
|
1265
|
-
return value;
|
|
423
|
+
const resp = await bridge.sendAutomationRequest('system_control', { action: 'get_engine_version' }, { timeoutMs: 15000 });
|
|
424
|
+
const raw = resp && typeof resp.result === 'object'
|
|
425
|
+
? resp.result
|
|
426
|
+
: (resp?.result ?? resp ?? {});
|
|
427
|
+
const version = typeof raw.version === 'string' ? raw.version : 'unknown';
|
|
428
|
+
const major = typeof raw.major === 'number' ? raw.major : 0;
|
|
429
|
+
const minor = typeof raw.minor === 'number' ? raw.minor : 0;
|
|
430
|
+
const patch = typeof raw.patch === 'number' ? raw.patch : 0;
|
|
431
|
+
const isUE56OrAbove = typeof raw.isUE56OrAbove === 'boolean'
|
|
432
|
+
? raw.isUE56OrAbove
|
|
433
|
+
: (major > 5 || (major === 5 && minor >= 6));
|
|
434
|
+
return { version, major, minor, patch, isUE56OrAbove };
|
|
1266
435
|
}
|
|
1267
436
|
catch (error) {
|
|
1268
|
-
this.log.warn('
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
437
|
+
this.log.warn('getEngineVersion failed', error);
|
|
438
|
+
return {
|
|
439
|
+
version: 'unknown',
|
|
440
|
+
major: 0,
|
|
441
|
+
minor: 0,
|
|
442
|
+
patch: 0,
|
|
443
|
+
isUE56OrAbove: false
|
|
444
|
+
};
|
|
1272
445
|
}
|
|
1273
446
|
}
|
|
1274
|
-
/**
|
|
1275
|
-
* Query feature flags (Python availability, editor subsystems) via Python.
|
|
1276
|
-
*/
|
|
1277
447
|
async getFeatureFlags() {
|
|
1278
|
-
|
|
1279
|
-
const script = `
|
|
1280
|
-
import unreal, json
|
|
1281
|
-
flags = {}
|
|
1282
|
-
# Python plugin availability (class exists)
|
|
1283
|
-
try:
|
|
1284
|
-
_ = unreal.PythonScriptLibrary
|
|
1285
|
-
flags['pythonEnabled'] = True
|
|
1286
|
-
except Exception:
|
|
1287
|
-
flags['pythonEnabled'] = False
|
|
1288
|
-
# Editor subsystems
|
|
1289
|
-
try:
|
|
1290
|
-
flags['unrealEditor'] = bool(unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem))
|
|
1291
|
-
except Exception:
|
|
1292
|
-
flags['unrealEditor'] = False
|
|
1293
|
-
try:
|
|
1294
|
-
flags['levelEditor'] = bool(unreal.get_editor_subsystem(unreal.LevelEditorSubsystem))
|
|
1295
|
-
except Exception:
|
|
1296
|
-
flags['levelEditor'] = False
|
|
1297
|
-
try:
|
|
1298
|
-
flags['editorActor'] = bool(unreal.get_editor_subsystem(unreal.EditorActorSubsystem))
|
|
1299
|
-
except Exception:
|
|
1300
|
-
flags['editorActor'] = False
|
|
1301
|
-
print('RESULT:' + json.dumps(flags))
|
|
1302
|
-
`.trim();
|
|
1303
|
-
const res = await this.executePythonWithResult(script);
|
|
448
|
+
if (process.env.MOCK_UNREAL_CONNECTION === 'true') {
|
|
1304
449
|
return {
|
|
1305
|
-
pythonEnabled: Boolean(res?.pythonEnabled),
|
|
1306
450
|
subsystems: {
|
|
1307
|
-
unrealEditor:
|
|
1308
|
-
levelEditor:
|
|
1309
|
-
editorActor:
|
|
451
|
+
unrealEditor: true,
|
|
452
|
+
levelEditor: true,
|
|
453
|
+
editorActor: true
|
|
1310
454
|
}
|
|
1311
455
|
};
|
|
1312
456
|
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
switch (functionName) {
|
|
1323
|
-
case 'SPAWN_ACTOR_AT_LOCATION':
|
|
1324
|
-
return this.executeConsoleCommand(`summon ${params?.class_path || 'StaticMeshActor'} ${params?.x || 0} ${params?.y || 0} ${params?.z || 0}`);
|
|
1325
|
-
case 'DELETE_ACTOR':
|
|
1326
|
-
// Use Python-based deletion to avoid unsafe console command and improve reliability
|
|
1327
|
-
return this.executePythonWithResult(this.PYTHON_TEMPLATES.DELETE_ACTOR.script.replace('{actor_name}', String(params?.actor_name || '')));
|
|
1328
|
-
case 'BUILD_LIGHTING':
|
|
1329
|
-
return this.executeConsoleCommand('BuildLighting');
|
|
1330
|
-
default:
|
|
1331
|
-
throw new Error(`No fallback available for ${functionName}`);
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
/**
|
|
1335
|
-
* SOLUTION 2: Safe ViewMode Switching
|
|
1336
|
-
* Prevent crashes by validating and safely switching viewmodes
|
|
1337
|
-
*/
|
|
1338
|
-
async setSafeViewMode(mode) {
|
|
1339
|
-
const acceptedModes = Array.from(new Set(this.VIEWMODE_ALIASES.values())).sort();
|
|
1340
|
-
if (typeof mode !== 'string') {
|
|
1341
|
-
return {
|
|
1342
|
-
success: false,
|
|
1343
|
-
error: 'View mode must be provided as a string',
|
|
1344
|
-
acceptedModes
|
|
1345
|
-
};
|
|
1346
|
-
}
|
|
1347
|
-
const key = mode.trim().toLowerCase().replace(/[\s_-]+/g, '');
|
|
1348
|
-
if (!key) {
|
|
1349
|
-
return {
|
|
1350
|
-
success: false,
|
|
1351
|
-
error: 'View mode cannot be empty',
|
|
1352
|
-
acceptedModes
|
|
1353
|
-
};
|
|
1354
|
-
}
|
|
1355
|
-
const targetMode = this.VIEWMODE_ALIASES.get(key);
|
|
1356
|
-
if (!targetMode) {
|
|
457
|
+
const bridge = this.getAutomationBridge();
|
|
458
|
+
try {
|
|
459
|
+
const resp = await bridge.sendAutomationRequest('system_control', { action: 'get_feature_flags' }, { timeoutMs: 15000 });
|
|
460
|
+
const raw = resp && typeof resp.result === 'object'
|
|
461
|
+
? resp.result
|
|
462
|
+
: (resp?.result ?? resp ?? {});
|
|
463
|
+
const subs = raw && typeof raw.subsystems === 'object'
|
|
464
|
+
? raw.subsystems
|
|
465
|
+
: {};
|
|
1357
466
|
return {
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
467
|
+
subsystems: {
|
|
468
|
+
unrealEditor: Boolean(subs.unrealEditor),
|
|
469
|
+
levelEditor: Boolean(subs.levelEditor),
|
|
470
|
+
editorActor: Boolean(subs.editorActor)
|
|
471
|
+
}
|
|
1361
472
|
};
|
|
1362
473
|
}
|
|
1363
|
-
|
|
1364
|
-
this.log.warn(
|
|
1365
|
-
const alternative = this.getSafeAlternative(targetMode);
|
|
1366
|
-
const altCommand = `viewmode ${alternative}`;
|
|
1367
|
-
const altResult = await this.executeConsoleCommand(altCommand);
|
|
1368
|
-
const altSummary = this.summarizeConsoleCommand(altCommand, altResult);
|
|
474
|
+
catch (error) {
|
|
475
|
+
this.log.warn('getFeatureFlags failed', error);
|
|
1369
476
|
return {
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
alternative
|
|
477
|
+
subsystems: {
|
|
478
|
+
unrealEditor: false,
|
|
479
|
+
levelEditor: false,
|
|
480
|
+
editorActor: false
|
|
481
|
+
}
|
|
1376
482
|
};
|
|
1377
483
|
}
|
|
1378
|
-
const command = `viewmode ${targetMode}`;
|
|
1379
|
-
const rawResult = await this.executeConsoleCommand(command);
|
|
1380
|
-
const summary = this.summarizeConsoleCommand(command, rawResult);
|
|
1381
|
-
const response = {
|
|
1382
|
-
...summary,
|
|
1383
|
-
success: summary.returnValue !== false,
|
|
1384
|
-
requestedMode: targetMode,
|
|
1385
|
-
viewMode: targetMode,
|
|
1386
|
-
message: `View mode set to ${targetMode}`
|
|
1387
|
-
};
|
|
1388
|
-
if (this.UNSAFE_VIEWMODES.includes(targetMode)) {
|
|
1389
|
-
response.warning = `View mode '${targetMode}' may be unstable on some engine versions.`;
|
|
1390
|
-
}
|
|
1391
|
-
if (summary.output && /unknown|invalid/i.test(summary.output)) {
|
|
1392
|
-
response.success = false;
|
|
1393
|
-
response.error = summary.output;
|
|
1394
|
-
}
|
|
1395
|
-
return response;
|
|
1396
484
|
}
|
|
1397
|
-
/**
|
|
1398
|
-
* Get safe alternative for unsafe viewmodes
|
|
1399
|
-
*/
|
|
1400
|
-
getSafeAlternative(unsafeMode) {
|
|
1401
|
-
const alternatives = {
|
|
1402
|
-
'BaseColor': 'Unlit',
|
|
1403
|
-
'WorldNormal': 'Lit',
|
|
1404
|
-
'Metallic': 'Lit',
|
|
1405
|
-
'Specular': 'Lit',
|
|
1406
|
-
'Roughness': 'Lit',
|
|
1407
|
-
'SubsurfaceColor': 'Lit',
|
|
1408
|
-
'Opacity': 'Lit',
|
|
1409
|
-
'LightComplexity': 'LightingOnly',
|
|
1410
|
-
'ShaderComplexity': 'Wireframe',
|
|
1411
|
-
'CollisionPawn': 'Wireframe',
|
|
1412
|
-
'CollisionVisibility': 'Wireframe'
|
|
1413
|
-
};
|
|
1414
|
-
return alternatives[unsafeMode] || 'Lit';
|
|
1415
|
-
}
|
|
1416
|
-
/**
|
|
1417
|
-
* SOLUTION 3: Command Throttling and Queueing
|
|
1418
|
-
* Prevent rapid command execution that can overwhelm the engine
|
|
1419
|
-
*/
|
|
1420
485
|
async executeThrottledCommand(command, priority = 5) {
|
|
1421
|
-
return
|
|
1422
|
-
this.commandQueue.push({
|
|
1423
|
-
command,
|
|
1424
|
-
resolve,
|
|
1425
|
-
reject,
|
|
1426
|
-
priority
|
|
1427
|
-
});
|
|
1428
|
-
// Sort by priority (lower number = higher priority)
|
|
1429
|
-
this.commandQueue.sort((a, b) => a.priority - b.priority);
|
|
1430
|
-
// Process queue if not already processing
|
|
1431
|
-
if (!this.isProcessing) {
|
|
1432
|
-
this.processCommandQueue();
|
|
1433
|
-
}
|
|
1434
|
-
});
|
|
486
|
+
return this.commandQueue.execute(command, priority);
|
|
1435
487
|
}
|
|
1436
|
-
/**
|
|
1437
|
-
* Process command queue with appropriate delays
|
|
1438
|
-
*/
|
|
1439
|
-
async processCommandQueue() {
|
|
1440
|
-
if (this.isProcessing || this.commandQueue.length === 0) {
|
|
1441
|
-
return;
|
|
1442
|
-
}
|
|
1443
|
-
this.isProcessing = true;
|
|
1444
|
-
while (this.commandQueue.length > 0) {
|
|
1445
|
-
const item = this.commandQueue.shift();
|
|
1446
|
-
if (!item)
|
|
1447
|
-
continue; // Skip if undefined
|
|
1448
|
-
// Calculate delay based on time since last command
|
|
1449
|
-
const timeSinceLastCommand = Date.now() - this.lastCommandTime;
|
|
1450
|
-
const requiredDelay = this.calculateDelay(item.priority);
|
|
1451
|
-
if (timeSinceLastCommand < requiredDelay) {
|
|
1452
|
-
await this.delay(requiredDelay - timeSinceLastCommand);
|
|
1453
|
-
}
|
|
1454
|
-
try {
|
|
1455
|
-
const result = await item.command();
|
|
1456
|
-
item.resolve(result);
|
|
1457
|
-
}
|
|
1458
|
-
catch (error) {
|
|
1459
|
-
// Retry logic for transient failures
|
|
1460
|
-
const msg = (error?.message || String(error)).toLowerCase();
|
|
1461
|
-
const notConnected = msg.includes('not connected to unreal');
|
|
1462
|
-
if (item.retryCount === undefined) {
|
|
1463
|
-
item.retryCount = 0;
|
|
1464
|
-
}
|
|
1465
|
-
if (!notConnected && item.retryCount < 3) {
|
|
1466
|
-
item.retryCount++;
|
|
1467
|
-
this.log.warn(`Command failed, retrying (${item.retryCount}/3)`);
|
|
1468
|
-
// Re-add to queue with increased priority
|
|
1469
|
-
this.commandQueue.unshift({
|
|
1470
|
-
command: item.command,
|
|
1471
|
-
resolve: item.resolve,
|
|
1472
|
-
reject: item.reject,
|
|
1473
|
-
priority: Math.max(1, item.priority - 1),
|
|
1474
|
-
retryCount: item.retryCount
|
|
1475
|
-
});
|
|
1476
|
-
// Add extra delay before retry
|
|
1477
|
-
await this.delay(500);
|
|
1478
|
-
}
|
|
1479
|
-
else {
|
|
1480
|
-
item.reject(error);
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
this.lastCommandTime = Date.now();
|
|
1484
|
-
}
|
|
1485
|
-
this.isProcessing = false;
|
|
1486
|
-
}
|
|
1487
|
-
/**
|
|
1488
|
-
* Calculate appropriate delay based on command priority and type
|
|
1489
|
-
*/
|
|
1490
|
-
calculateDelay(priority) {
|
|
1491
|
-
// Priority 1-3: Heavy operations (asset creation, lighting build)
|
|
1492
|
-
if (priority <= 3) {
|
|
1493
|
-
return this.MAX_COMMAND_DELAY;
|
|
1494
|
-
}
|
|
1495
|
-
// Priority 4-6: Medium operations (actor spawning, material changes)
|
|
1496
|
-
else if (priority <= 6) {
|
|
1497
|
-
return 200;
|
|
1498
|
-
}
|
|
1499
|
-
// Priority 8: Stat commands - need special handling
|
|
1500
|
-
else if (priority === 8) {
|
|
1501
|
-
// Check time since last stat command to avoid FindConsoleObject warnings
|
|
1502
|
-
const timeSinceLastStat = Date.now() - this.lastStatCommandTime;
|
|
1503
|
-
if (timeSinceLastStat < this.STAT_COMMAND_DELAY) {
|
|
1504
|
-
return this.STAT_COMMAND_DELAY;
|
|
1505
|
-
}
|
|
1506
|
-
this.lastStatCommandTime = Date.now();
|
|
1507
|
-
return 150;
|
|
1508
|
-
}
|
|
1509
|
-
// Priority 7,9-10: Light operations (console commands, queries)
|
|
1510
|
-
else {
|
|
1511
|
-
// For light operations, add some jitter to prevent thundering herd
|
|
1512
|
-
const baseDelay = this.MIN_COMMAND_DELAY;
|
|
1513
|
-
const jitter = Math.random() * 50; // Add up to 50ms random jitter
|
|
1514
|
-
return baseDelay + jitter;
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
/**
|
|
1518
|
-
* SOLUTION 4: Enhanced Asset Creation
|
|
1519
|
-
* Use Python scripting for complex asset creation that requires editor scripting
|
|
1520
|
-
*/
|
|
1521
|
-
async createComplexAsset(assetType, params) {
|
|
1522
|
-
const assetCreators = {
|
|
1523
|
-
'Material': 'MaterialFactoryNew',
|
|
1524
|
-
'MaterialInstance': 'MaterialInstanceConstantFactoryNew',
|
|
1525
|
-
'Blueprint': 'BlueprintFactory',
|
|
1526
|
-
'AnimationBlueprint': 'AnimBlueprintFactory',
|
|
1527
|
-
'ControlRig': 'ControlRigBlueprintFactory',
|
|
1528
|
-
'NiagaraSystem': 'NiagaraSystemFactoryNew',
|
|
1529
|
-
'NiagaraEmitter': 'NiagaraEmitterFactoryNew',
|
|
1530
|
-
'LandscapeGrassType': 'LandscapeGrassTypeFactory',
|
|
1531
|
-
'PhysicsAsset': 'PhysicsAssetFactory'
|
|
1532
|
-
};
|
|
1533
|
-
const factoryClass = assetCreators[assetType];
|
|
1534
|
-
if (!factoryClass) {
|
|
1535
|
-
throw new Error(`Unknown asset type: ${assetType}`);
|
|
1536
|
-
}
|
|
1537
|
-
const createParams = {
|
|
1538
|
-
factory_class: factoryClass,
|
|
1539
|
-
asset_class: `unreal.${assetType}`,
|
|
1540
|
-
asset_name: params.name || `New${assetType}`,
|
|
1541
|
-
package_path: params.path || '/Game/CreatedAssets',
|
|
1542
|
-
...params
|
|
1543
|
-
};
|
|
1544
|
-
return this.executeEditorFunction('CREATE_ASSET', createParams);
|
|
1545
|
-
}
|
|
1546
|
-
/**
|
|
1547
|
-
* Start the command processor
|
|
1548
|
-
*/
|
|
1549
|
-
startCommandProcessor() {
|
|
1550
|
-
// Periodic queue processing to handle any stuck commands
|
|
1551
|
-
setInterval(() => {
|
|
1552
|
-
if (!this.isProcessing && this.commandQueue.length > 0) {
|
|
1553
|
-
this.processCommandQueue();
|
|
1554
|
-
}
|
|
1555
|
-
}, 1000);
|
|
1556
|
-
// Clean console cache every 5 minutes
|
|
1557
|
-
setInterval(() => {
|
|
1558
|
-
this.cleanConsoleCache();
|
|
1559
|
-
}, this.CONSOLE_CACHE_TTL);
|
|
1560
|
-
}
|
|
1561
|
-
/**
|
|
1562
|
-
* Clean expired entries from console object cache
|
|
1563
|
-
*/
|
|
1564
|
-
cleanConsoleCache() {
|
|
1565
|
-
const now = Date.now();
|
|
1566
|
-
let cleaned = 0;
|
|
1567
|
-
for (const [key, value] of this.consoleObjectCache.entries()) {
|
|
1568
|
-
if (now - (value.timestamp || 0) > this.CONSOLE_CACHE_TTL) {
|
|
1569
|
-
this.consoleObjectCache.delete(key);
|
|
1570
|
-
cleaned++;
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
if (cleaned > 0) {
|
|
1574
|
-
this.log.debug(`Cleaned ${cleaned} expired console cache entries`);
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
/**
|
|
1578
|
-
* Helper delay function
|
|
1579
|
-
*/
|
|
1580
488
|
delay(ms) {
|
|
1581
489
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1582
490
|
}
|
|
1583
|
-
/**
|
|
1584
|
-
* Batch command execution with proper delays
|
|
1585
|
-
*/
|
|
1586
|
-
async executeBatch(commands) {
|
|
1587
|
-
return this.executeConsoleCommands(commands.map(cmd => cmd.command));
|
|
1588
|
-
}
|
|
1589
|
-
/**
|
|
1590
|
-
* Get safe console commands for common operations
|
|
1591
|
-
*/
|
|
1592
|
-
getSafeCommands() {
|
|
1593
|
-
return {
|
|
1594
|
-
// Health check (safe, no side effects)
|
|
1595
|
-
'HealthCheck': 'echo MCP Server Health Check',
|
|
1596
|
-
// Performance monitoring (safe)
|
|
1597
|
-
'ShowFPS': 'stat unit', // Use 'stat unit' instead of 'stat fps'
|
|
1598
|
-
'ShowMemory': 'stat memory',
|
|
1599
|
-
'ShowGame': 'stat game',
|
|
1600
|
-
'ShowRendering': 'stat scenerendering',
|
|
1601
|
-
'ClearStats': 'stat none',
|
|
1602
|
-
// Safe viewmodes
|
|
1603
|
-
'ViewLit': 'viewmode lit',
|
|
1604
|
-
'ViewUnlit': 'viewmode unlit',
|
|
1605
|
-
'ViewWireframe': 'viewmode wireframe',
|
|
1606
|
-
'ViewDetailLighting': 'viewmode detaillighting',
|
|
1607
|
-
'ViewLightingOnly': 'viewmode lightingonly',
|
|
1608
|
-
// Safe show flags
|
|
1609
|
-
'ShowBounds': 'show bounds',
|
|
1610
|
-
'ShowCollision': 'show collision',
|
|
1611
|
-
'ShowNavigation': 'show navigation',
|
|
1612
|
-
'ShowFog': 'show fog',
|
|
1613
|
-
'ShowGrid': 'show grid',
|
|
1614
|
-
// PIE controls
|
|
1615
|
-
'PlayInEditor': 'play',
|
|
1616
|
-
'StopPlay': 'stop',
|
|
1617
|
-
'PausePlay': 'pause',
|
|
1618
|
-
// Time control
|
|
1619
|
-
'SlowMotion': 'slomo 0.5',
|
|
1620
|
-
'NormalSpeed': 'slomo 1',
|
|
1621
|
-
'FastForward': 'slomo 2',
|
|
1622
|
-
// Camera controls
|
|
1623
|
-
'CameraSpeed1': 'camspeed 1',
|
|
1624
|
-
'CameraSpeed4': 'camspeed 4',
|
|
1625
|
-
'CameraSpeed8': 'camspeed 8',
|
|
1626
|
-
// Rendering quality (safe)
|
|
1627
|
-
'LowQuality': 'sg.ViewDistanceQuality 0',
|
|
1628
|
-
'MediumQuality': 'sg.ViewDistanceQuality 1',
|
|
1629
|
-
'HighQuality': 'sg.ViewDistanceQuality 2',
|
|
1630
|
-
'EpicQuality': 'sg.ViewDistanceQuality 3'
|
|
1631
|
-
};
|
|
1632
|
-
}
|
|
1633
491
|
}
|
|
1634
492
|
//# sourceMappingURL=unreal-bridge.js.map
|