cocos-web-inspector-mcp 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,7 @@ The current MVP supports:
12
12
  - Scene, node, component, and public-property inspection
13
13
  - Temporary DOM-only node highlighting
14
14
 
15
- By default it does not edit Cocos game state, launch browsers, expose browser network/storage data, or support Firefox/WebKit. Runtime mutations require the explicit `--allow-runtime-mutation` startup flag and affect the attached web build only.
15
+ By default it does not edit Cocos game state, launch browsers, expose browser console/network/storage data, or support Firefox/WebKit. Runtime mutations require the explicit `--allow-runtime-mutation` startup flag and affect the attached web build only. Console, network, and storage tools require the separate `--allow-browser-data` flag.
16
16
 
17
17
  ## Requirements
18
18
 
@@ -45,6 +45,27 @@ chrome.exe --remote-debugging-address=127.0.0.1 --remote-debugging-port=9222 --u
45
45
 
46
46
  Keep this browser profile separate from normal browsing. CDP provides code-execution-level access to attached pages.
47
47
 
48
+ ### Native builds (Android)
49
+
50
+ A Cocos native build with **Debug** checked runs a V8 inspector inside the app; release builds do not. Attach to it over adb instead of Chromium:
51
+
52
+ ```sh
53
+ adb logcat -d -s Cocos | grep "Debugger listening" -A1 # prints ws=IP_ADDR_OF_THIS_DEVICE:<port>/...
54
+ adb forward tcp:<port> tcp:<port>
55
+ npx --yes cocos-web-inspector-mcp --native-endpoint http://127.0.0.1:<port>
56
+ ```
57
+
58
+ The template asks for port 6086, but on Android the engine moves it above 37000 when it cannot list network interfaces (43086 on a Creator 3.8.8 build), so read the port from logcat. `--native-endpoint` must be loopback, and it replaces `--cdp-endpoint` for that server process.
59
+
60
+ The V8 inspector has no page, so native mode registers only the tools that read or change the Cocos graph, plus adb-backed input and logs:
61
+
62
+ - `cocos_click_node` and `cocos_drag_node` tap and swipe with `adb shell input`, at the node's viewport center, which equals screen pixels for a full-screen game. `steps` does not apply to a native drag. Set `ANDROID_SERIAL` when several devices are attached.
63
+ - `cocos_type_text` taps the EditBox, waits for the engine's Android input activity (`CocosEditBoxActivity`), clears it, and types with `adb shell input text`, so `text-changed` fires per character. Text must be printable ASCII without `%s`. The device keyboard still processes the keys, so autocorrect or a Vietnamese Telex layout can rewrite them (`test` becomes `tét`); the result reports `matches: false` with the text the game received. `submit` presses Enter, which closes a single-line box with `editing-did-ended`; `editing-return` fires only from the activity's confirm button. Clearing uses `input keycombination`, Android 12+.
64
+ - `cocos_console_messages` (with `--allow-browser-data`) reads the app process's `Cocos`-tagged logcat lines, so it includes logs from before the server attached, bounded by the device log buffer. `console.log` and `console.debug` both report as `log`; an uncaught error spans several `error` lines.
65
+ - Not registered: `cocos_capture_node`, `cocos_emulate_device`, `cocos_highlight_node`, `cocos_get_selection` (DOM overlays draw nothing), `cocos_analyze_batches` (the native 2D batcher runs in C++), `cocos_step_frame` (stepping from the inspector renders outside the native frame loop and crashed the app), and the network and storage tools.
66
+
67
+ The inspector accepts one session at a time, so the server connects per call; Chrome DevTools can attach between calls, and a call made while DevTools is attached fails with `CDP_UNAVAILABLE`. If calls start failing with `CDP_UNAVAILABLE` while the app runs, the adb server restarted and dropped the forward; run `adb forward` again.
68
+
48
69
  ## Install and run
49
70
 
50
71
  Run the published npm package with the default CDP endpoint, `http://127.0.0.1:9222`:
@@ -73,7 +94,9 @@ Endpoint precedence is:
73
94
  2. `COCOS_CDP_ENDPOINT`
74
95
  3. `http://127.0.0.1:9222`
75
96
 
