@alpsckr/unitycli 0.4.3 → 0.4.5

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 (425) hide show
  1. package/README.md +5 -5
  2. package/contracts/command-contract.json +109 -217
  3. package/contracts/package-files.json +374 -294
  4. package/contracts/stable-session/direct-call-semantics.json +8 -2
  5. package/contracts/tool-completion-contract.json +148 -672
  6. package/dist/bridge/admission.d.ts +17 -1
  7. package/dist/bridge/admission.js +2 -0
  8. package/dist/bridge/admission.js.map +1 -1
  9. package/dist/bridge/domains/asset.js +3 -1
  10. package/dist/bridge/domains/asset.js.map +1 -1
  11. package/dist/bridge/domains/camera.js +39 -7
  12. package/dist/bridge/domains/camera.js.map +1 -1
  13. package/dist/bridge/domains/console.js +2 -1
  14. package/dist/bridge/domains/console.js.map +1 -1
  15. package/dist/bridge/domains/doctor.d.ts +3 -0
  16. package/dist/bridge/domains/doctor.js +8 -0
  17. package/dist/bridge/domains/doctor.js.map +1 -0
  18. package/dist/bridge/domains/editor.js +0 -4
  19. package/dist/bridge/domains/editor.js.map +1 -1
  20. package/dist/bridge/domains/game-object.js +4 -12
  21. package/dist/bridge/domains/game-object.js.map +1 -1
  22. package/dist/bridge/domains/graphics.js +1 -16
  23. package/dist/bridge/domains/graphics.js.map +1 -1
  24. package/dist/bridge/domains/object.js +3 -0
  25. package/dist/bridge/domains/object.js.map +1 -1
  26. package/dist/bridge/domains/physics.js +1 -2
  27. package/dist/bridge/domains/physics.js.map +1 -1
  28. package/dist/bridge/domains/profiler.js +18 -7
  29. package/dist/bridge/domains/profiler.js.map +1 -1
  30. package/dist/bridge/domains/project-tool.js +4 -0
  31. package/dist/bridge/domains/project-tool.js.map +1 -1
  32. package/dist/bridge/domains/reflection.js +10 -2
  33. package/dist/bridge/domains/reflection.js.map +1 -1
  34. package/dist/bridge/domains/runtime-tool.js +3 -0
  35. package/dist/bridge/domains/runtime-tool.js.map +1 -1
  36. package/dist/bridge/domains/scene.js +3 -8
  37. package/dist/bridge/domains/scene.js.map +1 -1
  38. package/dist/bridge/domains/screenshot.js +30 -6
  39. package/dist/bridge/domains/screenshot.js.map +1 -1
  40. package/dist/bridge/domains/ui.js +3 -1
  41. package/dist/bridge/domains/ui.js.map +1 -1
  42. package/dist/bridge/index.d.ts +5 -5
  43. package/dist/bridge/index.js +6 -9
  44. package/dist/bridge/index.js.map +1 -1
  45. package/dist/bridge/registry.js +3 -5
  46. package/dist/bridge/registry.js.map +1 -1
  47. package/dist/bridge/session-supervisor.d.ts +6 -3
  48. package/dist/bridge/session-supervisor.js +145 -39
  49. package/dist/bridge/session-supervisor.js.map +1 -1
  50. package/dist/bridge/start-diagnostics.d.ts +19 -0
  51. package/dist/bridge/start-diagnostics.js +96 -0
  52. package/dist/bridge/start-diagnostics.js.map +1 -0
  53. package/dist/bridge/types.d.ts +6 -5
  54. package/dist/bridge/types.js +16 -2
  55. package/dist/bridge/types.js.map +1 -1
  56. package/dist/catalog-protocol.d.ts +64 -0
  57. package/dist/catalog-protocol.js +460 -0
  58. package/dist/catalog-protocol.js.map +1 -0
  59. package/dist/cli-commands/command.js +84 -61
  60. package/dist/cli-commands/command.js.map +1 -1
  61. package/dist/cli-commands/extensions.js +70 -50
  62. package/dist/cli-commands/extensions.js.map +1 -1
  63. package/dist/cli-commands/instances.js +1 -1
  64. package/dist/cli-commands/instances.js.map +1 -1
  65. package/dist/cli-commands/package.js +10 -2
  66. package/dist/cli-commands/package.js.map +1 -1
  67. package/dist/cli-process-owners.d.ts +6 -0
  68. package/dist/cli-process-owners.js +27 -0
  69. package/dist/cli-process-owners.js.map +1 -0
  70. package/dist/cli.d.ts +1 -1
  71. package/dist/cli.js +20 -5
  72. package/dist/cli.js.map +1 -1
  73. package/dist/contracts/command-contract.js +14 -4
  74. package/dist/contracts/command-contract.js.map +1 -1
  75. package/dist/doctor.d.ts +4 -6
  76. package/dist/doctor.js +451 -398
  77. package/dist/doctor.js.map +1 -1
  78. package/dist/editor-blocker-observer.d.ts +2 -1
  79. package/dist/editor-blocker-observer.js +40 -13
  80. package/dist/editor-blocker-observer.js.map +1 -1
  81. package/dist/extension-availability.d.ts +27 -9
  82. package/dist/extension-availability.js +96 -56
  83. package/dist/extension-availability.js.map +1 -1
  84. package/dist/extensions/descriptor-loader.d.ts +2 -2
  85. package/dist/extensions/descriptor-loader.js +3 -4
  86. package/dist/extensions/descriptor-loader.js.map +1 -1
  87. package/dist/extensions/index.d.ts +1 -1
  88. package/dist/extensions/index.js +1 -1
  89. package/dist/extensions/index.js.map +1 -1
  90. package/dist/extensions/installer.d.ts +57 -5
  91. package/dist/extensions/installer.js +263 -64
  92. package/dist/extensions/installer.js.map +1 -1
  93. package/dist/extensions/registry.js +3 -5
  94. package/dist/extensions/registry.js.map +1 -1
  95. package/dist/extensions/state-contract.d.ts +11 -0
  96. package/dist/extensions/state-contract.js +50 -0
  97. package/dist/extensions/state-contract.js.map +1 -0
  98. package/dist/instances.d.ts +19 -1
  99. package/dist/instances.js +39 -0
  100. package/dist/instances.js.map +1 -1
  101. package/dist/package-manager.d.ts +0 -24
  102. package/dist/package-manager.js +5 -58
  103. package/dist/package-manager.js.map +1 -1
  104. package/dist/project-tool-discovery.d.ts +5 -2
  105. package/dist/project-tool-discovery.js +22 -18
  106. package/dist/project-tool-discovery.js.map +1 -1
  107. package/dist/runtime-code.js +1 -1
  108. package/dist/runtime-code.js.map +1 -1
  109. package/dist/tool-name.d.ts +4 -0
  110. package/dist/tool-name.js +15 -0
  111. package/dist/tool-name.js.map +1 -0
  112. package/dist/tools/.generated-completion-registry.d.ts +2 -0
  113. package/dist/tools/.generated-completion-registry.js +763 -325
  114. package/dist/tools/.generated-completion-registry.js.map +1 -1
  115. package/dist/tools/.generated-registry.d.ts +6 -11
  116. package/dist/tools/.generated-registry.js +6 -11
  117. package/dist/tools/.generated-registry.js.map +1 -1
  118. package/dist/tools/.generated-schemas.js +11 -16
  119. package/dist/tools/.generated-schemas.js.map +1 -1
  120. package/dist/tools/asset/refresh-status.d.ts +3 -0
  121. package/dist/tools/asset/refresh-status.js +12 -0
  122. package/dist/tools/asset/refresh-status.js.map +1 -0
  123. package/dist/tools/asset/refresh.js +7 -3
  124. package/dist/tools/asset/refresh.js.map +1 -1
  125. package/dist/tools/build/player-status.d.ts +3 -0
  126. package/dist/tools/build/player-status.js +12 -0
  127. package/dist/tools/build/player-status.js.map +1 -0
  128. package/dist/tools/build/run.js +1 -1
  129. package/dist/tools/build/run.js.map +1 -1
  130. package/dist/tools/catalog-control.d.ts +22 -0
  131. package/dist/tools/catalog-control.js +335 -0
  132. package/dist/tools/catalog-control.js.map +1 -0
  133. package/dist/tools/catalog.d.ts +7 -9
  134. package/dist/tools/catalog.js +66 -45
  135. package/dist/tools/catalog.js.map +1 -1
  136. package/dist/tools/commands.d.ts +1 -1
  137. package/dist/tools/commands.js +44 -11
  138. package/dist/tools/commands.js.map +1 -1
  139. package/dist/tools/define.js +3 -2
  140. package/dist/tools/define.js.map +1 -1
  141. package/dist/tools/dispatcher.d.ts +53 -6
  142. package/dist/tools/dispatcher.js +277 -108
  143. package/dist/tools/dispatcher.js.map +1 -1
  144. package/dist/tools/editor/compile.js +1 -1
  145. package/dist/tools/editor/compile.js.map +1 -1
  146. package/dist/tools/editor/play-mode-status.d.ts +3 -0
  147. package/dist/tools/editor/play-mode-status.js +12 -0
  148. package/dist/tools/editor/play-mode-status.js.map +1 -0
  149. package/dist/tools/editor/readiness.d.ts +0 -1
  150. package/dist/tools/editor/readiness.js +0 -1
  151. package/dist/tools/editor/readiness.js.map +1 -1
  152. package/dist/tools/editor/refresh.d.ts +7 -3
  153. package/dist/tools/editor/refresh.js +20 -15
  154. package/dist/tools/editor/refresh.js.map +1 -1
  155. package/dist/tools/editor/state.d.ts +2 -2
  156. package/dist/tools/editor/state.js +13 -12
  157. package/dist/tools/editor/state.js.map +1 -1
  158. package/dist/tools/metadata/derived.js +1 -7
  159. package/dist/tools/metadata/derived.js.map +1 -1
  160. package/dist/tools/metadata/status.js +1 -12
  161. package/dist/tools/metadata/status.js.map +1 -1
  162. package/dist/tools/metadata/types.d.ts +0 -2
  163. package/dist/tools/navmesh/live.js +1 -1
  164. package/dist/tools/navmesh/live.js.map +1 -1
  165. package/dist/tools/occlusion/live.js +1 -1
  166. package/dist/tools/occlusion/live.js.map +1 -1
  167. package/dist/tools/package/deploy.js +2 -2
  168. package/dist/tools/package/deploy.js.map +1 -1
  169. package/dist/tools/package/embed.js +1 -1
  170. package/dist/tools/package/embed.js.map +1 -1
  171. package/dist/tools/package/install-status.d.ts +3 -0
  172. package/dist/tools/package/install-status.js +12 -0
  173. package/dist/tools/package/install-status.js.map +1 -0
  174. package/dist/tools/package/install.js +3 -3
  175. package/dist/tools/package/install.js.map +1 -1
  176. package/dist/tools/package/live-resolve.js +45 -12
  177. package/dist/tools/package/live-resolve.js.map +1 -1
  178. package/dist/tools/package/registry-clear.js.map +1 -1
  179. package/dist/tools/package/registry-set.js.map +1 -1
  180. package/dist/tools/package/remove-status.d.ts +3 -0
  181. package/dist/tools/package/remove-status.js +12 -0
  182. package/dist/tools/package/remove-status.js.map +1 -0
  183. package/dist/tools/package/remove.js +3 -3
  184. package/dist/tools/package/remove.js.map +1 -1
  185. package/dist/tools/package/resolve.js +3 -3
  186. package/dist/tools/package/resolve.js.map +1 -1
  187. package/dist/tools/package/restore.js +2 -2
  188. package/dist/tools/package/restore.js.map +1 -1
  189. package/dist/tools/package/scoped-registry-add.js.map +1 -1
  190. package/dist/tools/status/support.d.ts +13 -0
  191. package/dist/tools/status/support.js +28 -0
  192. package/dist/tools/status/support.js.map +1 -0
  193. package/dist/tools/test/cancel.js +2 -2
  194. package/dist/tools/test/cancel.js.map +1 -1
  195. package/dist/tools/test/run-status.d.ts +3 -0
  196. package/dist/tools/test/run-status.js +12 -0
  197. package/dist/tools/test/run-status.js.map +1 -0
  198. package/dist/tools/test/run.js +7 -4
  199. package/dist/tools/test/run.js.map +1 -1
  200. package/dist/tools/tools/group-summaries.js +10 -14
  201. package/dist/tools/tools/group-summaries.js.map +1 -1
  202. package/docs/architecture.md +4 -4
  203. package/docs/availability-differences.md +4 -4
  204. package/docs/command-execution.md +4 -3
  205. package/docs/error-codes.md +13 -5
  206. package/docs/extensions.md +1 -7
  207. package/docs/risk-and-side-effects.md +4 -3
  208. package/docs/safety-boundaries.md +3 -1
  209. package/docs/target-and-instance.md +1 -1
  210. package/docs/tool-discovery.md +17 -10
  211. package/docs/tool-execution.md +18 -7
  212. package/extensions/cinemachine/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  213. package/extensions/hdrp/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  214. package/extensions/probuilder/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  215. package/extensions/urp/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  216. package/extensions/vfx-graph/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  217. package/package.json +2 -4
  218. package/skills/unitycli/SKILL.md +11 -2
  219. package/skills/unitycli/manifest.json +1 -1
  220. package/skills/unitycli/references/operation-protocol.md +3 -3
  221. package/unitypkg/Editor/AssemblyInfo.cs +5 -0
  222. package/unitypkg/Editor/Bridge/BridgeCommandRegistry.cs +49 -14
  223. package/unitypkg/Editor/Bridge/BridgeHttpServer.cs +9 -35
  224. package/unitypkg/Editor/Bridge/BridgeQueueState.cs +672 -0
  225. package/unitypkg/Editor/Bridge/BridgeWorkQueue.cs +970 -120
  226. package/unitypkg/Editor/BridgeStartupStateMachine.cs +59 -1
  227. package/unitypkg/Editor/Commands/ProjectToolCommands.cs +4 -0
  228. package/unitypkg/Editor/Commands/RuntimeToolCommands.cs +3 -0
  229. package/unitypkg/Editor/Generated/UnityCliToolCompletionRegistry.g.cs +379 -14
  230. package/unitypkg/Editor/Infrastructure/CodeExecSupport.cs +28 -13
  231. package/unitypkg/Editor/Infrastructure/RegistryOwnershipResolver.cs +132 -105
  232. package/unitypkg/Editor/Infrastructure/UnityCliActiveOperation.cs +138 -29
  233. package/unitypkg/Editor/Infrastructure/UnityCliAdmission.cs +1856 -237
  234. package/unitypkg/Editor/Infrastructure/UnityCliDoctorDiagnostics.cs +220 -0
  235. package/unitypkg/Editor/Infrastructure/UnityCliDurableStatusCoordinator.cs +118 -0
  236. package/unitypkg/Editor/Infrastructure/UnityCliEditorStateSnapshot.cs +2 -1
  237. package/unitypkg/Editor/Infrastructure/UnityCliHierarchyPager.cs +203 -0
  238. package/unitypkg/Editor/Infrastructure/UnityCliInstanceCleanup.cs +2 -1
  239. package/unitypkg/Editor/Infrastructure/UnityCliInstanceRegistry.cs +201 -63
  240. package/unitypkg/Editor/Infrastructure/UnityCliJson.cs +2 -3
  241. package/unitypkg/Editor/Infrastructure/UnityCliLiveBuildCoordinator.cs +289 -46
  242. package/unitypkg/Editor/Infrastructure/UnityCliLiveRefreshCoordinator.cs +87 -53
  243. package/unitypkg/Editor/Infrastructure/UnityCliNavMeshBakeCoordinator.cs +4 -4
  244. package/unitypkg/Editor/Infrastructure/UnityCliObjectRef.cs +128 -25
  245. package/unitypkg/Editor/Infrastructure/UnityCliObjectSerializer.cs +435 -327
  246. package/unitypkg/Editor/Infrastructure/UnityCliOcclusionBakeCoordinator.cs +4 -4
  247. package/unitypkg/Editor/Infrastructure/UnityCliOperationCoordinator.cs +351 -53
  248. package/unitypkg/Editor/Infrastructure/UnityCliPackageResolveCoordinator.cs +118 -18
  249. package/unitypkg/Editor/Infrastructure/UnityCliPackageResolveState.cs +20 -1
  250. package/unitypkg/Editor/Infrastructure/UnityCliReadGeneration.cs +97 -0
  251. package/unitypkg/Editor/Infrastructure/UnityCliReflection.cs +644 -133
  252. package/unitypkg/Editor/Infrastructure/UnityCliReloadOperation.cs +128 -41
  253. package/unitypkg/Editor/Infrastructure/UnityCliRuntimeExecutor.cs +14 -84
  254. package/unitypkg/Editor/Infrastructure/UnityCliSerializer.cs +220 -0
  255. package/unitypkg/Editor/Infrastructure/UnityCliSessionMutationLease.cs +69 -29
  256. package/unitypkg/Editor/Infrastructure/UnityCliTestRunCoordinator.cs +440 -54
  257. package/unitypkg/Editor/Infrastructure/UnityCliWaiter.cs +128 -35
  258. package/unitypkg/Editor/ProjectTools/IUnityCliTool.cs +9 -1
  259. package/unitypkg/Editor/ProjectTools/OfficialAttributeToolRegistry.cs +32 -31
  260. package/unitypkg/Editor/ProjectTools/ProjectToolRegistry.cs +607 -458
  261. package/unitypkg/Editor/ProjectTools/ProjectToolSchemaValidator.cs +35 -7
  262. package/unitypkg/Editor/ProjectTools/RegisteredTool.cs +501 -60
  263. package/unitypkg/Editor/ProjectTools/UnityCliProjectToolContext.cs +21 -0
  264. package/unitypkg/Editor/ProjectTools/UnityCliSchemaDeriver.cs +36 -7
  265. package/unitypkg/Editor/ProjectTools/UnityCliToolName.cs +30 -0
  266. package/unitypkg/Editor/Properties/AssemblyInfo.cs +3 -0
  267. package/unitypkg/Editor/Tools/asset/get-data.cs +16 -7
  268. package/unitypkg/Editor/Tools/build/BuildSceneStateTool.cs +23 -3
  269. package/unitypkg/Editor/Tools/build/BuildToolDtos.cs +2 -1
  270. package/unitypkg/Editor/Tools/build/scene/add.cs +6 -3
  271. package/unitypkg/Editor/Tools/build/scene/disable.cs +1 -1
  272. package/unitypkg/Editor/Tools/build/scene/enable.cs +1 -1
  273. package/unitypkg/Editor/Tools/build/scene/list.cs +1 -1
  274. package/unitypkg/Editor/Tools/build/scene/remove.cs +13 -3
  275. package/unitypkg/Editor/Tools/camera/CameraToolSupport.cs +38 -38
  276. package/unitypkg/Editor/Tools/camera/multiview.cs +13 -6
  277. package/unitypkg/Editor/Tools/camera/screenshot.cs +14 -7
  278. package/unitypkg/Editor/Tools/component/get.cs +16 -9
  279. package/unitypkg/Editor/Tools/component/modify.cs +6 -4
  280. package/unitypkg/Editor/Tools/console/clear.cs +7 -6
  281. package/unitypkg/Editor/Tools/console/get.cs +48 -16
  282. package/unitypkg/Editor/Tools/editor/EditorToolSupport.cs +143 -28
  283. package/unitypkg/Editor/Tools/editor/menu-items.cs +1 -1
  284. package/unitypkg/Editor/Tools/editor/play.cs +2 -11
  285. package/unitypkg/Editor/Tools/editor/selection.cs +1 -1
  286. package/unitypkg/Editor/Tools/editor/stop.cs +2 -11
  287. package/unitypkg/Editor/Tools/editor/windows.cs +1 -1
  288. package/unitypkg/Editor/Tools/go/GameObjectToolSupport.cs +54 -4
  289. package/unitypkg/Editor/Tools/go/delete.cs +1 -1
  290. package/unitypkg/Editor/Tools/go/find.cs +167 -70
  291. package/unitypkg/Editor/Tools/graphics/GraphicsToolSupport.cs +25 -0
  292. package/unitypkg/Editor/Tools/graphics/light-probe-group.cs +9 -3
  293. package/unitypkg/Editor/Tools/graphics/pipeline-info.cs +1 -1
  294. package/unitypkg/Editor/Tools/graphics/pipeline-settings.cs +1 -1
  295. package/unitypkg/Editor/Tools/graphics/quality.cs +2 -2
  296. package/unitypkg/Editor/Tools/graphics/reflection-probe.cs +3 -3
  297. package/unitypkg/Editor/Tools/graphics/stats.cs +1 -1
  298. package/unitypkg/Editor/Tools/object/ObjectToolSupport.cs +4 -4
  299. package/unitypkg/Editor/Tools/object/get-data.cs +9 -7
  300. package/unitypkg/Editor/Tools/object/modify.cs +7 -5
  301. package/unitypkg/Editor/Tools/physics/raycast.cs +24 -6
  302. package/unitypkg/Editor/Tools/prefab/stage.cs +1 -1
  303. package/unitypkg/Editor/Tools/profiler/ProfilerFrameDebuggerSupport.cs +94 -21
  304. package/unitypkg/Editor/Tools/profiler/ProfilerMemorySnapshotSupport.cs +397 -10
  305. package/unitypkg/Editor/Tools/profiler/ProfilerToolSupport.cs +18 -4
  306. package/unitypkg/Editor/Tools/profiler/frame-debugger/enable.cs +1 -1
  307. package/unitypkg/Editor/Tools/profiler/frame-debugger/events.cs +88 -87
  308. package/unitypkg/Editor/Tools/profiler/hierarchy.cs +278 -112
  309. package/unitypkg/Editor/Tools/profiler/memory-snapshot/compare.cs +9 -5
  310. package/unitypkg/Editor/Tools/profiler/memory-snapshot/list.cs +37 -35
  311. package/unitypkg/Editor/Tools/profiler/memory-snapshot/take.cs +99 -97
  312. package/unitypkg/Editor/Tools/project/info.cs +1 -1
  313. package/unitypkg/Editor/Tools/project/layers.cs +1 -1
  314. package/unitypkg/Editor/Tools/project/tags.cs +1 -1
  315. package/unitypkg/Editor/Tools/reflection/method/call.cs +9 -7
  316. package/unitypkg/Editor/Tools/reflection/method/find.cs +27 -37
  317. package/unitypkg/Editor/Tools/reflection/type/schema.cs +24 -28
  318. package/unitypkg/Editor/Tools/scene/gameobject/component.cs +8 -5
  319. package/unitypkg/Editor/Tools/scene/gameobject/components.cs +10 -7
  320. package/unitypkg/Editor/Tools/scene/gameobject-support.cs +104 -8
  321. package/unitypkg/Editor/Tools/scene/gameobject.cs +37 -16
  322. package/unitypkg/Editor/Tools/scene/hierarchy.cs +146 -142
  323. package/unitypkg/Editor/Tools/screenshot/EditorElementCaptureToolSupport.cs +77 -25
  324. package/unitypkg/Editor/Tools/screenshot/ScreenshotToolSupport.cs +315 -95
  325. package/unitypkg/Editor/Tools/screenshot/editor-element.cs +7 -5
  326. package/unitypkg/Editor/Tools/screenshot/game-view.cs +35 -21
  327. package/unitypkg/Editor/Tools/screenshot/isolated-object.cs +29 -23
  328. package/unitypkg/Editor/Tools/screenshot/orbit.cs +37 -25
  329. package/unitypkg/Editor/Tools/screenshot/scene-view.cs +25 -18
  330. package/unitypkg/Editor/Tools/screenshot/screen.cs +177 -17
  331. package/unitypkg/Editor/Tools/screenshot/surround.cs +83 -79
  332. package/unitypkg/Editor/Tools/ui/render-preview.cs +16 -11
  333. package/unitypkg/Editor/UnityCliBridge.cs +97 -34
  334. package/unitypkg/Runtime/Infrastructure/UnityCliRuntimeBridge.cs +753 -122
  335. package/unitypkg/Runtime/Properties/AssemblyInfo.cs +5 -0
  336. package/unitypkg/Runtime/UnityCli.Runtime.asmdef +1 -1
  337. package/unitypkg/Shared/AssemblyInfo.cs +4 -0
  338. package/unitypkg/Shared/UnityCliAdmissionPool.cs +265 -75
  339. package/unitypkg/Shared/UnityCliBridgeQueuePolicy.cs +410 -0
  340. package/unitypkg/Shared/UnityCliBridgeWorkItemState.cs +47 -0
  341. package/unitypkg/Shared/UnityCliExceptionProjection.cs +151 -0
  342. package/unitypkg/Shared/UnityCliQueuePublication.cs +30 -0
  343. package/unitypkg/Shared/UnityCliRequestBudget.cs +352 -0
  344. package/unitypkg/Shared/UnityCliRuntimeRequestPublication.cs +107 -0
  345. package/unitypkg/Tests/Editor/UnityCli.Editor.Tests.asmdef +2 -2
  346. package/unitypkg/Tests/Editor/UnityCliAdmissionCoordinatorIntegrationTests.cs +1329 -0
  347. package/unitypkg/Tests/Editor/UnityCliAdmissionPoolTests.cs +311 -114
  348. package/unitypkg/Tests/Editor/UnityCliAdmissionRepairStateTests.cs +94 -0
  349. package/unitypkg/Tests/Editor/UnityCliBridgeAdmissionIntegrationTests.cs +1229 -0
  350. package/unitypkg/Tests/Editor/UnityCliBridgeQueuePolicyTests.cs +412 -0
  351. package/unitypkg/Tests/Editor/UnityCliContinuationIntegrationTests.cs +292 -0
  352. package/unitypkg/Tests/Editor/UnityCliDiagnosticBoundaryTests.cs +120 -0
  353. package/unitypkg/Tests/Editor/UnityCliDurableStatusTests.cs +84 -0
  354. package/unitypkg/Tests/Editor/UnityCliExceptionProjectionTests.cs +72 -0
  355. package/unitypkg/Tests/Editor/UnityCliInstancePublicationRetryTests.cs +180 -97
  356. package/unitypkg/Tests/Editor/UnityCliMemoryReflectionIntegrationTests.cs +434 -0
  357. package/unitypkg/Tests/Editor/UnityCliRequestBudgetTests.cs +615 -0
  358. package/unitypkg/Tests/Editor/UnityCliRuntimeBridgeIntegrationTests.cs +520 -0
  359. package/unitypkg/Tests/Editor/UnityCliToolCatalogTests.cs +562 -0
  360. package/unitypkg/Tests/Runtime/UnityCli.Runtime.Tests.asmdef +23 -0
  361. package/unitypkg/Tests/Runtime/UnityCliRuntimeCatalogValidationTests.cs +29 -0
  362. package/unitypkg/package.json +1 -1
  363. package/contracts/static-capability-catalog.json +0 -48226
  364. package/dist/bridge/domains/diagnose.d.ts +0 -2
  365. package/dist/bridge/domains/diagnose.js +0 -11
  366. package/dist/bridge/domains/diagnose.js.map +0 -1
  367. package/dist/bridge/domains/extension.d.ts +0 -8
  368. package/dist/bridge/domains/extension.js +0 -20
  369. package/dist/bridge/domains/extension.js.map +0 -1
  370. package/dist/capabilities.d.ts +0 -5
  371. package/dist/capabilities.js +0 -84
  372. package/dist/capabilities.js.map +0 -1
  373. package/dist/static-capability-catalog.d.ts +0 -13
  374. package/dist/static-capability-catalog.js +0 -41
  375. package/dist/static-capability-catalog.js.map +0 -1
  376. package/dist/tools/diagnose/capabilities.d.ts +0 -13
  377. package/dist/tools/diagnose/capabilities.js +0 -34
  378. package/dist/tools/diagnose/capabilities.js.map +0 -1
  379. package/dist/tools/package/info.d.ts +0 -19
  380. package/dist/tools/package/info.js +0 -27
  381. package/dist/tools/package/info.js.map +0 -1
  382. package/dist/tools/package/list.d.ts +0 -27
  383. package/dist/tools/package/list.js +0 -21
  384. package/dist/tools/package/list.js.map +0 -1
  385. package/dist/tools/package/registry-get.d.ts +0 -10
  386. package/dist/tools/package/registry-get.js +0 -22
  387. package/dist/tools/package/registry-get.js.map +0 -1
  388. package/dist/tools/package/scoped-registry-list.d.ts +0 -14
  389. package/dist/tools/package/scoped-registry-list.js +0 -22
  390. package/dist/tools/package/scoped-registry-list.js.map +0 -1
  391. package/dist/tools/package/search.d.ts +0 -17
  392. package/dist/tools/package/search.js +0 -39
  393. package/dist/tools/package/search.js.map +0 -1
  394. package/dist/tools/package/status.d.ts +0 -21
  395. package/dist/tools/package/status.js +0 -37
  396. package/dist/tools/package/status.js.map +0 -1
  397. package/dist/tools/tools/describe.d.ts +0 -27
  398. package/dist/tools/tools/describe.js +0 -35
  399. package/dist/tools/tools/describe.js.map +0 -1
  400. package/dist/tools/tools/group-list.d.ts +0 -16
  401. package/dist/tools/tools/group-list.js +0 -38
  402. package/dist/tools/tools/group-list.js.map +0 -1
  403. package/dist/tools/tools/group.d.ts +0 -13
  404. package/dist/tools/tools/group.js +0 -34
  405. package/dist/tools/tools/group.js.map +0 -1
  406. package/dist/tools/tools/pagination.d.ts +0 -17
  407. package/dist/tools/tools/pagination.js +0 -28
  408. package/dist/tools/tools/pagination.js.map +0 -1
  409. package/dist/tools/tools/search.d.ts +0 -15
  410. package/dist/tools/tools/search.js +0 -42
  411. package/dist/tools/tools/search.js.map +0 -1
  412. package/unitypkg/Editor/Tools/diagnose/preflight.cs +0 -269
  413. package/unitypkg/Editor/Tools/editor/console.cs +0 -26
  414. package/unitypkg/Editor/Tools/extension/status.cs +0 -87
  415. package/unitypkg/Editor/Tools/go/inspect.cs +0 -49
  416. package/unitypkg/Editor/Tools/graphics/pipeline-quality.cs +0 -24
  417. package/unitypkg/Editor/Tools/graphics/probe-positions.cs +0 -58
  418. package/unitypkg/Editor/Tools/graphics/settings.cs +0 -104
  419. package/unitypkg/Editor/Tools/graphics/stats-counters.cs +0 -28
  420. package/unitypkg/Editor/Tools/graphics/stats-memory.cs +0 -21
  421. package/unitypkg/Editor/Tools/instances/list.cs +0 -71
  422. package/unitypkg/Editor/Tools/physics/raycast-all.cs +0 -67
  423. package/unitypkg/Editor/Tools/scene/build-settings.cs +0 -69
  424. package/unitypkg/Editor/Tools/scene/data.cs +0 -125
  425. package/unitypkg/Editor/Tools/tests/list.cs +0 -98
