unreal-engine-mcp-server 0.5.4 → 0.5.6
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/CHANGELOG.md +350 -0
- package/dist/automation/bridge.d.ts.map +1 -0
- package/dist/automation/bridge.js +5 -4
- package/dist/automation/bridge.js.map +1 -0
- package/dist/automation/connection-manager.d.ts.map +1 -0
- package/dist/automation/connection-manager.js.map +1 -0
- package/dist/automation/handshake.d.ts.map +1 -0
- package/dist/automation/handshake.js.map +1 -0
- package/dist/automation/index.d.ts.map +1 -0
- package/dist/automation/index.js.map +1 -0
- package/dist/automation/message-handler.d.ts.map +1 -0
- package/dist/automation/message-handler.js.map +1 -0
- package/dist/automation/request-tracker.d.ts.map +1 -0
- package/dist/automation/request-tracker.js.map +1 -0
- package/dist/automation/types.d.ts +7 -0
- package/dist/automation/types.d.ts.map +1 -0
- package/dist/automation/types.js.map +1 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +6 -4
- package/dist/cli.js.map +1 -0
- package/dist/config/class-aliases.d.ts.map +1 -0
- package/dist/config/class-aliases.js.map +1 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js.map +1 -0
- package/dist/graphql/loaders.d.ts.map +1 -0
- package/dist/graphql/loaders.js.map +1 -0
- package/dist/graphql/resolvers.d.ts +174 -69
- package/dist/graphql/resolvers.d.ts.map +1 -0
- package/dist/graphql/resolvers.js +82 -67
- package/dist/graphql/resolvers.js.map +1 -0
- package/dist/graphql/schema.d.ts.map +1 -0
- package/dist/graphql/schema.js.map +1 -0
- package/dist/graphql/server.d.ts.map +1 -0
- package/dist/graphql/server.js.map +1 -0
- package/dist/graphql/types.d.ts.map +1 -0
- package/dist/graphql/types.js.map +1 -0
- package/dist/handlers/resource-handlers.d.ts.map +1 -0
- package/dist/handlers/resource-handlers.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +70 -9
- package/dist/index.js.map +1 -0
- package/dist/resources/actors.d.ts +7 -4
- package/dist/resources/actors.d.ts.map +1 -0
- package/dist/resources/actors.js +15 -12
- package/dist/resources/actors.js.map +1 -0
- package/dist/resources/assets.d.ts +43 -2
- package/dist/resources/assets.d.ts.map +1 -0
- package/dist/resources/assets.js +21 -12
- package/dist/resources/assets.js.map +1 -0
- package/dist/resources/levels.d.ts.map +1 -0
- package/dist/resources/levels.js +7 -5
- package/dist/resources/levels.js.map +1 -0
- package/dist/schemas/index.d.ts +4 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +4 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/parser.d.ts +20 -0
- package/dist/schemas/parser.d.ts.map +1 -0
- package/dist/schemas/parser.js +61 -0
- package/dist/schemas/parser.js.map +1 -0
- package/dist/schemas/primitives.d.ts +221 -0
- package/dist/schemas/primitives.d.ts.map +1 -0
- package/dist/schemas/primitives.js +115 -0
- package/dist/schemas/primitives.js.map +1 -0
- package/dist/schemas/responses.d.ts +362 -0
- package/dist/schemas/responses.d.ts.map +1 -0
- package/dist/schemas/responses.js +252 -0
- package/dist/schemas/responses.js.map +1 -0
- package/dist/server/resource-registry.d.ts.map +1 -0
- package/dist/server/resource-registry.js.map +1 -0
- package/dist/server/tool-registry.d.ts.map +1 -0
- package/dist/server/tool-registry.js +22 -17
- package/dist/server/tool-registry.js.map +1 -0
- package/dist/server-setup.d.ts.map +1 -0
- package/dist/server-setup.js.map +1 -0
- package/dist/services/health-monitor.d.ts +1 -1
- package/dist/services/health-monitor.d.ts.map +1 -0
- package/dist/services/health-monitor.js +4 -3
- package/dist/services/health-monitor.js.map +1 -0
- package/dist/services/metrics-server.d.ts.map +1 -0
- package/dist/services/metrics-server.js.map +1 -0
- package/dist/tools/actors.d.ts +27 -27
- package/dist/tools/actors.d.ts.map +1 -0
- package/dist/tools/actors.js +14 -10
- package/dist/tools/actors.js.map +1 -0
- package/dist/tools/animation.d.ts +15 -23
- package/dist/tools/animation.d.ts.map +1 -0
- package/dist/tools/animation.js +17 -13
- package/dist/tools/animation.js.map +1 -0
- package/dist/tools/assets.d.ts.map +1 -0
- package/dist/tools/assets.js +18 -12
- package/dist/tools/assets.js.map +1 -0
- package/dist/tools/audio.d.ts +10 -10
- package/dist/tools/audio.d.ts.map +1 -0
- package/dist/tools/audio.js.map +1 -0
- package/dist/tools/base-tool.d.ts.map +1 -0
- package/dist/tools/base-tool.js.map +1 -0
- package/dist/tools/behavior-tree.d.ts +24 -24
- package/dist/tools/behavior-tree.d.ts.map +1 -0
- package/dist/tools/behavior-tree.js.map +1 -0
- package/dist/tools/blueprint.d.ts +14 -3
- package/dist/tools/blueprint.d.ts.map +1 -0
- package/dist/tools/blueprint.js +5 -3
- package/dist/tools/blueprint.js.map +1 -0
- package/dist/tools/consolidated-tool-definitions.d.ts +32 -32
- package/dist/tools/consolidated-tool-definitions.d.ts.map +1 -0
- package/dist/tools/consolidated-tool-definitions.js.map +1 -0
- package/dist/tools/consolidated-tool-handlers.d.ts +1 -1
- package/dist/tools/consolidated-tool-handlers.d.ts.map +1 -0
- package/dist/tools/consolidated-tool-handlers.js +26 -21
- package/dist/tools/consolidated-tool-handlers.js.map +1 -0
- package/dist/tools/debug.d.ts +25 -7
- package/dist/tools/debug.d.ts.map +1 -0
- package/dist/tools/debug.js +3 -1
- package/dist/tools/debug.js.map +1 -0
- package/dist/tools/dynamic-handler-registry.d.ts +1 -1
- package/dist/tools/dynamic-handler-registry.d.ts.map +1 -0
- package/dist/tools/dynamic-handler-registry.js +3 -1
- package/dist/tools/dynamic-handler-registry.js.map +1 -0
- package/dist/tools/editor.d.ts.map +1 -0
- package/dist/tools/editor.js +8 -6
- package/dist/tools/editor.js.map +1 -0
- package/dist/tools/engine.d.ts +1 -1
- package/dist/tools/engine.d.ts.map +1 -0
- package/dist/tools/engine.js +4 -2
- package/dist/tools/engine.js.map +1 -0
- package/dist/tools/environment.d.ts.map +1 -0
- package/dist/tools/environment.js +4 -3
- package/dist/tools/environment.js.map +1 -0
- package/dist/tools/foliage.d.ts.map +1 -0
- package/dist/tools/foliage.js +8 -8
- package/dist/tools/foliage.js.map +1 -0
- package/dist/tools/handlers/actor-handlers.d.ts +2 -1
- package/dist/tools/handlers/actor-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/actor-handlers.js +56 -33
- package/dist/tools/handlers/actor-handlers.js.map +1 -0
- package/dist/tools/handlers/animation-handlers.d.ts +2 -1
- package/dist/tools/handlers/animation-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/animation-handlers.js +74 -67
- package/dist/tools/handlers/animation-handlers.js.map +1 -0
- package/dist/tools/handlers/argument-helper.d.ts +24 -4
- package/dist/tools/handlers/argument-helper.d.ts.map +1 -0
- package/dist/tools/handlers/argument-helper.js +139 -4
- package/dist/tools/handlers/argument-helper.js.map +1 -0
- package/dist/tools/handlers/asset-handlers.d.ts +2 -1
- package/dist/tools/handlers/asset-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/asset-handlers.js +155 -94
- package/dist/tools/handlers/asset-handlers.js.map +1 -0
- package/dist/tools/handlers/audio-handlers.d.ts +2 -1
- package/dist/tools/handlers/audio-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/audio-handlers.js +82 -80
- package/dist/tools/handlers/audio-handlers.js.map +1 -0
- package/dist/tools/handlers/blueprint-handlers.d.ts +3 -5
- package/dist/tools/handlers/blueprint-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/blueprint-handlers.js +150 -142
- package/dist/tools/handlers/blueprint-handlers.js.map +1 -0
- package/dist/tools/handlers/common-handlers.d.ts +2 -3
- package/dist/tools/handlers/common-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/common-handlers.js.map +1 -0
- package/dist/tools/handlers/editor-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/editor-handlers.js +12 -2
- package/dist/tools/handlers/editor-handlers.js.map +1 -0
- package/dist/tools/handlers/effect-handlers.d.ts +2 -1
- package/dist/tools/handlers/effect-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/effect-handlers.js +70 -68
- package/dist/tools/handlers/effect-handlers.js.map +1 -0
- package/dist/tools/handlers/environment-handlers.d.ts +2 -1
- package/dist/tools/handlers/environment-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/environment-handlers.js +86 -74
- package/dist/tools/handlers/environment-handlers.js.map +1 -0
- package/dist/tools/handlers/graph-handlers.d.ts +1 -1
- package/dist/tools/handlers/graph-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/graph-handlers.js +63 -2
- package/dist/tools/handlers/graph-handlers.js.map +1 -0
- package/dist/tools/handlers/input-handlers.d.ts +2 -5
- package/dist/tools/handlers/input-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/input-handlers.js +5 -4
- package/dist/tools/handlers/input-handlers.js.map +1 -0
- package/dist/tools/handlers/inspect-handlers.d.ts +2 -1
- package/dist/tools/handlers/inspect-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/inspect-handlers.js +61 -37
- package/dist/tools/handlers/inspect-handlers.js.map +1 -0
- package/dist/tools/handlers/level-handlers.d.ts +2 -2
- package/dist/tools/handlers/level-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/level-handlers.js +43 -39
- package/dist/tools/handlers/level-handlers.js.map +1 -0
- package/dist/tools/handlers/lighting-handlers.d.ts +12 -1
- package/dist/tools/handlers/lighting-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/lighting-handlers.js +90 -47
- package/dist/tools/handlers/lighting-handlers.js.map +1 -0
- package/dist/tools/handlers/performance-handlers.d.ts +2 -1
- package/dist/tools/handlers/performance-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/performance-handlers.js +55 -40
- package/dist/tools/handlers/performance-handlers.js.map +1 -0
- package/dist/tools/handlers/pipeline-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/pipeline-handlers.js.map +1 -0
- package/dist/tools/handlers/sequence-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/sequence-handlers.js.map +1 -0
- package/dist/tools/handlers/system-handlers.d.ts +3 -2
- package/dist/tools/handlers/system-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/system-handlers.js +105 -52
- package/dist/tools/handlers/system-handlers.js.map +1 -0
- package/dist/tools/input.d.ts.map +1 -0
- package/dist/tools/input.js +3 -1
- package/dist/tools/input.js.map +1 -0
- package/dist/tools/introspection.d.ts +14 -14
- package/dist/tools/introspection.d.ts.map +1 -0
- package/dist/tools/introspection.js +54 -45
- package/dist/tools/introspection.js.map +1 -0
- package/dist/tools/landscape.d.ts.map +1 -0
- package/dist/tools/landscape.js +15 -13
- package/dist/tools/landscape.js.map +1 -0
- package/dist/tools/level.d.ts.map +1 -0
- package/dist/tools/level.js +3 -2
- package/dist/tools/level.js.map +1 -0
- package/dist/tools/lighting.d.ts +32 -59
- package/dist/tools/lighting.d.ts.map +1 -0
- package/dist/tools/lighting.js +56 -19
- package/dist/tools/lighting.js.map +1 -0
- package/dist/tools/logs.d.ts.map +1 -0
- package/dist/tools/logs.js +2 -1
- package/dist/tools/logs.js.map +1 -0
- package/dist/tools/materials.d.ts +42 -14
- package/dist/tools/materials.d.ts.map +1 -0
- package/dist/tools/materials.js +15 -9
- package/dist/tools/materials.js.map +1 -0
- package/dist/tools/niagara.d.ts +63 -39
- package/dist/tools/niagara.d.ts.map +1 -0
- package/dist/tools/niagara.js +43 -33
- package/dist/tools/niagara.js.map +1 -0
- package/dist/tools/performance.d.ts +12 -11
- package/dist/tools/performance.d.ts.map +1 -0
- package/dist/tools/performance.js +3 -2
- package/dist/tools/performance.js.map +1 -0
- package/dist/tools/physics.d.ts +37 -20
- package/dist/tools/physics.d.ts.map +1 -0
- package/dist/tools/physics.js +37 -30
- package/dist/tools/physics.js.map +1 -0
- package/dist/tools/property-dictionary.d.ts.map +1 -0
- package/dist/tools/property-dictionary.js.map +1 -0
- package/dist/tools/sequence.d.ts +1 -1
- package/dist/tools/sequence.d.ts.map +1 -0
- package/dist/tools/sequence.js +8 -4
- package/dist/tools/sequence.js.map +1 -0
- package/dist/tools/tool-definition-utils.d.ts.map +1 -0
- package/dist/tools/tool-definition-utils.js.map +1 -0
- package/dist/tools/ui.d.ts +11 -11
- package/dist/tools/ui.d.ts.map +1 -0
- package/dist/tools/ui.js +7 -3
- package/dist/tools/ui.js.map +1 -0
- package/dist/types/automation-responses.d.ts.map +1 -0
- package/dist/types/automation-responses.js.map +1 -0
- package/dist/types/env.d.ts.map +1 -0
- package/dist/types/env.js.map +1 -0
- package/dist/types/handler-types.d.ts +112 -3
- package/dist/types/handler-types.d.ts.map +1 -0
- package/dist/types/handler-types.js.map +1 -0
- package/dist/types/tool-interfaces.d.ts +39 -21
- package/dist/types/tool-interfaces.d.ts.map +1 -0
- package/dist/types/tool-interfaces.js.map +1 -0
- package/dist/types/tool-types.d.ts +8 -8
- package/dist/types/tool-types.d.ts.map +1 -0
- package/dist/types/tool-types.js.map +1 -0
- package/dist/unreal-bridge.d.ts +8 -6
- package/dist/unreal-bridge.d.ts.map +1 -0
- package/dist/unreal-bridge.js +16 -3
- package/dist/unreal-bridge.js.map +1 -0
- package/dist/utils/command-validator.d.ts.map +1 -0
- package/dist/utils/command-validator.js.map +1 -0
- package/dist/utils/elicitation.d.ts +2 -5
- package/dist/utils/elicitation.d.ts.map +1 -0
- package/dist/utils/elicitation.js +3 -2
- package/dist/utils/elicitation.js.map +1 -0
- package/dist/utils/error-handler.d.ts.map +1 -0
- package/dist/utils/error-handler.js.map +1 -0
- package/dist/utils/ini-reader.d.ts +1 -1
- package/dist/utils/ini-reader.d.ts.map +1 -0
- package/dist/utils/ini-reader.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -4
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/normalize.d.ts +2 -2
- package/dist/utils/normalize.d.ts.map +1 -0
- package/dist/utils/normalize.js +4 -3
- package/dist/utils/normalize.js.map +1 -0
- package/dist/utils/path-security.d.ts.map +1 -0
- package/dist/utils/path-security.js.map +1 -0
- package/dist/utils/response-factory.d.ts +2 -2
- package/dist/utils/response-factory.d.ts.map +1 -0
- package/dist/utils/response-factory.js +3 -1
- package/dist/utils/response-factory.js.map +1 -0
- package/dist/utils/response-validator.d.ts +4 -4
- package/dist/utils/response-validator.d.ts.map +1 -0
- package/dist/utils/response-validator.js +31 -23
- package/dist/utils/response-validator.js.map +1 -0
- package/dist/utils/result-helpers.d.ts.map +1 -0
- package/dist/utils/result-helpers.js.map +1 -0
- package/dist/utils/safe-json.d.ts.map +1 -0
- package/dist/utils/safe-json.js.map +1 -0
- package/dist/utils/unreal-command-queue.d.ts +2 -2
- package/dist/utils/unreal-command-queue.d.ts.map +1 -0
- package/dist/utils/unreal-command-queue.js +4 -3
- package/dist/utils/unreal-command-queue.js.map +1 -0
- package/dist/utils/validation.d.ts +1 -1
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js.map +1 -0
- package/dist/wasm/index.d.ts +2 -2
- package/dist/wasm/index.d.ts.map +1 -0
- package/dist/wasm/index.js +11 -7
- package/dist/wasm/index.js.map +1 -0
- package/package.json +12 -34
- package/server.json +2 -2
- package/.dockerignore +0 -57
- package/.env.example +0 -26
- package/.env.production +0 -61
- package/.eslintrc.json +0 -0
- package/.eslintrc.override.json +0 -8
- package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -94
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -56
- package/.github/copilot-instructions.md +0 -478
- package/.github/dependabot.yml +0 -19
- package/.github/labeler.yml +0 -24
- package/.github/labels.yml +0 -70
- package/.github/pull_request_template.md +0 -42
- package/.github/release-drafter-config.yml +0 -51
- package/.github/workflows/auto-merge.yml +0 -38
- package/.github/workflows/ci.yml +0 -38
- package/.github/workflows/dependency-review.yml +0 -17
- package/.github/workflows/gemini-issue-triage.yml +0 -172
- package/.github/workflows/greetings.yml +0 -27
- package/.github/workflows/labeler.yml +0 -17
- package/.github/workflows/links.yml +0 -80
- package/.github/workflows/pr-size-labeler.yml +0 -137
- package/.github/workflows/publish-mcp.yml +0 -79
- package/.github/workflows/release-drafter.yml +0 -24
- package/.github/workflows/release.yml +0 -112
- package/.github/workflows/semantic-pull-request.yml +0 -35
- package/.github/workflows/smoke-test.yml +0 -36
- package/.github/workflows/stale.yml +0 -28
- package/CONTRIBUTING.md +0 -140
- package/Dockerfile +0 -37
- package/GEMINI.md +0 -115
- package/Public/Plugin_setup_guide.mp4 +0 -0
- package/Public/icon.png +0 -0
- package/claude_desktop_config_example.json +0 -15
- package/dist/types/responses.d.ts +0 -249
- package/dist/types/responses.js +0 -2
- package/docs/GraphQL-API.md +0 -888
- package/docs/Migration-Guide-v0.5.0.md +0 -684
- package/docs/Roadmap.md +0 -53
- package/docs/WebAssembly-Integration.md +0 -628
- package/docs/editor-plugin-extension.md +0 -370
- package/docs/handler-mapping.md +0 -249
- package/docs/native-automation-progress.md +0 -128
- package/docs/testing-guide.md +0 -423
- package/eslint.config.mjs +0 -68
- package/mcp-config-example.json +0 -14
- package/plugins/McpAutomationBridge/Config/FilterPlugin.ini +0 -8
- package/plugins/McpAutomationBridge/McpAutomationBridge.uplugin +0 -64
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/McpAutomationBridge.Build.cs +0 -189
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.cpp +0 -22
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.h +0 -30
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h +0 -1983
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeModule.cpp +0 -72
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSettings.cpp +0 -46
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +0 -846
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +0 -2393
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetQueryHandlers.cpp +0 -300
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetWorkflowHandlers.cpp +0 -2807
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AudioHandlers.cpp +0 -1087
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BehaviorTreeHandlers.cpp +0 -488
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.cpp +0 -643
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.h +0 -31
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +0 -1094
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +0 -5750
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers_List.cpp +0 -152
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ControlHandlers.cpp +0 -2614
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_DebugHandlers.cpp +0 -42
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EditorFunctionHandlers.cpp +0 -1237
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +0 -1725
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +0 -2265
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_FoliageHandlers.cpp +0 -954
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InputHandlers.cpp +0 -209
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InsightsHandlers.cpp +0 -41
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LandscapeHandlers.cpp +0 -1164
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelHandlers.cpp +0 -762
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +0 -663
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LogHandlers.cpp +0 -136
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_MaterialGraphHandlers.cpp +0 -494
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraGraphHandlers.cpp +0 -278
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraHandlers.cpp +0 -625
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PerformanceHandlers.cpp +0 -401
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PipelineHandlers.cpp +0 -67
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +0 -472
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PropertyHandlers.cpp +0 -2634
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_RenderHandlers.cpp +0 -189
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.cpp +0 -917
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.h +0 -39
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +0 -2706
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequencerHandlers.cpp +0 -519
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_TestHandlers.cpp +0 -38
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_UiHandlers.cpp +0 -668
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WorldPartitionHandlers.cpp +0 -346
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +0 -1345
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.h +0 -149
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +0 -782
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSettings.h +0 -115
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSubsystem.h +0 -796
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpConnectionManager.h +0 -117
- package/scripts/check-unreal-connection.mjs +0 -19
- package/scripts/clean-tmp.js +0 -23
- package/scripts/patch-wasm.js +0 -26
- package/scripts/run-all-tests.mjs +0 -136
- package/scripts/smoke-test.ts +0 -94
- package/scripts/sync-mcp-plugin.js +0 -143
- package/scripts/test-no-plugin-alternates.mjs +0 -113
- package/scripts/validate-server.js +0 -46
- package/scripts/verify-automation-bridge.js +0 -200
- package/src/automation/bridge.ts +0 -630
- package/src/automation/connection-manager.ts +0 -148
- package/src/automation/handshake.ts +0 -99
- package/src/automation/index.ts +0 -2
- package/src/automation/message-handler.ts +0 -192
- package/src/automation/request-tracker.ts +0 -155
- package/src/automation/types.ts +0 -108
- package/src/cli.ts +0 -34
- package/src/config/class-aliases.ts +0 -65
- package/src/config.ts +0 -73
- package/src/constants.ts +0 -29
- package/src/graphql/loaders.ts +0 -244
- package/src/graphql/resolvers.ts +0 -1008
- package/src/graphql/schema.ts +0 -452
- package/src/graphql/server.ts +0 -156
- package/src/graphql/types.ts +0 -10
- package/src/handlers/resource-handlers.ts +0 -186
- package/src/index.ts +0 -243
- package/src/resources/actors.ts +0 -127
- package/src/resources/assets.ts +0 -286
- package/src/resources/levels.ts +0 -68
- package/src/server/resource-registry.ts +0 -47
- package/src/server/tool-registry.ts +0 -354
- package/src/server-setup.ts +0 -114
- package/src/services/health-monitor.ts +0 -132
- package/src/services/metrics-server.ts +0 -176
- package/src/tools/actors.ts +0 -564
- package/src/tools/animation.ts +0 -941
- package/src/tools/assets.ts +0 -394
- package/src/tools/audio.ts +0 -499
- package/src/tools/base-tool.ts +0 -52
- package/src/tools/behavior-tree.ts +0 -45
- package/src/tools/blueprint.ts +0 -940
- package/src/tools/consolidated-tool-definitions.ts +0 -1256
- package/src/tools/consolidated-tool-handlers.ts +0 -302
- package/src/tools/debug.ts +0 -622
- package/src/tools/dynamic-handler-registry.ts +0 -33
- package/src/tools/editor.ts +0 -435
- package/src/tools/engine.ts +0 -43
- package/src/tools/environment.ts +0 -281
- package/src/tools/foliage.ts +0 -596
- package/src/tools/handlers/actor-handlers.ts +0 -244
- package/src/tools/handlers/animation-handlers.ts +0 -237
- package/src/tools/handlers/argument-helper.ts +0 -142
- package/src/tools/handlers/asset-handlers.ts +0 -550
- package/src/tools/handlers/audio-handlers.ts +0 -194
- package/src/tools/handlers/blueprint-handlers.ts +0 -380
- package/src/tools/handlers/common-handlers.ts +0 -108
- package/src/tools/handlers/editor-handlers.ts +0 -124
- package/src/tools/handlers/effect-handlers.ts +0 -224
- package/src/tools/handlers/environment-handlers.ts +0 -183
- package/src/tools/handlers/graph-handlers.ts +0 -117
- package/src/tools/handlers/input-handlers.ts +0 -28
- package/src/tools/handlers/inspect-handlers.ts +0 -450
- package/src/tools/handlers/level-handlers.ts +0 -253
- package/src/tools/handlers/lighting-handlers.ts +0 -151
- package/src/tools/handlers/performance-handlers.ts +0 -132
- package/src/tools/handlers/pipeline-handlers.ts +0 -194
- package/src/tools/handlers/sequence-handlers.ts +0 -438
- package/src/tools/handlers/system-handlers.ts +0 -564
- package/src/tools/input.ts +0 -160
- package/src/tools/introspection.ts +0 -689
- package/src/tools/landscape.ts +0 -649
- package/src/tools/level.ts +0 -989
- package/src/tools/lighting.ts +0 -1052
- package/src/tools/logs.ts +0 -219
- package/src/tools/materials.ts +0 -295
- package/src/tools/niagara.ts +0 -485
- package/src/tools/performance.ts +0 -661
- package/src/tools/physics.ts +0 -679
- package/src/tools/property-dictionary.ts +0 -98
- package/src/tools/sequence.ts +0 -385
- package/src/tools/tool-definition-utils.ts +0 -35
- package/src/tools/ui.ts +0 -452
- package/src/types/automation-responses.ts +0 -119
- package/src/types/env.ts +0 -17
- package/src/types/handler-types.ts +0 -442
- package/src/types/responses.ts +0 -355
- package/src/types/tool-interfaces.ts +0 -250
- package/src/types/tool-types.ts +0 -575
- package/src/unreal-bridge.ts +0 -693
- package/src/utils/command-validator.ts +0 -139
- package/src/utils/elicitation.ts +0 -132
- package/src/utils/error-handler.ts +0 -287
- package/src/utils/ini-reader.ts +0 -86
- package/src/utils/logger.ts +0 -35
- package/src/utils/normalize.test.ts +0 -162
- package/src/utils/normalize.ts +0 -146
- package/src/utils/path-security.ts +0 -43
- package/src/utils/response-factory.ts +0 -44
- package/src/utils/response-validator.ts +0 -395
- package/src/utils/result-helpers.ts +0 -195
- package/src/utils/safe-json.test.ts +0 -90
- package/src/utils/safe-json.ts +0 -70
- package/src/utils/unreal-command-queue.ts +0 -166
- package/src/utils/validation.test.ts +0 -184
- package/src/utils/validation.ts +0 -312
- package/src/wasm/index.ts +0 -838
- package/test-server.mjs +0 -100
- package/tests/test-animation.mjs +0 -369
- package/tests/test-asset-advanced.mjs +0 -82
- package/tests/test-asset-graph.mjs +0 -311
- package/tests/test-audio.mjs +0 -417
- package/tests/test-automation-timeouts.mjs +0 -98
- package/tests/test-behavior-tree.mjs +0 -444
- package/tests/test-blueprint-graph.mjs +0 -410
- package/tests/test-blueprint.mjs +0 -577
- package/tests/test-client-mode.mjs +0 -86
- package/tests/test-console-command.mjs +0 -56
- package/tests/test-control-actor.mjs +0 -425
- package/tests/test-control-editor.mjs +0 -112
- package/tests/test-graphql.mjs +0 -372
- package/tests/test-input.mjs +0 -349
- package/tests/test-inspect.mjs +0 -302
- package/tests/test-landscape.mjs +0 -316
- package/tests/test-lighting.mjs +0 -428
- package/tests/test-manage-asset.mjs +0 -438
- package/tests/test-manage-level.mjs +0 -89
- package/tests/test-materials.mjs +0 -356
- package/tests/test-niagara.mjs +0 -185
- package/tests/test-no-inline-python.mjs +0 -122
- package/tests/test-performance.mjs +0 -539
- package/tests/test-plugin-handshake.mjs +0 -82
- package/tests/test-runner.mjs +0 -993
- package/tests/test-sequence.mjs +0 -104
- package/tests/test-system.mjs +0 -96
- package/tests/test-wasm.mjs +0 -283
- package/tests/test-world-partition.mjs +0 -215
- package/tsconfig.json +0 -56
- package/vitest.config.ts +0 -35
- package/wasm/Cargo.lock +0 -363
- package/wasm/Cargo.toml +0 -42
- package/wasm/LICENSE +0 -21
- package/wasm/README.md +0 -253
- package/wasm/src/dependency_resolver.rs +0 -377
- package/wasm/src/lib.rs +0 -153
- package/wasm/src/property_parser.rs +0 -271
- package/wasm/src/transform_math.rs +0 -396
- package/wasm/tests/integration.rs +0 -109
package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h
DELETED
|
@@ -1,1983 +0,0 @@
|
|
|
1
|
-
// Helper utilities for McpAutomationBridgeSubsystem
|
|
2
|
-
#pragma once
|
|
3
|
-
|
|
4
|
-
#include "Containers/ScriptArray.h"
|
|
5
|
-
#include "Containers/StringConv.h"
|
|
6
|
-
#include "CoreMinimal.h"
|
|
7
|
-
#include "Dom/JsonObject.h"
|
|
8
|
-
#include "HAL/PlatformTime.h"
|
|
9
|
-
#include "JsonObjectConverter.h"
|
|
10
|
-
#include "Misc/FileHelper.h"
|
|
11
|
-
#include "Misc/OutputDevice.h"
|
|
12
|
-
#include "Misc/Paths.h"
|
|
13
|
-
#include "Misc/ScopeLock.h"
|
|
14
|
-
#include "UObject/UnrealType.h"
|
|
15
|
-
#include <type_traits>
|
|
16
|
-
|
|
17
|
-
// Globals used by registry helpers and fast-mode simulations
|
|
18
|
-
#include "McpAutomationBridgeGlobals.h"
|
|
19
|
-
#include "McpAutomationBridgeSubsystem.h"
|
|
20
|
-
|
|
21
|
-
#if WITH_EDITOR
|
|
22
|
-
#include "AssetRegistry/AssetRegistryModule.h"
|
|
23
|
-
#include "Engine/SCS_Node.h"
|
|
24
|
-
#include "Engine/SimpleConstructionScript.h"
|
|
25
|
-
#include "Modules/ModuleManager.h"
|
|
26
|
-
#include "UObject/UObjectIterator.h"
|
|
27
|
-
|
|
28
|
-
#if __has_include("EditorAssetLibrary.h")
|
|
29
|
-
#include "EditorAssetLibrary.h"
|
|
30
|
-
#else
|
|
31
|
-
#include "Editor/EditorAssetLibrary.h"
|
|
32
|
-
#endif
|
|
33
|
-
#include "Engine/Blueprint.h"
|
|
34
|
-
#endif
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Removes control characters (ASCII codes less than 32) from the input JSON
|
|
38
|
-
* string.
|
|
39
|
-
* @param In Input string that may contain control characters.
|
|
40
|
-
* @returns String with all characters with ASCII value < 32 removed.
|
|
41
|
-
*/
|
|
42
|
-
static inline FString SanitizeIncomingJson(const FString &In) {
|
|
43
|
-
FString Out;
|
|
44
|
-
Out.Reserve(In.Len());
|
|
45
|
-
for (int32 i = 0; i < In.Len(); ++i) {
|
|
46
|
-
const TCHAR C = In[i];
|
|
47
|
-
if (C >= 32)
|
|
48
|
-
Out.AppendChar(C);
|
|
49
|
-
}
|
|
50
|
-
return Out;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Sanitize a project-relative path to prevent traversal attacks.
|
|
54
|
-
// Ensures the path starts with a valid root (e.g. /Game, /Engine, /Plugin) and
|
|
55
|
-
/**
|
|
56
|
-
* Normalize and validate a project-relative asset path.
|
|
57
|
-
*
|
|
58
|
-
* Ensures the returned path is normalized, begins with a leading '/', rejects
|
|
59
|
-
* any path containing directory traversal sequences (".."), and accepts common
|
|
60
|
-
* roots (/Game, /Engine, /Script) or plugin-like roots (heuristic). If a
|
|
61
|
-
* traversal sequence is found the function logs a warning and returns an empty
|
|
62
|
-
* string.
|
|
63
|
-
*
|
|
64
|
-
* @param InPath Input path to sanitize.
|
|
65
|
-
* @returns Sanitized project-relative path beginning with '/', or an empty
|
|
66
|
-
* string if the input was empty or rejected (for example, when containing
|
|
67
|
-
* "..").
|
|
68
|
-
*/
|
|
69
|
-
static inline FString SanitizeProjectRelativePath(const FString &InPath) {
|
|
70
|
-
if (InPath.IsEmpty())
|
|
71
|
-
return FString();
|
|
72
|
-
|
|
73
|
-
FString CleanPath = InPath;
|
|
74
|
-
FPaths::NormalizeFilename(CleanPath);
|
|
75
|
-
|
|
76
|
-
// Reject paths containing traversal
|
|
77
|
-
if (CleanPath.Contains(TEXT(".."))) {
|
|
78
|
-
UE_LOG(
|
|
79
|
-
LogMcpAutomationBridgeSubsystem, Warning,
|
|
80
|
-
TEXT("SanitizeProjectRelativePath: Rejected path containing '..': %s"),
|
|
81
|
-
*InPath);
|
|
82
|
-
return FString();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Ensure path starts with a slash
|
|
86
|
-
if (!CleanPath.StartsWith(TEXT("/"))) {
|
|
87
|
-
CleanPath = TEXT("/") + CleanPath;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Whitelist valid roots
|
|
91
|
-
const bool bValidRoot = CleanPath.StartsWith(TEXT("/Game")) ||
|
|
92
|
-
CleanPath.StartsWith(TEXT("/Engine")) ||
|
|
93
|
-
CleanPath.StartsWith(TEXT("/Script"));
|
|
94
|
-
|
|
95
|
-
// Allow plugin content paths too (e.g. /MyPlugin/) - heuristic: starts with /
|
|
96
|
-
// and has second /
|
|
97
|
-
bool bLooksLikePlugin = false;
|
|
98
|
-
if (!bValidRoot && CleanPath.Len() > 1) {
|
|
99
|
-
int32 SecondSlash = -1;
|
|
100
|
-
if (CleanPath.FindChar(TEXT('/'), SecondSlash)) {
|
|
101
|
-
// Check if we have a second slash, e.g. /PluginName/Folder
|
|
102
|
-
// FindChar finds the *first* char. We want the second one.
|
|
103
|
-
if (CleanPath.FindLastChar(TEXT('/'), SecondSlash) && SecondSlash > 0) {
|
|
104
|
-
bLooksLikePlugin = true;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// For strict safety, we might enforce /Game or /Engine, but plugins are
|
|
110
|
-
// common. The critical part is no ".." and it looks like an asset path.
|
|
111
|
-
|
|
112
|
-
return CleanPath;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Validate a basic asset path format.
|
|
117
|
-
*
|
|
118
|
-
* @returns `true` if Path is non-empty, begins with a leading '/', and does not
|
|
119
|
-
* contain the parent-traversal segment ("..") or consecutive slashes ("//");
|
|
120
|
-
* `false` otherwise.
|
|
121
|
-
*/
|
|
122
|
-
static inline bool IsValidAssetPath(const FString &Path) {
|
|
123
|
-
return !Path.IsEmpty() && Path.StartsWith(TEXT("/")) &&
|
|
124
|
-
!Path.Contains(TEXT("..")) && !Path.Contains(TEXT("//"));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Normalize an asset path to ensure it's in valid long package name format.
|
|
128
|
-
// Uses engine FPackageName API for proper validation.
|
|
129
|
-
// - If path doesn't start with '/', prepends '/Game/'
|
|
130
|
-
// - Removes trailing slashes
|
|
131
|
-
// - Returns the normalized path and whether it's valid
|
|
132
|
-
// - Reference: Engine/Source/Runtime/CoreUObject/Public/Misc/PackageName.h
|
|
133
|
-
#if WITH_EDITOR
|
|
134
|
-
#include "Misc/PackageName.h"
|
|
135
|
-
|
|
136
|
-
struct FNormalizedAssetPath {
|
|
137
|
-
FString Path;
|
|
138
|
-
bool bIsValid;
|
|
139
|
-
FString ErrorMessage;
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Normalize an input asset path to a valid long package name and validate it.
|
|
144
|
-
*
|
|
145
|
-
* @param InPath The asset path or object path to normalize (may be short,
|
|
146
|
-
* relative, or an object path).
|
|
147
|
-
* @returns An FNormalizedAssetPath containing:
|
|
148
|
-
* - Path: the normalized package path candidate (may be unchanged if
|
|
149
|
-
* invalid),
|
|
150
|
-
* - bIsValid: `true` when the path is a valid long package name and, when
|
|
151
|
-
* applicable, the package exists,
|
|
152
|
-
* - ErrorMessage: populated with a validation error when `bIsValid` is
|
|
153
|
-
* `false`.
|
|
154
|
-
*/
|
|
155
|
-
static inline FNormalizedAssetPath NormalizeAssetPath(const FString &InPath) {
|
|
156
|
-
FNormalizedAssetPath Result;
|
|
157
|
-
Result.bIsValid = false;
|
|
158
|
-
|
|
159
|
-
if (InPath.IsEmpty()) {
|
|
160
|
-
Result.ErrorMessage = TEXT("Asset path is empty");
|
|
161
|
-
return Result;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
FString CleanPath = InPath;
|
|
165
|
-
|
|
166
|
-
// Remove trailing slashes
|
|
167
|
-
while (CleanPath.EndsWith(TEXT("/"))) {
|
|
168
|
-
CleanPath.RemoveAt(CleanPath.Len() - 1);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Handle object paths (extract package name)
|
|
172
|
-
// Object paths look like: /Game/Package.Object:SubObject
|
|
173
|
-
FString PackageName = FPackageName::ObjectPathToPackageName(CleanPath);
|
|
174
|
-
if (!PackageName.IsEmpty()) {
|
|
175
|
-
CleanPath = PackageName;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// If path doesn't start with '/', try prepending /Game/
|
|
179
|
-
if (!CleanPath.StartsWith(TEXT("/"))) {
|
|
180
|
-
CleanPath = TEXT("/Game/") + CleanPath;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Validate using engine API
|
|
184
|
-
FText Reason;
|
|
185
|
-
if (FPackageName::IsValidLongPackageName(CleanPath, true, &Reason)) {
|
|
186
|
-
Result.Path = CleanPath;
|
|
187
|
-
Result.bIsValid = true;
|
|
188
|
-
return Result;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// If not in valid root, try other common roots
|
|
192
|
-
TArray<FString> RootsToTry = {TEXT("/Game/"), TEXT("/Engine/"),
|
|
193
|
-
TEXT("/Script/")};
|
|
194
|
-
FString BaseName = InPath;
|
|
195
|
-
if (BaseName.StartsWith(TEXT("/"))) {
|
|
196
|
-
// Extract just the asset name without the invalid root
|
|
197
|
-
int32 LastSlash = -1;
|
|
198
|
-
if (BaseName.FindLastChar(TEXT('/'), LastSlash) && LastSlash > 0) {
|
|
199
|
-
BaseName = BaseName.RightChop(LastSlash + 1);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
for (const FString &Root : RootsToTry) {
|
|
204
|
-
FString TestPath = Root + BaseName;
|
|
205
|
-
FText DummyReason;
|
|
206
|
-
if (FPackageName::IsValidLongPackageName(TestPath, true, &DummyReason)) {
|
|
207
|
-
// Check if this asset actually exists
|
|
208
|
-
if (FPackageName::DoesPackageExist(TestPath)) {
|
|
209
|
-
Result.Path = TestPath;
|
|
210
|
-
Result.bIsValid = true;
|
|
211
|
-
return Result;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Return what we have, with the validation error
|
|
217
|
-
Result.Path = CleanPath;
|
|
218
|
-
Result.ErrorMessage = FString::Printf(
|
|
219
|
-
TEXT("Invalid asset path '%s': %s. Expected format: "
|
|
220
|
-
"/Game/Folder/AssetName or /Engine/Folder/AssetName"),
|
|
221
|
-
*InPath, *Reason.ToString());
|
|
222
|
-
return Result;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Convenience helper that tries to resolve the path and returns it, or empty if
|
|
226
|
-
// invalid Also outputs the resolved path to a pointer if provided
|
|
227
|
-
static inline FString TryResolveAssetPath(const FString &InPath,
|
|
228
|
-
FString *OutResolvedPath = nullptr,
|
|
229
|
-
FString *OutError = nullptr) {
|
|
230
|
-
FNormalizedAssetPath Norm = NormalizeAssetPath(InPath);
|
|
231
|
-
if (OutResolvedPath) {
|
|
232
|
-
*OutResolvedPath = Norm.Path;
|
|
233
|
-
}
|
|
234
|
-
if (OutError && !Norm.bIsValid) {
|
|
235
|
-
*OutError = Norm.ErrorMessage;
|
|
236
|
-
}
|
|
237
|
-
return Norm.bIsValid ? Norm.Path : FString();
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Resolves an asset path from a partial path or short name.
|
|
242
|
-
* 1. Checks if InputPath exists exactly.
|
|
243
|
-
* 2. If not, and InputPath is a short name, searches AssetRegistry.
|
|
244
|
-
* 3. Returns the full package name if found uniquely.
|
|
245
|
-
*/
|
|
246
|
-
static inline FString ResolveAssetPath(const FString &InputPath) {
|
|
247
|
-
if (InputPath.IsEmpty())
|
|
248
|
-
return FString();
|
|
249
|
-
|
|
250
|
-
// 1. Exact match check
|
|
251
|
-
if (UEditorAssetLibrary::DoesAssetExist(InputPath)) {
|
|
252
|
-
return InputPath;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// 2. Exact match with /Game/ prepended if it looks like a relative path but
|
|
256
|
-
// missing root
|
|
257
|
-
if (!InputPath.StartsWith(TEXT("/"))) {
|
|
258
|
-
FString GamePath = TEXT("/Game/") + InputPath;
|
|
259
|
-
if (UEditorAssetLibrary::DoesAssetExist(GamePath)) {
|
|
260
|
-
return GamePath;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// 3. Search by name if it's a short name (no slashes)
|
|
265
|
-
// NOTE: This section is disabled because FARFilter::AssetName is not
|
|
266
|
-
// available in UE5.7 and iterating all assets is too expensive. Relative
|
|
267
|
-
// paths are still resolved via /Game/ prepend above.
|
|
268
|
-
/*
|
|
269
|
-
FString BaseName = FPaths::GetBaseFilename(InputPath);
|
|
270
|
-
FAssetRegistryModule &AssetRegistryModule =
|
|
271
|
-
FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
|
|
272
|
-
IAssetRegistry &AssetRegistry = AssetRegistryModule.Get();
|
|
273
|
-
|
|
274
|
-
TArray<FAssetData> AssetDataList;
|
|
275
|
-
FARFilter Filter;
|
|
276
|
-
// Filter.AssetName = FName(*BaseName); // Compilation Error: AssetName not
|
|
277
|
-
member of FARFilter
|
|
278
|
-
|
|
279
|
-
// AssetRegistry.GetAssets(Filter, AssetDataList);
|
|
280
|
-
|
|
281
|
-
if (AssetDataList.Num() == 1) {
|
|
282
|
-
return AssetDataList[0].PackageName.ToString();
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (AssetDataList.Num() > 1) {
|
|
286
|
-
for (const FAssetData &Data : AssetDataList) {
|
|
287
|
-
if (Data.PackageName.ToString().StartsWith(TEXT("/Game/"))) {
|
|
288
|
-
return Data.PackageName.ToString();
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
*/
|
|
293
|
-
|
|
294
|
-
return FString();
|
|
295
|
-
}
|
|
296
|
-
#endif
|
|
297
|
-
|
|
298
|
-
#if WITH_EDITOR
|
|
299
|
-
// Resolve a UClass by a variety of heuristics: try full path lookup, attempt
|
|
300
|
-
// to load an asset by path (UBlueprint or UClass), then fall back to scanning
|
|
301
|
-
// loaded classes by name or path suffix. This replaces previous usages of
|
|
302
|
-
// FindObject<...>(ANY_PACKAGE, ...) which is deprecated.
|
|
303
|
-
static inline UClass *ResolveClassByName(const FString &ClassNameOrPath) {
|
|
304
|
-
if (ClassNameOrPath.IsEmpty())
|
|
305
|
-
return nullptr;
|
|
306
|
-
|
|
307
|
-
// 1) If it's an asset path, prefer loading the asset and deriving the class
|
|
308
|
-
// Skip /Script/ paths as they are native classes, not assets
|
|
309
|
-
if ((ClassNameOrPath.StartsWith(TEXT("/")) ||
|
|
310
|
-
ClassNameOrPath.Contains(TEXT("/"))) &&
|
|
311
|
-
!ClassNameOrPath.StartsWith(TEXT("/Script/"))) {
|
|
312
|
-
UObject *Loaded = nullptr;
|
|
313
|
-
// Prefer EditorAssetLibrary when available
|
|
314
|
-
#if WITH_EDITOR
|
|
315
|
-
Loaded = UEditorAssetLibrary::LoadAsset(ClassNameOrPath);
|
|
316
|
-
#endif
|
|
317
|
-
if (Loaded) {
|
|
318
|
-
if (UBlueprint *BP = Cast<UBlueprint>(Loaded))
|
|
319
|
-
return BP->GeneratedClass;
|
|
320
|
-
if (UClass *C = Cast<UClass>(Loaded))
|
|
321
|
-
return C;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// 2) Try a direct FindObject using nullptr/explicit outer (expects full path)
|
|
326
|
-
if (UClass *Direct = FindObject<UClass>(nullptr, *ClassNameOrPath))
|
|
327
|
-
return Direct;
|
|
328
|
-
|
|
329
|
-
// 2.5) Try guessing generic engine locations for common components (e.g.
|
|
330
|
-
// StaticMeshComponent -> /Script/Engine.StaticMeshComponent) This helps when
|
|
331
|
-
// the class has not been loaded yet so TObjectIterator won't find it.
|
|
332
|
-
if (!ClassNameOrPath.Contains(TEXT("/")) &&
|
|
333
|
-
!ClassNameOrPath.Contains(TEXT("."))) {
|
|
334
|
-
FString EnginePath =
|
|
335
|
-
FString::Printf(TEXT("/Script/Engine.%s"), *ClassNameOrPath);
|
|
336
|
-
if (UClass *EngineClass = FindObject<UClass>(nullptr, *EnginePath))
|
|
337
|
-
return EngineClass;
|
|
338
|
-
|
|
339
|
-
// Attempt load for engine class (unlikely to need load for native, but just
|
|
340
|
-
// in case)
|
|
341
|
-
if (UClass *EngineClassLoaded = LoadObject<UClass>(nullptr, *EnginePath))
|
|
342
|
-
return EngineClassLoaded;
|
|
343
|
-
|
|
344
|
-
FString UMGPath = FString::Printf(TEXT("/Script/UMG.%s"), *ClassNameOrPath);
|
|
345
|
-
if (UClass *UMGClass = FindObject<UClass>(nullptr, *UMGPath))
|
|
346
|
-
return UMGClass;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Special handling for common ambiguous types
|
|
350
|
-
if (ClassNameOrPath.Equals(TEXT("NiagaraComponent"),
|
|
351
|
-
ESearchCase::IgnoreCase)) {
|
|
352
|
-
if (UClass *NiagaraComp = FindObject<UClass>(
|
|
353
|
-
nullptr, TEXT("/Script/Niagara.NiagaraComponent"))) {
|
|
354
|
-
return NiagaraComp;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// 3) Fallback: iterate loaded classes and match by short name or path suffix
|
|
359
|
-
UClass *BestMatch = nullptr;
|
|
360
|
-
for (TObjectIterator<UClass> It; It; ++It) {
|
|
361
|
-
UClass *C = *It;
|
|
362
|
-
if (!C)
|
|
363
|
-
continue;
|
|
364
|
-
|
|
365
|
-
// Exact short name match
|
|
366
|
-
if (C->GetName().Equals(ClassNameOrPath, ESearchCase::IgnoreCase)) {
|
|
367
|
-
// Prefer /Script/ (native) classes over others if multiple match
|
|
368
|
-
if (C->GetPathName().StartsWith(TEXT("/Script/")))
|
|
369
|
-
return C;
|
|
370
|
-
if (!BestMatch)
|
|
371
|
-
BestMatch = C;
|
|
372
|
-
}
|
|
373
|
-
// Match on ".ClassName" suffix (path-based short form)
|
|
374
|
-
else if (C->GetPathName().EndsWith(
|
|
375
|
-
FString::Printf(TEXT(".%s"), *ClassNameOrPath),
|
|
376
|
-
ESearchCase::IgnoreCase)) {
|
|
377
|
-
if (!BestMatch)
|
|
378
|
-
BestMatch = C;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
return BestMatch;
|
|
383
|
-
}
|
|
384
|
-
#endif
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Extracts top-level JSON objects from a string.
|
|
388
|
-
*
|
|
389
|
-
* @param In The input string that may contain one or more JSON objects mixed
|
|
390
|
-
* with other text.
|
|
391
|
-
* @returns An array of substring FStrings, each containing a complete top-level
|
|
392
|
-
* JSON object in the same order they appear in the input; empty if none are
|
|
393
|
-
* found.
|
|
394
|
-
*/
|
|
395
|
-
static inline TArray<FString> ExtractTopLevelJsonObjects(const FString &In) {
|
|
396
|
-
TArray<FString> Results;
|
|
397
|
-
int32 Depth = 0;
|
|
398
|
-
int32 Start = INDEX_NONE;
|
|
399
|
-
for (int32 i = 0; i < In.Len(); ++i) {
|
|
400
|
-
const TCHAR C = In[i];
|
|
401
|
-
if (C == '{') {
|
|
402
|
-
if (Depth == 0)
|
|
403
|
-
Start = i;
|
|
404
|
-
Depth++;
|
|
405
|
-
} else if (C == '}') {
|
|
406
|
-
Depth--;
|
|
407
|
-
if (Depth == 0 && Start != INDEX_NONE) {
|
|
408
|
-
Results.Add(In.Mid(Start, i - Start + 1));
|
|
409
|
-
Start = INDEX_NONE;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
return Results;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Produce a lowercase hexadecimal representation of the UTF-8 encoding of a
|
|
418
|
-
* string for diagnostic use.
|
|
419
|
-
* @param In The input string to encode as UTF-8 bytes.
|
|
420
|
-
* @returns A lowercase hex string representing the UTF-8 bytes of `In` (two hex
|
|
421
|
-
* characters per byte).
|
|
422
|
-
*/
|
|
423
|
-
static inline FString HexifyUtf8(const FString &In) {
|
|
424
|
-
FTCHARToUTF8 Converter(*In);
|
|
425
|
-
const uint8 *Bytes = reinterpret_cast<const uint8 *>(Converter.Get());
|
|
426
|
-
int32 Len = Converter.Length();
|
|
427
|
-
FString Hex;
|
|
428
|
-
Hex.Reserve(Len * 2);
|
|
429
|
-
for (int32 i = 0; i < Len; ++i) {
|
|
430
|
-
Hex += FString::Printf(TEXT("%02x"), Bytes[i]);
|
|
431
|
-
}
|
|
432
|
-
return Hex;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// Lightweight output capture to collect log lines emitted during
|
|
436
|
-
/**
|
|
437
|
-
* Captures log output written to GLog into an in-memory list of lines.
|
|
438
|
-
*
|
|
439
|
-
* Instances can be attached as an FOutputDevice to collect serialized log
|
|
440
|
-
* messages. The captured lines have trailing newline characters removed and are
|
|
441
|
-
* stored in FIFO order. The Serialize override ignores null input.
|
|
442
|
-
*
|
|
443
|
-
* @returns For Consume(): an array of captured log lines; the captured list is
|
|
444
|
-
* cleared from the instance.
|
|
445
|
-
*/
|
|
446
|
-
struct FMcpOutputCapture : public FOutputDevice {
|
|
447
|
-
TArray<FString> Lines;
|
|
448
|
-
/**
|
|
449
|
-
* Capture a log line, trim any trailing newline characters, and append the
|
|
450
|
-
* result to the internal Lines buffer.
|
|
451
|
-
* @param V Null-terminated string containing the log message; ignored if
|
|
452
|
-
* null.
|
|
453
|
-
* @param Verbosity Verbosity level of the log message.
|
|
454
|
-
* @param Category Log category name.
|
|
455
|
-
*/
|
|
456
|
-
virtual void Serialize(const TCHAR *V, ELogVerbosity::Type Verbosity,
|
|
457
|
-
const FName &Category) override {
|
|
458
|
-
if (!V)
|
|
459
|
-
return;
|
|
460
|
-
FString S(V);
|
|
461
|
-
// Remove trailing newlines for cleaner payloads
|
|
462
|
-
while (S.EndsWith(TEXT("\n")))
|
|
463
|
-
S.RemoveAt(S.Len() - 1);
|
|
464
|
-
Lines.Add(S);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
TArray<FString> Consume() {
|
|
468
|
-
TArray<FString> Tmp = MoveTemp(Lines);
|
|
469
|
-
Lines.Empty();
|
|
470
|
-
return Tmp;
|
|
471
|
-
}
|
|
472
|
-
};
|
|
473
|
-
|
|
474
|
-
// Export a single UProperty value from an object into a JSON value.
|
|
475
|
-
/**
|
|
476
|
-
* Convert a single Unreal property value from a container into a JSON value.
|
|
477
|
-
*
|
|
478
|
-
* Supported property kinds include: strings and names, booleans, numeric types
|
|
479
|
-
* (float, double, int32, int64, byte), enum properties (name when available or
|
|
480
|
-
* numeric value), object references (returns path string or JSON null), soft
|
|
481
|
-
* object/class references (soft path string or JSON null), common structs
|
|
482
|
-
* (FVector/FVector-like exported as [x,y,z], FRotator exported as
|
|
483
|
-
* [pitch,yaw,roll], other structs exported as textual representation),
|
|
484
|
-
* arrays, maps (stringifiable keys with basic value types), and sets.
|
|
485
|
-
*
|
|
486
|
-
* @param TargetContainer Pointer to the memory/container that holds the
|
|
487
|
-
* property's value.
|
|
488
|
-
* @param Property The property definition to export.
|
|
489
|
-
* @returns A shared pointer to an FJsonValue representing the property's value,
|
|
490
|
-
* or `nullptr` if the inputs are invalid or the property type is not
|
|
491
|
-
* supported. JSON `null` values are returned for valid null object or
|
|
492
|
-
* soft-reference properties when appropriate.
|
|
493
|
-
*/
|
|
494
|
-
static inline TSharedPtr<FJsonValue>
|
|
495
|
-
ExportPropertyToJsonValue(void *TargetContainer, FProperty *Property) {
|
|
496
|
-
if (!TargetContainer || !Property)
|
|
497
|
-
return nullptr;
|
|
498
|
-
|
|
499
|
-
// Strings
|
|
500
|
-
if (FStrProperty *Str = CastField<FStrProperty>(Property)) {
|
|
501
|
-
return MakeShared<FJsonValueString>(
|
|
502
|
-
Str->GetPropertyValue_InContainer(TargetContainer));
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// Names
|
|
506
|
-
if (FNameProperty *NP = CastField<FNameProperty>(Property)) {
|
|
507
|
-
return MakeShared<FJsonValueString>(
|
|
508
|
-
NP->GetPropertyValue_InContainer(TargetContainer).ToString());
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Booleans
|
|
512
|
-
if (FBoolProperty *BP = CastField<FBoolProperty>(Property)) {
|
|
513
|
-
return MakeShared<FJsonValueBoolean>(
|
|
514
|
-
BP->GetPropertyValue_InContainer(TargetContainer));
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Numeric (handle concrete numeric property types to avoid engine-API
|
|
518
|
-
// differences)
|
|
519
|
-
if (FFloatProperty *FP = CastField<FFloatProperty>(Property)) {
|
|
520
|
-
return MakeShared<FJsonValueNumber>(
|
|
521
|
-
(double)FP->GetPropertyValue_InContainer(TargetContainer));
|
|
522
|
-
}
|
|
523
|
-
if (FDoubleProperty *DP = CastField<FDoubleProperty>(Property)) {
|
|
524
|
-
return MakeShared<FJsonValueNumber>(
|
|
525
|
-
(double)DP->GetPropertyValue_InContainer(TargetContainer));
|
|
526
|
-
}
|
|
527
|
-
if (FIntProperty *IP = CastField<FIntProperty>(Property)) {
|
|
528
|
-
return MakeShared<FJsonValueNumber>(
|
|
529
|
-
(double)IP->GetPropertyValue_InContainer(TargetContainer));
|
|
530
|
-
}
|
|
531
|
-
if (FInt64Property *I64P = CastField<FInt64Property>(Property)) {
|
|
532
|
-
return MakeShared<FJsonValueNumber>(
|
|
533
|
-
(double)I64P->GetPropertyValue_InContainer(TargetContainer));
|
|
534
|
-
}
|
|
535
|
-
if (FByteProperty *BP = CastField<FByteProperty>(Property)) {
|
|
536
|
-
// Byte property may be an enum; return enum name if available, else numeric
|
|
537
|
-
// value
|
|
538
|
-
const uint8 ByteVal = BP->GetPropertyValue_InContainer(TargetContainer);
|
|
539
|
-
if (UEnum *Enum = BP->Enum) {
|
|
540
|
-
const FString EnumName = Enum->GetNameStringByValue(ByteVal);
|
|
541
|
-
if (!EnumName.IsEmpty()) {
|
|
542
|
-
return MakeShared<FJsonValueString>(EnumName);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
return MakeShared<FJsonValueNumber>((double)ByteVal);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// Enum property (newer engine versions use FEnumProperty instead of
|
|
549
|
-
// FByteProperty for enums)
|
|
550
|
-
if (FEnumProperty *EP = CastField<FEnumProperty>(Property)) {
|
|
551
|
-
if (UEnum *Enum = EP->GetEnum()) {
|
|
552
|
-
void *ValuePtr = EP->ContainerPtrToValuePtr<void>(TargetContainer);
|
|
553
|
-
if (FNumericProperty *UnderlyingProp = EP->GetUnderlyingProperty()) {
|
|
554
|
-
const int64 EnumVal =
|
|
555
|
-
UnderlyingProp->GetSignedIntPropertyValue(ValuePtr);
|
|
556
|
-
const FString EnumName = Enum->GetNameStringByValue(EnumVal);
|
|
557
|
-
if (!EnumName.IsEmpty()) {
|
|
558
|
-
return MakeShared<FJsonValueString>(EnumName);
|
|
559
|
-
}
|
|
560
|
-
return MakeShared<FJsonValueNumber>((double)EnumVal);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
return MakeShared<FJsonValueNumber>(0.0);
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// Object references -> return path if available
|
|
567
|
-
if (FObjectProperty *OP = CastField<FObjectProperty>(Property)) {
|
|
568
|
-
UObject *O = OP->GetObjectPropertyValue_InContainer(TargetContainer);
|
|
569
|
-
if (O)
|
|
570
|
-
return MakeShared<FJsonValueString>(O->GetPathName());
|
|
571
|
-
return MakeShared<FJsonValueNull>();
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Soft object references (FSoftObjectPtr, FSoftObjectPath)
|
|
575
|
-
if (FSoftObjectProperty *SOP = CastField<FSoftObjectProperty>(Property)) {
|
|
576
|
-
const void *ValuePtr = SOP->ContainerPtrToValuePtr<void>(TargetContainer);
|
|
577
|
-
const FSoftObjectPtr *SoftObjPtr =
|
|
578
|
-
static_cast<const FSoftObjectPtr *>(ValuePtr);
|
|
579
|
-
if (SoftObjPtr && !SoftObjPtr->IsNull()) {
|
|
580
|
-
return MakeShared<FJsonValueString>(
|
|
581
|
-
SoftObjPtr->ToSoftObjectPath().ToString());
|
|
582
|
-
}
|
|
583
|
-
return MakeShared<FJsonValueNull>();
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// Soft class references (FSoftClassPtr)
|
|
587
|
-
if (FSoftClassProperty *SCP = CastField<FSoftClassProperty>(Property)) {
|
|
588
|
-
const void *ValuePtr = SCP->ContainerPtrToValuePtr<void>(TargetContainer);
|
|
589
|
-
const FSoftObjectPtr *SoftClassPtr =
|
|
590
|
-
static_cast<const FSoftObjectPtr *>(ValuePtr);
|
|
591
|
-
if (SoftClassPtr && !SoftClassPtr->IsNull()) {
|
|
592
|
-
return MakeShared<FJsonValueString>(
|
|
593
|
-
SoftClassPtr->ToSoftObjectPath().ToString());
|
|
594
|
-
}
|
|
595
|
-
return MakeShared<FJsonValueNull>();
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// Structs: FVector and FRotator common cases
|
|
599
|
-
if (FStructProperty *SP = CastField<FStructProperty>(Property)) {
|
|
600
|
-
const FString TypeName = SP->Struct ? SP->Struct->GetName() : FString();
|
|
601
|
-
if (TypeName.Equals(TEXT("Vector"), ESearchCase::IgnoreCase)) {
|
|
602
|
-
const FVector *V = SP->ContainerPtrToValuePtr<FVector>(TargetContainer);
|
|
603
|
-
TArray<TSharedPtr<FJsonValue>> Arr;
|
|
604
|
-
Arr.Add(MakeShared<FJsonValueNumber>(V->X));
|
|
605
|
-
Arr.Add(MakeShared<FJsonValueNumber>(V->Y));
|
|
606
|
-
Arr.Add(MakeShared<FJsonValueNumber>(V->Z));
|
|
607
|
-
return MakeShared<FJsonValueArray>(Arr);
|
|
608
|
-
} else if (TypeName.Equals(TEXT("Rotator"), ESearchCase::IgnoreCase)) {
|
|
609
|
-
const FRotator *R = SP->ContainerPtrToValuePtr<FRotator>(TargetContainer);
|
|
610
|
-
TArray<TSharedPtr<FJsonValue>> Arr;
|
|
611
|
-
Arr.Add(MakeShared<FJsonValueNumber>(R->Pitch));
|
|
612
|
-
Arr.Add(MakeShared<FJsonValueNumber>(R->Yaw));
|
|
613
|
-
Arr.Add(MakeShared<FJsonValueNumber>(R->Roll));
|
|
614
|
-
return MakeShared<FJsonValueArray>(Arr);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// Fallback: export textual representation
|
|
618
|
-
FString Exported;
|
|
619
|
-
SP->Struct->ExportText(Exported,
|
|
620
|
-
SP->ContainerPtrToValuePtr<void>(TargetContainer),
|
|
621
|
-
nullptr, nullptr, 0, nullptr, true);
|
|
622
|
-
return MakeShared<FJsonValueString>(Exported);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// Arrays: try to export inner values as strings
|
|
626
|
-
if (FArrayProperty *AP = CastField<FArrayProperty>(Property)) {
|
|
627
|
-
FScriptArrayHelper Helper(
|
|
628
|
-
AP, AP->ContainerPtrToValuePtr<void>(TargetContainer));
|
|
629
|
-
TArray<TSharedPtr<FJsonValue>> Out;
|
|
630
|
-
for (int32 i = 0; i < Helper.Num(); ++i) {
|
|
631
|
-
void *ElemPtr = Helper.GetRawPtr(i);
|
|
632
|
-
if (FProperty *Inner = AP->Inner) {
|
|
633
|
-
// Handle common inner types directly from element memory
|
|
634
|
-
if (FStrProperty *StrInner = CastField<FStrProperty>(Inner)) {
|
|
635
|
-
const FString &Val = *reinterpret_cast<FString *>(ElemPtr);
|
|
636
|
-
Out.Add(MakeShared<FJsonValueString>(Val));
|
|
637
|
-
continue;
|
|
638
|
-
}
|
|
639
|
-
if (FNameProperty *NameInner = CastField<FNameProperty>(Inner)) {
|
|
640
|
-
const FName &N = *reinterpret_cast<FName *>(ElemPtr);
|
|
641
|
-
Out.Add(MakeShared<FJsonValueString>(N.ToString()));
|
|
642
|
-
continue;
|
|
643
|
-
}
|
|
644
|
-
if (FBoolProperty *BoolInner = CastField<FBoolProperty>(Inner)) {
|
|
645
|
-
const bool B = (*reinterpret_cast<const uint8 *>(ElemPtr)) != 0;
|
|
646
|
-
Out.Add(MakeShared<FJsonValueBoolean>(B));
|
|
647
|
-
continue;
|
|
648
|
-
}
|
|
649
|
-
if (FFloatProperty *FInner = CastField<FFloatProperty>(Inner)) {
|
|
650
|
-
const double Val =
|
|
651
|
-
(double)(*reinterpret_cast<const float *>(ElemPtr));
|
|
652
|
-
Out.Add(MakeShared<FJsonValueNumber>(Val));
|
|
653
|
-
continue;
|
|
654
|
-
}
|
|
655
|
-
if (FDoubleProperty *DInner = CastField<FDoubleProperty>(Inner)) {
|
|
656
|
-
const double Val = *reinterpret_cast<const double *>(ElemPtr);
|
|
657
|
-
Out.Add(MakeShared<FJsonValueNumber>(Val));
|
|
658
|
-
continue;
|
|
659
|
-
}
|
|
660
|
-
if (FIntProperty *IInner = CastField<FIntProperty>(Inner)) {
|
|
661
|
-
const double Val =
|
|
662
|
-
(double)(*reinterpret_cast<const int32 *>(ElemPtr));
|
|
663
|
-
Out.Add(MakeShared<FJsonValueNumber>(Val));
|
|
664
|
-
continue;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// Fallback: stringified placeholder for unsupported inner types
|
|
668
|
-
Out.Add(MakeShared<FJsonValueString>(TEXT("<unsupported_array_elem>")));
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
return MakeShared<FJsonValueArray>(Out);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// Maps: export as JSON object with key-value pairs
|
|
675
|
-
if (FMapProperty *MP = CastField<FMapProperty>(Property)) {
|
|
676
|
-
TSharedPtr<FJsonObject> MapObj = MakeShared<FJsonObject>();
|
|
677
|
-
FScriptMapHelper Helper(MP,
|
|
678
|
-
MP->ContainerPtrToValuePtr<void>(TargetContainer));
|
|
679
|
-
|
|
680
|
-
for (int32 i = 0; i < Helper.Num(); ++i) {
|
|
681
|
-
if (!Helper.IsValidIndex(i))
|
|
682
|
-
continue;
|
|
683
|
-
|
|
684
|
-
// Get key and value pointers
|
|
685
|
-
const uint8 *KeyPtr = Helper.GetKeyPtr(i);
|
|
686
|
-
const uint8 *ValuePtr = Helper.GetValuePtr(i);
|
|
687
|
-
|
|
688
|
-
// Convert key to string (maps typically use string or name keys)
|
|
689
|
-
FString KeyStr;
|
|
690
|
-
FProperty *KeyProp = MP->KeyProp;
|
|
691
|
-
if (FStrProperty *StrKey = CastField<FStrProperty>(KeyProp)) {
|
|
692
|
-
KeyStr = *reinterpret_cast<const FString *>(KeyPtr);
|
|
693
|
-
} else if (FNameProperty *NameKey = CastField<FNameProperty>(KeyProp)) {
|
|
694
|
-
KeyStr = reinterpret_cast<const FName *>(KeyPtr)->ToString();
|
|
695
|
-
} else if (FIntProperty *IntKey = CastField<FIntProperty>(KeyProp)) {
|
|
696
|
-
KeyStr = FString::FromInt(*reinterpret_cast<const int32 *>(KeyPtr));
|
|
697
|
-
} else {
|
|
698
|
-
KeyStr = FString::Printf(TEXT("key_%d"), i);
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// Convert value to JSON
|
|
702
|
-
FProperty *ValueProp = MP->ValueProp;
|
|
703
|
-
if (FStrProperty *StrVal = CastField<FStrProperty>(ValueProp)) {
|
|
704
|
-
MapObj->SetStringField(KeyStr,
|
|
705
|
-
*reinterpret_cast<const FString *>(ValuePtr));
|
|
706
|
-
} else if (FIntProperty *IntVal = CastField<FIntProperty>(ValueProp)) {
|
|
707
|
-
MapObj->SetNumberField(
|
|
708
|
-
KeyStr, (double)*reinterpret_cast<const int32 *>(ValuePtr));
|
|
709
|
-
} else if (FFloatProperty *FloatVal =
|
|
710
|
-
CastField<FFloatProperty>(ValueProp)) {
|
|
711
|
-
MapObj->SetNumberField(
|
|
712
|
-
KeyStr, (double)*reinterpret_cast<const float *>(ValuePtr));
|
|
713
|
-
} else if (FBoolProperty *BoolVal = CastField<FBoolProperty>(ValueProp)) {
|
|
714
|
-
MapObj->SetBoolField(KeyStr,
|
|
715
|
-
(*reinterpret_cast<const uint8 *>(ValuePtr)) != 0);
|
|
716
|
-
} else {
|
|
717
|
-
MapObj->SetStringField(KeyStr, TEXT("<unsupported_value_type>"));
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
return MakeShared<FJsonValueObject>(MapObj);
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
// Sets: export as JSON array
|
|
725
|
-
if (FSetProperty *SP = CastField<FSetProperty>(Property)) {
|
|
726
|
-
TArray<TSharedPtr<FJsonValue>> Out;
|
|
727
|
-
FScriptSetHelper Helper(SP,
|
|
728
|
-
SP->ContainerPtrToValuePtr<void>(TargetContainer));
|
|
729
|
-
|
|
730
|
-
for (int32 i = 0; i < Helper.Num(); ++i) {
|
|
731
|
-
if (!Helper.IsValidIndex(i))
|
|
732
|
-
continue;
|
|
733
|
-
|
|
734
|
-
const uint8 *ElemPtr = Helper.GetElementPtr(i);
|
|
735
|
-
FProperty *ElemProp = SP->ElementProp;
|
|
736
|
-
|
|
737
|
-
if (FStrProperty *StrElem = CastField<FStrProperty>(ElemProp)) {
|
|
738
|
-
Out.Add(MakeShared<FJsonValueString>(
|
|
739
|
-
*reinterpret_cast<const FString *>(ElemPtr)));
|
|
740
|
-
} else if (FNameProperty *NameElem = CastField<FNameProperty>(ElemProp)) {
|
|
741
|
-
Out.Add(MakeShared<FJsonValueString>(
|
|
742
|
-
reinterpret_cast<const FName *>(ElemPtr)->ToString()));
|
|
743
|
-
} else if (FIntProperty *IntElem = CastField<FIntProperty>(ElemProp)) {
|
|
744
|
-
Out.Add(MakeShared<FJsonValueNumber>(
|
|
745
|
-
(double)*reinterpret_cast<const int32 *>(ElemPtr)));
|
|
746
|
-
} else if (FFloatProperty *FloatElem =
|
|
747
|
-
CastField<FFloatProperty>(ElemProp)) {
|
|
748
|
-
Out.Add(MakeShared<FJsonValueNumber>(
|
|
749
|
-
(double)*reinterpret_cast<const float *>(ElemPtr)));
|
|
750
|
-
} else {
|
|
751
|
-
Out.Add(MakeShared<FJsonValueString>(TEXT("<unsupported_set_elem>")));
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
return MakeShared<FJsonValueArray>(Out);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
return nullptr;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
#if WITH_EDITOR
|
|
762
|
-
// Throttled wrapper around UEditorAssetLibrary::SaveLoadedAsset to avoid
|
|
763
|
-
// triggering rapid repeated SavePackage calls which can cause engine
|
|
764
|
-
// warnings (FlushRenderingCommands called recursively) during heavy
|
|
765
|
-
// test activity. The helper consults a plugin-wide map of recent save
|
|
766
|
-
// timestamps (GRecentAssetSaveTs) and skips saves that occur within the
|
|
767
|
-
// configured throttle window. Skipped saves return 'true' to preserve
|
|
768
|
-
// idempotent behavior for callers that treat a skipped save as a success.
|
|
769
|
-
// Throttled wrapper around UEditorAssetLibrary::SaveLoadedAsset to avoid
|
|
770
|
-
// triggering rapid repeated SavePackage calls which can cause engine
|
|
771
|
-
// warnings (FlushRenderingCommands called recursively) during heavy
|
|
772
|
-
// test activity. The helper consults a plugin-wide map of recent save
|
|
773
|
-
// timestamps (GRecentAssetSaveTs) and skips saves that occur within the
|
|
774
|
-
// configured throttle window. Skipped saves return 'true' to preserve
|
|
775
|
-
// idempotent behavior for callers that treat a skipped save as a success.
|
|
776
|
-
//
|
|
777
|
-
// bForce: If true, ignore throttling and force an immediate save.
|
|
778
|
-
static inline bool
|
|
779
|
-
SaveLoadedAssetThrottled(UObject *Asset, double ThrottleSecondsOverride = -1.0,
|
|
780
|
-
bool bForce = false) {
|
|
781
|
-
if (!Asset)
|
|
782
|
-
return false;
|
|
783
|
-
const double Now = FPlatformTime::Seconds();
|
|
784
|
-
const double Throttle = (ThrottleSecondsOverride >= 0.0)
|
|
785
|
-
? ThrottleSecondsOverride
|
|
786
|
-
: GRecentAssetSaveThrottleSeconds;
|
|
787
|
-
FString Key = Asset->GetPathName();
|
|
788
|
-
if (Key.IsEmpty())
|
|
789
|
-
Key = Asset->GetName();
|
|
790
|
-
|
|
791
|
-
{
|
|
792
|
-
FScopeLock Lock(&GRecentAssetSaveMutex);
|
|
793
|
-
if (!bForce) {
|
|
794
|
-
if (double *Last = GRecentAssetSaveTs.Find(Key)) {
|
|
795
|
-
const double Elapsed = Now - *Last;
|
|
796
|
-
if (Elapsed < Throttle) {
|
|
797
|
-
UE_LOG(LogMcpAutomationBridgeSubsystem, VeryVerbose,
|
|
798
|
-
TEXT("SaveLoadedAssetThrottled: skipping save for '%s' "
|
|
799
|
-
"(last=%.3fs, throttle=%.3fs)"),
|
|
800
|
-
*Key, Elapsed, Throttle);
|
|
801
|
-
// Treat skip as success to avoid bubbling save failures into tests
|
|
802
|
-
return true;
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
// Perform the save and record timestamp on success
|
|
809
|
-
const bool bSaved = UEditorAssetLibrary::SaveLoadedAsset(Asset);
|
|
810
|
-
if (bSaved) {
|
|
811
|
-
FScopeLock Lock(&GRecentAssetSaveMutex);
|
|
812
|
-
GRecentAssetSaveTs.Add(Key, Now);
|
|
813
|
-
UE_LOG(LogMcpAutomationBridgeSubsystem, VeryVerbose,
|
|
814
|
-
TEXT("SaveLoadedAssetThrottled: saved '%s' (throttle reset)"), *Key);
|
|
815
|
-
} else {
|
|
816
|
-
UE_LOG(LogMcpAutomationBridgeSubsystem, Warning,
|
|
817
|
-
TEXT("SaveLoadedAssetThrottled: failed to save '%s'"), *Key);
|
|
818
|
-
}
|
|
819
|
-
return bSaved;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
// Force a synchronous scan of a specific package or folder path to ensure
|
|
823
|
-
// the Asset Registry is up-to-date immediately after asset creation.
|
|
824
|
-
static inline void ScanPathSynchronous(const FString &InPath,
|
|
825
|
-
bool bRecursive = true) {
|
|
826
|
-
FAssetRegistryModule &AssetRegistryModule =
|
|
827
|
-
FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
|
|
828
|
-
IAssetRegistry &AssetRegistry = AssetRegistryModule.Get();
|
|
829
|
-
|
|
830
|
-
// Scan specific path
|
|
831
|
-
TArray<FString> PathsToScan;
|
|
832
|
-
PathsToScan.Add(InPath);
|
|
833
|
-
AssetRegistry.ScanPathsSynchronous(PathsToScan, bRecursive);
|
|
834
|
-
}
|
|
835
|
-
#else
|
|
836
|
-
static inline bool
|
|
837
|
-
SaveLoadedAssetThrottled(void *Asset, double ThrottleSecondsOverride = -1.0,
|
|
838
|
-
bool bForce = false) {
|
|
839
|
-
(void)Asset;
|
|
840
|
-
(void)ThrottleSecondsOverride;
|
|
841
|
-
(void)bForce;
|
|
842
|
-
return false;
|
|
843
|
-
}
|
|
844
|
-
static inline void ScanPathSynchronous(const FString &InPath,
|
|
845
|
-
bool bRecursive = true) {
|
|
846
|
-
(void)InPath;
|
|
847
|
-
(void)bRecursive;
|
|
848
|
-
}
|
|
849
|
-
#endif
|
|
850
|
-
|
|
851
|
-
// Apply a JSON value to an FProperty on a UObject. Returns true on success and
|
|
852
|
-
/**
|
|
853
|
-
* Apply a JSON value to a reflected property on a target container (object or
|
|
854
|
-
* struct).
|
|
855
|
-
*
|
|
856
|
-
* Converts and assigns common JSON types to the matching Unreal property type
|
|
857
|
-
* (bool, string/name, numeric types, enums/byte, object and soft references,
|
|
858
|
-
* structs for Vector/Rotator or JSON-string-to-struct, and arrays with common
|
|
859
|
-
* inner types). On failure it sets a descriptive message in OutError.
|
|
860
|
-
*
|
|
861
|
-
* @param TargetContainer Pointer to the memory/container that holds the
|
|
862
|
-
* property value (e.g., UObject or struct instance).
|
|
863
|
-
* @param Property The reflected FProperty to assign into.
|
|
864
|
-
* @param ValueField The JSON value to apply.
|
|
865
|
-
* @param OutError Receives a descriptive error message when the function
|
|
866
|
-
* returns false.
|
|
867
|
-
* @returns `true` if the JSON value was successfully converted and assigned to
|
|
868
|
-
* the property, `false` otherwise.
|
|
869
|
-
*/
|
|
870
|
-
static inline bool
|
|
871
|
-
ApplyJsonValueToProperty(void *TargetContainer, FProperty *Property,
|
|
872
|
-
const TSharedPtr<FJsonValue> &ValueField,
|
|
873
|
-
FString &OutError) {
|
|
874
|
-
OutError.Empty();
|
|
875
|
-
if (!TargetContainer || !Property || !ValueField) {
|
|
876
|
-
OutError = TEXT("Invalid target/property/value");
|
|
877
|
-
return false;
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// Bool
|
|
881
|
-
if (FBoolProperty *BP = CastField<FBoolProperty>(Property)) {
|
|
882
|
-
if (ValueField->Type == EJson::Boolean) {
|
|
883
|
-
BP->SetPropertyValue_InContainer(TargetContainer, ValueField->AsBool());
|
|
884
|
-
return true;
|
|
885
|
-
}
|
|
886
|
-
if (ValueField->Type == EJson::Number) {
|
|
887
|
-
BP->SetPropertyValue_InContainer(TargetContainer,
|
|
888
|
-
ValueField->AsNumber() != 0.0);
|
|
889
|
-
return true;
|
|
890
|
-
}
|
|
891
|
-
if (ValueField->Type == EJson::String) {
|
|
892
|
-
BP->SetPropertyValue_InContainer(
|
|
893
|
-
TargetContainer,
|
|
894
|
-
ValueField->AsString().Equals(TEXT("true"), ESearchCase::IgnoreCase));
|
|
895
|
-
return true;
|
|
896
|
-
}
|
|
897
|
-
OutError = TEXT("Unsupported JSON type for bool property");
|
|
898
|
-
return false;
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
// String and Name
|
|
902
|
-
if (FStrProperty *SP = CastField<FStrProperty>(Property)) {
|
|
903
|
-
if (ValueField->Type == EJson::String) {
|
|
904
|
-
SP->SetPropertyValue_InContainer(TargetContainer, ValueField->AsString());
|
|
905
|
-
return true;
|
|
906
|
-
}
|
|
907
|
-
OutError = TEXT("Expected string for string property");
|
|
908
|
-
return false;
|
|
909
|
-
}
|
|
910
|
-
if (FNameProperty *NP = CastField<FNameProperty>(Property)) {
|
|
911
|
-
if (ValueField->Type == EJson::String) {
|
|
912
|
-
NP->SetPropertyValue_InContainer(TargetContainer,
|
|
913
|
-
FName(*ValueField->AsString()));
|
|
914
|
-
return true;
|
|
915
|
-
}
|
|
916
|
-
OutError = TEXT("Expected string for name property");
|
|
917
|
-
return false;
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
// Numeric: handle concrete numeric property types explicitly
|
|
921
|
-
if (FFloatProperty *FP = CastField<FFloatProperty>(Property)) {
|
|
922
|
-
double Val = 0.0;
|
|
923
|
-
if (ValueField->Type == EJson::Number)
|
|
924
|
-
Val = ValueField->AsNumber();
|
|
925
|
-
else if (ValueField->Type == EJson::String)
|
|
926
|
-
Val = FCString::Atod(*ValueField->AsString());
|
|
927
|
-
else {
|
|
928
|
-
OutError = TEXT("Unsupported JSON type for float property");
|
|
929
|
-
return false;
|
|
930
|
-
}
|
|
931
|
-
FP->SetPropertyValue_InContainer(TargetContainer, static_cast<float>(Val));
|
|
932
|
-
return true;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// ...existing code...
|
|
936
|
-
if (FDoubleProperty *DP = CastField<FDoubleProperty>(Property)) {
|
|
937
|
-
double Val = 0.0;
|
|
938
|
-
if (ValueField->Type == EJson::Number)
|
|
939
|
-
Val = ValueField->AsNumber();
|
|
940
|
-
else if (ValueField->Type == EJson::String)
|
|
941
|
-
Val = FCString::Atod(*ValueField->AsString());
|
|
942
|
-
else {
|
|
943
|
-
OutError = TEXT("Unsupported JSON type for double property");
|
|
944
|
-
return false;
|
|
945
|
-
}
|
|
946
|
-
DP->SetPropertyValue_InContainer(TargetContainer, Val);
|
|
947
|
-
return true;
|
|
948
|
-
}
|
|
949
|
-
if (FIntProperty *IP = CastField<FIntProperty>(Property)) {
|
|
950
|
-
int64 Val = 0;
|
|
951
|
-
if (ValueField->Type == EJson::Number)
|
|
952
|
-
Val = static_cast<int64>(ValueField->AsNumber());
|
|
953
|
-
else if (ValueField->Type == EJson::String)
|
|
954
|
-
Val = static_cast<int64>(FCString::Atoi64(*ValueField->AsString()));
|
|
955
|
-
else {
|
|
956
|
-
OutError = TEXT("Unsupported JSON type for int property");
|
|
957
|
-
return false;
|
|
958
|
-
}
|
|
959
|
-
IP->SetPropertyValue_InContainer(TargetContainer, static_cast<int32>(Val));
|
|
960
|
-
return true;
|
|
961
|
-
}
|
|
962
|
-
if (FInt64Property *I64P = CastField<FInt64Property>(Property)) {
|
|
963
|
-
int64 Val = 0;
|
|
964
|
-
if (ValueField->Type == EJson::Number)
|
|
965
|
-
Val = static_cast<int64>(ValueField->AsNumber());
|
|
966
|
-
else if (ValueField->Type == EJson::String)
|
|
967
|
-
Val = static_cast<int64>(FCString::Atoi64(*ValueField->AsString()));
|
|
968
|
-
else {
|
|
969
|
-
OutError = TEXT("Unsupported JSON type for int64 property");
|
|
970
|
-
return false;
|
|
971
|
-
}
|
|
972
|
-
I64P->SetPropertyValue_InContainer(TargetContainer, Val);
|
|
973
|
-
return true;
|
|
974
|
-
}
|
|
975
|
-
if (FByteProperty *Bp = CastField<FByteProperty>(Property)) {
|
|
976
|
-
// Check if this is an enum byte property
|
|
977
|
-
if (UEnum *Enum = Bp->Enum) {
|
|
978
|
-
if (ValueField->Type == EJson::String) {
|
|
979
|
-
// Try to match by name (with or without namespace)
|
|
980
|
-
const FString InStr = ValueField->AsString();
|
|
981
|
-
int64 EnumVal = Enum->GetValueByNameString(InStr);
|
|
982
|
-
if (EnumVal == INDEX_NONE) {
|
|
983
|
-
// Try with namespace prefix
|
|
984
|
-
const FString FullName = Enum->GenerateFullEnumName(*InStr);
|
|
985
|
-
EnumVal = Enum->GetValueByName(FName(*FullName));
|
|
986
|
-
}
|
|
987
|
-
if (EnumVal == INDEX_NONE) {
|
|
988
|
-
OutError =
|
|
989
|
-
FString::Printf(TEXT("Invalid enum value '%s' for enum '%s'"),
|
|
990
|
-
*InStr, *Enum->GetName());
|
|
991
|
-
return false;
|
|
992
|
-
}
|
|
993
|
-
Bp->SetPropertyValue_InContainer(TargetContainer,
|
|
994
|
-
static_cast<uint8>(EnumVal));
|
|
995
|
-
return true;
|
|
996
|
-
} else if (ValueField->Type == EJson::Number) {
|
|
997
|
-
// Validate numeric value is in range
|
|
998
|
-
const int64 Val = static_cast<int64>(ValueField->AsNumber());
|
|
999
|
-
if (!Enum->IsValidEnumValue(Val)) {
|
|
1000
|
-
OutError = FString::Printf(
|
|
1001
|
-
TEXT("Numeric value %lld is not valid for enum '%s'"), Val,
|
|
1002
|
-
*Enum->GetName());
|
|
1003
|
-
return false;
|
|
1004
|
-
}
|
|
1005
|
-
Bp->SetPropertyValue_InContainer(TargetContainer,
|
|
1006
|
-
static_cast<uint8>(Val));
|
|
1007
|
-
return true;
|
|
1008
|
-
}
|
|
1009
|
-
OutError = TEXT("Enum property requires string or number");
|
|
1010
|
-
return false;
|
|
1011
|
-
}
|
|
1012
|
-
// Regular byte property (not an enum)
|
|
1013
|
-
int64 Val = 0;
|
|
1014
|
-
if (ValueField->Type == EJson::Number)
|
|
1015
|
-
Val = static_cast<int64>(ValueField->AsNumber());
|
|
1016
|
-
else if (ValueField->Type == EJson::String)
|
|
1017
|
-
Val = static_cast<int64>(FCString::Atoi64(*ValueField->AsString()));
|
|
1018
|
-
else {
|
|
1019
|
-
OutError = TEXT("Unsupported JSON type for byte property");
|
|
1020
|
-
return false;
|
|
1021
|
-
}
|
|
1022
|
-
Bp->SetPropertyValue_InContainer(TargetContainer, static_cast<uint8>(Val));
|
|
1023
|
-
return true;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
// Enum property (newer engine versions)
|
|
1027
|
-
if (FEnumProperty *EP = CastField<FEnumProperty>(Property)) {
|
|
1028
|
-
if (UEnum *Enum = EP->GetEnum()) {
|
|
1029
|
-
void *ValuePtr = EP->ContainerPtrToValuePtr<void>(TargetContainer);
|
|
1030
|
-
if (FNumericProperty *UnderlyingProp = EP->GetUnderlyingProperty()) {
|
|
1031
|
-
if (ValueField->Type == EJson::String) {
|
|
1032
|
-
const FString InStr = ValueField->AsString();
|
|
1033
|
-
int64 EnumVal = Enum->GetValueByNameString(InStr);
|
|
1034
|
-
if (EnumVal == INDEX_NONE) {
|
|
1035
|
-
const FString FullName = Enum->GenerateFullEnumName(*InStr);
|
|
1036
|
-
EnumVal = Enum->GetValueByName(FName(*FullName));
|
|
1037
|
-
}
|
|
1038
|
-
if (EnumVal == INDEX_NONE) {
|
|
1039
|
-
OutError =
|
|
1040
|
-
FString::Printf(TEXT("Invalid enum value '%s' for enum '%s'"),
|
|
1041
|
-
*InStr, *Enum->GetName());
|
|
1042
|
-
return false;
|
|
1043
|
-
}
|
|
1044
|
-
UnderlyingProp->SetIntPropertyValue(ValuePtr, EnumVal);
|
|
1045
|
-
return true;
|
|
1046
|
-
} else if (ValueField->Type == EJson::Number) {
|
|
1047
|
-
const int64 Val = static_cast<int64>(ValueField->AsNumber());
|
|
1048
|
-
if (!Enum->IsValidEnumValue(Val)) {
|
|
1049
|
-
OutError = FString::Printf(
|
|
1050
|
-
TEXT("Numeric value %lld is not valid for enum '%s'"), Val,
|
|
1051
|
-
*Enum->GetName());
|
|
1052
|
-
return false;
|
|
1053
|
-
}
|
|
1054
|
-
UnderlyingProp->SetIntPropertyValue(ValuePtr, Val);
|
|
1055
|
-
return true;
|
|
1056
|
-
}
|
|
1057
|
-
OutError = TEXT("Enum property requires string or number");
|
|
1058
|
-
return false;
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
OutError = TEXT("Enum property has no valid enum definition");
|
|
1062
|
-
return false;
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
// Object reference
|
|
1066
|
-
if (FObjectProperty *OP = CastField<FObjectProperty>(Property)) {
|
|
1067
|
-
if (ValueField->Type == EJson::String) {
|
|
1068
|
-
const FString Path = ValueField->AsString();
|
|
1069
|
-
UObject *Res = nullptr;
|
|
1070
|
-
if (!Path.IsEmpty()) {
|
|
1071
|
-
// Try LoadObject first
|
|
1072
|
-
Res = LoadObject<UObject>(nullptr, *Path);
|
|
1073
|
-
// If unsuccessful, try finding by object path if it's a short path or
|
|
1074
|
-
// package path
|
|
1075
|
-
if (!Res && !Path.Contains(TEXT("."))) {
|
|
1076
|
-
// Fallback to StaticLoadObject which can sometimes handle vague paths
|
|
1077
|
-
// better
|
|
1078
|
-
Res = StaticLoadObject(UObject::StaticClass(), nullptr, *Path);
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
if (!Res && !Path.IsEmpty()) {
|
|
1082
|
-
OutError =
|
|
1083
|
-
FString::Printf(TEXT("Failed to load object at path: %s"), *Path);
|
|
1084
|
-
return false;
|
|
1085
|
-
}
|
|
1086
|
-
OP->SetObjectPropertyValue_InContainer(TargetContainer, Res);
|
|
1087
|
-
return true;
|
|
1088
|
-
}
|
|
1089
|
-
OutError = TEXT("Unsupported JSON type for object property");
|
|
1090
|
-
return false;
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
// Soft object references (FSoftObjectPtr)
|
|
1094
|
-
if (FSoftObjectProperty *SOP = CastField<FSoftObjectProperty>(Property)) {
|
|
1095
|
-
if (ValueField->Type == EJson::String) {
|
|
1096
|
-
const FString Path = ValueField->AsString();
|
|
1097
|
-
void *ValuePtr = SOP->ContainerPtrToValuePtr<void>(TargetContainer);
|
|
1098
|
-
FSoftObjectPtr *SoftObjPtr = static_cast<FSoftObjectPtr *>(ValuePtr);
|
|
1099
|
-
if (SoftObjPtr) {
|
|
1100
|
-
if (Path.IsEmpty()) {
|
|
1101
|
-
*SoftObjPtr = FSoftObjectPtr();
|
|
1102
|
-
} else {
|
|
1103
|
-
*SoftObjPtr = FSoftObjectPath(Path);
|
|
1104
|
-
}
|
|
1105
|
-
return true;
|
|
1106
|
-
}
|
|
1107
|
-
OutError = TEXT("Failed to access soft object property");
|
|
1108
|
-
return false;
|
|
1109
|
-
} else if (ValueField->Type == EJson::Null) {
|
|
1110
|
-
void *ValuePtr = SOP->ContainerPtrToValuePtr<void>(TargetContainer);
|
|
1111
|
-
FSoftObjectPtr *SoftObjPtr = static_cast<FSoftObjectPtr *>(ValuePtr);
|
|
1112
|
-
if (SoftObjPtr) {
|
|
1113
|
-
*SoftObjPtr = FSoftObjectPtr();
|
|
1114
|
-
return true;
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
OutError = TEXT("Soft object property requires string path or null");
|
|
1118
|
-
return false;
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
// Soft class references (FSoftClassPtr)
|
|
1122
|
-
if (FSoftClassProperty *SCP = CastField<FSoftClassProperty>(Property)) {
|
|
1123
|
-
if (ValueField->Type == EJson::String) {
|
|
1124
|
-
const FString Path = ValueField->AsString();
|
|
1125
|
-
void *ValuePtr = SCP->ContainerPtrToValuePtr<void>(TargetContainer);
|
|
1126
|
-
FSoftObjectPtr *SoftClassPtr = static_cast<FSoftObjectPtr *>(ValuePtr);
|
|
1127
|
-
if (SoftClassPtr) {
|
|
1128
|
-
if (Path.IsEmpty()) {
|
|
1129
|
-
*SoftClassPtr = FSoftObjectPtr();
|
|
1130
|
-
} else {
|
|
1131
|
-
*SoftClassPtr = FSoftObjectPath(Path);
|
|
1132
|
-
}
|
|
1133
|
-
return true;
|
|
1134
|
-
}
|
|
1135
|
-
OutError = TEXT("Failed to access soft class property");
|
|
1136
|
-
return false;
|
|
1137
|
-
} else if (ValueField->Type == EJson::Null) {
|
|
1138
|
-
void *ValuePtr = SCP->ContainerPtrToValuePtr<void>(TargetContainer);
|
|
1139
|
-
FSoftObjectPtr *SoftClassPtr = static_cast<FSoftObjectPtr *>(ValuePtr);
|
|
1140
|
-
if (SoftClassPtr) {
|
|
1141
|
-
*SoftClassPtr = FSoftObjectPtr();
|
|
1142
|
-
return true;
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
OutError = TEXT("Soft class property requires string path or null");
|
|
1146
|
-
return false;
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
// Structs (Vector/Rotator)
|
|
1150
|
-
if (FStructProperty *SP = CastField<FStructProperty>(Property)) {
|
|
1151
|
-
const FString TypeName = SP->Struct ? SP->Struct->GetName() : FString();
|
|
1152
|
-
if (ValueField->Type == EJson::Array) {
|
|
1153
|
-
const TArray<TSharedPtr<FJsonValue>> &Arr = ValueField->AsArray();
|
|
1154
|
-
if (TypeName.Equals(TEXT("Vector"), ESearchCase::IgnoreCase) &&
|
|
1155
|
-
Arr.Num() >= 3) {
|
|
1156
|
-
FVector V((float)Arr[0]->AsNumber(), (float)Arr[1]->AsNumber(),
|
|
1157
|
-
(float)Arr[2]->AsNumber());
|
|
1158
|
-
SP->Struct->CopyScriptStruct(
|
|
1159
|
-
SP->ContainerPtrToValuePtr<void>(TargetContainer), &V);
|
|
1160
|
-
return true;
|
|
1161
|
-
}
|
|
1162
|
-
if (TypeName.Equals(TEXT("Rotator"), ESearchCase::IgnoreCase) &&
|
|
1163
|
-
Arr.Num() >= 3) {
|
|
1164
|
-
FRotator R((float)Arr[0]->AsNumber(), (float)Arr[1]->AsNumber(),
|
|
1165
|
-
(float)Arr[2]->AsNumber());
|
|
1166
|
-
SP->Struct->CopyScriptStruct(
|
|
1167
|
-
SP->ContainerPtrToValuePtr<void>(TargetContainer), &R);
|
|
1168
|
-
return true;
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
// Try import from string for other structs. Prefer JSON conversion via
|
|
1173
|
-
// FJsonObjectConverter when the incoming text is valid JSON. Older
|
|
1174
|
-
// engine versions that provide ImportText on UScriptStruct are
|
|
1175
|
-
// supported via a guarded fallback for legacy builds.
|
|
1176
|
-
if (ValueField->Type == EJson::String) {
|
|
1177
|
-
const FString Txt = ValueField->AsString();
|
|
1178
|
-
if (SP->Struct) {
|
|
1179
|
-
// First attempt: parse the string as JSON and convert to struct
|
|
1180
|
-
// using the robust JsonObjectConverter which avoids relying on
|
|
1181
|
-
// engine-private textual import semantics.
|
|
1182
|
-
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Txt);
|
|
1183
|
-
TSharedPtr<FJsonObject> ParsedObj;
|
|
1184
|
-
if (FJsonSerializer::Deserialize(Reader, ParsedObj) &&
|
|
1185
|
-
ParsedObj.IsValid()) {
|
|
1186
|
-
if (FJsonObjectConverter::JsonObjectToUStruct(
|
|
1187
|
-
ParsedObj.ToSharedRef(), SP->Struct,
|
|
1188
|
-
SP->ContainerPtrToValuePtr<void>(TargetContainer), 0, 0)) {
|
|
1189
|
-
return true;
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
// NOTE: ImportText-based struct parsing is intentionally omitted
|
|
1194
|
-
// because engine textual import signatures differ across engine
|
|
1195
|
-
// revisions and can produce fragile compilation failures. If a
|
|
1196
|
-
// non-JSON textual import format is required in the future we
|
|
1197
|
-
// can implement a safe parser here or add an explicit engine
|
|
1198
|
-
// compatibility shim guarded by a feature macro.
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
OutError = TEXT("Unsupported JSON type for struct property");
|
|
1203
|
-
return false;
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
// Arrays: handle common inner element types directly. Unsupported inner
|
|
1207
|
-
// types will return an error to avoid relying on ImportText-like APIs.
|
|
1208
|
-
if (FArrayProperty *AP = CastField<FArrayProperty>(Property)) {
|
|
1209
|
-
if (ValueField->Type != EJson::Array) {
|
|
1210
|
-
OutError = TEXT("Expected array for array property");
|
|
1211
|
-
return false;
|
|
1212
|
-
}
|
|
1213
|
-
FScriptArrayHelper Helper(
|
|
1214
|
-
AP, AP->ContainerPtrToValuePtr<void>(TargetContainer));
|
|
1215
|
-
Helper.EmptyValues();
|
|
1216
|
-
const TArray<TSharedPtr<FJsonValue>> &Src = ValueField->AsArray();
|
|
1217
|
-
for (int32 i = 0; i < Src.Num(); ++i) {
|
|
1218
|
-
Helper.AddValue();
|
|
1219
|
-
void *ElemPtr = Helper.GetRawPtr(Helper.Num() - 1);
|
|
1220
|
-
FProperty *Inner = AP->Inner;
|
|
1221
|
-
const TSharedPtr<FJsonValue> &V = Src[i];
|
|
1222
|
-
if (FStrProperty *SIP = CastField<FStrProperty>(Inner)) {
|
|
1223
|
-
FString &Dest = *reinterpret_cast<FString *>(ElemPtr);
|
|
1224
|
-
Dest = (V->Type == EJson::String)
|
|
1225
|
-
? V->AsString()
|
|
1226
|
-
: FString::Printf(TEXT("%g"), V->AsNumber());
|
|
1227
|
-
continue;
|
|
1228
|
-
}
|
|
1229
|
-
if (FNameProperty *NIP = CastField<FNameProperty>(Inner)) {
|
|
1230
|
-
FName &Dest = *reinterpret_cast<FName *>(ElemPtr);
|
|
1231
|
-
Dest = (V->Type == EJson::String)
|
|
1232
|
-
? FName(*V->AsString())
|
|
1233
|
-
: FName(*FString::Printf(TEXT("%g"), V->AsNumber()));
|
|
1234
|
-
continue;
|
|
1235
|
-
}
|
|
1236
|
-
if (FBoolProperty *BIP = CastField<FBoolProperty>(Inner)) {
|
|
1237
|
-
uint8 &Dest = *reinterpret_cast<uint8 *>(ElemPtr);
|
|
1238
|
-
Dest = (V->Type == EJson::Boolean) ? (V->AsBool() ? 1 : 0)
|
|
1239
|
-
: (V->AsNumber() != 0.0 ? 1 : 0);
|
|
1240
|
-
continue;
|
|
1241
|
-
}
|
|
1242
|
-
if (FFloatProperty *FIP = CastField<FFloatProperty>(Inner)) {
|
|
1243
|
-
float &Dest = *reinterpret_cast<float *>(ElemPtr);
|
|
1244
|
-
Dest = (V->Type == EJson::Number)
|
|
1245
|
-
? (float)V->AsNumber()
|
|
1246
|
-
: (float)FCString::Atod(*V->AsString());
|
|
1247
|
-
continue;
|
|
1248
|
-
}
|
|
1249
|
-
if (FDoubleProperty *DIP = CastField<FDoubleProperty>(Inner)) {
|
|
1250
|
-
double &Dest = *reinterpret_cast<double *>(ElemPtr);
|
|
1251
|
-
Dest = (V->Type == EJson::Number) ? V->AsNumber()
|
|
1252
|
-
: FCString::Atod(*V->AsString());
|
|
1253
|
-
continue;
|
|
1254
|
-
}
|
|
1255
|
-
if (FIntProperty *IIP = CastField<FIntProperty>(Inner)) {
|
|
1256
|
-
int32 &Dest = *reinterpret_cast<int32 *>(ElemPtr);
|
|
1257
|
-
Dest = (V->Type == EJson::Number) ? (int32)V->AsNumber()
|
|
1258
|
-
: FCString::Atoi(*V->AsString());
|
|
1259
|
-
continue;
|
|
1260
|
-
}
|
|
1261
|
-
if (FInt64Property *I64IP = CastField<FInt64Property>(Inner)) {
|
|
1262
|
-
int64 &Dest = *reinterpret_cast<int64 *>(ElemPtr);
|
|
1263
|
-
Dest = (V->Type == EJson::Number) ? (int64)V->AsNumber()
|
|
1264
|
-
: FCString::Atoi64(*V->AsString());
|
|
1265
|
-
continue;
|
|
1266
|
-
}
|
|
1267
|
-
if (FByteProperty *BYP = CastField<FByteProperty>(Inner)) {
|
|
1268
|
-
uint8 &Dest = *reinterpret_cast<uint8 *>(ElemPtr);
|
|
1269
|
-
Dest = (V->Type == EJson::Number)
|
|
1270
|
-
? (uint8)V->AsNumber()
|
|
1271
|
-
: (uint8)FCString::Atoi(*V->AsString());
|
|
1272
|
-
continue;
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
// Unsupported inner type -> fail explicitly
|
|
1276
|
-
OutError =
|
|
1277
|
-
TEXT("Unsupported array inner property type for JSON assignment");
|
|
1278
|
-
return false;
|
|
1279
|
-
}
|
|
1280
|
-
return true;
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
OutError = TEXT("Unsupported property type for JSON assignment");
|
|
1284
|
-
return false;
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
/**
|
|
1288
|
-
* Populate Out with the vector found at the given JSON field, or use Default if
|
|
1289
|
-
* the field is missing or invalid.
|
|
1290
|
-
*
|
|
1291
|
-
* @param Obj JSON object to read the field from; may be null.
|
|
1292
|
-
* @param FieldName Name of the field containing the vector (object with x/y/z
|
|
1293
|
-
* or an array of three numbers).
|
|
1294
|
-
* @param Out Receives the resulting FVector.
|
|
1295
|
-
* @param Default Fallback FVector used when the field is absent or cannot be
|
|
1296
|
-
* parsed.
|
|
1297
|
-
*/
|
|
1298
|
-
static inline void ReadVectorField(const TSharedPtr<FJsonObject> &Obj,
|
|
1299
|
-
const TCHAR *FieldName, FVector &Out,
|
|
1300
|
-
const FVector &Default) {
|
|
1301
|
-
if (!Obj.IsValid()) {
|
|
1302
|
-
Out = Default;
|
|
1303
|
-
return;
|
|
1304
|
-
}
|
|
1305
|
-
const TSharedPtr<FJsonObject> *FieldObj = nullptr;
|
|
1306
|
-
if (Obj->TryGetObjectField(FieldName, FieldObj) && FieldObj &&
|
|
1307
|
-
(*FieldObj).IsValid()) {
|
|
1308
|
-
double X = Default.X, Y = Default.Y, Z = Default.Z;
|
|
1309
|
-
if (!(*FieldObj)->TryGetNumberField(TEXT("x"), X))
|
|
1310
|
-
(*FieldObj)->TryGetNumberField(TEXT("X"), X);
|
|
1311
|
-
if (!(*FieldObj)->TryGetNumberField(TEXT("y"), Y))
|
|
1312
|
-
(*FieldObj)->TryGetNumberField(TEXT("Y"), Y);
|
|
1313
|
-
if (!(*FieldObj)->TryGetNumberField(TEXT("z"), Z))
|
|
1314
|
-
(*FieldObj)->TryGetNumberField(TEXT("Z"), Z);
|
|
1315
|
-
Out = FVector((float)X, (float)Y, (float)Z);
|
|
1316
|
-
return;
|
|
1317
|
-
}
|
|
1318
|
-
const TArray<TSharedPtr<FJsonValue>> *Arr = nullptr;
|
|
1319
|
-
if (Obj->TryGetArrayField(FieldName, Arr) && Arr && Arr->Num() >= 3) {
|
|
1320
|
-
Out = FVector((float)(*Arr)[0]->AsNumber(), (float)(*Arr)[1]->AsNumber(),
|
|
1321
|
-
(float)(*Arr)[2]->AsNumber());
|
|
1322
|
-
return;
|
|
1323
|
-
}
|
|
1324
|
-
Out = Default;
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
/**
|
|
1328
|
-
* Read a rotator field from a JSON object into an FRotator.
|
|
1329
|
-
*
|
|
1330
|
-
* Attempts to read a rotator located at FieldName in Obj. Supports either an
|
|
1331
|
-
* object form with numeric fields "pitch"/"yaw"/"roll" (case-insensitive) or an
|
|
1332
|
-
* array form [pitch, yaw, roll]. If the field is missing or invalid, Out is
|
|
1333
|
-
* set to Default.
|
|
1334
|
-
*
|
|
1335
|
-
* @param Obj JSON object to read from.
|
|
1336
|
-
* @param FieldName Name of the field within Obj containing the rotator.
|
|
1337
|
-
* @param Out Output rotator populated from the JSON field or Default on
|
|
1338
|
-
* failure.
|
|
1339
|
-
* @param Default Fallback rotator used when the JSON field is absent or
|
|
1340
|
-
* invalid.
|
|
1341
|
-
*/
|
|
1342
|
-
static inline void ReadRotatorField(const TSharedPtr<FJsonObject> &Obj,
|
|
1343
|
-
const TCHAR *FieldName, FRotator &Out,
|
|
1344
|
-
const FRotator &Default) {
|
|
1345
|
-
if (!Obj.IsValid()) {
|
|
1346
|
-
Out = Default;
|
|
1347
|
-
return;
|
|
1348
|
-
}
|
|
1349
|
-
const TSharedPtr<FJsonObject> *FieldObj = nullptr;
|
|
1350
|
-
if (Obj->TryGetObjectField(FieldName, FieldObj) && FieldObj &&
|
|
1351
|
-
(*FieldObj).IsValid()) {
|
|
1352
|
-
double Pitch = Default.Pitch, Yaw = Default.Yaw, Roll = Default.Roll;
|
|
1353
|
-
if (!(*FieldObj)->TryGetNumberField(TEXT("pitch"), Pitch))
|
|
1354
|
-
(*FieldObj)->TryGetNumberField(TEXT("Pitch"), Pitch);
|
|
1355
|
-
if (!(*FieldObj)->TryGetNumberField(TEXT("yaw"), Yaw))
|
|
1356
|
-
(*FieldObj)->TryGetNumberField(TEXT("Yaw"), Yaw);
|
|
1357
|
-
if (!(*FieldObj)->TryGetNumberField(TEXT("roll"), Roll))
|
|
1358
|
-
(*FieldObj)->TryGetNumberField(TEXT("Roll"), Roll);
|
|
1359
|
-
Out = FRotator((float)Pitch, (float)Yaw, (float)Roll);
|
|
1360
|
-
return;
|
|
1361
|
-
}
|
|
1362
|
-
const TArray<TSharedPtr<FJsonValue>> *Arr = nullptr;
|
|
1363
|
-
if (Obj->TryGetArrayField(FieldName, Arr) && Arr && Arr->Num() >= 3) {
|
|
1364
|
-
Out = FRotator((float)(*Arr)[0]->AsNumber(), (float)(*Arr)[1]->AsNumber(),
|
|
1365
|
-
(float)(*Arr)[2]->AsNumber());
|
|
1366
|
-
return;
|
|
1367
|
-
}
|
|
1368
|
-
Out = Default;
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
/**
|
|
1372
|
-
* Extracts a FVector from a JSON object field, returning a default when the
|
|
1373
|
-
* field is absent or invalid.
|
|
1374
|
-
* @param Source JSON object to read from.
|
|
1375
|
-
* @param FieldName Name of the field to extract (expects an object with x/y/z
|
|
1376
|
-
* or an array).
|
|
1377
|
-
* @param DefaultValue Value to return when the field is missing or cannot be
|
|
1378
|
-
* parsed.
|
|
1379
|
-
* @returns The parsed FVector from the specified field, or DefaultValue if
|
|
1380
|
-
* parsing failed.
|
|
1381
|
-
*/
|
|
1382
|
-
static inline FVector ExtractVectorField(const TSharedPtr<FJsonObject> &Source,
|
|
1383
|
-
const TCHAR *FieldName,
|
|
1384
|
-
const FVector &DefaultValue) {
|
|
1385
|
-
FVector Parsed = DefaultValue;
|
|
1386
|
-
ReadVectorField(Source, FieldName, Parsed, DefaultValue);
|
|
1387
|
-
return Parsed;
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
/**
|
|
1391
|
-
* Extracts a rotator value from a JSON object field, returning the provided
|
|
1392
|
-
* default when the field is absent or cannot be parsed.
|
|
1393
|
-
* @param Source JSON object to read the field from.
|
|
1394
|
-
* @param FieldName Name of the field to extract.
|
|
1395
|
-
* @param DefaultValue Value returned when the field is missing or invalid.
|
|
1396
|
-
* @returns Parsed FRotator from the specified field, or DefaultValue if
|
|
1397
|
-
* extraction fails.
|
|
1398
|
-
*/
|
|
1399
|
-
static inline FRotator
|
|
1400
|
-
ExtractRotatorField(const TSharedPtr<FJsonObject> &Source,
|
|
1401
|
-
const TCHAR *FieldName, const FRotator &DefaultValue) {
|
|
1402
|
-
FRotator Parsed = DefaultValue;
|
|
1403
|
-
ReadRotatorField(Source, FieldName, Parsed, DefaultValue);
|
|
1404
|
-
return Parsed;
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
// Resolve a nested property path (e.g., "Transform.Location.X" or
|
|
1408
|
-
// "MyComponent.Intensity"). Returns the final property and target object, or
|
|
1409
|
-
// nullptr on failure. OutError is populated with a descriptive error message on
|
|
1410
|
-
// failure.
|
|
1411
|
-
// Resolve a nested property path (e.g., "Transform.Location.X" or
|
|
1412
|
-
// "MyComponent.Intensity"). Returns the final property and the pointer to the
|
|
1413
|
-
// container holding it. OutError is populated with a descriptive error message
|
|
1414
|
-
/**
|
|
1415
|
-
* Resolve a dotted property path against a root UObject and locate the terminal
|
|
1416
|
-
* property and its owning container.
|
|
1417
|
-
*
|
|
1418
|
-
* @param RootObject Root UObject to begin lookup from.
|
|
1419
|
-
* @param PropertyPath Dotted property path (e.g., "Transform.Location.X").
|
|
1420
|
-
* @param OutContainerPtr Set to a pointer to the container that holds the
|
|
1421
|
-
* resolved property on success; remains nullptr on failure.
|
|
1422
|
-
* @param OutError Set to a descriptive error message on failure; cleared on
|
|
1423
|
-
* entry.
|
|
1424
|
-
* @returns Pointer to the resolved FProperty for the final segment, or nullptr
|
|
1425
|
-
* if resolution failed.
|
|
1426
|
-
*/
|
|
1427
|
-
static inline FProperty *ResolveNestedPropertyPath(UObject *RootObject,
|
|
1428
|
-
const FString &PropertyPath,
|
|
1429
|
-
void *&OutContainerPtr,
|
|
1430
|
-
FString &OutError) {
|
|
1431
|
-
OutError.Empty();
|
|
1432
|
-
OutContainerPtr = nullptr;
|
|
1433
|
-
|
|
1434
|
-
if (!RootObject) {
|
|
1435
|
-
OutError = TEXT("Root object is null");
|
|
1436
|
-
return nullptr;
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
if (PropertyPath.IsEmpty()) {
|
|
1440
|
-
OutError = TEXT("Property path is empty");
|
|
1441
|
-
return nullptr;
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
TArray<FString> PathSegments;
|
|
1445
|
-
PropertyPath.ParseIntoArray(PathSegments, TEXT("."), true);
|
|
1446
|
-
|
|
1447
|
-
if (PathSegments.Num() == 0) {
|
|
1448
|
-
OutError = TEXT("Invalid property path format");
|
|
1449
|
-
return nullptr;
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
UStruct *CurrentTypeScope = RootObject->GetClass();
|
|
1453
|
-
void *CurrentContainer = RootObject;
|
|
1454
|
-
FProperty *CurrentProperty = nullptr;
|
|
1455
|
-
|
|
1456
|
-
for (int32 i = 0; i < PathSegments.Num(); ++i) {
|
|
1457
|
-
const FString &Segment = PathSegments[i];
|
|
1458
|
-
const bool bIsLastSegment = (i == PathSegments.Num() - 1);
|
|
1459
|
-
|
|
1460
|
-
// Find property in current scope
|
|
1461
|
-
CurrentProperty =
|
|
1462
|
-
FindFProperty<FProperty>(CurrentTypeScope, FName(*Segment));
|
|
1463
|
-
|
|
1464
|
-
if (!CurrentProperty) {
|
|
1465
|
-
OutError = FString::Printf(
|
|
1466
|
-
TEXT("Property '%s' not found in scope '%s' (segment %d of %d)"),
|
|
1467
|
-
*Segment, *CurrentTypeScope->GetName(), i + 1, PathSegments.Num());
|
|
1468
|
-
return nullptr;
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
// If this is the last segment, we've found our target
|
|
1472
|
-
if (bIsLastSegment) {
|
|
1473
|
-
OutContainerPtr = CurrentContainer;
|
|
1474
|
-
return CurrentProperty;
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
// Traverse deeper
|
|
1478
|
-
if (FObjectProperty *ObjectProp =
|
|
1479
|
-
CastField<FObjectProperty>(CurrentProperty)) {
|
|
1480
|
-
UObject *NextObject =
|
|
1481
|
-
ObjectProp->GetObjectPropertyValue_InContainer(CurrentContainer);
|
|
1482
|
-
if (!NextObject) {
|
|
1483
|
-
OutError = FString::Printf(
|
|
1484
|
-
TEXT("Object property '%s' is null (segment %d of %d)"), *Segment,
|
|
1485
|
-
i + 1, PathSegments.Num());
|
|
1486
|
-
return nullptr;
|
|
1487
|
-
}
|
|
1488
|
-
CurrentContainer = NextObject;
|
|
1489
|
-
CurrentTypeScope = NextObject->GetClass();
|
|
1490
|
-
} else if (FStructProperty *StructProp =
|
|
1491
|
-
CastField<FStructProperty>(CurrentProperty)) {
|
|
1492
|
-
CurrentContainer =
|
|
1493
|
-
StructProp->ContainerPtrToValuePtr<void>(CurrentContainer);
|
|
1494
|
-
CurrentTypeScope = StructProp->Struct;
|
|
1495
|
-
} else {
|
|
1496
|
-
OutError = FString::Printf(
|
|
1497
|
-
TEXT("Cannot traverse into property '%s' of type '%s'"), *Segment,
|
|
1498
|
-
*CurrentProperty->GetClass()->GetName());
|
|
1499
|
-
return nullptr;
|
|
1500
|
-
}
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
OutError = TEXT("Unexpected end of property path resolution");
|
|
1504
|
-
return nullptr;
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
// Helper to find an SCS node by a (case-insensitive) name. Uses reflection
|
|
1508
|
-
// to iterate the internal AllNodes array so this implementation does not
|
|
1509
|
-
/**
|
|
1510
|
-
* Finds a Simple Construction Script node with the given name in the provided
|
|
1511
|
-
* USimpleConstructionScript.
|
|
1512
|
-
*
|
|
1513
|
-
* Matches case-insensitively first against a node's `VariableName` property
|
|
1514
|
-
* when present, and falls back to the node's object name.
|
|
1515
|
-
* @param SCS Pointer to the USimpleConstructionScript to search; may be
|
|
1516
|
-
* nullptr.
|
|
1517
|
-
* @param Name Name to match against nodes (case-insensitive).
|
|
1518
|
-
* @returns Pointer to the matching USCS_Node, or nullptr if no match is found
|
|
1519
|
-
* or input is invalid.
|
|
1520
|
-
*/
|
|
1521
|
-
static inline USCS_Node *FindScsNodeByName(USimpleConstructionScript *SCS,
|
|
1522
|
-
const FString &Name) {
|
|
1523
|
-
if (!SCS || Name.IsEmpty())
|
|
1524
|
-
return nullptr;
|
|
1525
|
-
|
|
1526
|
-
// Attempt to find an array property named "AllNodes" on the SCS
|
|
1527
|
-
if (UClass *SCSClass = SCS->GetClass()) {
|
|
1528
|
-
if (FArrayProperty *ArrayProp =
|
|
1529
|
-
FindFProperty<FArrayProperty>(SCSClass, TEXT("AllNodes"))) {
|
|
1530
|
-
// Helper to iterate elements
|
|
1531
|
-
FScriptArrayHelper Helper(ArrayProp,
|
|
1532
|
-
ArrayProp->ContainerPtrToValuePtr<void>(SCS));
|
|
1533
|
-
for (int32 Idx = 0; Idx < Helper.Num(); ++Idx) {
|
|
1534
|
-
void *ElemPtr = Helper.GetRawPtr(Idx);
|
|
1535
|
-
if (!ElemPtr)
|
|
1536
|
-
continue;
|
|
1537
|
-
if (FObjectProperty *ObjProp =
|
|
1538
|
-
CastField<FObjectProperty>(ArrayProp->Inner)) {
|
|
1539
|
-
UObject *ElemObj = ObjProp->GetObjectPropertyValue(ElemPtr);
|
|
1540
|
-
if (!ElemObj)
|
|
1541
|
-
continue;
|
|
1542
|
-
// Match by explicit VariableName property when present
|
|
1543
|
-
if (FProperty *VarProp = ElemObj->GetClass()->FindPropertyByName(
|
|
1544
|
-
TEXT("VariableName"))) {
|
|
1545
|
-
if (FNameProperty *NP = CastField<FNameProperty>(VarProp)) {
|
|
1546
|
-
const FName V = NP->GetPropertyValue_InContainer(ElemObj);
|
|
1547
|
-
if (!V.IsNone() &&
|
|
1548
|
-
V.ToString().Equals(Name, ESearchCase::IgnoreCase)) {
|
|
1549
|
-
return reinterpret_cast<USCS_Node *>(ElemObj);
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
// Fallback: match the object name
|
|
1554
|
-
if (ElemObj->GetName().Equals(Name, ESearchCase::IgnoreCase)) {
|
|
1555
|
-
return reinterpret_cast<USCS_Node *>(ElemObj);
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
return nullptr;
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
#if WITH_EDITOR
|
|
1565
|
-
// Attempt to locate and load a Blueprint by several heuristics. Returns nullptr
|
|
1566
|
-
/**
|
|
1567
|
-
* Locate and load a Blueprint asset from a variety of request formats and
|
|
1568
|
-
* return the loaded Blueprint.
|
|
1569
|
-
*
|
|
1570
|
-
* Attempts to resolve the input `Req` as an exact asset path (package.object),
|
|
1571
|
-
* a package path (with /Game/ prepended when missing), or by querying the Asset
|
|
1572
|
-
* Registry for a matching package name. On success `OutNormalized` is set to a
|
|
1573
|
-
* normalized package path (without the object suffix) and the loaded
|
|
1574
|
-
* `UBlueprint*` is returned; on failure `OutError` is set and nullptr is
|
|
1575
|
-
* returned.
|
|
1576
|
-
*
|
|
1577
|
-
* @param Req The requested asset identifier; may be an absolute package path,
|
|
1578
|
-
* an object-qualified path (Package.Asset), or a short path relative to /Game
|
|
1579
|
-
* (e.g., "Folder/Asset" or "/Game/Folder/Asset").
|
|
1580
|
-
* @param OutNormalized Out parameter that will receive the normalized package
|
|
1581
|
-
* path for the resolved asset (no object suffix) on success.
|
|
1582
|
-
* @param OutError Out parameter that will receive a descriptive error message
|
|
1583
|
-
* if resolution or loading fails.
|
|
1584
|
-
* @returns The loaded `UBlueprint*` when the asset is found and loaded, or
|
|
1585
|
-
* `nullptr` on failure.
|
|
1586
|
-
*/
|
|
1587
|
-
static inline UBlueprint *LoadBlueprintAsset(const FString &Req,
|
|
1588
|
-
FString &OutNormalized,
|
|
1589
|
-
FString &OutError) {
|
|
1590
|
-
OutNormalized.Empty();
|
|
1591
|
-
OutError.Empty();
|
|
1592
|
-
if (Req.IsEmpty()) {
|
|
1593
|
-
OutError = TEXT("Empty request");
|
|
1594
|
-
return nullptr;
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
UBlueprint *BP = nullptr;
|
|
1598
|
-
if (Req.Contains(TEXT("."))) {
|
|
1599
|
-
if (UEditorAssetLibrary::DoesAssetExist(Req)) {
|
|
1600
|
-
BP = LoadObject<UBlueprint>(nullptr, *Req);
|
|
1601
|
-
}
|
|
1602
|
-
if (BP) {
|
|
1603
|
-
OutNormalized = BP->GetPathName();
|
|
1604
|
-
if (OutNormalized.Contains(TEXT(".")))
|
|
1605
|
-
OutNormalized = OutNormalized.Left(OutNormalized.Find(TEXT(".")));
|
|
1606
|
-
return BP;
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
FString Candidate = Req;
|
|
1611
|
-
if (!Candidate.StartsWith(TEXT("/")))
|
|
1612
|
-
Candidate = FString::Printf(TEXT("/Game/%s"), *Req);
|
|
1613
|
-
|
|
1614
|
-
// Smart detection: duplicate the clean filename only if it's not already
|
|
1615
|
-
// there. This handles inputs like "/Game/Path/Asset.Asset" (idempotent) vs
|
|
1616
|
-
// "/Game/Path/Asset" (append).
|
|
1617
|
-
FString AssetRef = Candidate;
|
|
1618
|
-
const FString CleanName = FPaths::GetCleanFilename(Candidate);
|
|
1619
|
-
// If Candidate does not look like "Package.Asset", append ".Asset"
|
|
1620
|
-
// Note: This check is heuristic; if clean name contains dot, it might assume
|
|
1621
|
-
// it's already properly formatted.
|
|
1622
|
-
if (!Candidate.EndsWith(FString::Printf(TEXT(".%s"), *CleanName))) {
|
|
1623
|
-
AssetRef = FString::Printf(TEXT("%s.%s"), *Candidate, *CleanName);
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
if (UEditorAssetLibrary::DoesAssetExist(AssetRef)) {
|
|
1627
|
-
BP = LoadObject<UBlueprint>(nullptr, *AssetRef);
|
|
1628
|
-
}
|
|
1629
|
-
if (BP) {
|
|
1630
|
-
OutNormalized = Candidate;
|
|
1631
|
-
return BP;
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
FAssetRegistryModule &ARM =
|
|
1635
|
-
FModuleManager::LoadModuleChecked<FAssetRegistryModule>(
|
|
1636
|
-
TEXT("AssetRegistry"));
|
|
1637
|
-
FAssetData Found;
|
|
1638
|
-
TArray<FAssetData> Results;
|
|
1639
|
-
ARM.Get().GetAssetsByPackageName(FName(*Req), Results);
|
|
1640
|
-
if (Results.Num() > 0) {
|
|
1641
|
-
Found = Results[0];
|
|
1642
|
-
} else {
|
|
1643
|
-
FString Pkg = Req;
|
|
1644
|
-
if (!Pkg.StartsWith(TEXT("/")))
|
|
1645
|
-
Pkg = FString::Printf(TEXT("/Game/%s"), *Req);
|
|
1646
|
-
ARM.Get().GetAssetsByPackageName(FName(*Pkg), Results);
|
|
1647
|
-
if (Results.Num() > 0) {
|
|
1648
|
-
Found = Results[0];
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
if (Found.IsValid()) {
|
|
1653
|
-
BP = Cast<UBlueprint>(Found.GetAsset());
|
|
1654
|
-
if (!BP) {
|
|
1655
|
-
const FString PathStr = Found.ToSoftObjectPath().ToString();
|
|
1656
|
-
BP = LoadObject<UBlueprint>(nullptr, *PathStr);
|
|
1657
|
-
}
|
|
1658
|
-
if (BP) {
|
|
1659
|
-
OutNormalized = Found.ToSoftObjectPath().ToString();
|
|
1660
|
-
if (OutNormalized.Contains(TEXT(".")))
|
|
1661
|
-
OutNormalized = OutNormalized.Left(OutNormalized.Find(TEXT(".")));
|
|
1662
|
-
return BP;
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
OutError = FString::Printf(TEXT("Blueprint asset not found: %s"), *Req);
|
|
1667
|
-
return nullptr;
|
|
1668
|
-
}
|
|
1669
|
-
#endif
|
|
1670
|
-
|
|
1671
|
-
/**
|
|
1672
|
-
* Return the input FString unchanged.
|
|
1673
|
-
*
|
|
1674
|
-
* @param In The string to convert.
|
|
1675
|
-
* @returns The same FString provided as input.
|
|
1676
|
-
*/
|
|
1677
|
-
static inline FString ConvertToString(const FString &In) { return In; }
|
|
1678
|
-
/**
|
|
1679
|
-
* Convert a FName to its FString representation.
|
|
1680
|
-
* @param In The name to convert.
|
|
1681
|
-
* @returns The FString produced by calling ToString() on the input name.
|
|
1682
|
-
*/
|
|
1683
|
-
static inline FString ConvertToString(const FName &In) { return In.ToString(); }
|
|
1684
|
-
/**
|
|
1685
|
-
* Convert an FText to its string representation.
|
|
1686
|
-
* @param In Text to convert.
|
|
1687
|
-
* @returns FString containing the text's contents.
|
|
1688
|
-
*/
|
|
1689
|
-
static inline FString ConvertToString(const FText &In) { return In.ToString(); }
|
|
1690
|
-
|
|
1691
|
-
// Attempt to resolve a blueprint path to a normalized form without necessarily
|
|
1692
|
-
/**
|
|
1693
|
-
* Find a normalized Blueprint package path for the given request string without
|
|
1694
|
-
* loading the asset.
|
|
1695
|
-
*
|
|
1696
|
-
* Normalizes common forms (prepends /Game when missing a root, strips a
|
|
1697
|
-
* trailing `.uasset` extension, and removes object-path suffixes like
|
|
1698
|
-
* `/PackageName.ObjectName`) and checks for the asset's existence using a
|
|
1699
|
-
* lightweight existence test.
|
|
1700
|
-
*
|
|
1701
|
-
* @param Req Input path or identifier (may be relative, start with `/`, include
|
|
1702
|
-
* `.uasset`, or be an object path).
|
|
1703
|
-
* @param OutNormalized Output set to the normalized package path (e.g.,
|
|
1704
|
-
* `/Game/...`) when found.
|
|
1705
|
-
* @returns `true` if an existing normalized blueprint path was found and
|
|
1706
|
-
* written to OutNormalized, `false` otherwise.
|
|
1707
|
-
*/
|
|
1708
|
-
static inline bool FindBlueprintNormalizedPath(const FString &Req,
|
|
1709
|
-
FString &OutNormalized) {
|
|
1710
|
-
OutNormalized.Empty();
|
|
1711
|
-
if (Req.IsEmpty())
|
|
1712
|
-
return false;
|
|
1713
|
-
#if WITH_EDITOR
|
|
1714
|
-
// Use lightweight existence check - DO NOT use LoadBlueprintAsset here
|
|
1715
|
-
// as it causes Editor hangs when called repeatedly in polling loops
|
|
1716
|
-
FString CheckPath = Req;
|
|
1717
|
-
|
|
1718
|
-
// Ensure path starts with /Game if it doesn't have a valid root
|
|
1719
|
-
if (!CheckPath.StartsWith(TEXT("/Game")) &&
|
|
1720
|
-
!CheckPath.StartsWith(TEXT("/Engine")) &&
|
|
1721
|
-
!CheckPath.StartsWith(TEXT("/Script"))) {
|
|
1722
|
-
if (CheckPath.StartsWith(TEXT("/"))) {
|
|
1723
|
-
CheckPath = TEXT("/Game") + CheckPath;
|
|
1724
|
-
} else {
|
|
1725
|
-
CheckPath = TEXT("/Game/") + CheckPath;
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
// Remove .uasset extension if present
|
|
1730
|
-
if (CheckPath.EndsWith(TEXT(".uasset"))) {
|
|
1731
|
-
CheckPath = CheckPath.LeftChop(7);
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
// Remove object path suffix (e.g., /Game/BP.BP -> /Game/BP)
|
|
1735
|
-
int32 DotIdx;
|
|
1736
|
-
if (CheckPath.FindLastChar(TEXT('.'), DotIdx)) {
|
|
1737
|
-
// Check if this looks like an object path (PackagePath.ObjectName)
|
|
1738
|
-
FString AfterDot = CheckPath.Mid(DotIdx + 1);
|
|
1739
|
-
FString BeforeDot = CheckPath.Left(DotIdx);
|
|
1740
|
-
// If the part after the dot matches the asset name, strip it
|
|
1741
|
-
int32 LastSlashIdx;
|
|
1742
|
-
if (BeforeDot.FindLastChar(TEXT('/'), LastSlashIdx)) {
|
|
1743
|
-
FString AssetName = BeforeDot.Mid(LastSlashIdx + 1);
|
|
1744
|
-
if (AssetName.Equals(AfterDot, ESearchCase::IgnoreCase)) {
|
|
1745
|
-
CheckPath = BeforeDot;
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
if (UEditorAssetLibrary::DoesAssetExist(CheckPath)) {
|
|
1751
|
-
OutNormalized = CheckPath;
|
|
1752
|
-
return true;
|
|
1753
|
-
}
|
|
1754
|
-
return false;
|
|
1755
|
-
#else
|
|
1756
|
-
return false;
|
|
1757
|
-
#endif
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
/**
|
|
1761
|
-
* Resolve a UClass from a string that may be a full path, a blueprint class
|
|
1762
|
-
* path, or a short class name.
|
|
1763
|
-
*
|
|
1764
|
-
* @param Input The input string representing the class (examples:
|
|
1765
|
-
* "/Script/Engine.Actor", "/Game/MyBP.MyBP_C", or "Actor").
|
|
1766
|
-
* @returns A pointer to the resolved UClass if found, `nullptr` otherwise.
|
|
1767
|
-
*/
|
|
1768
|
-
static inline UClass *ResolveUClass(const FString &Input) {
|
|
1769
|
-
if (Input.IsEmpty())
|
|
1770
|
-
return nullptr;
|
|
1771
|
-
|
|
1772
|
-
// 1. Try finding it directly (full path or already loaded)
|
|
1773
|
-
UClass *Found = FindObject<UClass>(nullptr, *Input);
|
|
1774
|
-
if (Found)
|
|
1775
|
-
return Found;
|
|
1776
|
-
|
|
1777
|
-
// 2. Try loading it directly
|
|
1778
|
-
Found = LoadObject<UClass>(nullptr, *Input);
|
|
1779
|
-
if (Found)
|
|
1780
|
-
return Found;
|
|
1781
|
-
|
|
1782
|
-
// 3. Handle Blueprint Generated Classes explicitly
|
|
1783
|
-
// parsing "MyBP" -> "/Game/MyBP.MyBP_C" logic is hard without path,
|
|
1784
|
-
// but if input ends in _C, treat as class path.
|
|
1785
|
-
if (Input.EndsWith(TEXT("_C"))) {
|
|
1786
|
-
// Already tried loading, maybe it needs a package path fix?
|
|
1787
|
-
// Assuming the user provided a full path if they included _C.
|
|
1788
|
-
return nullptr;
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
// 4. Short name resolution
|
|
1792
|
-
// Check common script packages
|
|
1793
|
-
const TArray<FString> ScriptPackages = {TEXT("/Script/Engine"),
|
|
1794
|
-
TEXT("/Script/CoreUObject"),
|
|
1795
|
-
TEXT("/Script/UMG"),
|
|
1796
|
-
TEXT("/Script/AIModule"),
|
|
1797
|
-
TEXT("/Script/NavigationSystem"),
|
|
1798
|
-
TEXT("/Script/Niagara")};
|
|
1799
|
-
|
|
1800
|
-
for (const FString &Pkg : ScriptPackages) {
|
|
1801
|
-
FString TryPath = FString::Printf(TEXT("%s.%s"), *Pkg, *Input);
|
|
1802
|
-
Found = FindObject<UClass>(nullptr, *TryPath);
|
|
1803
|
-
if (Found)
|
|
1804
|
-
return Found;
|
|
1805
|
-
Found = LoadObject<UClass>(nullptr, *TryPath);
|
|
1806
|
-
if (Found)
|
|
1807
|
-
return Found;
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
// 5. Native class search by iteration (slow fallback, but useful for obscure
|
|
1811
|
-
// plugins)
|
|
1812
|
-
// Only doing this for exact short name matches to avoid false positives
|
|
1813
|
-
for (TObjectIterator<UClass> It; It; ++It) {
|
|
1814
|
-
if (It->GetName() == Input) {
|
|
1815
|
-
return *It;
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
return nullptr;
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
// Standardized Response Helpers
|
|
1823
|
-
// See: https://google.github.io/styleguide/jsoncstyleguide.xml
|
|
1824
|
-
|
|
1825
|
-
/**
|
|
1826
|
-
* Sends a standardized success response with a "data" envelope.
|
|
1827
|
-
*
|
|
1828
|
-
* Format:
|
|
1829
|
-
* {
|
|
1830
|
-
* "success": true,
|
|
1831
|
-
* "data": { ... },
|
|
1832
|
-
* "warnings": [],
|
|
1833
|
-
* "error": null
|
|
1834
|
-
* }
|
|
1835
|
-
*/
|
|
1836
|
-
static inline void SendStandardSuccessResponse(
|
|
1837
|
-
UMcpAutomationBridgeSubsystem *Subsystem,
|
|
1838
|
-
TSharedPtr<FMcpBridgeWebSocket> Socket, const FString &RequestId,
|
|
1839
|
-
const FString &Message, const TSharedPtr<FJsonObject> &Data,
|
|
1840
|
-
const TArray<FString> &Warnings = TArray<FString>()) {
|
|
1841
|
-
if (!Subsystem)
|
|
1842
|
-
return;
|
|
1843
|
-
|
|
1844
|
-
TSharedPtr<FJsonObject> Envelope = MakeShared<FJsonObject>();
|
|
1845
|
-
Envelope->SetBoolField(TEXT("success"), true);
|
|
1846
|
-
Envelope->SetObjectField(TEXT("data"),
|
|
1847
|
-
Data.IsValid() ? Data : MakeShared<FJsonObject>());
|
|
1848
|
-
|
|
1849
|
-
TArray<TSharedPtr<FJsonValue>> WarningVals;
|
|
1850
|
-
for (const FString &W : Warnings) {
|
|
1851
|
-
WarningVals.Add(MakeShared<FJsonValueString>(W));
|
|
1852
|
-
}
|
|
1853
|
-
Envelope->SetArrayField(TEXT("warnings"), WarningVals);
|
|
1854
|
-
|
|
1855
|
-
Envelope->SetField(TEXT("error"), MakeShared<FJsonValueNull>());
|
|
1856
|
-
|
|
1857
|
-
Subsystem->SendAutomationResponse(Socket, RequestId, true, Message, Envelope,
|
|
1858
|
-
FString());
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
/**
|
|
1862
|
-
* Sends a standardized error response with structured error details.
|
|
1863
|
-
*
|
|
1864
|
-
* Format:
|
|
1865
|
-
* {
|
|
1866
|
-
* "success": false,
|
|
1867
|
-
* "error": {
|
|
1868
|
-
* "code": "ERROR_CODE",
|
|
1869
|
-
* "message": "Human readable message",
|
|
1870
|
-
* "parameter": "optional_param_name",
|
|
1871
|
-
* ...
|
|
1872
|
-
* }
|
|
1873
|
-
* }
|
|
1874
|
-
*/
|
|
1875
|
-
static inline void SendStandardErrorResponse(
|
|
1876
|
-
UMcpAutomationBridgeSubsystem *Subsystem,
|
|
1877
|
-
TSharedPtr<FMcpBridgeWebSocket> Socket, const FString &RequestId,
|
|
1878
|
-
const FString &ErrorCode, const FString &ErrorMessage,
|
|
1879
|
-
const TSharedPtr<FJsonObject> &ErrorDetails = nullptr) {
|
|
1880
|
-
if (!Subsystem)
|
|
1881
|
-
return;
|
|
1882
|
-
|
|
1883
|
-
TSharedPtr<FJsonObject> Envelope = MakeShared<FJsonObject>();
|
|
1884
|
-
Envelope->SetBoolField(TEXT("success"), false);
|
|
1885
|
-
|
|
1886
|
-
TSharedPtr<FJsonObject> ErrorObj = MakeShared<FJsonObject>();
|
|
1887
|
-
ErrorObj->SetStringField(TEXT("code"), ErrorCode);
|
|
1888
|
-
ErrorObj->SetStringField(TEXT("message"), ErrorMessage);
|
|
1889
|
-
|
|
1890
|
-
if (ErrorDetails.IsValid()) {
|
|
1891
|
-
// Merge details into error object
|
|
1892
|
-
for (const auto &Pair : ErrorDetails->Values) {
|
|
1893
|
-
ErrorObj->SetField(Pair.Key, Pair.Value);
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
Envelope->SetObjectField(TEXT("error"), ErrorObj);
|
|
1898
|
-
|
|
1899
|
-
Subsystem->SendAutomationResponse(Socket, RequestId, false, ErrorMessage,
|
|
1900
|
-
Envelope, ErrorCode);
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
// ============================================================================
|
|
1904
|
-
// ROBUST ACTOR SPAWNING HELPER
|
|
1905
|
-
// ============================================================================
|
|
1906
|
-
//
|
|
1907
|
-
// SpawnActorInActiveWorld solves the "transient actor" issue where actors
|
|
1908
|
-
// spawned via EditorActorSubsystem->SpawnActorFromClass may end up in the
|
|
1909
|
-
// /Engine/Transient package, making them invisible in the World Outliner.
|
|
1910
|
-
//
|
|
1911
|
-
// This helper properly handles both PIE (Play-In-Editor) and regular Editor
|
|
1912
|
-
// modes by:
|
|
1913
|
-
// 1. Checking if GEditor->PlayWorld is active (PIE mode)
|
|
1914
|
-
// 2. Using TargetWorld->SpawnActor for PIE (proper world context)
|
|
1915
|
-
// 3. Using EditorActorSubsystem for Editor mode with explicit transform
|
|
1916
|
-
// 4. Optionally setting an actor label for easy identification
|
|
1917
|
-
//
|
|
1918
|
-
// Usage:
|
|
1919
|
-
// AActor* MyActor = SpawnActorInActiveWorld<AActor>(
|
|
1920
|
-
// ADirectionalLight::StaticClass(),
|
|
1921
|
-
// FVector(0, 0, 100),
|
|
1922
|
-
// FRotator(-45, 0, 0),
|
|
1923
|
-
// TEXT("MySunLight")
|
|
1924
|
-
// );
|
|
1925
|
-
//
|
|
1926
|
-
// See: ControlHandlers.cpp HandleControlActorSpawn for the original pattern.
|
|
1927
|
-
// ============================================================================
|
|
1928
|
-
|
|
1929
|
-
#if WITH_EDITOR
|
|
1930
|
-
#include "Editor.h"
|
|
1931
|
-
#include "GameFramework/Actor.h"
|
|
1932
|
-
#if __has_include("Subsystems/EditorActorSubsystem.h")
|
|
1933
|
-
#include "Subsystems/EditorActorSubsystem.h"
|
|
1934
|
-
#elif __has_include("EditorActorSubsystem.h")
|
|
1935
|
-
#include "EditorActorSubsystem.h"
|
|
1936
|
-
#endif
|
|
1937
|
-
|
|
1938
|
-
template <typename T = AActor>
|
|
1939
|
-
static inline T *
|
|
1940
|
-
SpawnActorInActiveWorld(UClass *ActorClass, const FVector &Location,
|
|
1941
|
-
const FRotator &Rotation,
|
|
1942
|
-
const FString &OptionalLabel = FString()) {
|
|
1943
|
-
static_assert(std::is_base_of<AActor, T>::value,
|
|
1944
|
-
"T must be derived from AActor");
|
|
1945
|
-
|
|
1946
|
-
if (!GEditor || !ActorClass)
|
|
1947
|
-
return nullptr;
|
|
1948
|
-
|
|
1949
|
-
AActor *Spawned = nullptr;
|
|
1950
|
-
|
|
1951
|
-
// Check if PIE is active
|
|
1952
|
-
UWorld *TargetWorld = GEditor->PlayWorld;
|
|
1953
|
-
|
|
1954
|
-
if (TargetWorld) {
|
|
1955
|
-
// PIE Path: Use World->SpawnActor for proper world context
|
|
1956
|
-
FActorSpawnParameters SpawnParams;
|
|
1957
|
-
SpawnParams.SpawnCollisionHandlingOverride =
|
|
1958
|
-
ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
|
|
1959
|
-
Spawned =
|
|
1960
|
-
TargetWorld->SpawnActor(ActorClass, &Location, &Rotation, SpawnParams);
|
|
1961
|
-
} else {
|
|
1962
|
-
// Editor Path: Use EditorActorSubsystem with explicit transform
|
|
1963
|
-
UEditorActorSubsystem *ActorSS =
|
|
1964
|
-
GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
|
|
1965
|
-
if (ActorSS) {
|
|
1966
|
-
Spawned = ActorSS->SpawnActorFromClass(ActorClass, Location, Rotation);
|
|
1967
|
-
if (Spawned) {
|
|
1968
|
-
// Explicit transform to ensure proper placement and registration
|
|
1969
|
-
Spawned->SetActorLocationAndRotation(Location, Rotation, false, nullptr,
|
|
1970
|
-
ETeleportType::TeleportPhysics);
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
// Set optional label for easy identification in World Outliner
|
|
1976
|
-
if (Spawned && !OptionalLabel.IsEmpty()) {
|
|
1977
|
-
Spawned->SetActorLabel(OptionalLabel);
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
|
-
return Cast<T>(Spawned);
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
#endif
|