76
- `--cdp-endpoint` and `--allow-runtime-mutation` are the only server options; `launch` and `doctor` are separate commands. `--cdp-endpoint` identifies the browser CDP endpoint, not a game page URL.
97
+ `--native-endpoint` attaches to a native debug build instead (see Native builds).
98
+
99
+ `--cdp-endpoint`, `--native-endpoint`, and the `--allow-*` flags are the server options; `launch` and `doctor` are separate commands. `--cdp-endpoint` identifies the browser CDP endpoint, not a game page URL.
77
100
 
78
101
  The server uses stdio for MCP. Standard output is reserved for protocol traffic; startup diagnostics are written to standard error.
79
102
 
@@ -113,9 +136,11 @@ The endpoint may instead be provided through the MCP process environment:
113
136
  }
114
137
  ```
115
138
 
139
+ Add `--allow-runtime-mutation` for debugger tools, `--allow-browser-data` for console, network, and storage tools, and `--allow-method-call` for `cocos_call_method`; each flag registers only its own tools.
140
+
116
141
  ## Tools
117
142
 
118
- All tool input objects are strict. Unknown fields are rejected. Inspection tools are annotated as read-only, idempotent, non-destructive, and closed-world. The highlight tool is annotated as a non-destructive, non-idempotent, closed-world temporary mutation because repeated calls reset its removal timer. Runtime debugger tools are non-read-only and closed-world; `cocos_click_node` is additionally destructive and open-world because game click handlers can reach real servers or make irreversible changes, and it and `cocos_step_frame` are non-idempotent.
143
+ All tool input objects are strict. Unknown fields are rejected. Inspection tools are annotated as read-only, idempotent, non-destructive, and closed-world. The highlight tool is annotated as a non-destructive, non-idempotent, closed-world temporary mutation because repeated calls reset its removal timer. Runtime debugger tools are non-read-only and closed-world; `cocos_click_node`, `cocos_drag_node`, and `cocos_type_text` are additionally destructive and open-world because game input handlers can reach real servers or make irreversible changes, and they, `cocos_step_frame`, and `cocos_analyze_batches` are non-idempotent.
119
144
 
120
145
  | Tool | Purpose | Inputs |
121
146
  | --- | --- | --- |
@@ -125,10 +150,14 @@ All tool input objects are strict. Unknown fields are rejected. Inspection tools
125
150
  | `cocos_set_node_active` | Set one node's active state. Registered only with `--allow-runtime-mutation`; returns before/after state. | `pageUrl?`; `uuid`; `active` boolean |
126
151
  | `cocos_set_transform` | Update supplied position, rotation, and/or scale fields for one node. | `pageUrl?`; `uuid`; `position?`, `rotation?`, `scale?` finite vectors |
127
152
  | `cocos_set_property` | Update one bounded public component data property. | `pageUrl?`; node `uuid`; `componentUuid`; `key`; primitive/vector/size/color `value` matching current shape |
128
- | `cocos_click_node` | Dispatch a real mouse click at the visible center of one UI node so Button/touch handlers run. Registered only with `--allow-runtime-mutation`. | `pageUrl?`; `uuid` |
153
+ | `cocos_click_node` | Dispatch a real click at the visible center of one UI node so Button/touch handlers run (a tap under mobile emulation). Registered only with `--allow-runtime-mutation`. | `pageUrl?`; `uuid` |
154
+ | `cocos_drag_node` | Drag from the visible center of one UI node with real pointer input (touch under mobile emulation), to scroll a ScrollView, flip a PageView, or move a Slider. | `pageUrl?`; `uuid`; `dx`, `dy` CSS pixels `-4000..4000`, not both zero; `steps?` `1..60`, default `10`; `durationMs?` `0..5000`, default `300` |
155
+ | `cocos_type_text` | Tap one EditBox and type with real keyboard input, replacing its content, so `text-changed` and editing events fire. Returns the resulting text, or `redacted` for password boxes. | `pageUrl?`; `uuid`; `text` up to 2,000 chars; `submit?` presses Enter |
156
+ | `cocos_analyze_batches` | Capture the 2D draw batches of the next rendered frame: the node that starts each batch and why the previous one broke. | `pageUrl?`; `limit?` `1..500`, default `100`; `tintMs?` `100..30000` overlays each batch in its own color |
129
157
  | `cocos_pause` | Pause the Cocos director when its public API supports it. | `pageUrl?` |
130
158
  | `cocos_resume` | Resume the Cocos director when its public API supports it. | `pageUrl?` |
131
159
  | `cocos_step_frame` | Advance a paused game by 1–60 fixed-delta frames through `cc.game.step`, then stay paused. Fails unless paused first. | `pageUrl?`, `frames?` |
160
+ | `cocos_set_time_scale` | Speed up or slow down the whole game by scaling each frame's delta time; `1` restores it, no `scale` reads it. Registered only with `--allow-runtime-mutation`. | `pageUrl?`; `scale?` `(0, 100]` |
132
161
  | `cocos_show_stats` | Show or hide the engine's FPS, draw-call, and triangle overlay through the public `profiler` API. | `pageUrl?`; `visible` boolean |
133
162
  | `cocos_emulate_device` | Emulate a mobile device like the Chrome device toolbar: viewport, DPR, touch (mouse input arrives as touch), user agent and `navigator.platform`, and orientation; optionally slow the CPU or network. Settings merge across calls. | `pageUrl?`; `preset?` (`iphone-se`, `iphone-14`, `iphone-14-pro-max`, `pixel-7`, `galaxy-s20`, `ipad-mini`) or `width`+`height` `200..4000` with `deviceScaleFactor?` `1..4` and `mobile?`; `orientation?`; `cpuSlowdown?` `1..20`; `network?` (`online`, `offline`, `slow-3g`, `fast-3g`, `fast-4g`); `reload?`; or `reset: true` |
134
163
  | `cocos_scene_tree` | Return a bounded scene tree with node and component summaries. | `pageUrl?`; `maxDepth?` integer `0..20`, default `6`; `maxNodes?` integer `1..5000`, default `500` |
@@ -140,10 +169,27 @@ All tool input objects are strict. Unknown fields are rejected. Inspection tools
140
169
  | `cocos_capture_node` | Return an in-memory viewport-clipped image for one visible UI node: PNG, falling back to JPEG when PNG exceeds the response limit; no file is written. | `pageUrl?`; `uuid` |
141
170
  | `cocos_get_properties` | Serialize public properties for a node or one component selected by type or UUID. | `pageUrl?`; `uuid`; `componentType?` or `componentUuid?`; `maxDepth?` integer `0..6`, default `3`; `0` returns top-level primitives |
142
171
  | `cocos_wait_for_property` | Poll one top-level property until it strictly equals a primitive value or the timeout passes. | `pageUrl?`; `uuid`; `componentType?` or `componentUuid?`; `key`; `equals`; `timeoutMs?` `100..30000`, default `5000`; `intervalMs?` `50..5000`, default `200` |
172
+ | `cocos_explain_click` | Explain whether a tap reaches a node, or what a tap at a viewport point would hit, by replaying the engine's touch dispatch order without dispatching anything: the claiming node, the hit stack, and reasons. | `pageUrl?`; `uuid` or `path`, and/or `x`+`y` viewport CSS pixels |
173
+ | `cocos_listener_report` | Report timers, update callbacks, tweens, and director/game/view listeners that outlive their owner: on destroyed or detached nodes and components, and unowned callbacks (arrow functions, `bind`) grouped by event and name. | `pageUrl?`; `limit?` `1..500`, default `100` |
174
+ | `cocos_dynamic_atlas` | Report the dynamic atlas: config, each page with its packed textures (position, size, a node that draws each), fill ratio and GPU bytes, and why visible sprites stayed out. | `pageUrl?`; `limit?` `1..500`, default `100` |
175
+ | `cocos_asset_report` | List assets in the asset cache with type, `refCount`, bundle, texture GPU bytes, and a `status`: `used` (a live renderer references it), `dependency` (reached from one, the scene, or a persist-root node), `builtin`, or `unused`. Unused assets sort first. | `pageUrl?`; `type?` asset class such as `Texture2D`; `unusedOnly?`; `limit?` `1..500`, default `100` |
176
+ | `cocos_call_method` | Call one public method of a node or component and return its bounded result or thrown error. Runs game code. Registered only with `--allow-method-call`. | `pageUrl?`; node `uuid`; `componentUuid?` (omit to call on the node); `method` identifier; `args?` up to 20 JSON values, where `{"$node": uuid}`, `{"$component": uuid}`, and `{"$asset": uuid}` pass live objects; `awaitMs?` `0..30000` waits for a returned promise; `maxDepth?` `0..6`, default `2` |
177
+ | `cocos_console_messages` | Recent console messages and uncaught page errors since the server attached, newest last, with secrets masked. Registered only with `--allow-browser-data`. | `pageUrl?`; `types?` (`log`, `debug`, `info`, `error`, `warning`, `assert`, `trace`, `pageerror`); `textContains?`; `limit?` `1..200`, default `50` |
178
+ | `cocos_network_requests` | Recent requests since the server attached: id, method, masked URL, resource type, status, failure, duration. | `pageUrl?`; `urlContains?`; `resourceType?`; `failedOnly?` (network failures and HTTP 4xx/5xx); `limit?` `1..200`, default `50` |
179
+ | `cocos_network_request` | One request by id: masked headers, status, timing, and with `includeBody` the request and text response bodies, masked by key, up to 20 KB. | `pageUrl?`; `id`; `includeBody?` |
180
+ | `cocos_storage` | `localStorage` or `sessionStorage` entries with secret-like keys and JSON fields masked, or cookie names and attributes without values. | `pageUrl?`; `area` (`local`, `session`, `cookies`); `keyContains?`; `limit?` `1..500`, default `100` |
143
181
  | `cocos_get_selection` | Return the node the user last Alt+clicked on the game canvas: node summary and path, viewport box, and the hit stack under the point (topmost first). The first call installs the picker. | `pageUrl?`; `disable?` removes the picker, overlay, and selection |
144
182
  | `cocos_highlight_node` | Draw a temporary pointer-transparent overlay around a UI node. | `pageUrl?`; `uuid`; `durationMs?` integer `100..10000`, default `2000` |
145
183
 
146
- `cocos_runtime_diagnostics` does not enable profiler/statistics systems. Render metrics are read from the values `Root` and the GFX device already update every frame (the same sources as `root.fps` and `device.numDrawCalls`), whether or not the profiler is shown; draw calls include the profiler overlay when it is visible. Generic invalid-reference checks still return `UNSUPPORTED_PUBLIC_API`. `cocos_get_selection` lets the user point at a node instead of describing it: it installs a capture-phase listener that consumes only Alt+clicks on the game canvas, so the game never sees them, and draws a labelled pointer-transparent overlay. It picks the topmost active node whose box contains the point and that draws or takes input (a 2D renderer such as Sprite or Label, or a Button, Toggle, EditBox, or Slider). Inactive nodes, layout-only containers, and nodes at zero opacity (including through a parent) are skipped, so invisible full-screen blockers do not swallow picks. The selection is reported `stale` once that node leaves the scene, and the picker disappears on reload. `cocos_highlight_node` temporarily mutates the page DOM only. It does not mutate the Cocos node/component graph or game state. `cocos_capture_node` clips only to the visible browser viewport, never falls back to full-page capture, bounds captures by the visible viewport and encoded response size rather than a fixed pixel cap, returns PNG (or JPEG fallback) base64 in-memory, downscales down to 0.25× (reported as `scale`) when JPEG quality steps are not enough, and rejects responses still oversized after that. Mutation results provide `before` values for manual inverse calls, but restoration cannot undo lifecycle callbacks or other runtime side effects. `cocos_step_frame` uses the public `cc.game.step` (fixed `game.frameTime` delta); because `director.tick` skips logic while the director is paused, it resumes the director only for the synchronous step call and pauses it again. `cocos_show_stats` is an explicit configuration change; while the overlay is visible, draw-call metrics include it. `cocos_emulate_device` holds a CDP session per page: emulation ends on `reset`, when the server disconnects, or when the server process exits, and `reset` also clears viewport overrides set by other CDP clients on that page. Cocos reads the user agent and touch support at startup, so pass `reload: true` for the game to see a new device class. Network throttling only delays traffic; it never reads requests or responses.
184
+ `cocos_runtime_diagnostics` does not enable profiler/statistics systems. Render metrics are read from the values `Root` and the GFX device already update every frame (the same sources as `root.fps` and `device.numDrawCalls`), whether or not the profiler is shown; draw calls include the profiler overlay when it is visible. Generic invalid-reference checks still return `UNSUPPORTED_PUBLIC_API`. `cocos_get_selection` lets the user point at a node instead of describing it: it installs a capture-phase listener that consumes only Alt+clicks on the game canvas, so the game never sees them, and draws a labelled pointer-transparent overlay. It picks the topmost active node whose box contains the point and that draws or takes input (a 2D renderer such as Sprite or Label, or a Button, Toggle, EditBox, or Slider). Inactive nodes, layout-only containers, and nodes at zero opacity (including through a parent) are skipped, so invisible full-screen blockers do not swallow picks. The selection is reported `stale` once that node leaves the scene, and the picker disappears on reload. `cocos_highlight_node` temporarily mutates the page DOM only. It does not mutate the Cocos node/component graph or game state. `cocos_capture_node` clips only to the visible browser viewport, never falls back to full-page capture, bounds captures by the visible viewport and encoded response size rather than a fixed pixel cap, returns PNG (or JPEG fallback) base64 in-memory, downscales down to 0.25× (reported as `scale`) when JPEG quality steps are not enough, and rejects responses still oversized after that. Mutation results provide `before` values for manual inverse calls, but restoration cannot undo lifecycle callbacks or other runtime side effects. `cocos_step_frame` uses the public `cc.game.step` (fixed `game.frameTime` delta); because `director.tick` skips logic while the director is paused, it resumes the director only for the synchronous step call and pauses it again. `cocos_show_stats` is an explicit configuration change; while the overlay is visible, draw-call metrics include it. `cocos_emulate_device` holds a CDP session per page: emulation ends on `reset`, when the server disconnects, or when the server process exits, and `reset` also clears viewport overrides set by other CDP clients on that page. Cocos reads the user agent and touch support at startup, so pass `reload: true` for the game to see a new device class. The tool returns after the canvas has resized to the new viewport, so a following click or capture sees the new layout. Network throttling only delays traffic; it never reads requests or responses.
185
+
186
+ `cocos_type_text` exists because `EditBox.string` is an accessor that `cocos_set_property` refuses, and assigning it would skip the `text-changed` and `editing-did-ended` events that login forms listen for. It taps the EditBox, waits for the DOM input the engine opens, selects its content, and inserts the text through the browser keyboard. `submit` presses Enter: single-line boxes fire `editing-return` and close; multi-line boxes (the default `InputMode.ANY`) insert a newline instead. Password boxes (`InputFlag.PASSWORD`) report `redacted`, and `cocos_get_properties` omits their text. `cocos_drag_node` and `cocos_click_node` send mouse events normally and CDP touch events under mobile emulation, where Chrome would otherwise convert and never acknowledge the mouse input; Cocos only listens for touch once it detected a touch device at startup, so emulate with `reload: true` first.
187
+
188
+ `cocos_analyze_batches` explains draw calls. The 2D batcher merges consecutive renderers that share texture, material, stencil state, and layer into one draw call; anything else in between splits them. Batches are rebuilt every frame and keep no node reference, so the tool wraps the batcher's `commitComp`, `commitModel`, `commitMiddleware`, and `commitIA` for one rendered frame (from `EVENT_BEFORE_DRAW` to `EVENT_AFTER_DRAW`), then restores them. Each batch reports the node and component that started it and a reason: `TEXTURE`, `MATERIAL`, `STENCIL`, `LAYER`, `MASK` (a Mask enters a stencil level), `MODEL` (Graphics and UIMeshRenderer draw on their own), `MIDDLEWARE` (Spine or DragonBones that cannot merge), `CUSTOM_IA`, `BUFFER` (the vertex buffer filled up or the render data changed), `FIRST`, `STATE_RESET`, or `AFTER_MODEL`. With `tintMs`, a pointer-transparent DOM overlay outlines every node of each batch in that batch's color, labels the first node with its index and reason, and removes itself after `tintMs`; each batch reports its `color` and the result reports how many nodes were `tinted` (up to 1,000; nodes without a UI transform or off screen are skipped). The game must be running; a paused director renders no frames. Reasons are read from private `Batcher2D` fields verified on Creator 3.7.4 through 3.8.8, and `drawCalls` from the GFX device also includes non-2D passes and the stats overlay.
189
+
190
+ ### Node targets
191
+
192
+ Every tool that acts on one node takes either `uuid` or `path`, never both. A path is the absolute scene path that `cocos_find_node` and `cocos_scene_tree` report, such as `/LoginScene/Canvas/panLogin/btnLogin`. Paths survive scene reloads, while node UUIDs created at runtime change on each load, so agents can act in one call instead of looking the UUID up first. Sibling nodes may share a name, so a path that matches more than one node fails with `AMBIGUOUS_NODE` and lists the candidate UUIDs; no tool acts on an ambiguous path. `cocos_call_method` arguments accept `{"$path": path}` the same way.
147
193
 
148
194
  ### Page selection
149
195
 
@@ -188,13 +234,18 @@ CDP endpoints may use HTTP, HTTPS, WS, or WSS only on these loopback hosts:
188
234
 
189
235
  CDP endpoint credentials, query strings, and fragments are rejected. Page targets must use HTTP or HTTPS on loopback.
190
236
 
191
- The server intentionally exposes no tools for:
237
+ The server never exposes arbitrary JavaScript evaluation or cookie values. `cocos_call_method` (only with `--allow-method-call`) runs existing public methods of game objects; it cannot run new code, refuses `_`-prefixed, secret-like, `constructor`, and `destroy` members, and returns results through the property serializer, so getters are not invoked and secret-like keys are dropped. A called method still runs with the game's full privileges.
192
238
 
193
- - Arbitrary JavaScript evaluation
194
- - Cookies or storage state
195
- - Request/response bodies or headers
196
- - Authorization headers
197
- - Network or console logs
239
+ Console messages, network requests, and storage are available only with `--allow-browser-data`, as `cocos_console_messages`, `cocos_network_requests`, `cocos_network_request`, and `cocos_storage`. Without the flag those tools are not registered. With it, redaction is best effort and runs before data leaves the server:
240
+
241
+ - Authorization, cookie, set-cookie, API-key, and CSRF headers are replaced with `[redacted]`.
242
+ - URL credentials are dropped and secret-like query parameters masked.
243
+ - JSON and form bodies, and JSON storage values, are masked by key name with the same secret-like key list as property serialization.
244
+ - Free text (console messages, plain-text bodies) is masked for JWTs, `Bearer`/`Basic` tokens, and `key=value` or `"key": value` pairs with secret-like keys.
245
+ - Cookie values are never returned; names and attributes are.
246
+ - Bodies are text only and capped at 20 KB; binary responses report their content type.
247
+
248
+ Secrets in free-form text that match none of these patterns can still be returned. Use the flag only against a disposable profile and test accounts, and treat everything these tools return as untrusted page output.
198
249
 
199
250
  Inspection still executes fixed bridge code inside the attached page. Treat all page content as untrusted data, including text that resembles instructions or prompt injection.
200
251
 
@@ -221,6 +272,20 @@ npm run check
221
272
 
222
273
  The test suite covers URL policy, CDP connection reuse and recovery, scene traversal, property redaction and cycle handling, output bounds, Cocos version rejection, strict tool schemas, exact tool annotations, real page selection, inspector and debugger tools, visual bounds/capture, snapshots, diagnostics, and browser reconnection. CI installs the matching Playwright Chromium revision, runs the full check on Ubuntu and Windows with Node.js 20, 22, and 24, and rejects high-severity production dependency advisories.
223
274
 
275
+ ### Tap and callback debugging
276
+
277
+ `cocos_explain_click` answers "why does tapping this do nothing". It rebuilds the order the engine's pointer dispatcher uses (higher camera priority first, then nodes drawn later before what they cover), runs the engine's own `UITransform.hitTest` (which also applies Masks) on every node listening for touch, and reports the first that would claim the tap. Reasons: `INACTIVE`, `ZERO_SIZE`, `OUTSIDE_VIEWPORT`, `BUTTON_NOT_INTERACTABLE`, `BUTTON_DISABLED`, `NO_TOUCH_LISTENER`, `BLOCKED_BY_BLOCK_INPUT_EVENTS`, `COVERED_BY_OTHER_NODE`, and `MASKED_OR_OUTSIDE_HIT_AREA`. A tap claimed by a descendant still counts, because touch events bubble up to the target. Nothing is dispatched.
278
+
279
+ `cocos_listener_report` complements `cocos_asset_report`. The engine purges callbacks bound to a destroyed Cocos object on its own (listeners on the next emit, tweens on the next frame, component timers on destroy), so those rarely leak. What it can never purge is a callback whose target is not a Cocos object: an arrow function or `bind(this)` registered on `director`, `game`, `view`, or `systemEvent` each time a popup opens keeps the popup's closure alive and runs again. The report groups such unowned callbacks by kind, event, and function name with a count; compare two calls around opening and closing a popup, and a growing count is the leak. Callbacks on destroyed objects are listed as `destroyed`, and those on nodes outside the scene as `detached`, which pooled nodes legitimately are.
280
+
281
+ `cocos_set_time_scale` multiplies the delta time `director.tick` passes to components, the scheduler, tweens, animation, physics, and rendering, so the whole game runs faster or slower; frame stepping with `cocos_step_frame` still uses the fixed frame time. Reload or `scale: 1` restores normal speed.
282
+
283
+ ### Dynamic atlas and asset leaks
284
+
285
+ `cocos_dynamic_atlas` answers why sprites do or do not batch through the dynamic atlas. Each page lists its packed textures with their position, size, and one node that draws each, its `fill` (packed texture area over page area) and `shelfFill` (the height the shelf packer has used), and its GPU bytes. Runtime textures, such as Label text rendered in bitmap cache mode, have no asset uuid and report `runtime: true`. For every visible sprite outside the atlas it gives the reason, checked in the engine's own order: `DISABLED`, `NOT_TEXTURE2D`, `COMPRESSED`, `TOO_LARGE` (over `maxFrameSize`), `NOT_PACKABLE`, `FILTER` (not linear, or mipmapped), `ATLAS_FULL`, or `NOT_YET_RENDERED`. Pages are never compacted: removed textures leave holes until the atlas resets on scene change, so a high `shelfFill` with a low `fill` means wasted space.
286
+
287
+ `cocos_asset_report` is for leak hunting. One call is a snapshot, so compare two: take one, open and close the popup or scene under test, take another, and look for assets whose status is `unused` or whose `refCount` grew. An `unused` asset is still cached although no active renderer, scene dependency, or persist-root node reaches it; it usually means a missing `decRef` or `releaseAsset`. Asset names come from `_name` and are empty in release builds, so identify assets by `uuid`, `type`, and `bundle`. `gpuMemory` comes from the GFX device and covers every texture and buffer, including atlases and render targets the asset cache does not list. Both tools are read-only: they read backing fields such as `_ref`, `_atlases`, and `Cache._map`, call no getters except the side-effect-free `isCompressed`, and verified on Creator 3.7.4 through 3.8.8.
288
+
224
289
  ## Troubleshooting
225
290
 
226
291
  - `CDP_UNAVAILABLE`: start Chromium with loopback remote debugging; rerun `npm run install:chromium` for development tests. A `404` usually means Chrome's built-in remote debugging toggle (`chrome://inspect/#remote-debugging`) holds the port; turn it off or use another port, and check listeners with `lsof -nP -iTCP:9222 -sTCP:LISTEN`. When `127.0.0.1` returns 404 or refuses the connection, the server retries `[::1]` on the same port.