@@ -3,8 +3,8 @@ using System;
3
3
  using System.Collections;
4
4
  using System.Collections.Generic;
5
5
  using System.Globalization;
6
- using System.Linq;
7
6
  using System.Reflection;
7
+ using System.Text;
8
8
  using System.Text.RegularExpressions;
9
9
  using UnityEditor;
10
10
  using UnityEngine;
@@ -13,19 +13,23 @@ namespace UnityCli
13
13
  {
14
14
  public static partial class UnityCliBridge
15
15
  {
16
- /// <summary>
17
- /// 深序列化器:将任意 Unity Object / Component / C# 对象序列化为 SerializedMember 树。
18
- /// 对齐 IvanMurzak SerializedMember 模型:typeName/name/value/fields/props/path/readOnly/writable/error/truncated。
19
- /// 支持 maxDepth、paths 局部读取、数组/list/dictionary、Unity Object 引用、循环引用检测。
20
- /// </summary>
16
+ /// <summary>在共享 request budget 下深读 Unity/CLR 对象;任何展开、getter 或枚举前先裁决。</summary>
21
17
  internal static class ObjectSerializer
22
18
  {
23
- private const int DefaultMaxDepth = 4;
24
- private const int DefaultMaxArrayItems = 50;
25
- private const int AbsoluteMaxArrayItems = 1000;
26
- private const int MaxMembers = 256;
19
+ internal const int DefaultMaxDepth = 4;
20
+ internal const int AbsoluteMaxDepth = 16;
21
+ internal const int DefaultMaxArrayItems = 50;
22
+ internal const int AbsoluteMaxArrayItems = 1000;
23
+ internal const int AbsoluteMaxMembers = 256;
24
+ internal const int AbsoluteMaxPaths = 128;
25
+ private const int AbsoluteMaxMemberCandidates = 512;
26
+ private const int AbsoluteMaxMemberMetadataUtf8Bytes = 64 * 1024;
27
+ private const int AbsoluteMaxCachedMemberTypes = 128;
28
+ private const int AbsoluteMaxTypeHierarchyDepth = 32;
29
+ private static readonly object MemberCacheLock = new object();
30
+ private static readonly Dictionary<Type, MemberCandidate[]> MemberCandidateCache = new Dictionary<Type, MemberCandidate[]>();
31
+ private static string memberCacheGeneration;
27
32
 
28
- /// <summary>序列化选项。</summary>
29
33
  public sealed class Options
30
34
  {
31
35
  public int MaxDepth = DefaultMaxDepth;
@@ -33,59 +37,91 @@ namespace UnityCli
33
37
  public bool IncludeFields = true;
34
38
  public bool IncludeProperties = true;
35
39
  public bool DeepSerialization = true;
36
- public HashSet<string> Paths;
40
+ public List<string> Paths;
37
41
  public HashSet<string> NameFilters;
38
- public HashSet<string> TypeFilters;
39
42
  public string ViewPath;
40
43
  public string NamePattern;
41
44
  public string TypeFilter;
42
45
  }
43
46
 
44
- /// <summary>构造 SerializedMember 值树;paths/viewQuery 存在时优先做范围化读取,避免大对象展开。</summary>
45
- public static object Serialize(object obj, Options options)
47
+ public static object Serialize(object obj, Options options, UnityCliRequestBudget budget)
46
48
  {
47
- if (options == null) options = new Options();
49
+ if (budget == null) throw new ArgumentNullException("budget");
50
+ options = options ?? new Options();
51
+ ValidateOptions(options);
52
+ var active = new HashSet<object>(ReferenceComparer.Instance);
53
+ SerializedMember root;
48
54
  if (options.Paths != null && options.Paths.Count > 0)
49
55
  {
50
- return MemberToData(BuildPathReadAggregate(obj, options));
56
+ root = BuildPathReadAggregate(obj, options, active, budget);
51
57
  }
52
- if (!string.IsNullOrEmpty(options.ViewPath))
58
+ else
53
59
  {
54
- var viewRoot = TryReadPath(obj, NormalizeReadPath(options.ViewPath), out var error);
55
- if (error != null)
60
+ if (!string.IsNullOrEmpty(options.ViewPath))
56
61
  {
57
- return MemberToData(ErrorMember("$" + "." + NormalizeReadPath(options.ViewPath), error));
62
+ var normalized = NormalizeReadPath(options.ViewPath);
63
+ obj = TryReadPath(obj, normalized, budget, out var error);
64
+ if (error != null) return MemberToData(ErrorMember("$." + normalized, error));
58
65
  }
59
- obj = viewRoot;
66
+ var path = string.IsNullOrEmpty(options.ViewPath) ? "$" : "$." + NormalizeReadPath(options.ViewPath);
67
+ root = ToMember(obj, path, obj == null ? "null" : obj.GetType().FullName, options, active, 0, budget);
60
68
  }
61
- var visited = new HashSet<int>();
62
- return MemberToData(ToMember(obj, string.IsNullOrEmpty(options.ViewPath) ? "$" : "$" + "." + NormalizeReadPath(options.ViewPath), obj == null ? "null" : obj.GetType().FullName, options, visited, 0));
69
+ return MemberToData(root);
63
70
  }
64
71
 
65
- /// <summary>按请求路径逐项读取,并用稳定 fields envelope 返回每个路径的结果或可恢复错误。</summary>
66
- private static SerializedMember BuildPathReadAggregate(object obj, Options options)
72
+ internal static int ParseMaxDepth(object value)
67
73
  {
68
- var aggregate = new SerializedMember
74
+ try
69
75
  {
70
- Path = "$",
71
- TypeName = "UnityCli.PathReadAggregate"
72
- };
73
- foreach (var rawPath in options.Paths)
76
+ var result = Convert.ToInt32(value, CultureInfo.InvariantCulture);
77
+ if (result < 0 || result > AbsoluteMaxDepth) throw new OverflowException();
78
+ return result;
79
+ }
80
+ catch (Exception exception) when (exception is FormatException || exception is InvalidCastException || exception is OverflowException)
74
81
  {
82
+ throw new BridgeCommandException("E_OBJECT_READ_LIMIT_INVALID", "viewQuery.maxDepth must be an integer between 0 and 16.", "Pass a maxDepth in the documented range.");
83
+ }
84
+ }
85
+
86
+ private static void ValidateOptions(Options options)
87
+ {
88
+ if (options.MaxDepth < 0 || options.MaxDepth > AbsoluteMaxDepth)
89
+ throw new BridgeCommandException("E_OBJECT_READ_LIMIT_INVALID", "maxDepth must be between 0 and 16.", "Pass a maxDepth in the documented range.");
90
+ if (options.MaxArrayItems < 1 || options.MaxArrayItems > AbsoluteMaxArrayItems)
91
+ throw new BridgeCommandException("E_OBJECT_READ_LIMIT_INVALID", "maxArrayItems must be between 1 and 1000.", "Pass a maxArrayItems in the documented range.");
92
+ if (options.Paths != null && options.Paths.Count > AbsoluteMaxPaths)
93
+ throw new BridgeCommandException("E_OBJECT_READ_LIMIT_INVALID", "paths accepts at most 128 entries.", "Split the exact member reads across requests.");
94
+ }
95
+
96
+ /// <summary>所有 paths 共用 active graph 和同一个 budget;输入顺序即稳定 continuation 顺序。</summary>
97
+ private static SerializedMember BuildPathReadAggregate(object obj, Options options, HashSet<object> active, UnityCliRequestBudget budget)
98
+ {
99
+ var aggregate = NewMember("$", "UnityCli.PathReadAggregate", budget);
100
+ if (aggregate.Truncated) return aggregate;
101
+ var childOptions = WithoutPaths(options);
102
+ for (var index = 0; index < options.Paths.Count; index++)
103
+ {
104
+ var rawPath = options.Paths[index];
105
+ if (string.IsNullOrWhiteSpace(rawPath)) continue;
106
+ if (!budget.TryCheckpoint()) { aggregate.Truncated = true; aggregate.NextPath = rawPath; break; }
75
107
  var normalizedPath = NormalizeReadPath(rawPath);
76
- var value = TryReadPath(obj, normalizedPath, out var error);
77
- aggregate.Fields.Add(new SerializedMember.KV
108
+ var value = TryReadPath(obj, normalizedPath, budget, out var error);
109
+ var path = string.IsNullOrEmpty(normalizedPath) ? "$" : "$." + normalizedPath;
110
+ var child = error == null
111
+ ? ToMember(value, path, value == null ? "null" : value.GetType().FullName, childOptions, active, 0, budget)
112
+ : ErrorMember(path, error);
113
+ if (!budget.TryReserveSerializedUtf8(rawPath)) { aggregate.Truncated = true; aggregate.NextPath = rawPath; aggregate.Error = BudgetError(budget); break; }
114
+ aggregate.Fields.Add(new SerializedMember.KV { Key = rawPath, Value = child });
115
+ if (child.Truncated && budget.IsExhausted)
78
116
  {
79
- Key = rawPath,
80
- Value = error == null
81
- ? ToMember(value, string.IsNullOrEmpty(normalizedPath) ? "$" : "$" + "." + normalizedPath, value == null ? "null" : value.GetType().FullName, WithoutPaths(options), new HashSet<int>(), 0)
82
- : ErrorMember(string.IsNullOrEmpty(normalizedPath) ? "$" : "$" + "." + normalizedPath, error)
83
- });
117
+ aggregate.Truncated = true;
118
+ aggregate.NextPath = rawPath;
119
+ break;
120
+ }
84
121
  }
85
122
  return aggregate;
86
123
  }
87
124
 
88
- /// <summary>移除 paths 选项,避免读取到的子树再次被路径过滤。</summary>
89
125
  private static Options WithoutPaths(Options options)
90
126
  {
91
127
  return new Options
@@ -96,13 +132,11 @@ namespace UnityCli
96
132
  IncludeProperties = options.IncludeProperties,
97
133
  DeepSerialization = options.DeepSerialization,
98
134
  NameFilters = options.NameFilters,
99
- TypeFilters = options.TypeFilters,
100
135
  NamePattern = options.NamePattern,
101
136
  TypeFilter = options.TypeFilter
102
137
  };
103
138
  }
