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.
Files changed (561) hide show
  1. package/CHANGELOG.md +350 -0
  2. package/dist/automation/bridge.d.ts.map +1 -0
  3. package/dist/automation/bridge.js +5 -4
  4. package/dist/automation/bridge.js.map +1 -0
  5. package/dist/automation/connection-manager.d.ts.map +1 -0
  6. package/dist/automation/connection-manager.js.map +1 -0
  7. package/dist/automation/handshake.d.ts.map +1 -0
  8. package/dist/automation/handshake.js.map +1 -0
  9. package/dist/automation/index.d.ts.map +1 -0
  10. package/dist/automation/index.js.map +1 -0
  11. package/dist/automation/message-handler.d.ts.map +1 -0
  12. package/dist/automation/message-handler.js.map +1 -0
  13. package/dist/automation/request-tracker.d.ts.map +1 -0
  14. package/dist/automation/request-tracker.js.map +1 -0
  15. package/dist/automation/types.d.ts +7 -0
  16. package/dist/automation/types.d.ts.map +1 -0
  17. package/dist/automation/types.js.map +1 -0
  18. package/dist/cli.d.ts.map +1 -0
  19. package/dist/cli.js +6 -4
  20. package/dist/cli.js.map +1 -0
  21. package/dist/config/class-aliases.d.ts.map +1 -0
  22. package/dist/config/class-aliases.js.map +1 -0
  23. package/dist/config.d.ts.map +1 -0
  24. package/dist/config.js.map +1 -0
  25. package/dist/constants.d.ts.map +1 -0
  26. package/dist/constants.js.map +1 -0
  27. package/dist/graphql/loaders.d.ts.map +1 -0
  28. package/dist/graphql/loaders.js.map +1 -0
  29. package/dist/graphql/resolvers.d.ts +174 -69
  30. package/dist/graphql/resolvers.d.ts.map +1 -0
  31. package/dist/graphql/resolvers.js +82 -67
  32. package/dist/graphql/resolvers.js.map +1 -0
  33. package/dist/graphql/schema.d.ts.map +1 -0
  34. package/dist/graphql/schema.js.map +1 -0
  35. package/dist/graphql/server.d.ts.map +1 -0
  36. package/dist/graphql/server.js.map +1 -0
  37. package/dist/graphql/types.d.ts.map +1 -0
  38. package/dist/graphql/types.js.map +1 -0
  39. package/dist/handlers/resource-handlers.d.ts.map +1 -0
  40. package/dist/handlers/resource-handlers.js.map +1 -0
  41. package/dist/index.d.ts +2 -1
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +70 -9
  44. package/dist/index.js.map +1 -0
  45. package/dist/resources/actors.d.ts +7 -4
  46. package/dist/resources/actors.d.ts.map +1 -0
  47. package/dist/resources/actors.js +15 -12
  48. package/dist/resources/actors.js.map +1 -0
  49. package/dist/resources/assets.d.ts +43 -2
  50. package/dist/resources/assets.d.ts.map +1 -0
  51. package/dist/resources/assets.js +21 -12
  52. package/dist/resources/assets.js.map +1 -0
  53. package/dist/resources/levels.d.ts.map +1 -0
  54. package/dist/resources/levels.js +7 -5
  55. package/dist/resources/levels.js.map +1 -0
  56. package/dist/schemas/index.d.ts +4 -0
  57. package/dist/schemas/index.d.ts.map +1 -0
  58. package/dist/schemas/index.js +4 -0
  59. package/dist/schemas/index.js.map +1 -0
  60. package/dist/schemas/parser.d.ts +20 -0
  61. package/dist/schemas/parser.d.ts.map +1 -0
  62. package/dist/schemas/parser.js +61 -0
  63. package/dist/schemas/parser.js.map +1 -0
  64. package/dist/schemas/primitives.d.ts +221 -0
  65. package/dist/schemas/primitives.d.ts.map +1 -0
  66. package/dist/schemas/primitives.js +115 -0
  67. package/dist/schemas/primitives.js.map +1 -0
  68. package/dist/schemas/responses.d.ts +362 -0
  69. package/dist/schemas/responses.d.ts.map +1 -0
  70. package/dist/schemas/responses.js +252 -0
  71. package/dist/schemas/responses.js.map +1 -0
  72. package/dist/server/resource-registry.d.ts.map +1 -0
  73. package/dist/server/resource-registry.js.map +1 -0
  74. package/dist/server/tool-registry.d.ts.map +1 -0
  75. package/dist/server/tool-registry.js +22 -17
  76. package/dist/server/tool-registry.js.map +1 -0
  77. package/dist/server-setup.d.ts.map +1 -0
  78. package/dist/server-setup.js.map +1 -0
  79. package/dist/services/health-monitor.d.ts +1 -1
  80. package/dist/services/health-monitor.d.ts.map +1 -0
  81. package/dist/services/health-monitor.js +4 -3
  82. package/dist/services/health-monitor.js.map +1 -0
  83. package/dist/services/metrics-server.d.ts.map +1 -0
  84. package/dist/services/metrics-server.js.map +1 -0
  85. package/dist/tools/actors.d.ts +27 -27
  86. package/dist/tools/actors.d.ts.map +1 -0
  87. package/dist/tools/actors.js +14 -10
  88. package/dist/tools/actors.js.map +1 -0
  89. package/dist/tools/animation.d.ts +15 -23
  90. package/dist/tools/animation.d.ts.map +1 -0
  91. package/dist/tools/animation.js +17 -13
  92. package/dist/tools/animation.js.map +1 -0
  93. package/dist/tools/assets.d.ts.map +1 -0
  94. package/dist/tools/assets.js +18 -12
  95. package/dist/tools/assets.js.map +1 -0
  96. package/dist/tools/audio.d.ts +10 -10
  97. package/dist/tools/audio.d.ts.map +1 -0
  98. package/dist/tools/audio.js.map +1 -0
  99. package/dist/tools/base-tool.d.ts.map +1 -0
  100. package/dist/tools/base-tool.js.map +1 -0
  101. package/dist/tools/behavior-tree.d.ts +24 -24
  102. package/dist/tools/behavior-tree.d.ts.map +1 -0
  103. package/dist/tools/behavior-tree.js.map +1 -0
  104. package/dist/tools/blueprint.d.ts +14 -3
  105. package/dist/tools/blueprint.d.ts.map +1 -0
  106. package/dist/tools/blueprint.js +5 -3
  107. package/dist/tools/blueprint.js.map +1 -0
  108. package/dist/tools/consolidated-tool-definitions.d.ts +32 -32
  109. package/dist/tools/consolidated-tool-definitions.d.ts.map +1 -0
  110. package/dist/tools/consolidated-tool-definitions.js.map +1 -0
  111. package/dist/tools/consolidated-tool-handlers.d.ts +1 -1
  112. package/dist/tools/consolidated-tool-handlers.d.ts.map +1 -0
  113. package/dist/tools/consolidated-tool-handlers.js +26 -21
  114. package/dist/tools/consolidated-tool-handlers.js.map +1 -0
  115. package/dist/tools/debug.d.ts +25 -7
  116. package/dist/tools/debug.d.ts.map +1 -0
  117. package/dist/tools/debug.js +3 -1
  118. package/dist/tools/debug.js.map +1 -0
  119. package/dist/tools/dynamic-handler-registry.d.ts +1 -1
  120. package/dist/tools/dynamic-handler-registry.d.ts.map +1 -0
  121. package/dist/tools/dynamic-handler-registry.js +3 -1
  122. package/dist/tools/dynamic-handler-registry.js.map +1 -0
  123. package/dist/tools/editor.d.ts.map +1 -0
  124. package/dist/tools/editor.js +8 -6
  125. package/dist/tools/editor.js.map +1 -0
  126. package/dist/tools/engine.d.ts +1 -1
  127. package/dist/tools/engine.d.ts.map +1 -0
  128. package/dist/tools/engine.js +4 -2
  129. package/dist/tools/engine.js.map +1 -0
  130. package/dist/tools/environment.d.ts.map +1 -0
  131. package/dist/tools/environment.js +4 -3
  132. package/dist/tools/environment.js.map +1 -0
  133. package/dist/tools/foliage.d.ts.map +1 -0
  134. package/dist/tools/foliage.js +8 -8
  135. package/dist/tools/foliage.js.map +1 -0
  136. package/dist/tools/handlers/actor-handlers.d.ts +2 -1
  137. package/dist/tools/handlers/actor-handlers.d.ts.map +1 -0
  138. package/dist/tools/handlers/actor-handlers.js +56 -33
  139. package/dist/tools/handlers/actor-handlers.js.map +1 -0
  140. package/dist/tools/handlers/animation-handlers.d.ts +2 -1
  141. package/dist/tools/handlers/animation-handlers.d.ts.map +1 -0
  142. package/dist/tools/handlers/animation-handlers.js +74 -67
  143. package/dist/tools/handlers/animation-handlers.js.map +1 -0
  144. package/dist/tools/handlers/argument-helper.d.ts +24 -4
  145. package/dist/tools/handlers/argument-helper.d.ts.map +1 -0
  146. package/dist/tools/handlers/argument-helper.js +139 -4
  147. package/dist/tools/handlers/argument-helper.js.map +1 -0
  148. package/dist/tools/handlers/asset-handlers.d.ts +2 -1
  149. package/dist/tools/handlers/asset-handlers.d.ts.map +1 -0
  150. package/dist/tools/handlers/asset-handlers.js +155 -94
  151. package/dist/tools/handlers/asset-handlers.js.map +1 -0
  152. package/dist/tools/handlers/audio-handlers.d.ts +2 -1
  153. package/dist/tools/handlers/audio-handlers.d.ts.map +1 -0
  154. package/dist/tools/handlers/audio-handlers.js +82 -80
  155. package/dist/tools/handlers/audio-handlers.js.map +1 -0
  156. package/dist/tools/handlers/blueprint-handlers.d.ts +3 -5
  157. package/dist/tools/handlers/blueprint-handlers.d.ts.map +1 -0
  158. package/dist/tools/handlers/blueprint-handlers.js +150 -142
  159. package/dist/tools/handlers/blueprint-handlers.js.map +1 -0
  160. package/dist/tools/handlers/common-handlers.d.ts +2 -3
  161. package/dist/tools/handlers/common-handlers.d.ts.map +1 -0
  162. package/dist/tools/handlers/common-handlers.js.map +1 -0
  163. package/dist/tools/handlers/editor-handlers.d.ts.map +1 -0
  164. package/dist/tools/handlers/editor-handlers.js +12 -2
  165. package/dist/tools/handlers/editor-handlers.js.map +1 -0
  166. package/dist/tools/handlers/effect-handlers.d.ts +2 -1
  167. package/dist/tools/handlers/effect-handlers.d.ts.map +1 -0
  168. package/dist/tools/handlers/effect-handlers.js +70 -68
  169. package/dist/tools/handlers/effect-handlers.js.map +1 -0
  170. package/dist/tools/handlers/environment-handlers.d.ts +2 -1
  171. package/dist/tools/handlers/environment-handlers.d.ts.map +1 -0
  172. package/dist/tools/handlers/environment-handlers.js +86 -74
  173. package/dist/tools/handlers/environment-handlers.js.map +1 -0
  174. package/dist/tools/handlers/graph-handlers.d.ts +1 -1
  175. package/dist/tools/handlers/graph-handlers.d.ts.map +1 -0
  176. package/dist/tools/handlers/graph-handlers.js +63 -2
  177. package/dist/tools/handlers/graph-handlers.js.map +1 -0
  178. package/dist/tools/handlers/input-handlers.d.ts +2 -5
  179. package/dist/tools/handlers/input-handlers.d.ts.map +1 -0
  180. package/dist/tools/handlers/input-handlers.js +5 -4
  181. package/dist/tools/handlers/input-handlers.js.map +1 -0
  182. package/dist/tools/handlers/inspect-handlers.d.ts +2 -1
  183. package/dist/tools/handlers/inspect-handlers.d.ts.map +1 -0
  184. package/dist/tools/handlers/inspect-handlers.js +61 -37
  185. package/dist/tools/handlers/inspect-handlers.js.map +1 -0
  186. package/dist/tools/handlers/level-handlers.d.ts +2 -2
  187. package/dist/tools/handlers/level-handlers.d.ts.map +1 -0
  188. package/dist/tools/handlers/level-handlers.js +43 -39
  189. package/dist/tools/handlers/level-handlers.js.map +1 -0
  190. package/dist/tools/handlers/lighting-handlers.d.ts +12 -1
  191. package/dist/tools/handlers/lighting-handlers.d.ts.map +1 -0
  192. package/dist/tools/handlers/lighting-handlers.js +90 -47
  193. package/dist/tools/handlers/lighting-handlers.js.map +1 -0
  194. package/dist/tools/handlers/performance-handlers.d.ts +2 -1
  195. package/dist/tools/handlers/performance-handlers.d.ts.map +1 -0
  196. package/dist/tools/handlers/performance-handlers.js +55 -40
  197. package/dist/tools/handlers/performance-handlers.js.map +1 -0
  198. package/dist/tools/handlers/pipeline-handlers.d.ts.map +1 -0
  199. package/dist/tools/handlers/pipeline-handlers.js.map +1 -0
  200. package/dist/tools/handlers/sequence-handlers.d.ts.map +1 -0
  201. package/dist/tools/handlers/sequence-handlers.js.map +1 -0
  202. package/dist/tools/handlers/system-handlers.d.ts +3 -2
  203. package/dist/tools/handlers/system-handlers.d.ts.map +1 -0
  204. package/dist/tools/handlers/system-handlers.js +105 -52
  205. package/dist/tools/handlers/system-handlers.js.map +1 -0
  206. package/dist/tools/input.d.ts.map +1 -0
  207. package/dist/tools/input.js +3 -1
  208. package/dist/tools/input.js.map +1 -0
  209. package/dist/tools/introspection.d.ts +14 -14
  210. package/dist/tools/introspection.d.ts.map +1 -0
  211. package/dist/tools/introspection.js +54 -45
  212. package/dist/tools/introspection.js.map +1 -0
  213. package/dist/tools/landscape.d.ts.map +1 -0
  214. package/dist/tools/landscape.js +15 -13
  215. package/dist/tools/landscape.js.map +1 -0
  216. package/dist/tools/level.d.ts.map +1 -0
  217. package/dist/tools/level.js +3 -2
  218. package/dist/tools/level.js.map +1 -0
  219. package/dist/tools/lighting.d.ts +32 -59
  220. package/dist/tools/lighting.d.ts.map +1 -0
  221. package/dist/tools/lighting.js +56 -19
  222. package/dist/tools/lighting.js.map +1 -0
  223. package/dist/tools/logs.d.ts.map +1 -0
  224. package/dist/tools/logs.js +2 -1
  225. package/dist/tools/logs.js.map +1 -0
  226. package/dist/tools/materials.d.ts +42 -14
  227. package/dist/tools/materials.d.ts.map +1 -0
  228. package/dist/tools/materials.js +15 -9
  229. package/dist/tools/materials.js.map +1 -0
  230. package/dist/tools/niagara.d.ts +63 -39
  231. package/dist/tools/niagara.d.ts.map +1 -0
  232. package/dist/tools/niagara.js +43 -33
  233. package/dist/tools/niagara.js.map +1 -0
  234. package/dist/tools/performance.d.ts +12 -11
  235. package/dist/tools/performance.d.ts.map +1 -0
  236. package/dist/tools/performance.js +3 -2
  237. package/dist/tools/performance.js.map +1 -0
  238. package/dist/tools/physics.d.ts +37 -20
  239. package/dist/tools/physics.d.ts.map +1 -0
  240. package/dist/tools/physics.js +37 -30
  241. package/dist/tools/physics.js.map +1 -0
  242. package/dist/tools/property-dictionary.d.ts.map +1 -0
  243. package/dist/tools/property-dictionary.js.map +1 -0
  244. package/dist/tools/sequence.d.ts +1 -1
  245. package/dist/tools/sequence.d.ts.map +1 -0
  246. package/dist/tools/sequence.js +8 -4
  247. package/dist/tools/sequence.js.map +1 -0
  248. package/dist/tools/tool-definition-utils.d.ts.map +1 -0
  249. package/dist/tools/tool-definition-utils.js.map +1 -0
  250. package/dist/tools/ui.d.ts +11 -11
  251. package/dist/tools/ui.d.ts.map +1 -0
  252. package/dist/tools/ui.js +7 -3
  253. package/dist/tools/ui.js.map +1 -0
  254. package/dist/types/automation-responses.d.ts.map +1 -0
  255. package/dist/types/automation-responses.js.map +1 -0
  256. package/dist/types/env.d.ts.map +1 -0
  257. package/dist/types/env.js.map +1 -0
  258. package/dist/types/handler-types.d.ts +112 -3
  259. package/dist/types/handler-types.d.ts.map +1 -0
  260. package/dist/types/handler-types.js.map +1 -0
  261. package/dist/types/tool-interfaces.d.ts +39 -21
  262. package/dist/types/tool-interfaces.d.ts.map +1 -0
  263. package/dist/types/tool-interfaces.js.map +1 -0
  264. package/dist/types/tool-types.d.ts +8 -8
  265. package/dist/types/tool-types.d.ts.map +1 -0
  266. package/dist/types/tool-types.js.map +1 -0
  267. package/dist/unreal-bridge.d.ts +8 -6
  268. package/dist/unreal-bridge.d.ts.map +1 -0
  269. package/dist/unreal-bridge.js +16 -3
  270. package/dist/unreal-bridge.js.map +1 -0
  271. package/dist/utils/command-validator.d.ts.map +1 -0
  272. package/dist/utils/command-validator.js.map +1 -0
  273. package/dist/utils/elicitation.d.ts +2 -5
  274. package/dist/utils/elicitation.d.ts.map +1 -0
  275. package/dist/utils/elicitation.js +3 -2
  276. package/dist/utils/elicitation.js.map +1 -0
  277. package/dist/utils/error-handler.d.ts.map +1 -0
  278. package/dist/utils/error-handler.js.map +1 -0
  279. package/dist/utils/ini-reader.d.ts +1 -1
  280. package/dist/utils/ini-reader.d.ts.map +1 -0
  281. package/dist/utils/ini-reader.js.map +1 -0
  282. package/dist/utils/logger.d.ts +4 -4
  283. package/dist/utils/logger.d.ts.map +1 -0
  284. package/dist/utils/logger.js.map +1 -0
  285. package/dist/utils/normalize.d.ts +2 -2
  286. package/dist/utils/normalize.d.ts.map +1 -0
  287. package/dist/utils/normalize.js +4 -3
  288. package/dist/utils/normalize.js.map +1 -0
  289. package/dist/utils/path-security.d.ts.map +1 -0
  290. package/dist/utils/path-security.js.map +1 -0
  291. package/dist/utils/response-factory.d.ts +2 -2
  292. package/dist/utils/response-factory.d.ts.map +1 -0
  293. package/dist/utils/response-factory.js +3 -1
  294. package/dist/utils/response-factory.js.map +1 -0
  295. package/dist/utils/response-validator.d.ts +4 -4
  296. package/dist/utils/response-validator.d.ts.map +1 -0
  297. package/dist/utils/response-validator.js +31 -23
  298. package/dist/utils/response-validator.js.map +1 -0
  299. package/dist/utils/result-helpers.d.ts.map +1 -0
  300. package/dist/utils/result-helpers.js.map +1 -0
  301. package/dist/utils/safe-json.d.ts.map +1 -0
  302. package/dist/utils/safe-json.js.map +1 -0
  303. package/dist/utils/unreal-command-queue.d.ts +2 -2
  304. package/dist/utils/unreal-command-queue.d.ts.map +1 -0
  305. package/dist/utils/unreal-command-queue.js +4 -3
  306. package/dist/utils/unreal-command-queue.js.map +1 -0
  307. package/dist/utils/validation.d.ts +1 -1
  308. package/dist/utils/validation.d.ts.map +1 -0
  309. package/dist/utils/validation.js.map +1 -0
  310. package/dist/wasm/index.d.ts +2 -2
  311. package/dist/wasm/index.d.ts.map +1 -0
  312. package/dist/wasm/index.js +11 -7
  313. package/dist/wasm/index.js.map +1 -0
  314. package/package.json +12 -34
  315. package/server.json +2 -2
  316. package/.dockerignore +0 -57
  317. package/.env.example +0 -26
  318. package/.env.production +0 -61
  319. package/.eslintrc.json +0 -0
  320. package/.eslintrc.override.json +0 -8
  321. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -94
  322. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  323. package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -56
  324. package/.github/copilot-instructions.md +0 -478
  325. package/.github/dependabot.yml +0 -19
  326. package/.github/labeler.yml +0 -24
  327. package/.github/labels.yml +0 -70
  328. package/.github/pull_request_template.md +0 -42
  329. package/.github/release-drafter-config.yml +0 -51
  330. package/.github/workflows/auto-merge.yml +0 -38
  331. package/.github/workflows/ci.yml +0 -38
  332. package/.github/workflows/dependency-review.yml +0 -17
  333. package/.github/workflows/gemini-issue-triage.yml +0 -172
  334. package/.github/workflows/greetings.yml +0 -27
  335. package/.github/workflows/labeler.yml +0 -17
  336. package/.github/workflows/links.yml +0 -80
  337. package/.github/workflows/pr-size-labeler.yml +0 -137
  338. package/.github/workflows/publish-mcp.yml +0 -79
  339. package/.github/workflows/release-drafter.yml +0 -24
  340. package/.github/workflows/release.yml +0 -112
  341. package/.github/workflows/semantic-pull-request.yml +0 -35
  342. package/.github/workflows/smoke-test.yml +0 -36
  343. package/.github/workflows/stale.yml +0 -28
  344. package/CONTRIBUTING.md +0 -140
  345. package/Dockerfile +0 -37
  346. package/GEMINI.md +0 -115
  347. package/Public/Plugin_setup_guide.mp4 +0 -0
  348. package/Public/icon.png +0 -0
  349. package/claude_desktop_config_example.json +0 -15
  350. package/dist/types/responses.d.ts +0 -249
  351. package/dist/types/responses.js +0 -2
  352. package/docs/GraphQL-API.md +0 -888
  353. package/docs/Migration-Guide-v0.5.0.md +0 -684
  354. package/docs/Roadmap.md +0 -53
  355. package/docs/WebAssembly-Integration.md +0 -628
  356. package/docs/editor-plugin-extension.md +0 -370
  357. package/docs/handler-mapping.md +0 -249
  358. package/docs/native-automation-progress.md +0 -128
  359. package/docs/testing-guide.md +0 -423
  360. package/eslint.config.mjs +0 -68
  361. package/mcp-config-example.json +0 -14
  362. package/plugins/McpAutomationBridge/Config/FilterPlugin.ini +0 -8
  363. package/plugins/McpAutomationBridge/McpAutomationBridge.uplugin +0 -64
  364. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/McpAutomationBridge.Build.cs +0 -189
  365. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.cpp +0 -22
  366. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.h +0 -30
  367. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h +0 -1983
  368. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeModule.cpp +0 -72
  369. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSettings.cpp +0 -46
  370. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +0 -846
  371. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +0 -2393
  372. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetQueryHandlers.cpp +0 -300
  373. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetWorkflowHandlers.cpp +0 -2807
  374. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AudioHandlers.cpp +0 -1087
  375. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BehaviorTreeHandlers.cpp +0 -488
  376. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.cpp +0 -643
  377. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.h +0 -31
  378. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +0 -1094
  379. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +0 -5750
  380. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers_List.cpp +0 -152
  381. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ControlHandlers.cpp +0 -2614
  382. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_DebugHandlers.cpp +0 -42
  383. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EditorFunctionHandlers.cpp +0 -1237
  384. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +0 -1725
  385. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +0 -2265
  386. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_FoliageHandlers.cpp +0 -954
  387. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InputHandlers.cpp +0 -209
  388. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InsightsHandlers.cpp +0 -41
  389. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LandscapeHandlers.cpp +0 -1164
  390. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelHandlers.cpp +0 -762
  391. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +0 -663
  392. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LogHandlers.cpp +0 -136
  393. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_MaterialGraphHandlers.cpp +0 -494
  394. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraGraphHandlers.cpp +0 -278
  395. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraHandlers.cpp +0 -625
  396. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PerformanceHandlers.cpp +0 -401
  397. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PipelineHandlers.cpp +0 -67
  398. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +0 -472
  399. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PropertyHandlers.cpp +0 -2634
  400. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_RenderHandlers.cpp +0 -189
  401. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.cpp +0 -917
  402. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.h +0 -39
  403. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +0 -2706
  404. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequencerHandlers.cpp +0 -519
  405. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_TestHandlers.cpp +0 -38
  406. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_UiHandlers.cpp +0 -668
  407. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WorldPartitionHandlers.cpp +0 -346
  408. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +0 -1345
  409. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.h +0 -149
  410. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +0 -782
  411. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSettings.h +0 -115
  412. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSubsystem.h +0 -796
  413. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpConnectionManager.h +0 -117
  414. package/scripts/check-unreal-connection.mjs +0 -19
  415. package/scripts/clean-tmp.js +0 -23
  416. package/scripts/patch-wasm.js +0 -26
  417. package/scripts/run-all-tests.mjs +0 -136
  418. package/scripts/smoke-test.ts +0 -94
  419. package/scripts/sync-mcp-plugin.js +0 -143
  420. package/scripts/test-no-plugin-alternates.mjs +0 -113
  421. package/scripts/validate-server.js +0 -46
  422. package/scripts/verify-automation-bridge.js +0 -200
  423. package/src/automation/bridge.ts +0 -630
  424. package/src/automation/connection-manager.ts +0 -148
  425. package/src/automation/handshake.ts +0 -99
  426. package/src/automation/index.ts +0 -2
  427. package/src/automation/message-handler.ts +0 -192
  428. package/src/automation/request-tracker.ts +0 -155
  429. package/src/automation/types.ts +0 -108
  430. package/src/cli.ts +0 -34
  431. package/src/config/class-aliases.ts +0 -65
  432. package/src/config.ts +0 -73
  433. package/src/constants.ts +0 -29
  434. package/src/graphql/loaders.ts +0 -244
  435. package/src/graphql/resolvers.ts +0 -1008
  436. package/src/graphql/schema.ts +0 -452
  437. package/src/graphql/server.ts +0 -156
  438. package/src/graphql/types.ts +0 -10
  439. package/src/handlers/resource-handlers.ts +0 -186
  440. package/src/index.ts +0 -243
  441. package/src/resources/actors.ts +0 -127
  442. package/src/resources/assets.ts +0 -286
  443. package/src/resources/levels.ts +0 -68
  444. package/src/server/resource-registry.ts +0 -47
  445. package/src/server/tool-registry.ts +0 -354
  446. package/src/server-setup.ts +0 -114
  447. package/src/services/health-monitor.ts +0 -132
  448. package/src/services/metrics-server.ts +0 -176
  449. package/src/tools/actors.ts +0 -564
  450. package/src/tools/animation.ts +0 -941
  451. package/src/tools/assets.ts +0 -394
  452. package/src/tools/audio.ts +0 -499
  453. package/src/tools/base-tool.ts +0 -52
  454. package/src/tools/behavior-tree.ts +0 -45
  455. package/src/tools/blueprint.ts +0 -940
  456. package/src/tools/consolidated-tool-definitions.ts +0 -1256
  457. package/src/tools/consolidated-tool-handlers.ts +0 -302
  458. package/src/tools/debug.ts +0 -622
  459. package/src/tools/dynamic-handler-registry.ts +0 -33
  460. package/src/tools/editor.ts +0 -435
  461. package/src/tools/engine.ts +0 -43
  462. package/src/tools/environment.ts +0 -281
  463. package/src/tools/foliage.ts +0 -596
  464. package/src/tools/handlers/actor-handlers.ts +0 -244
  465. package/src/tools/handlers/animation-handlers.ts +0 -237
  466. package/src/tools/handlers/argument-helper.ts +0 -142
  467. package/src/tools/handlers/asset-handlers.ts +0 -550
  468. package/src/tools/handlers/audio-handlers.ts +0 -194
  469. package/src/tools/handlers/blueprint-handlers.ts +0 -380
  470. package/src/tools/handlers/common-handlers.ts +0 -108
  471. package/src/tools/handlers/editor-handlers.ts +0 -124
  472. package/src/tools/handlers/effect-handlers.ts +0 -224
  473. package/src/tools/handlers/environment-handlers.ts +0 -183
  474. package/src/tools/handlers/graph-handlers.ts +0 -117
  475. package/src/tools/handlers/input-handlers.ts +0 -28
  476. package/src/tools/handlers/inspect-handlers.ts +0 -450
  477. package/src/tools/handlers/level-handlers.ts +0 -253
  478. package/src/tools/handlers/lighting-handlers.ts +0 -151
  479. package/src/tools/handlers/performance-handlers.ts +0 -132
  480. package/src/tools/handlers/pipeline-handlers.ts +0 -194
  481. package/src/tools/handlers/sequence-handlers.ts +0 -438
  482. package/src/tools/handlers/system-handlers.ts +0 -564
  483. package/src/tools/input.ts +0 -160
  484. package/src/tools/introspection.ts +0 -689
  485. package/src/tools/landscape.ts +0 -649
  486. package/src/tools/level.ts +0 -989
  487. package/src/tools/lighting.ts +0 -1052
  488. package/src/tools/logs.ts +0 -219
  489. package/src/tools/materials.ts +0 -295
  490. package/src/tools/niagara.ts +0 -485
  491. package/src/tools/performance.ts +0 -661
  492. package/src/tools/physics.ts +0 -679
  493. package/src/tools/property-dictionary.ts +0 -98
  494. package/src/tools/sequence.ts +0 -385
  495. package/src/tools/tool-definition-utils.ts +0 -35
  496. package/src/tools/ui.ts +0 -452
  497. package/src/types/automation-responses.ts +0 -119
  498. package/src/types/env.ts +0 -17
  499. package/src/types/handler-types.ts +0 -442
  500. package/src/types/responses.ts +0 -355
  501. package/src/types/tool-interfaces.ts +0 -250
  502. package/src/types/tool-types.ts +0 -575
  503. package/src/unreal-bridge.ts +0 -693
  504. package/src/utils/command-validator.ts +0 -139
  505. package/src/utils/elicitation.ts +0 -132
  506. package/src/utils/error-handler.ts +0 -287
  507. package/src/utils/ini-reader.ts +0 -86
  508. package/src/utils/logger.ts +0 -35
  509. package/src/utils/normalize.test.ts +0 -162
  510. package/src/utils/normalize.ts +0 -146
  511. package/src/utils/path-security.ts +0 -43
  512. package/src/utils/response-factory.ts +0 -44
  513. package/src/utils/response-validator.ts +0 -395
  514. package/src/utils/result-helpers.ts +0 -195
  515. package/src/utils/safe-json.test.ts +0 -90
  516. package/src/utils/safe-json.ts +0 -70
  517. package/src/utils/unreal-command-queue.ts +0 -166
  518. package/src/utils/validation.test.ts +0 -184
  519. package/src/utils/validation.ts +0 -312
  520. package/src/wasm/index.ts +0 -838
  521. package/test-server.mjs +0 -100
  522. package/tests/test-animation.mjs +0 -369
  523. package/tests/test-asset-advanced.mjs +0 -82
  524. package/tests/test-asset-graph.mjs +0 -311
  525. package/tests/test-audio.mjs +0 -417
  526. package/tests/test-automation-timeouts.mjs +0 -98
  527. package/tests/test-behavior-tree.mjs +0 -444
  528. package/tests/test-blueprint-graph.mjs +0 -410
  529. package/tests/test-blueprint.mjs +0 -577
  530. package/tests/test-client-mode.mjs +0 -86
  531. package/tests/test-console-command.mjs +0 -56
  532. package/tests/test-control-actor.mjs +0 -425
  533. package/tests/test-control-editor.mjs +0 -112
  534. package/tests/test-graphql.mjs +0 -372
  535. package/tests/test-input.mjs +0 -349
  536. package/tests/test-inspect.mjs +0 -302
  537. package/tests/test-landscape.mjs +0 -316
  538. package/tests/test-lighting.mjs +0 -428
  539. package/tests/test-manage-asset.mjs +0 -438
  540. package/tests/test-manage-level.mjs +0 -89
  541. package/tests/test-materials.mjs +0 -356
  542. package/tests/test-niagara.mjs +0 -185
  543. package/tests/test-no-inline-python.mjs +0 -122
  544. package/tests/test-performance.mjs +0 -539
  545. package/tests/test-plugin-handshake.mjs +0 -82
  546. package/tests/test-runner.mjs +0 -993
  547. package/tests/test-sequence.mjs +0 -104
  548. package/tests/test-system.mjs +0 -96
  549. package/tests/test-wasm.mjs +0 -283
  550. package/tests/test-world-partition.mjs +0 -215
  551. package/tsconfig.json +0 -56
  552. package/vitest.config.ts +0 -35
  553. package/wasm/Cargo.lock +0 -363
  554. package/wasm/Cargo.toml +0 -42
  555. package/wasm/LICENSE +0 -21
  556. package/wasm/README.md +0 -253
  557. package/wasm/src/dependency_resolver.rs +0 -377
  558. package/wasm/src/lib.rs +0 -153
  559. package/wasm/src/property_parser.rs +0 -271
  560. package/wasm/src/transform_math.rs +0 -396
  561. package/wasm/tests/integration.rs +0 -109
@@ -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