@@ -230,6 +295,15 @@ The test suite covers URL policy, CDP connection reuse and recovery, scene trave
230
295
  - Bounds/capture unavailable: select a visible UI node with `UITransform`. `INACTIVE` means the node or an ancestor is inactive. Capture is viewport-only, bounded, and never falls back to a full-page screenshot; `RESPONSE_LIMIT` means even a 0.25× JPEG exceeded the response budget, so capture a smaller node.
231
296
  - `cocos_snapshot_subtree` returns `RESPONSE_LIMIT` with a partial tree: snapshot a deeper node, or lower `maxDepth`.
232
297
  - `cocos_step_frame` returns `INVALID_MUTATION`: call `cocos_pause` first.
298
+ - `cocos_explain_click` says `NO_TOUCH_LISTENER` for a node that reacts in the game: the game may listen on an ancestor or through a global `input.on`; check the ancestors in `hitStack` or explain the tap by `x`/`y`.
299
+ - `cocos_call_method` returns `pending: true`: the method returned a promise; pass `awaitMs` to wait for it.
300
+ - `cocos_console_messages` or `cocos_network_requests` misses startup output: they list only what happened after the server attached; reload the page and call again.
301
+ - `cocos_network_request` returns `REQUEST_NOT_FOUND`: Playwright drops old requests and bodies to bound memory; list requests again and inspect recent ones promptly.
302
+ - `cocos_asset_report` lists an asset as `unused` right after a scene change: the previous scene's assets stay cached until `autoReleaseAssets` or the game's own release runs; take the comparison snapshot a moment later.
303
+ - `cocos_dynamic_atlas` reports `NOT_YET_RENDERED`: the sprite packs on its first render; check again after a frame.
304
+ - `cocos_analyze_batches` returns `INVALID_MUTATION` with "no frame rendered": call `cocos_resume`; batches are captured from a live frame.
305
+ - `cocos_type_text` returns `NOT_FOCUSED`: the EditBox did not open its input, usually because another node covers it or it is disabled.
306
+ - `cocos_drag_node` moves nothing under mobile emulation: call `cocos_emulate_device` with `reload: true` so Cocos starts listening for touch.
233
307
  - `doctor` reports HTTP 404 on the port: Chrome's built-in remote debugging holds it; see `CDP_UNAVAILABLE` above.