104
139
 
105
- /// <summary>规范化读取路径;支持 #/、$/、点路径和 slash 路径。</summary>
106
140
  private static string NormalizeReadPath(string path)
107
141
  {
108
142
  if (string.IsNullOrEmpty(path)) return string.Empty;
@@ -110,441 +144,505 @@ namespace UnityCli
110
144
  if (normalized.StartsWith("#/", StringComparison.Ordinal)) normalized = normalized.Substring(2);
111
145
  if (normalized.StartsWith("$.", StringComparison.Ordinal)) normalized = normalized.Substring(2);
112
146
  if (normalized.StartsWith("$/", StringComparison.Ordinal)) normalized = normalized.Substring(2);
113
- normalized = normalized.Replace('/', '.');
114
- return normalized.Trim('.');
147
+ if (normalized.StartsWith("$[", StringComparison.Ordinal)) normalized = normalized.Substring(1);
148
+ return normalized.Replace('/', '.').Trim('.');
115
149
  }
116
150
 
117
- /// <summary>沿字段、属性、数组索引或字典 key 读取单一路径。</summary>
118
- private static object TryReadPath(object root, string path, out string error)
151
+ private static object TryReadPath(object root, string path, UnityCliRequestBudget budget, out string error)
119
152
  {
120
153
  error = null;
121
- if (root == null)
122
- {
123
- error = "root object is null";
124
- return null;
125
- }
154
+ if (root == null) { error = "root object is null"; return null; }
126
155
  if (string.IsNullOrEmpty(path)) return root;
127
156
  var current = root;
128
157
  foreach (var rawSegment in path.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries))
129
158
  {
130
- current = ReadPathSegment(current, rawSegment, out error);
159
+ if (!budget.TryConsumeNode()) { error = "request-budget-exhausted:" + budget.ExhaustedDimension; return null; }
160
+ current = ReadPathSegment(current, rawSegment, budget, out error);
131
161
  if (error != null) return null;
132
162
  }
133
163
  return current;
134
164
  }
135
165
 
136
- /// <summary>读取路径中的一个 segment,支持 field/property name[index]。</summary>
137
- private static object ReadPathSegment(object current, string segment, out string error)
166
+ private static object ReadPathSegment(object current, string segment, UnityCliRequestBudget budget, out string error)
138
167
  {
139
168
  error = null;
140
- if (current == null)
141
- {
142
- error = "encountered null before segment '" + segment + "'";
143
- return null;
144
- }
145
-
169
+ if (current == null) { error = "encountered null before segment '" + segment + "'"; return null; }
146
170
  var name = segment;
147
171
  string indexer = null;
148
172
  var bracketIndex = segment.IndexOf('[');
149
- if (bracketIndex >= 0)
150
- {
151
- name = segment.Substring(0, bracketIndex);
152
- indexer = segment.Substring(bracketIndex).Trim();
153
- }
154
-
173
+ if (bracketIndex >= 0) { name = segment.Substring(0, bracketIndex); indexer = segment.Substring(bracketIndex).Trim(); }
155
174
  object value = current;
156
175
  if (!string.IsNullOrEmpty(name))
157
176
  {
158
177
  var type = current.GetType();
178
+ if (!budget.TryConsumeReflectionCall()) { error = BudgetError(budget); return null; }
159
179
  var field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
160
180
  if (field != null)
161
181
  {
182
+ if (!budget.TryConsumeGetter() || !budget.TryConsumeReflectionCall()) { error = BudgetError(budget); return null; }
162
183
  value = field.GetValue(current);
163
184
  }
164
185
  else
165
186
  {
166
- var prop = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
167
- if (prop == null || !prop.CanRead || prop.GetIndexParameters().Length > 0)
168
- {
169
- error = "member not found or unreadable: " + name;
170
- return null;
171
- }
172
- value = prop.GetValue(current, null);
187
+ if (!budget.TryConsumeReflectionCall()) { error = BudgetError(budget); return null; }
188
+ var property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
189
+ if (property == null || !property.CanRead || property.GetIndexParameters().Length > 0) { error = "member not found or unreadable: " + name; return null; }
190
+ if (!budget.TryConsumeGetter() || !budget.TryConsumeReflectionCall()) { error = BudgetError(budget); return null; }
191
+ value = property.GetValue(current, null);
173
192
  }
174
193
  }
175
-
176
- if (!string.IsNullOrEmpty(indexer))
177
- {
178
- value = ReadIndexer(value, indexer, out error);
179
- }
180
- return value;
194
+ return string.IsNullOrEmpty(indexer) ? value : ReadIndexer(value, indexer, budget, out error);
181
195
  }