234
308
  - Game still behaves like desktop after `cocos_emulate_device`: call it again with `reload: true`; Cocos detects mobile and touch at startup.
235
309
  - Render metrics `fps` reads 0: the engine publishes FPS once per elapsed second, so read again after the scene has run for a second.
@@ -67,6 +67,10 @@ export type BridgeRequest = {
67
67
  } | {
68
68
  action: 'showStats';
69
69
  visible: boolean;
70
+ } | {
71
+ action: 'analyzeBatches';
72
+ limit?: number | undefined;
73
+ tintMs?: number | undefined;
70
74
  } | {
71
75
  action: 'selection';
72
76
  disable?: boolean | undefined;
@@ -74,12 +78,40 @@ export type BridgeRequest = {
74
78
  action: 'pickAt';
75
79
  x: number;
76
80
  y: number;
81
+ } | {
82
+ action: 'dynamicAtlas';
83
+ limit?: number | undefined;
84
+ } | {
85
+ action: 'explainClick';
86
+ uuid?: string | undefined;
87
+ x?: number | undefined;
88
+ y?: number | undefined;
89
+ } | {
90
+ action: 'listenerReport';
91
+ limit?: number | undefined;
92
+ } | {
93
+ action: 'timeScale';
94
+ scale?: number | undefined;
95
+ } | {
96
+ action: 'callMethod';
97
+ uuid: string;
98
+ componentUuid?: string | undefined;
99
+ method: string;
100
+ args: MethodArgument[];
101
+ awaitMs?: number | undefined;
102
+ maxDepth?: number | undefined;
103
+ } | {
104
+ action: 'assetReport';
105
+ type?: string | undefined;
106
+ unusedOnly?: boolean | undefined;
107
+ limit?: number | undefined;
77
108
  };
78
109
  type Vector3 = {
79
110
  x: number;
80
111
  y: number;
81
112
  z: number;
82
113
  };
114
+ export type MethodArgument = unknown;
83
115
  type Quaternion = {
84
116
  x: number;
85
117
  y: number;
@@ -102,7 +134,15 @@ type PropertyValue = boolean | number | string | {
102
134
  };
103
135
  export declare function runBridge(page: Page, request: BridgeRequest): Promise<unknown>;
104
136
  export declare function captureNode(page: Page, uuid: string): Promise<unknown>;
137
+ export declare function nodeCenter(page: Page, uuid: string): Promise<{
138
+ x: number;
139
+ y: number;
140
+ } | {
141
+ reason: string;
142
+ }>;
105
143
  export declare function clickNode(page: Page, uuid: string): Promise<unknown>;
144
+ export declare function dragNode(page: Page, uuid: string, dx: number, dy: number, steps?: number, durationMs?: number): Promise<unknown>;
145
+ export declare function typeText(page: Page, uuid: string, text: string, submit: boolean): Promise<unknown>;
106
146
  export declare const devicePresets: {
107
147
  readonly 'iphone-se': {
108
148
  readonly width: 375;