182
196
 
183
- /// <summary>读取数组/list/dictionary 索引,格式为 [i] [key]。</summary>
184
- private static object ReadIndexer(object value, string indexer, out string error)
197
+ private static object ReadIndexer(object value, string indexer, UnityCliRequestBudget budget, out string error)
185
198
  {
186
199
  error = null;
187
- if (value == null)
188
- {
189
- error = "cannot index null value";
190
- return null;
191
- }
200
+ if (value == null) { error = "cannot index null value"; return null; }
192
201
  var token = indexer.Trim();
193
- if (!token.StartsWith("[", StringComparison.Ordinal) || !token.EndsWith("]", StringComparison.Ordinal))
194
- {
195
- error = "invalid index syntax: " + indexer;
196
- return null;
197
- }
198
- token = token.Substring(1, token.Length - 2).Trim().Trim('"');
202
+ if (!token.StartsWith("[", StringComparison.Ordinal) || !token.EndsWith("]", StringComparison.Ordinal)) { error = "invalid index syntax: " + indexer; return null; }
203
+ token = UnescapeDictionaryPathKey(token.Substring(1, token.Length - 2).Trim().Trim('"'));
199
204
  if (value is IList list)
200
205
  {
206
+ if (!budget.TryConsumeGetter()) { error = BudgetError(budget); return null; }
207
+ var count = list.Count;
201
208
  int index;
202
- if (!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out index) || index < 0 || index >= list.Count)
203
- {
204
- error = "list index out of range: " + token;
205
- return null;
206
- }
209
+ if (!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out index) || index < 0 || index >= count) { error = "list index out of range: " + token; return null; }
210
+ if (!budget.TryConsumeGetter()) { error = BudgetError(budget); return null; }
207
211
  return list[index];
208
212
  }
209
- if (value is IDictionary dict)
213
+ if (value is IDictionary dictionary)
210
214
  {
211
- foreach (DictionaryEntry entry in dict)
215
+ var enumerator = dictionary.GetEnumerator();
216
+ try
212
217
  {
213
- if (string.Equals(Convert.ToString(entry.Key, CultureInfo.InvariantCulture), token, StringComparison.Ordinal))
218
+ while (budget.TryConsumeScannedRecord() && enumerator.MoveNext())
214
219
  {
215
- return entry.Value;
220
+ if (!budget.TryConsumeGetter()) { error = BudgetError(budget); return null; }
221
+ var entry = enumerator.Entry;
222
+ if (string.Equals(Convert.ToString(entry.Key, CultureInfo.InvariantCulture), token, StringComparison.Ordinal)) return entry.Value;
216
223
  }
217
224
  }
218
- error = "dictionary key not found: " + token;
225
+ finally { (enumerator as IDisposable)?.Dispose(); }
226
+ error = budget.IsExhausted ? BudgetError(budget) : "dictionary key not found: " + token;
219
227
  return null;
220
228
  }
221
229
  error = "value is not indexable";
222
230
  return null;
223
231
  }
224
232
 
225
- private static SerializedMember ToMember(object obj, string path, string declaredType, Options options, HashSet<int> visited, int depth)
233
+ private static SerializedMember ToMember(object obj, string path, string declaredType, Options options, HashSet<object> active, int depth, UnityCliRequestBudget budget)
226
234
  {
227
- var member = new SerializedMember
228
- {
229
- Path = path,
230
- TypeName = declaredType ?? "null"
231
- };
232
-
233
- if (obj == null)
234
- {
235
- member.Value = null;
236
- member.IsNull = true;
237
- return member;
238
- }
239
-
235
+ var member = NewMember(path, declaredType ?? "null", budget);
236
+ if (member.Truncated) return member;
237
+ if (obj == null) { member.IsNull = true; return member; }
240
238
  var type = obj.GetType();
241
239
  member.TypeName = type.FullName;
242
-
243
- // 基元与简单值类型直接输出
244
- if (IsSimpleType(type))
240
+ if (IsSimpleType(type) || obj is string)
245
241
  {
246
242
  member.Value = obj;
243
+ if (!budget.TryReserveSerializedUtf8(Convert.ToString(obj, CultureInfo.InvariantCulture))) Truncate(member, budget, path);
247
244
  return member;
248
245
  }
249
246
 
250
- // 字符串
251
- if (obj is string s)
247
+ // depth 与 cycle 必须先于 Unity 引用、集合枚举和反射成员展开。
248
+ if (depth >= options.MaxDepth)
252
249
  {
253
- member.Value = s;
250
+ member.Value = UnsupportedValue(type, "max-depth-reached");
251
+ member.Truncated = true;
252
+ member.NextPath = path;
254
253
  return member;
255
254
  }
256
-
257
- // Unity Object 引用:输出引用摘要,不无限递归
258
- if (obj is UnityEngine.Object uo)
255
+ var trackReference = !type.IsValueType;
256
+ if (trackReference && !active.Add(obj))
259
257
  {
260
- member.IsUnityObject = true;
261
- member.UnityObjectRef = UnityObjectRefJson(uo);
262
- // 允许在 depth 允许时浅层展开公共属性,但不递归到子 Unity Object
263
- if (depth < options.MaxDepth && options.DeepSerialization)
264
- {
265
- ExpandMembers(obj, type, member, options, visited, depth);
266
- }
258
+ member.Error = "cycle-detected";
259
+ member.Truncated = true;
260
+ member.NextPath = path;
267
261
  return member;
268
262
  }
269
263
 
270
- // 数组 / IList
271
- if (obj is IList list)
264
+ try
272
265
  {
273
- member.IsArray = true;
274
- member.ArrayLength = list.Count;
275
- var maxArrayItems = ClampMaxArrayItems(options.MaxArrayItems);
276
- var limit = Math.Min(list.Count, maxArrayItems);
277
- member.Truncated = list.Count > maxArrayItems;
278
- for (var i = 0; i < limit; i++)
266
+ if (obj is UnityEngine.Object unityObject)
279
267
  {
280
- var itemPath = path + "[" + i + "]";
281
- if (!PathAllowed(itemPath, options)) continue;
282
- member.ArrayItems.Add(ToMember(list[i], itemPath, null, options, visited, depth + 1));
268
+ member.IsUnityObject = true;
269
+ member.UnityObjectRef = UnityObjectRefJson(unityObject, budget);
270
+ if (options.DeepSerialization) ExpandMembers(obj, type, member, options, active, depth, budget);
271
+ return member;
283
272
  }
273
+ if (obj is IList list) { ExpandList(list, member, options, active, depth, budget); return member; }
274
+ if (obj is IDictionary dictionary) { ExpandDictionary(dictionary, member, options, active, depth, budget); return member; }
275
+ if (obj is IEnumerable enumerable && !(obj is GameObject)) { ExpandEnumerable(enumerable, member, options, active, depth, budget); return member; }
276
+ ExpandMembers(obj, type, member, options, active, depth, budget);
284
277
  return member;
285
278
  }
286
-
287
- // IDictionary
288
- if (obj is IDictionary dict)
279
+ finally
289
280
  {
290
- member.IsDictionary = true;
291
- member.ArrayLength = dict.Count;
292
- var limit = 0;
293
- foreach (DictionaryEntry entry in dict)
294
- {
295
- if (limit >= ClampMaxArrayItems(options.MaxArrayItems)) { member.Truncated = true; break; }
296
- var keyStr = Convert.ToString(entry.Key, CultureInfo.InvariantCulture);
297
- var entryPath = path + "[" + keyStr + "]";
298
- if (!PathAllowed(entryPath, options)) continue;
299
- member.DictionaryEntries.Add(new SerializedMember.KV
300
- {
301
- Key = keyStr,
302
- Value = ToMember(entry.Value, entryPath, null, options, visited, depth + 1)
303
- });
304
- limit++;
305
- }
306
- return member;
281
+ if (trackReference) active.Remove(obj);
307
282
  }
283
+ }
308
284
 
309
- // IEnumerable ( IList)
310
- if (obj is IEnumerable enumerable && !(obj is GameObject))
311
- {
312
- member.IsArray = true;
313
- var count = 0;
314
- foreach (var item in enumerable)
285
+ private static SerializedMember NewMember(string path, string typeName, UnityCliRequestBudget budget)
286
+ {
287
+ if (!budget.TryConsumeNode() || !budget.TryReserveSerializedUtf8Bytes(128L) || !budget.TryReserveSerializedUtf8(path) || !budget.TryReserveSerializedUtf8(typeName))
288
+ return new SerializedMember { Path = path, TypeName = typeName, Truncated = true, NextPath = path, Error = BudgetError(budget) };
289
+ return new SerializedMember { Path = path, TypeName = typeName };
290
+ }
291
+
292
+ private static void ExpandList(IList list, SerializedMember member, Options options, HashSet<object> active, int depth, UnityCliRequestBudget budget)
293
+ {
294
+ member.IsArray = true;
295
+ if (!budget.TryConsumeGetter()) { Truncate(member, budget, member.Path + "[0]"); return; }
296
+ var count = list.Count;
297
+ member.ArrayLength = count;
298
+ member.HasKnownTotal = true;
299
+ var limit = Math.Min(count, options.MaxArrayItems);
300
+ for (var index = 0; index < limit; index++)
301
+ {
302
+ var itemPath = member.Path + "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
303
+ if (!budget.TryConsumeScannedRecord() || !budget.TryConsumeGetter()) { Truncate(member, budget, itemPath); return; }
304
+ member.ArrayItems.Add(ToMember(list[index], itemPath, null, options, active, depth + 1, budget));
305
+ if (budget.IsExhausted) { Truncate(member, budget, itemPath); return; }
306
+ }
307
+ if (count > limit) { member.Truncated = true; member.NextPath = member.Path + "[" + limit.ToString(CultureInfo.InvariantCulture) + "]"; }
308
+ }
309
+
310
+ private static void ExpandDictionary(IDictionary dictionary, SerializedMember member, Options options, HashSet<object> active, int depth, UnityCliRequestBudget budget)
311
+ {
312
+ member.IsDictionary = true;
313
+ if (!budget.TryConsumeGetter()) { TruncateNonResumableDictionary(member); return; }
314
+ var count = dictionary.Count;
315
+ member.ArrayLength = count;
316
+ member.HasKnownTotal = true;
317
+ var enumerator = dictionary.GetEnumerator();
318
+ var emitted = 0;
319
+ try
320
+ {
321
+ while (emitted < options.MaxArrayItems)
315
322
  {
316
- if (count >= ClampMaxArrayItems(options.MaxArrayItems)) { member.Truncated = true; break; }
317
- var itemPath = path + "[" + count + "]";
318
- if (PathAllowed(itemPath, options))
319
- {
320
- member.ArrayItems.Add(ToMember(item, itemPath, null, options, visited, depth + 1));
321
- }
322
- count++;
323
+ if (!budget.TryConsumeScannedRecord()) { TruncateNonResumableDictionary(member); return; }
324
+ if (!enumerator.MoveNext()) return;
325
+ if (!budget.TryConsumeGetter()) { TruncateNonResumableDictionary(member); return; }
326
+ var entry = enumerator.Entry;
327
+ var key = Convert.ToString(entry.Key, CultureInfo.InvariantCulture);
328
+ var entryPath = DictionaryEntryPath(member.Path, key);
329
+ if (!budget.TryReserveSerializedUtf8(key)) { TruncateNonResumableDictionary(member); return; }
330
+ member.DictionaryEntries.Add(new SerializedMember.KV { Key = key, Value = ToMember(entry.Value, entryPath, null, options, active, depth + 1, budget) });
331
+ emitted++;
332
+ if (budget.IsExhausted) { TruncateNonResumableDictionary(member); return; }
333
+ }
334
+ if (emitted < count)
335
+ {
336
+ member.Truncated = true;
337
+ member.NextPath = null;
338
+ member.Error = "non-resumable-dictionary-truncation";
323
339
  }
324
- member.ArrayLength = count;
325
- return member;
326
340
  }
341
+ finally { (enumerator as IDisposable)?.Dispose(); }
342
+ }
327
343
 
328
- // 深度耗尽时保留类型和截断状态,不能把未知动态值伪装成字符串。
329
- if (depth >= options.MaxDepth)
330
- {
331
- member.Truncated = true;
332
- member.Value = UnsupportedValue(type, "max-depth-reached");
333
- return member;
334
- }
344
+ private static void TruncateNonResumableDictionary(SerializedMember member)
345
+ {
346
+ member.Truncated = true;
347
+ member.NextPath = null;
348
+ member.Error = "non-resumable-dictionary-truncation";
349
+ }
335
350
 
336
- // 循环引用检测
337
- var hash = RuntimeHelpersHashCode(obj);
338
- if (visited.Contains(hash))
351
+ /// <summary>非 IList enumerable 只流式返回当前片段;它没有可重放的索引语义,截断时不发布 nextPath。</summary>
352
+ private static void ExpandEnumerable(IEnumerable enumerable, SerializedMember member, Options options, HashSet<object> active, int depth, UnityCliRequestBudget budget)
353
+ {
354
+ member.IsArray = true;
355
+ var enumerator = enumerable.GetEnumerator();
356
+ var index = 0;
357
+ try
339
358
  {
340
- member.Error = "cycle-detected";
359
+ while (index < options.MaxArrayItems)
360
+ {
361
+ if (!budget.TryConsumeScannedRecord()) { TruncateNonResumableEnumerable(member, budget); return; }
362
+ if (!enumerator.MoveNext()) { member.ArrayLength = index; return; }
363
+ if (!budget.TryConsumeGetter()) { TruncateNonResumableEnumerable(member, budget); return; }
364
+ var itemPath = member.Path + "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
365
+ member.ArrayItems.Add(ToMember(enumerator.Current, itemPath, null, options, active, depth + 1, budget));
366
+ index++;
367
+ if (budget.IsExhausted) { TruncateNonResumableEnumerable(member, budget); return; }
368
+ }
369
+ member.ArrayLength = index;
370
+ if (!budget.TryConsumeScannedRecord()) { TruncateNonResumableEnumerable(member, budget); return; }
371
+ if (!enumerator.MoveNext()) return;
341
372
  member.Truncated = true;
342
- return member;
373
+ member.NextPath = null;
374
+ member.Error = "non-resumable-enumerable-truncation";
343
375
  }
344
- visited.Add(hash);
345
-
346
- ExpandMembers(obj, type, member, options, visited, depth);
347
- return member;
376
+ finally { (enumerator as IDisposable)?.Dispose(); }
348
377
  }
349
378
 
350
- private static void ExpandMembers(object obj, Type type, SerializedMember member, Options options, HashSet<int> visited, int depth)
379
+ private static void ExpandMembers(object obj, Type type, SerializedMember member, Options options, HashSet<object> active, int depth, UnityCliRequestBudget budget)
351
380
  {
352
381
  var memberCount = 0;
353
- if (options.IncludeFields)
354
- {
355
- foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public))
382
+ foreach (var candidate in GetMemberCandidates(type, budget))
383
+ {
384
+ if ((candidate.IsProperty && !options.IncludeProperties) || (!candidate.IsProperty && !options.IncludeFields)) continue;
385
+ if (!MemberAllowed(candidate.Name, options)) continue;
386
+ if (memberCount >= AbsoluteMaxMembers) { member.Truncated = true; member.NextPath = member.Path + "." + candidate.Name; return; }
387
+ var candidatePath = member.Path + "." + candidate.Name;
388
+ if (!budget.TryConsumeGetter() || !budget.TryConsumeReflectionCall()) { Truncate(member, budget, candidatePath); return; }
389
+ try
356
390
  {
357
- if (memberCount >= MaxMembers) { member.Truncated = true; break; }
358
- if (!MemberAllowed(field.Name, options)) continue;
359
- try
391
+ var value = candidate.IsProperty
392
+ ? ((PropertyInfo)candidate.Member).GetValue(obj, null)
393
+ : ((FieldInfo)candidate.Member).GetValue(obj);
394
+ var child = ToMember(value, candidatePath, candidate.ValueTypeName, options, active, depth + 1, budget);
395
+ if (TypeAllowed(child.TypeName, options))
360
396
  {
361
- var value = field.GetValue(obj);
362
- var child = ToMember(value, member.Path + "." + field.Name, field.FieldType.FullName, options, visited, depth + 1);
363
- if (!TypeAllowed(child.TypeName, options)) continue;
364
- member.Fields.Add(new SerializedMember.KV
365
- {
366
- Key = field.Name,
367
- Value = child
368
- });
397
+ child.ReadOnly = candidate.IsProperty && !candidate.CanWrite;
398
+ child.Writable = candidate.IsProperty && candidate.CanWrite;
399
+ if (!budget.TryReserveSerializedUtf8(candidate.Name)) { Truncate(member, budget, candidatePath); return; }
400
+ var destination = candidate.IsProperty ? member.Props : member.Fields;
401
+ destination.Add(new SerializedMember.KV { Key = candidate.Name, Value = child });
369
402
  memberCount++;
370
403
  }
371
- catch (Exception e)
372
- {
373
- member.Fields.Add(new SerializedMember.KV { Key = field.Name, Value = ErrorMember(member.Path + "." + field.Name, e.Message) });
374
- }
404
+ if (budget.IsExhausted) { Truncate(member, budget, candidatePath); return; }
375
405
  }
376
- }
377
-
378
- if (options.IncludeProperties)
379
- {
380
- foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
406
+ catch (UnityCliRequestBudgetExceededException) { Truncate(member, budget, candidatePath); return; }
407
+ catch (Exception exception)
381
408
  {
382
- if (memberCount >= MaxMembers) { member.Truncated = true; break; }
383
- if (!prop.CanRead || prop.GetIndexParameters().Length > 0) continue;
384
- if (!MemberAllowed(prop.Name, options)) continue;
385
- try
386
- {
387
- var value = prop.GetValue(obj, null);
388
- var child = ToMember(value, member.Path + "." + prop.Name, prop.PropertyType.FullName, options, visited, depth + 1);
389
- if (!TypeAllowed(child.TypeName, options)) continue;
390
- child.ReadOnly = !prop.CanWrite;
391
- child.Writable = prop.CanWrite;
392
- member.Props.Add(new SerializedMember.KV { Key = prop.Name, Value = child });
393
- memberCount++;
394
- }
395
- catch (Exception e)
396
- {
397
- member.Props.Add(new SerializedMember.KV { Key = prop.Name, Value = ErrorMember(member.Path + "." + prop.Name, e.Message) });
398
- }
409
+ var destination = candidate.IsProperty ? member.Props : member.Fields;
410
+ destination.Add(new SerializedMember.KV { Key = candidate.Name, Value = ErrorMember(candidatePath, exception.Message) });
411
+ memberCount++;
399
412
  }
400
413
  }
401
414
  }
402
415
 
403
- private static int ClampMaxArrayItems(int value)
416
+ private static string DictionaryEntryPath(string path, string key)
404
417
  {
405
- return Math.Max(0, Math.Min(value <= 0 ? DefaultMaxArrayItems : value, AbsoluteMaxArrayItems));
418
+ var value = key ?? string.Empty;
419
+ var builder = new StringBuilder(value.Length + 8);
420
+ foreach (var character in value)
421
+ {
422
+ if (char.IsLetterOrDigit(character) || character == '_' || character == '-') builder.Append(character);
423
+ else builder.Append("\\u").Append(((int)character).ToString("x4", CultureInfo.InvariantCulture));
424
+ }
425
+ return path + "[\"" + builder + "\"]";
406
426
  }
407
427
 
408
- private static bool PathAllowed(string path, Options options)
428
+ private static string UnescapeDictionaryPathKey(string value)
409
429
  {
410
- if (options.Paths == null || options.Paths.Count == 0) return true;
411
- foreach (var prefix in options.Paths)
430
+ if (string.IsNullOrEmpty(value) || value.IndexOf("\\u", StringComparison.Ordinal) < 0) return value;
431
+ var builder = new StringBuilder(value.Length);
432
+ for (var index = 0; index < value.Length; index++)
412
433
  {
413
- if (path == prefix || path.StartsWith(prefix + ".", StringComparison.Ordinal) || path.StartsWith(prefix + "[", StringComparison.Ordinal))
434
+ if (value[index] == '\\' && index + 5 < value.Length && value[index + 1] == 'u'
435
+ && int.TryParse(value.Substring(index + 2, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var code))
414
436
  {
415
- return true;
437
+ builder.Append((char)code);
438
+ index += 5;
416
439
  }
440
+ else builder.Append(value[index]);
417
441
  }
418
- return false;
442
+ return builder.ToString();
419
443
  }
420
444
 
421
- private static bool MemberAllowed(string name, Options options)
445
+ /// <summary>逐项扫描 declared metadata,并把单次 domain reload 内的有界结果复用给后续读取。</summary>
446
+ private static MemberCandidate[] GetMemberCandidates(Type type, UnityCliRequestBudget budget)
422
447
  {
423
- if (options.NameFilters != null && options.NameFilters.Count > 0)
448
+ MemberCandidate[] cached;
449
+ var generation = UnityCliReadGeneration.Reflection();
450
+ lock (MemberCacheLock)
424
451
  {
425
- return options.NameFilters.Contains(name);
452
+ if (!string.Equals(memberCacheGeneration, generation, StringComparison.Ordinal))
453
+ {
454
+ MemberCandidateCache.Clear();
455
+ memberCacheGeneration = generation;
456
+ }
457
+ if (MemberCandidateCache.TryGetValue(type, out cached))
458
+ {
459
+ if (!budget.TryConsumeScannedRecords(cached.Length)) throw new UnityCliRequestBudgetExceededException(budget.ExhaustedDimension);
460
+ return cached;
461
+ }
426
462
  }
427
- if (!string.IsNullOrEmpty(options.NamePattern) && !Regex.IsMatch(name, options.NamePattern, RegexOptions.IgnoreCase))
463
+
464
+ var candidates = new List<MemberCandidate>();
465
+ var names = new HashSet<string>(StringComparer.Ordinal);
466
+ var metadataBytes = 0;
467
+ var current = type;
468
+ for (var depth = 0; current != null && depth < AbsoluteMaxTypeHierarchyDepth; depth++)
428
469
  {
429
- return false;
470
+ if (!budget.TryConsumeReflectionCall()) throw new UnityCliRequestBudgetExceededException(budget.ExhaustedDimension);
471
+ foreach (var field in UnityCliReflectionSnapshot.DeclaredFields(current, budget, AbsoluteMaxMemberCandidates))
472
+ {
473
+ ConsumeMemberCandidateBudget(budget);
474
+ if (!field.IsPublic || field.IsStatic || !names.Add("F:" + field.Name)) continue;
475
+ AddMemberCandidate(candidates, new MemberCandidate(field, false, true, field.FieldType), ref metadataBytes);
476
+ }
477
+ if (!budget.TryConsumeReflectionCall()) throw new UnityCliRequestBudgetExceededException(budget.ExhaustedDimension);
478
+ foreach (var property in UnityCliReflectionSnapshot.DeclaredProperties(current, budget, AbsoluteMaxMemberCandidates))
479
+ {
480
+ ConsumeMemberCandidateBudget(budget);
481
+ var getter = property.GetMethod;
482
+ if (getter == null || !getter.IsPublic || getter.IsStatic || property.GetIndexParameters().Length > 0 || !names.Add("P:" + property.Name)) continue;
483
+ AddMemberCandidate(candidates, new MemberCandidate(property, true, property.CanWrite, property.PropertyType), ref metadataBytes);
484
+ }
485
+ if (!budget.TryConsumeReflectionCall()) throw new UnityCliRequestBudgetExceededException(budget.ExhaustedDimension);
486
+ current = current.GetTypeInfo().BaseType;
430
487
  }
431
- return true;
488
+ if (current != null) throw MetadataLimit("type hierarchy depth");
489
+ candidates.Sort((left, right) =>
490
+ {
491
+ var nameOrder = string.CompareOrdinal(left.Name, right.Name);
492
+ return nameOrder != 0 ? nameOrder : left.IsProperty.CompareTo(right.IsProperty);
493
+ });
494
+ cached = candidates.ToArray();
495
+ lock (MemberCacheLock)
496
+ {
497
+ if (MemberCandidateCache.Count >= AbsoluteMaxCachedMemberTypes) MemberCandidateCache.Clear();
498
+ MemberCandidateCache[type] = cached;
499
+ }
500
+ return cached;
432
501
  }
433
502
 
434
- private static bool TypeAllowed(string typeName, Options options)
503
+ private static void ConsumeMemberCandidateBudget(UnityCliRequestBudget budget)
435
504
  {
436
- return string.IsNullOrEmpty(options.TypeFilter)
437
- || (!string.IsNullOrEmpty(typeName) && typeName.IndexOf(options.TypeFilter, StringComparison.OrdinalIgnoreCase) >= 0);
505
+ if (!budget.TryConsumeScannedRecord() || !budget.TryConsumeReflectionCall()) throw new UnityCliRequestBudgetExceededException(budget.ExhaustedDimension);
438
506
  }
439
507
 
440
- private static bool IsSimpleType(Type type)
508
+ private static void AddMemberCandidate(List<MemberCandidate> candidates, MemberCandidate candidate, ref int metadataBytes)
441
509
  {
442
- return type.IsPrimitive || type.IsEnum || type == typeof(decimal) || type == typeof(DateTime)
443
- || type == typeof(TimeSpan) || type == typeof(Guid) || type == typeof(Vector2) || type == typeof(Vector3)
444
- || type == typeof(Vector4) || type == typeof(Quaternion) || type == typeof(Color) || type == typeof(Rect)
445
- || type == typeof(Bounds) || type == typeof(LayerMask);
510
+ if (candidates.Count >= AbsoluteMaxMemberCandidates) throw MetadataLimit("member count");
511
+ metadataBytes += Encoding.UTF8.GetByteCount(candidate.Name ?? string.Empty);
512
+ metadataBytes += Encoding.UTF8.GetByteCount(candidate.ValueTypeName ?? string.Empty);
513
+ if (metadataBytes > AbsoluteMaxMemberMetadataUtf8Bytes) throw MetadataLimit("metadata bytes");
514
+ candidates.Add(candidate);
446
515
  }
447
516
 
448
- private static SerializedMember ErrorMember(string path, string message)
517
+ private static BridgeCommandException MetadataLimit(string dimension)
518
+ {
519
+ return new BridgeCommandException("E_REQUEST_BUDGET_EXCEEDED", "Object member metadata exceeded its bounded " + dimension + ".", "Use paths, nameFilters, or a narrower object type.");
520
+ }
521
+
522
+ private static void Truncate(SerializedMember member, UnityCliRequestBudget budget, string nextPath)
449
523
  {
450
- return new SerializedMember { Path = path, Error = message, Truncated = true };
524
+ member.Truncated = true;
525
+ member.NextPath = nextPath;
526
+ if (member.Error == null && budget.IsExhausted) member.Error = BudgetError(budget);
451
527
  }
452
528
 
453
- private static int RuntimeHelpersHashCode(object obj)
529
+ private static void TruncateNonResumableEnumerable(SerializedMember member, UnityCliRequestBudget budget)
454
530
  {
455
- return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
531
+ member.Truncated = true;
532
+ member.NextPath = null;
533
+ member.Error = budget.IsExhausted ? BudgetError(budget) + ";non-resumable-enumerable" : "non-resumable-enumerable-truncation";
456
534
  }
457
535
 
458
- /// <summary>构造 Unity Object 引用摘要值树。</summary>
459
- public static Dictionary<string, object> UnityObjectRefJson(UnityEngine.Object uo)
536
+ private static string BudgetError(UnityCliRequestBudget budget) { return "request-budget-exhausted:" + (budget.ExhaustedDimension ?? "unknown"); }
537
+
538
+ private static bool MemberAllowed(string name, Options options)
539
+ {
540
+ if (options.NameFilters != null && options.NameFilters.Count > 0) return options.NameFilters.Contains(name);
541
+ if (string.IsNullOrEmpty(options.NamePattern)) return true;
542
+ try { return Regex.IsMatch(name, options.NamePattern, RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(50)); }
543
+ catch (RegexMatchTimeoutException) { throw new BridgeCommandException("E_OBJECT_READ_PATTERN_INVALID", "namePattern exceeded the bounded regex evaluation time.", "Use a simpler anchored namePattern."); }
544
+ }
545
+
546
+ private static bool TypeAllowed(string typeName, Options options)
547
+ {
548
+ return string.IsNullOrEmpty(options.TypeFilter) || (!string.IsNullOrEmpty(typeName) && typeName.IndexOf(options.TypeFilter, StringComparison.OrdinalIgnoreCase) >= 0);
549
+ }
550
+
551
+ private static bool IsSimpleType(Type type)
460
552
  {
553
+ return type.IsPrimitive || type.IsEnum || type == typeof(decimal) || type == typeof(DateTime) || type == typeof(TimeSpan)
554
+ || type == typeof(Guid) || type == typeof(Vector2) || type == typeof(Vector3) || type == typeof(Vector4)
555
+ || type == typeof(Quaternion) || type == typeof(Color) || type == typeof(Rect) || type == typeof(Bounds) || type == typeof(LayerMask);
556
+ }
557
+
558
+ private static SerializedMember ErrorMember(string path, string message)
559
+ {
560
+ return new SerializedMember { Path = path, TypeName = "error", Error = message, Truncated = true, NextPath = path };
561
+ }
562
+
563
+ public static Dictionary<string, object> UnityObjectRefJson(UnityEngine.Object value, UnityCliRequestBudget budget)
564
+ {
565
+ if (!budget.TryConsumeGetter()) throw new UnityCliRequestBudgetExceededException(budget.ExhaustedDimension);
566
+ var name = value.name;
567
+ var typeName = value.GetType().FullName;
568
+ if (!budget.TryReserveSerializedUtf8(name) || !budget.TryReserveSerializedUtf8(typeName)) throw new UnityCliRequestBudgetExceededException(budget.ExhaustedDimension);
461
569
  var result = new Dictionary<string, object>
462
570
  {
463
- { "instanceId", (long)uo.GetInstanceID() },
464
- { "name", uo.name },
465
- { "typeName", uo.GetType().FullName }
571
+ { "instanceId", (long)value.GetInstanceID() }, { "name", name }, { "typeName", typeName }
466
572
  };
467
- if (uo is GameObject go)
573
+ if (value is GameObject gameObject)
468
574
  {
469
575
  result["kind"] = "GameObject";
470
- result["path"] = GameObjectFullPath(go);
576
+ result["path"] = GameObjectFullPath(gameObject, budget);
471
577
  }
472
578
  else
473
579
  {
474
- result["kind"] = uo is Component ? "Component" : "Asset";
475
- var assetPath = AssetDatabase.GetAssetPath(uo);
476
- if (!string.IsNullOrEmpty(assetPath))
477
- {
478
- result["assetPath"] = NormalizeAssetPath(assetPath);
479
- result["guid"] = AssetDatabase.AssetPathToGUID(assetPath);
480
- }
580
+ result["kind"] = value is Component ? "Component" : "Asset";
581
+ if (!budget.TryConsumeGetter()) throw new UnityCliRequestBudgetExceededException(budget.ExhaustedDimension);
582
+ var assetPath = AssetDatabase.GetAssetPath(value);
583
+ if (!string.IsNullOrEmpty(assetPath)) { result["assetPath"] = NormalizeAssetPath(assetPath); result["guid"] = AssetDatabase.AssetPathToGUID(assetPath); }
481
584
  }
482
585
  return result;
483
586
  }
484
587
 
485
- private static string GameObjectFullPath(GameObject go)
588
+ private static string GameObjectFullPath(GameObject gameObject, UnityCliRequestBudget budget)
486
589
  {
487
- var path = go.name;
488
- var t = go.transform.parent;
489
- while (t != null)
590
+ if (!budget.TryConsumeGetter()) throw new UnityCliRequestBudgetExceededException(budget.ExhaustedDimension);
591
+ var path = gameObject.name;
592
+ var current = gameObject.transform.parent;
593
+ while (current != null)
490
594
  {
491
- path = t.name + "/" + path;
492
- t = t.parent;
595
+ if (!budget.TryConsumeNode() || !budget.TryConsumeGetter()) throw new UnityCliRequestBudgetExceededException(budget.ExhaustedDimension);
596
+ path = current.name + "/" + path;
597
+ current = current.parent;
493
598
  }
494
599
  return path;
495
600
  }
496
601
 
497
- // 将内部 SerializedMember 模型投影为稳定的 AnyJson 值树。
498
602
  private static Dictionary<string, object> MemberToData(SerializedMember member)
499
603
  {
500
- var data = new Dictionary<string, object>
501
- {
502
- { "path", member.Path },
503
- { "typeName", member.TypeName }
504
- };
604
+ var data = new Dictionary<string, object> { { "path", member.Path }, { "typeName", member.TypeName } };
505
605
  if (member.Error != null) data["error"] = member.Error;
506
606
  if (member.Truncated) data["truncated"] = true;
607
+ if (member.NextPath != null) data["nextPath"] = member.NextPath;
507
608
  if (member.IsNull) data["value"] = null;
508
609
  else if (member.Value != null) data["value"] = ValueData(member.Value);
509
610
  if (member.IsUnityObject) data["unityObjectRef"] = member.UnityObjectRef;
510
611
  if (member.IsArray)
511
612
  {
512
613
  data["isArray"] = true;
513
- data["arrayLength"] = (long)member.ArrayLength;
514
- data["totalCount"] = (long)member.ArrayLength;
614
+ if (member.HasKnownTotal) data["totalCount"] = (long)member.ArrayLength;
515
615
  data["returnedCount"] = (long)member.ArrayItems.Count;
516
- if (member.Truncated) data["nextPath"] = member.Path + "[" + member.ArrayItems.Count.ToString(CultureInfo.InvariantCulture) + "]";
517
- data["items"] = member.ArrayItems.Select(MemberToData).Cast<object>().ToList();
616
+ var items = new List<object>();
617
+ foreach (var item in member.ArrayItems) items.Add(MemberToData(item));
618
+ data["items"] = items;
518
619
  }
519
620
  if (member.IsDictionary)
520
621
  {
521
622
  data["isDictionary"] = true;
522
- data["totalCount"] = (long)member.ArrayLength;
623
+ if (member.HasKnownTotal) data["totalCount"] = (long)member.ArrayLength;
523
624
  data["returnedCount"] = (long)member.DictionaryEntries.Count;
524
- data["entries"] = member.DictionaryEntries.Select(entry => (object)new Dictionary<string, object>
525
- {
526
- { "key", entry.Key }, { "value", MemberToData(entry.Value) }
527
- }).ToList();
625
+ var entries = new List<object>();
626
+ foreach (var entry in member.DictionaryEntries) entries.Add(new Dictionary<string, object> { { "key", entry.Key }, { "value", MemberToData(entry.Value) } });
627
+ data["entries"] = entries;
528
628
  }
529
629
  if (member.Fields.Count > 0)
530
630
  {
531
- data["fields"] = member.Fields.Select(field => (object)new Dictionary<string, object>
532
- {
533
- { "name", field.Key }, { "value", MemberToData(field.Value) }
534
- }).ToList();
631
+ var fields = new List<object>();
632
+ foreach (var field in member.Fields) fields.Add(new Dictionary<string, object> { { "name", field.Key }, { "value", MemberToData(field.Value) } });
633
+ data["fields"] = fields;
535
634
  }
536
635
  if (member.Props.Count > 0)
537
636
  {
538
- data["props"] = member.Props.Select(property =>
637
+ var properties = new List<object>();
638
+ foreach (var property in member.Props)
539
639
  {
540
- var item = new Dictionary<string, object>
541
- {
542
- { "name", property.Key }, { "value", MemberToData(property.Value) }
543
- };
640
+ var item = new Dictionary<string, object> { { "name", property.Key }, { "value", MemberToData(property.Value) } };
544
641
  if (property.Value.ReadOnly) item["readOnly"] = true;
545
642
  if (property.Value.Writable) item["writable"] = true;
546
- return (object)item;
547
- }).ToList();
643
+ properties.Add(item);
644
+ }
645
+ data["props"] = properties;
548
646
  }
549
647
  return data;
550
648
  }
@@ -555,20 +653,14 @@ namespace UnityCli
555
653
  if (value is Vector2 v2) return new Dictionary<string, object> { { "x", JsonNumberData(v2.x) }, { "y", JsonNumberData(v2.y) } };
556
654
  if (value is Vector3 v3) return new Dictionary<string, object> { { "x", JsonNumberData(v3.x) }, { "y", JsonNumberData(v3.y) }, { "z", JsonNumberData(v3.z) } };
557
655
  if (value is Vector4 v4) return new Dictionary<string, object> { { "x", JsonNumberData(v4.x) }, { "y", JsonNumberData(v4.y) }, { "z", JsonNumberData(v4.z) }, { "w", JsonNumberData(v4.w) } };
558
- if (value is Quaternion q) return new Dictionary<string, object> { { "x", JsonNumberData(q.x) }, { "y", JsonNumberData(q.y) }, { "z", JsonNumberData(q.z) }, { "w", JsonNumberData(q.w) } };
559
- if (value is Color c) return new Dictionary<string, object> { { "r", JsonNumberData(c.r) }, { "g", JsonNumberData(c.g) }, { "b", JsonNumberData(c.b) }, { "a", JsonNumberData(c.a) } };
656
+ if (value is Quaternion quaternion) return new Dictionary<string, object> { { "x", JsonNumberData(quaternion.x) }, { "y", JsonNumberData(quaternion.y) }, { "z", JsonNumberData(quaternion.z) }, { "w", JsonNumberData(quaternion.w) } };
657
+ if (value is Color color) return new Dictionary<string, object> { { "r", JsonNumberData(color.r) }, { "g", JsonNumberData(color.g) }, { "b", JsonNumberData(color.b) }, { "a", JsonNumberData(color.a) } };
560
658
  if (value is float single) return JsonNumberData(single);
561
659
  if (value is double number) return JsonNumberData(number);
562
- if (value is string || value is bool || value is byte || value is sbyte || value is short || value is ushort
563
- || value is int || value is uint || value is long || value is ulong || value is decimal)
564
- {
565
- return value;
566
- }
567
- // AnyJson 无法表达的 CLR 值必须显式标记,禁止 ToString() 制造可解析假象。
660
+ if (value is string || value is bool || value is byte || value is sbyte || value is short || value is ushort || value is int || value is uint || value is long || value is ulong || value is decimal) return value;
568
661
  return UnsupportedValue(value.GetType(), "unsupported-dynamic-value");
569
662
  }
570
663
 
571
- /// <summary>AnyJson 中以稳定字符串表达 CLR 非有限数;有限值仍保持 JSON number。</summary>
572
664
  private static object JsonNumberData(double value)
573
665
  {
574
666
  if (double.IsNaN(value)) return "NaN";
@@ -579,15 +671,34 @@ namespace UnityCli
579
671
 
580
672
  private static Dictionary<string, object> UnsupportedValue(Type type, string reason)
581
673
  {
582
- return new Dictionary<string, object>
674
+ return new Dictionary<string, object> { { "unsupported", true }, { "reason", reason }, { "typeName", type == null ? null : type.FullName } };
675
+ }
676
+
677
+ private sealed class ReferenceComparer : IEqualityComparer<object>
678
+ {
679
+ internal static readonly ReferenceComparer Instance = new ReferenceComparer();
680
+ public new bool Equals(object left, object right) { return ReferenceEquals(left, right); }
681
+ public int GetHashCode(object value) { return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(value); }
682
+ }
683
+
684
+ private sealed class MemberCandidate
685
+ {
686
+ internal MemberCandidate(MemberInfo member, bool isProperty, bool canWrite, Type valueType)
583
687
  {
584
- { "unsupported", true },
585
- { "reason", reason },
586
- { "typeName", type == null ? null : type.FullName }
587
- };
688
+ Member = member;
689
+ IsProperty = isProperty;
690
+ CanWrite = canWrite;
691
+ Name = member.Name;
692
+ ValueTypeName = valueType == null ? null : valueType.FullName;
693
+ }
694
+
695
+ internal MemberInfo Member { get; private set; }
696
+ internal bool IsProperty { get; private set; }
697
+ internal bool CanWrite { get; private set; }
698
+ internal string Name { get; private set; }
699
+ internal string ValueTypeName { get; private set; }
588
700
  }
589
701
 
590
- /// <summary>SerializedMember 内存模型。</summary>
591
702
  private sealed class SerializedMember
592
703
  {
593
704
  public string Path { get; set; }
@@ -598,8 +709,10 @@ namespace UnityCli
598
709
  [UnityCliAnyJson] public object UnityObjectRef { get; set; }
599
710
  public bool IsArray { get; set; }
600
711
  public bool IsDictionary { get; set; }
712
+ public bool HasKnownTotal { get; set; }
601
713
  public int ArrayLength { get; set; }
602
714
  public bool Truncated { get; set; }
715
+ public string NextPath { get; set; }
603
716
  public string Error { get; set; }
604
717
  public bool ReadOnly { get; set; }
605
718
  public bool Writable { get; set; }
@@ -607,12 +720,7 @@ namespace UnityCli
607
720
  public List<KV> Props { get; } = new List<KV>();
608
721
  public List<SerializedMember> ArrayItems { get; } = new List<SerializedMember>();
609
722
  public List<KV> DictionaryEntries { get; } = new List<KV>();
610
-
611
- public sealed class KV
612
- {
613
- public string Key { get; set; }
614
- public SerializedMember Value { get; set; }
615
- }
723
+ public sealed class KV { public string Key { get; set; } public SerializedMember Value { get; set; } }
616
724
  }
617
725
  }
618
726
  }