#!/usr/bin/env python3 """Capture the generated Cocos Creator web-mobile runtime through Chrome CDP. The script validates the actual Cocos runtime scene before taking the screenshot. It intentionally does not read reference screenshots or infer layout values. """ from __future__ import annotations import argparse import base64 import json import shutil import socket import subprocess import tempfile import time import urllib.request from io import BytesIO from pathlib import Path from PIL import Image ROOT = Path(__file__).resolve().parents[1] TOP_HUD_SOURCE_JSON = ROOT / "source_export/stage1_top_hud_source.json" DEFAULT_URL = "http://127.0.0.1:8787/index.html" DEFAULT_OUT = ROOT / "artifacts/creator_capture/stage1_creator_capture.png" DEFAULT_REPORT = ROOT / "artifacts/creator_capture/stage1_creator_capture_report.json" EDGE = Path("/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge") CHROME = Path("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome") def browser_path() -> Path: if CHROME.exists(): return CHROME if EDGE.exists(): return EDGE found = shutil.which("Google Chrome") or shutil.which("Microsoft Edge") or shutil.which("chrome") if found: return Path(found) raise SystemExit("Could not find Chrome or Edge for headless Creator capture.") def find_free_port() -> int: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.bind(("127.0.0.1", 0)) return int(sock.getsockname()[1]) class CdpClient: def __init__(self, ws_url: str) -> None: try: import websocket # type: ignore except ImportError as exc: # pragma: no cover - depends on local env. raise SystemExit( "Missing websocket-client package; install or expose it before runtime capture." ) from exc self.ws = websocket.create_connection(ws_url, timeout=30) self.next_id = 1 self.events: list[dict] = [] def close(self) -> None: self.ws.close() def call(self, method: str, params: dict | None = None, timeout: float = 30.0) -> dict: msg_id = self.next_id self.next_id += 1 self.ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}})) deadline = time.time() + timeout while time.time() < deadline: raw = self.ws.recv() data = json.loads(raw) if data.get("id") == msg_id: if "error" in data: raise RuntimeError(f"CDP {method} failed: {data['error']}") return data.get("result", {}) self.events.append(data) raise TimeoutError(f"Timed out waiting for CDP response: {method}") def wait_for_target_ws(port: int) -> str: url = f"http://127.0.0.1:{port}/json/list" deadline = time.time() + 20 last_error: Exception | None = None while time.time() < deadline: try: with urllib.request.urlopen(url, timeout=1) as response: targets = json.loads(response.read().decode("utf-8")) for target in targets: if target.get("type") == "page" and target.get("webSocketDebuggerUrl"): return str(target["webSocketDebuggerUrl"]) except Exception as exc: # pragma: no cover - diagnostic fallback. last_error = exc time.sleep(0.2) raise RuntimeError(f"CDP page target was not ready: {last_error}") def extract_eval_value(result: dict) -> dict: value = result.get("result", {}).get("value") if not isinstance(value, dict): raise RuntimeError(f"Unexpected runtime evaluation result: {result}") return value def canvas_png_expression() -> str: return """ (async () => { const canvas = document.getElementById('GameCanvas') || document.querySelector('canvas'); if (!canvas) { return { ok: false, reason: 'missing_canvas' }; } try { if (window.cc) { if (cc.game && typeof cc.game.resume === 'function') { cc.game.resume(); } if (cc.director && typeof cc.director.resume === 'function') { cc.director.resume(); } } await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); } catch (error) { return { ok: false, reason: 'frame_wait_failed', error: String(error) }; } try { return { ok: true, width: canvas.width, height: canvas.height, clientWidth: canvas.clientWidth, clientHeight: canvas.clientHeight, dataUrl: canvas.toDataURL('image/png') }; } catch (error) { return { ok: false, reason: 'to_data_url_failed', error: String(error) }; } })() """ def validate_rendered_pixels(image: Image.Image, label: str) -> dict: sample = image.convert("RGB").resize((64, 64)) colors = sample.getcolors(maxcolors=4096) color_count = None if colors is None else len(colors) pixels = list(sample.getdata()) dominant_ratio = None if colors: dominant_ratio = max(count for count, _ in colors) / float(64 * 64) mean_luma = sum((0.2126 * red + 0.7152 * green + 0.0722 * blue) for red, green, blue in pixels) / len(pixels) if color_count is not None and color_count <= 12: raise RuntimeError( f"{label} has too few sampled colors for a rendered Cocos frame: " f"colorCount={color_count}, dominantRatio={dominant_ratio:.4f}, meanLuma={mean_luma:.2f}" ) if dominant_ratio is not None and dominant_ratio >= 0.85: raise RuntimeError( f"{label} is dominated by one sampled color: " f"colorCount={color_count}, dominantRatio={dominant_ratio:.4f}, meanLuma={mean_luma:.2f}" ) if mean_luma <= 8: raise RuntimeError( f"{label} is too dark for the recovered farm scene: " f"colorCount={color_count}, dominantRatio={dominant_ratio}, meanLuma={mean_luma:.2f}" ) return {"sampleColorCount": color_count, "dominantRatio": dominant_ratio, "meanLuma": mean_luma} def validate_png_frame(data: bytes, expected_width: int, expected_height: int, label: str) -> dict: with Image.open(BytesIO(data)) as image: if image.size != (expected_width, expected_height): raise RuntimeError(f"{label} size expected {(expected_width, expected_height)}, got {image.size}") validation = validate_rendered_pixels(image, label) return {"width": image.size[0], "height": image.size[1], **validation} def normalize_page_png_frame(data: bytes, expected_width: int, expected_height: int, label: str) -> tuple[bytes, dict]: with Image.open(BytesIO(data)) as image: if image.size != (expected_width, expected_height): raise RuntimeError(f"{label} size expected {(expected_width, expected_height)}, got {image.size}") validation = validate_rendered_pixels(image, label) return data, {"width": image.size[0], "height": image.size[1], **validation} def decode_canvas_png(value: dict, expected_width: int, expected_height: int) -> tuple[bytes, dict]: data_url = value.get("dataUrl") if not isinstance(data_url, str) or not data_url.startswith("data:image/png;base64,"): raise RuntimeError(f"Canvas capture did not return a PNG data URL: {value}") data = base64.b64decode(data_url.split(",", 1)[1]) validation = validate_png_frame(data, expected_width, expected_height, "Canvas capture") return data, validation def capture_page_png(client: CdpClient, expected_width: int, expected_height: int, from_surface: bool) -> tuple[bytes, dict]: screenshot = client.call( "Page.captureScreenshot", { "format": "png", "fromSurface": from_surface, "clip": {"x": 0, "y": 0, "width": expected_width, "height": expected_height, "scale": 1}, }, timeout=60, ) data = base64.b64decode(screenshot["data"]) return normalize_page_png_frame( data, expected_width, expected_height, f"Page.captureScreenshot(fromSurface={from_surface})", ) def capture_current_frame(client: CdpClient, expected_width: int, expected_height: int) -> tuple[bytes, dict]: try: client.call("Page.bringToFront") except Exception: pass canvas_capture = extract_eval_value( client.call( "Runtime.evaluate", {"returnByValue": True, "awaitPromise": True, "expression": canvas_png_expression()}, timeout=60, ) ) if canvas_capture.get("ok"): try: canvas_data, canvas_validation = decode_canvas_png(canvas_capture, expected_width, expected_height) return canvas_data, { "method": "canvas.toDataURL", "canvas": {key: canvas_capture.get(key) for key in ("width", "height", "clientWidth", "clientHeight")}, "validation": canvas_validation, } except Exception as exc: canvas_capture = {**canvas_capture, "decodeError": str(exc)} page_attempts: list[dict] = [] for from_surface in (False, True): try: page_data, page_validation = capture_page_png(client, expected_width, expected_height, from_surface) return page_data, { "method": "Page.captureScreenshot", "fromSurface": from_surface, "validation": page_validation, "canvasAttempt": {key: value for key, value in canvas_capture.items() if key != "dataUrl"}, "pageAttempts": page_attempts, } except Exception as exc: page_attempts.append({"fromSurface": from_surface, "error": str(exc)}) raise RuntimeError( "Could not capture a rendered Cocos frame: " + json.dumps( { "canvasAttempt": {key: value for key, value in canvas_capture.items() if key != "dataUrl"}, "pageAttempts": page_attempts, }, ensure_ascii=False, ) ) def dispatch_click(client: CdpClient, x: float, y: float) -> None: touch_point = {"x": x, "y": y, "radiusX": 1, "radiusY": 1, "force": 1, "id": 1} client.call("Input.dispatchTouchEvent", {"type": "touchStart", "touchPoints": [touch_point]}) client.call("Input.dispatchTouchEvent", {"type": "touchEnd", "touchPoints": []}) def js_string(value: str) -> str: return json.dumps(value, ensure_ascii=False) def load_json(path: Path) -> dict: return json.loads(path.read_text(encoding="utf-8")) def expected_top_hud_labels(top_hud_evidence: dict) -> dict[str, str]: mock_user = top_hud_evidence["mockUser"] return { "txtName": str(mock_user["nickname"]), "txtLevel": f" {mock_user['level']} ", "txtExp": f"{mock_user['exp']}/{mock_user['expMax']}", "txtGold": str(mock_user["goldDisplay"]), "txtDiamond": str(mock_user["diamond"]), } def expected_top_hud_progress(top_hud_evidence: dict) -> float: mock_user = top_hud_evidence["mockUser"] exp_max = float(mock_user["expMax"]) if exp_max <= 0: return 0.0 return max(0.0, min(1.0, float(mock_user["exp"]) / exp_max)) def view_info_expression() -> str: return """ function stage1ViewInfo() { const view = window.cc && cc.view; function size(value) { return value ? { width: value.width, height: value.height } : null; } function rect(value) { return value ? { x: value.x, y: value.y, width: value.width, height: value.height } : null; } let resolutionPolicy = null; if (view && typeof view.getResolutionPolicy === 'function') { const policy = view.getResolutionPolicy(); resolutionPolicy = policy ? { name: policy.constructor && policy.constructor.name ? policy.constructor.name : null, contentStrategy: policy._contentStrategy && policy._contentStrategy.constructor ? policy._contentStrategy.constructor.name : null, containerStrategy: policy._containerStrategy && policy._containerStrategy.constructor ? policy._containerStrategy.constructor.name : null, } : null; } return view ? { designResolutionSize: typeof view.getDesignResolutionSize === 'function' ? size(view.getDesignResolutionSize()) : null, visibleSize: typeof view.getVisibleSize === 'function' ? size(view.getVisibleSize()) : null, frameSize: typeof view.getFrameSize === 'function' ? size(view.getFrameSize()) : null, viewportRect: typeof view.getViewportRect === 'function' ? rect(view.getViewportRect()) : null, scaleX: typeof view.getScaleX === 'function' ? view.getScaleX() : view._scaleX, scaleY: typeof view.getScaleY === 'function' ? view.getScaleY() : view._scaleY, devicePixelRatio: typeof view.getDevicePixelRatio === 'function' ? view.getDevicePixelRatio() : null, resolutionPolicy, } : null; } """ def top_hud_state_expression() -> str: return """ (() => { const scene = window.cc && cc.director && cc.director.getScene && cc.director.getScene(); function find(root, name) { if (!root) return null; if (root.name === name) return root; for (const child of root.children || []) { const match = find(child, name); if (match) return match; } return null; } function path(root, childPath) { return childPath.split('/').reduce((current, part) => current ? current.getChildByName(part) : null, root); } const mainUi = find(scene, 'main_ui_v2'); const uiCameraNode = find(scene, 'uiCamera'); const uiCamera = uiCameraNode && uiCameraNode.getComponent(cc.Camera); if (uiCamera && uiCamera.camera && uiCamera.camera.update) { uiCamera.camera.update(true); } function label(childPath) { const node = mainUi ? path(mainUi, childPath) : null; const component = node && node.getComponent(cc.Label); return component ? component.string : null; } function nodeInfo(childPath) { const node = mainUi ? (childPath ? path(mainUi, childPath) : mainUi) : null; if (!node) return null; const ui = node.getComponent('cc.UITransform'); const sprite = node.getComponent(cc.Sprite); const widget = node.getComponent(cc.Widget); let bounds = null; if (ui && uiCamera) { const size = ui.contentSize; const anchor = ui.anchorPoint; const points = [ new cc.Vec3(-anchor.x * size.width, -anchor.y * size.height, 0), new cc.Vec3((1 - anchor.x) * size.width, -anchor.y * size.height, 0), new cc.Vec3(-anchor.x * size.width, (1 - anchor.y) * size.height, 0), new cc.Vec3((1 - anchor.x) * size.width, (1 - anchor.y) * size.height, 0), ].map((point) => uiCamera.worldToScreen(ui.convertToWorldSpaceAR(point))); const xs = points.map((point) => point.x); const ys = points.map((point) => point.y); bounds = { screenCenter: { x: (Math.max(...xs) + Math.min(...xs)) / 2, y: (Math.max(...ys) + Math.min(...ys)) / 2 }, screenWidth: Math.max(...xs) - Math.min(...xs), screenHeight: Math.max(...ys) - Math.min(...ys), sourceSize: { width: size.width, height: size.height }, anchor: { x: anchor.x, y: anchor.y }, }; } return { name: node.name, active: node.active, activeInHierarchy: node.activeInHierarchy, layer: node.layer, localPosition: { x: node.position.x, y: node.position.y, z: node.position.z }, worldPosition: { x: node.worldPosition.x, y: node.worldPosition.y, z: node.worldPosition.z }, hasSprite: !!sprite, uiTransform: ui ? { width: ui.contentSize.width, height: ui.contentSize.height, anchor: { x: ui.anchorPoint.x, y: ui.anchorPoint.y }, } : null, widget: widget ? { alignFlags: widget.alignFlags, alignMode: widget.alignMode, left: widget.left, right: widget.right, top: widget.top, bottom: widget.bottom, } : null, bounds, }; } const expNode = mainUi ? path(mainUi, 'HeadInfo/SubHeadInfo/expBar') : null; const progress = expNode && expNode.getComponent('cc.ProgressBar'); const maskNode = mainUi ? path(mainUi, 'HeadInfo/SubHeadInfo/headMask') : null; const mask = maskNode && maskNode.getComponent('cc.Mask'); return { exists: !!mainUi, active: !!(mainUi && mainUi.active), layer: mainUi ? mainUi.layer : null, labels: { txtName: label('HeadInfo/SubHeadInfo/txtName'), txtLevel: label('HeadInfo/SubHeadInfo/txtLevel'), txtExp: label('HeadInfo/SubHeadInfo/txtExp'), txtGold: label('Source/root/txtGold'), txtDiamond: label('Source/root/txtDiamond'), }, progress: progress ? { progress: progress.progress, totalLength: progress.totalLength } : null, mask: mask ? { type: mask.type, segments: mask.segments } : null, uiCamera: uiCamera ? { nodeWorldPosition: { x: uiCamera.node.worldPosition.x, y: uiCamera.node.worldPosition.y, z: uiCamera.node.worldPosition.z, }, orthoHeight: uiCamera.orthoHeight, internalOrthoHeight: uiCamera.camera ? uiCamera.camera.orthoHeight : null, internalWindow: uiCamera.camera && uiCamera.camera.window ? { width: uiCamera.camera.window.width, height: uiCamera.camera.window.height, } : null, visibility: uiCamera.visibility, } : null, nodes: { mainUi: nodeInfo(''), headInfo: nodeInfo('HeadInfo'), source: nodeInfo('Source'), btnExit: nodeInfo('HeadInfo/btnExit'), headDi: nodeInfo('HeadInfo/SubHeadInfo/head_di'), expBar: nodeInfo('HeadInfo/SubHeadInfo/expBar'), imgGold: nodeInfo('Source/root/img_gold'), imgDiamond: nodeInfo('Source/root/img_diamond'), } }; })() """ def interaction_state_expression() -> str: return """ (() => { %s const scene = window.cc && cc.director && cc.director.getScene && cc.director.getScene(); function find(root, name) { if (!root) return null; if (root.name === name) return root; for (const child of root.children || []) { const match = find(child, name); if (match) return match; } return null; } function path(root, childPath) { return childPath.split('/').reduce((current, part) => current ? current.getChildByName(part) : null, root); } const interaction = find(scene, 'plant_interactive_v2'); const cameraNode = find(scene, 'Camera'); const uiCameraNode = find(scene, 'uiCamera'); const camera = cameraNode && cameraNode.getComponent(cc.Camera); const uiCamera = uiCameraNode && uiCameraNode.getComponent(cc.Camera); const group = interaction ? path(interaction, 'followNode/seedGroup/group_5') : null; function parentChain(node) { const result = []; let current = node; while (current) { result.push({ name: current.name, layer: current.layer }); current = current.parent; } return result; } function cameraInfo(node, component) { if (!node || !component) return null; return { node: node.name, layer: node.layer, orthoHeight: component.orthoHeight, near: component.near, far: component.far, visibility: component.visibility, priority: component.priority, clearFlags: component.clearFlags, position: { x: node.position.x, y: node.position.y, z: node.position.z }, }; } function cameraForNode(node) { if (node && uiCamera && (uiCamera.visibility & node.layer)) return uiCamera; if (node && camera && (camera.visibility & node.layer)) return camera; return camera || uiCamera; } function projectedBounds(node) { const projectCamera = cameraForNode(node); if (!node || !projectCamera) return null; const ui = node.getComponent('cc.UITransform'); if (!ui) return null; const size = ui.contentSize; const anchor = ui.anchorPoint; const minX = -anchor.x * size.width; const maxX = (1 - anchor.x) * size.width; const minY = -anchor.y * size.height; const maxY = (1 - anchor.y) * size.height; const points = [ new cc.Vec3(minX, minY, 0), new cc.Vec3(maxX, minY, 0), new cc.Vec3(minX, maxY, 0), new cc.Vec3(maxX, maxY, 0), ].map((point) => { const world = ui.convertToWorldSpaceAR(point); return projectCamera.worldToScreen(world); }); const xs = points.map((point) => point.x); const ys = points.map((point) => point.y); return { active: node.active, activeInHierarchy: node.activeInHierarchy, layer: node.layer, localPosition: { x: node.position.x, y: node.position.y, z: node.position.z }, worldPosition: { x: node.worldPosition.x, y: node.worldPosition.y, z: node.worldPosition.z }, localScale: { x: node.scale.x, y: node.scale.y, z: node.scale.z }, worldScale: { x: node.worldScale.x, y: node.worldScale.y, z: node.worldScale.z }, camera: projectCamera === uiCamera ? 'uiCamera' : 'Camera', sourceSize: { width: size.width, height: size.height }, screenWidth: Math.max(...xs) - Math.min(...xs), screenHeight: Math.max(...ys) - Math.min(...ys), screenCenter: { x: (Math.max(...xs) + Math.min(...xs)) / 2, y: (Math.max(...ys) + Math.min(...ys)) / 2, }, }; } const slots = []; if (group) { for (let index = 1; index <= 5; index += 1) { const slot = group.getChildByName(`node${index}`); const count = slot ? path(slot, 'count') : null; const label = count && count.getComponent(cc.Label); slots.push({ name: `node${index}`, active: !!(slot && slot.active), count: label ? label.string : null, position: slot ? { x: slot.position.x, y: slot.position.y, z: slot.position.z } : null }); } } const landBubble = interaction ? (path(interaction, 'followNode/detailNode/land') || path(interaction, 'land_detail/land')) : null; const seedBg3 = interaction ? path(interaction, 'followNode/seedGroup/bg_node3') : null; const seedBg5 = interaction ? path(interaction, 'followNode/seedGroup/bg_node5') : null; const selected = interaction ? path(interaction, 'landTarget/land_valid_selected') : null; const followNode = interaction ? path(interaction, 'followNode') : null; const landTarget = interaction ? path(interaction, 'landTarget') : null; const detailNode = interaction ? path(interaction, 'followNode/detailNode') : null; const selectedSprite = selected && selected.getComponent(cc.Sprite); function nodePosition(node) { return node ? { active: node.active, activeInHierarchy: node.activeInHierarchy, localPosition: { x: node.position.x, y: node.position.y, z: node.position.z }, worldPosition: { x: node.worldPosition.x, y: node.worldPosition.y, z: node.worldPosition.z }, localScale: { x: node.scale.x, y: node.scale.y, z: node.scale.z }, worldScale: { x: node.worldScale.x, y: node.worldScale.y, z: node.worldScale.z } } : null; } return { interactionActive: !!(interaction && interaction.active), interactionParentChain: interaction ? parentChain(interaction) : [], interactionPosition: interaction ? { x: interaction.position.x, y: interaction.position.y, z: interaction.position.z } : null, interactionLayer: interaction ? interaction.layer : null, cameras: { main: cameraInfo(cameraNode, camera), ui: cameraInfo(uiCameraNode, uiCamera), }, view: stage1ViewInfo(), selectedLandVisual: selected ? { active: selected.active, position: { x: selected.position.x, y: selected.position.y, z: selected.position.z }, hasSprite: !!selectedSprite } : null, landBubbleActive: !!(landBubble && landBubble.active), seedBackgrounds: { bg_node3: seedBg3 ? { active: seedBg3.active, activeInHierarchy: seedBg3.activeInHierarchy } : null, bg_node5: seedBg5 ? { active: seedBg5.active, activeInHierarchy: seedBg5.activeInHierarchy } : null, }, targetNodes: { interactionRoot: nodePosition(interaction), followNode: nodePosition(followNode), landTarget: nodePosition(landTarget), detailNode: nodePosition(detailNode), }, projectedBounds: { landBubble: projectedBounds(landBubble), seedGroup: projectedBounds(interaction ? path(interaction, 'followNode/seedGroup') : null), seedBg3: projectedBounds(seedBg3), seedBg5: projectedBounds(seedBg5), selectedLandVisual: projectedBounds(selected) }, seedSlots: slots }; })() """ % view_info_expression() def decor_state_expression() -> str: return """ (() => { const scene = window.cc && cc.director && cc.director.getScene && cc.director.getScene(); function find(root, name) { if (!root) return null; if (root.name === name) return root; for (const child of root.children || []) { const match = find(child, name); if (match) return match; } return null; } function path(root, childPath) { return childPath.split('/').reduce((current, part) => current ? current.getChildByName(part) : null, root); } function parentChain(node) { const result = []; let current = node; while (current) { result.push({ name: current.name, layer: current.layer }); current = current.parent; } return result; } const cameraNode = find(scene, 'Camera'); const camera = cameraNode && cameraNode.getComponent(cc.Camera); const root = find(scene, 'farm_scene_v3'); function skeletonInfo(node) { if (!node) return null; const skeleton = node.getComponent('sp.Skeleton'); const screen = camera ? camera.worldToScreen(node.worldPosition) : null; let bounds = null; if (skeleton) { try { if (typeof skeleton.getBoundingBox === 'function') { const box = skeleton.getBoundingBox(); bounds = box ? { x: box.x, y: box.y, width: box.width, height: box.height } : null; } else if (typeof skeleton.getRenderData === 'function') { const renderData = skeleton.getRenderData(); bounds = renderData ? { hasRenderData: true } : null; } } catch (error) { bounds = { error: String(error && error.message ? error.message : error) }; } } const protoNames = skeleton ? Object.getOwnPropertyNames(Object.getPrototypeOf(skeleton)).filter((name) => !name.startsWith('_')).slice(0, 80) : []; let internalState = null; if (skeleton) { const drawList = skeleton.drawList || skeleton._drawList; let vertexBounds = null; const vBuffer = skeleton._vBuffer; if (vBuffer && typeof skeleton._vLength === 'number' && skeleton._vLength > 0) { const sample = []; const xs = []; const ys = []; const stride = 8; const limit = Math.min(skeleton._vLength, vBuffer.length || skeleton._vLength); for (let index = 0; index + 1 < limit; index += stride) { const x = Number(vBuffer[index]); const y = Number(vBuffer[index + 1]); if (Number.isFinite(x) && Number.isFinite(y)) { xs.push(x); ys.push(y); if (sample.length < 8) sample.push({ x, y }); } } if (xs.length > 0) { vertexBounds = { stride, vertexCount: xs.length, sample, minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys), }; } } function boneInfo(name) { try { const bone = skeleton.findBone(name); if (!bone) return null; const info = { found: true }; for (const key of ['x', 'y', 'worldX', 'worldY', 'rotation', 'worldRotationX', 'worldRotationY', 'scaleX', 'scaleY', 'a', 'b', 'c', 'd']) { try { const value = bone[key]; if (typeof value === 'number') info[key] = value; } catch (_) {} } for (const method of ['getX', 'getY', 'getWorldX', 'getWorldY', 'getRotation', 'getScaleX', 'getScaleY']) { try { if (typeof bone[method] === 'function') info[method] = bone[method](); } catch (_) {} } return info; } catch (error) { return { error: String(error && error.message ? error.message : error) }; } } function slotInfo(name) { try { const slot = skeleton.findSlot(name); if (!slot) return null; const attachment = slot.getAttachment ? slot.getAttachment() : slot.attachment; const info = { found: true, attachmentName: attachment && attachment.getName ? attachment.getName() : attachment && attachment.name ? attachment.name : null, attachmentType: attachment && attachment.constructor ? attachment.constructor.name : null, }; const vertices = new Array(32).fill(0); try { if (attachment && typeof attachment.computeWorldVertices === 'function') { attachment.computeWorldVertices(slot.bone, vertices, 0, 2); const xs = []; const ys = []; for (let index = 0; index + 1 < vertices.length; index += 2) { const x = Number(vertices[index]); const y = Number(vertices[index + 1]); if (Number.isFinite(x) && Number.isFinite(y) && (x !== 0 || y !== 0)) { xs.push(x); ys.push(y); } } if (xs.length > 0) { info.worldVertices = { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys), sample: vertices.slice(0, 12), }; } } } catch (error) { info.worldVerticesError = String(error && error.message ? error.message : error); } return info; } catch (error) { return { error: String(error && error.message ? error.message : error) }; } } internalState = { ownKeys: Object.keys(skeleton).slice(0, 80), animation: skeleton.animation, skinName: skeleton._skinName || null, animationName: skeleton._animationName || null, hasRuntimeData: !!skeleton._runtimeData, hasSkeletonObject: !!skeleton._skeleton, hasInstance: !!skeleton._instance, drawListLength: drawList && typeof drawList.length === 'number' ? drawList.length : null, drawListArrayLength: drawList && drawList._array && typeof drawList._array.length === 'number' ? drawList._array.length : null, vertexLength: skeleton._vLength || null, indexLength: skeleton._iLength || null, vertexBounds, runtimeProtoNames: skeleton._skeleton ? Object.getOwnPropertyNames(Object.getPrototypeOf(skeleton._skeleton)).slice(0, 80) : [], boneProbe: { root: boneInfo('root'), house: boneInfo('house'), suofang: boneInfo('suofang'), yanwu: boneInfo('yanwu') }, slotProbe: { xiaowu: slotInfo('xiaowu'), gouwo2_new: slotInfo('gouwo2_new'), gouwo3_new: slotInfo('gouwo3_new'), goupen: slotInfo('goupen') }, renderEntityEnabled: skeleton._renderEntity ? skeleton._renderEntity.enabled : null, }; } return { active: node.active, activeInHierarchy: node.activeInHierarchy, layer: node.layer, localPosition: { x: node.position.x, y: node.position.y, z: node.position.z }, worldPosition: { x: node.worldPosition.x, y: node.worldPosition.y, z: node.worldPosition.z }, screenPosition: screen ? { x: screen.x, y: screen.y, z: screen.z } : null, parentChain: parentChain(node), hasSkeleton: !!skeleton, skeleton: skeleton ? { enabled: skeleton.enabled, defaultSkin: skeleton.defaultSkin, defaultAnimation: skeleton.defaultAnimation, loop: skeleton.loop, hasSkeletonData: !!skeleton.skeletonData, skeletonDataName: skeleton.skeletonData ? skeleton.skeletonData.name : null, bounds, protoNames, internalState } : null }; } const fgLayer = root ? path(root, 'Scene/FgLayer') : null; return { fgLayerChildOrder: fgLayer ? fgLayer.children.map((child) => child.name) : [], farmSkin: skeletonInfo(root ? path(root, 'Scene/FgLayer/FarmSkin') : null), layer2: skeletonInfo(root ? path(root, 'Scene/FgLayer/FarmSkin/Layer2') : null), house: skeletonInfo(root ? path(root, 'Scene/FgLayer/FarmSkin/Layer2/house_201001') : null), dogLayer: skeletonInfo(root ? path(root, 'Scene/FgLayer/DogLayer') : null), dogHouse: skeletonInfo(root ? path(root, 'Scene/FgLayer/DogLayer/dogHouse') : null) }; })() """ def click_position_expression(grid_name: str) -> str: return f""" (() => {{ const canvas = document.getElementById('GameCanvas') || document.querySelector('canvas'); const scene = window.cc && cc.director && cc.director.getScene && cc.director.getScene(); function find(root, name) {{ if (!root) return null; if (root.name === name) return root; for (const child of root.children || []) {{ const match = find(child, name); if (match) return match; }} return null; }} const cameraNode = find(scene, 'Camera'); const camera = cameraNode && cameraNode.getComponent(cc.Camera); const grid = find(scene, {js_string(grid_name)}); const icon = grid && grid.getChildByName('icon'); if (!canvas || !camera || !grid || !icon) {{ return {{ ok: false, gridName: {js_string(grid_name)}, reason: 'missing node or camera' }}; }} const screen = camera.worldToScreen(icon.worldPosition); const x = screen.x * (canvas.clientWidth / canvas.width); const y = (canvas.height - screen.y) * (canvas.clientHeight / canvas.height); return {{ ok: true, gridName: {js_string(grid_name)}, screen: {{ x, y }}, world: {{ x: icon.worldPosition.x, y: icon.worldPosition.y, z: icon.worldPosition.z }}, gridPosition: {{ x: grid.position.x, y: grid.position.y, z: grid.position.z }}, iconPosition: {{ x: icon.position.x, y: icon.position.y, z: icon.position.z }} }}; }})() """ def seed_click_position_expression(slot_name: str) -> str: return f""" (() => {{ const canvas = document.getElementById('GameCanvas') || document.querySelector('canvas'); const scene = window.cc && cc.director && cc.director.getScene && cc.director.getScene(); function find(root, name) {{ if (!root) return null; if (root.name === name) return root; for (const child of root.children || []) {{ const match = find(child, name); if (match) return match; }} return null; }} function path(root, childPath) {{ return childPath.split('/').reduce((current, part) => current ? current.getChildByName(part) : null, root); }} function cameraFor(node) {{ const cameraNode = find(scene, 'Camera'); const uiCameraNode = find(scene, 'uiCamera'); const camera = cameraNode && cameraNode.getComponent(cc.Camera); const uiCamera = uiCameraNode && uiCameraNode.getComponent(cc.Camera); if (node && uiCamera && (uiCamera.visibility & node.layer)) return uiCamera; if (node && camera && (camera.visibility & node.layer)) return camera; return uiCamera || camera; }} const interaction = find(scene, 'plant_interactive_v2'); const slot = interaction ? path(interaction, 'followNode/seedGroup/group_5/' + {js_string(slot_name)}) : null; const ui = slot && slot.getComponent('cc.UITransform'); const camera = slot && cameraFor(slot); if (!canvas || !slot || !slot.activeInHierarchy || !ui || !camera) {{ return {{ ok: false, slotName: {js_string(slot_name)}, reason: 'missing active seed slot or camera' }}; }} const center = camera.worldToScreen(slot.worldPosition); const x = center.x * (canvas.clientWidth / canvas.width); const y = (canvas.height - center.y) * (canvas.clientHeight / canvas.height); return {{ ok: true, slotName: {js_string(slot_name)}, screen: {{ x, y }}, world: {{ x: slot.worldPosition.x, y: slot.worldPosition.y, z: slot.worldPosition.z }}, sourceSize: {{ width: ui.contentSize.width, height: ui.contentSize.height }}, slotPosition: {{ x: slot.position.x, y: slot.position.y, z: slot.position.z }} }}; }})() """ def crop_state_expression() -> str: return """ (() => { const scene = window.cc && cc.director && cc.director.getScene && cc.director.getScene(); function find(root, name) { if (!root) return null; if (root.name === name) return root; for (const child of root.children || []) { const match = find(child, name); if (match) return match; } return null; } function path(root, childPath) { return childPath.split('/').reduce((current, part) => current ? current.getChildByName(part) : null, root); } const gridOrigin = find(scene, 'GridOrigin'); const interaction = find(scene, 'plant_interactive_v2'); const landBubble = interaction ? (path(interaction, 'followNode/detailNode/land') || path(interaction, 'land_detail/land')) : null; const cropInfo = interaction ? path(interaction, 'followNode/detailNode/mock_crop_info') : null; const title = landBubble ? path(landBubble, 'title') : null; const titleLabel = title && title.getComponent(cc.Label); const cropInfoTitle = cropInfo ? path(cropInfo, 'title') : null; const cropInfoTitleLabel = cropInfoTitle && cropInfoTitle.getComponent(cc.Label); const cropInfoProgress = cropInfo ? path(cropInfo, 'progress_fill') : null; const cropInfoProgressSprite = cropInfoProgress && cropInfoProgress.getComponent(cc.Sprite); const cropInfoIcon = cropInfo ? path(cropInfo, 'icon') : null; const cropInfoIconSprite = cropInfoIcon && cropInfoIcon.getComponent(cc.Sprite); function nodeState(node) { if (!node) return null; const ui = node.getComponent('cc.UITransform'); return { active: node.active, activeInHierarchy: node.activeInHierarchy, localPosition: { x: node.position.x, y: node.position.y, z: node.position.z }, worldPosition: { x: node.worldPosition.x, y: node.worldPosition.y, z: node.worldPosition.z }, sourceSize: ui ? { width: ui.contentSize.width, height: ui.contentSize.height } : null, }; } const group = interaction ? path(interaction, 'followNode/seedGroup/group_5') : null; const crops = []; if (gridOrigin) { for (const grid of gridOrigin.children || []) { const crop = grid.getChildByName('mock_crop'); if (!crop) continue; const sprite = crop.getComponent(cc.Sprite); const ui = crop.getComponent('cc.UITransform'); crops.push({ gridName: grid.name, active: crop.active, activeInHierarchy: crop.activeInHierarchy, spriteFrameName: sprite && sprite.spriteFrame ? sprite.spriteFrame.name : null, spriteEnabled: sprite ? sprite.enabled : null, localPosition: { x: crop.position.x, y: crop.position.y, z: crop.position.z }, worldPosition: { x: crop.worldPosition.x, y: crop.worldPosition.y, z: crop.worldPosition.z }, sourceSize: ui ? { width: ui.contentSize.width, height: ui.contentSize.height } : null, siblingIndex: grid.children.indexOf(crop), gridChildren: grid.children.map((child) => child.name), }); } } const seedSlots = []; if (group) { for (let index = 1; index <= 5; index += 1) { const slot = group.getChildByName(`node${index}`); const count = slot ? path(slot, 'count') : null; const label = count && count.getComponent(cc.Label); seedSlots.push({ name: `node${index}`, active: !!(slot && slot.active), activeInHierarchy: !!(slot && slot.activeInHierarchy), count: label ? label.string : null, }); } } return { cropCount: crops.length, crops, interactionActive: !!(interaction && interaction.active), seedGroupActive: !!(interaction && path(interaction, 'followNode/seedGroup') && path(interaction, 'followNode/seedGroup').active), landBubbleTitle: titleLabel ? titleLabel.string : null, cropInfo: { node: nodeState(cropInfo), title: cropInfoTitleLabel ? cropInfoTitleLabel.string : null, progressFillRange: cropInfoProgressSprite ? cropInfoProgressSprite.fillRange : null, iconSpriteFrameName: cropInfoIconSprite && cropInfoIconSprite.spriteFrame ? cropInfoIconSprite.spriteFrame.name : null, stageDots: cropInfo ? cropInfo.children.filter((child) => /^stage_dot_/.test(child.name)).map((child) => ({ name: child.name, active: child.active, localPosition: { x: child.position.x, y: child.position.y, z: child.position.z }, })) : [], }, seedSlots, }; })() """ def hitbox_diagnostics_expression() -> str: return """ (() => { const canvas = document.getElementById('GameCanvas') || document.querySelector('canvas'); const scene = window.cc && cc.director && cc.director.getScene && cc.director.getScene(); function find(root, name) { if (!root) return null; if (root.name === name) return root; for (const child of root.children || []) { const match = find(child, name); if (match) return match; } return null; } const cameraNode = find(scene, 'Camera'); const camera = cameraNode && cameraNode.getComponent(cc.Camera); const gridOrigin = find(scene, 'GridOrigin'); function bounds(node) { if (!node || !camera) return null; const ui = node.getComponent('cc.UITransform'); if (!ui) return null; const size = ui.contentSize; const anchor = ui.anchorPoint; const minX = -anchor.x * size.width; const maxX = (1 - anchor.x) * size.width; const minY = -anchor.y * size.height; const maxY = (1 - anchor.y) * size.height; const points = [ new cc.Vec3(minX, minY, 0), new cc.Vec3(maxX, minY, 0), new cc.Vec3(minX, maxY, 0), new cc.Vec3(maxX, maxY, 0), ].map((point) => { const world = ui.convertToWorldSpaceAR(point); const screen = camera.worldToScreen(world); return { world: { x: world.x, y: world.y, z: world.z }, screen: { x: screen.x, y: screen.y }, input: canvas ? { x: screen.x * (canvas.clientWidth / canvas.width), y: (canvas.height - screen.y) * (canvas.clientHeight / canvas.height) } : null, }; }); const xs = points.map((point) => point.world.x); const ys = points.map((point) => point.world.y); const inputXs = points.map((point) => point.input ? point.input.x : 0); const inputYs = points.map((point) => point.input ? point.input.y : 0); return { sourceSize: { width: size.width, height: size.height }, anchor: { x: anchor.x, y: anchor.y }, localPosition: { x: node.position.x, y: node.position.y, z: node.position.z }, worldPosition: { x: node.worldPosition.x, y: node.worldPosition.y, z: node.worldPosition.z }, worldRect: { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys) }, inputRect: canvas ? { minX: Math.min(...inputXs), maxX: Math.max(...inputXs), minY: Math.min(...inputYs), maxY: Math.max(...inputYs) } : null, }; } function containsWorld(rect, point) { return !!rect && point.x >= rect.minX && point.x <= rect.maxX && point.y >= rect.minY && point.y <= rect.maxY; } const names = ['grid_0_5', 'grid_1_4', 'grid_2_4']; const grids = names.map((name) => { const grid = gridOrigin && gridOrigin.getChildByName(name); const icon = grid && grid.getChildByName('icon'); return { name, siblingIndex: grid && gridOrigin ? gridOrigin.children.indexOf(grid) : -1, gridBounds: bounds(grid), iconBounds: bounds(icon), }; }); const targetGrid = gridOrigin && gridOrigin.getChildByName('grid_0_5'); const targetIcon = targetGrid && targetGrid.getChildByName('icon'); const targetPoint = targetIcon ? { x: targetIcon.worldPosition.x, y: targetIcon.worldPosition.y, z: targetIcon.worldPosition.z } : null; const parentHitCandidatesAtGrid05IconCenter = []; if (gridOrigin && targetPoint) { for (const grid of gridOrigin.children || []) { const hitBounds = bounds(grid); if (hitBounds && containsWorld(hitBounds.worldRect, targetPoint)) { parentHitCandidatesAtGrid05IconCenter.push({ name: grid.name, siblingIndex: gridOrigin.children.indexOf(grid), worldRect: hitBounds.worldRect, }); } } } return { targetPoint: targetPoint, grids, parentHitCandidatesAtGrid05IconCenter, }; })() """ def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--url", default=DEFAULT_URL) parser.add_argument("--output", type=Path, default=DEFAULT_OUT) parser.add_argument("--report", type=Path, default=DEFAULT_REPORT) parser.add_argument("--width", type=int, default=720) parser.add_argument("--height", type=int, default=1280) parser.add_argument("--wait", type=float, default=12.0) parser.add_argument("--click-grid", default="") parser.add_argument("--locked-grid", default="") parser.add_argument("--click-seed-slot", default="") parser.add_argument("--after-seed-wait", type=float, default=0.8) parser.add_argument("--crop-wait", type=float, action="append", default=[]) parser.add_argument("--blank-click", action="store_true", help="Click a fixed non-land sky point after the selectable click and record the hidden interaction state.") parser.add_argument("--headed", action="store_true", help="Run a visible browser window for WebGL screenshot capture.") args = parser.parse_args() top_hud_evidence = load_json(TOP_HUD_SOURCE_JSON) expected_labels = expected_top_hud_labels(top_hud_evidence) expected_progress = expected_top_hud_progress(top_hud_evidence) browser = browser_path() args.output.parent.mkdir(parents=True, exist_ok=True) args.report.parent.mkdir(parents=True, exist_ok=True) port = find_free_port() user_data = tempfile.TemporaryDirectory(prefix="farm-cdp-") cmd = [ str(browser), f"--remote-debugging-port={port}", "--remote-allow-origins=*", f"--user-data-dir={user_data.name}", f"--window-size={args.width},{args.height}", "--force-device-scale-factor=1", "--no-first-run", "--no-default-browser-check", "--hide-scrollbars", "--mute-audio", "--enable-unsafe-swiftshader", "--use-angle=swiftshader", "--use-gl=angle", "about:blank", ] if not args.headed: cmd.insert(1, "--headless=new") proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, text=True) client: CdpClient | None = None try: ws_url = wait_for_target_ws(port) client = CdpClient(ws_url) client.call("Page.enable") client.call("Runtime.enable") client.call("Log.enable") client.call( "Page.addScriptToEvaluateOnNewDocument", { "source": """ (() => { const originalGetContext = HTMLCanvasElement.prototype.getContext; HTMLCanvasElement.prototype.getContext = function patchedGetContext(type, attributes) { if (type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl') { attributes = Object.assign({}, attributes || {}, { preserveDrawingBuffer: true }); } return originalGetContext.call(this, type, attributes); }; })(); """ }, ) client.call( "Emulation.setDeviceMetricsOverride", { "width": args.width, "height": args.height, "deviceScaleFactor": 1, "mobile": True, }, ) client.call("Emulation.setTouchEmulationEnabled", {"enabled": True, "maxTouchPoints": 1}) try: client.call("Emulation.setVisibleSize", {"width": args.width, "height": args.height}) except Exception: pass client.call("Page.navigate", {"url": args.url}) time.sleep(args.wait) state = extract_eval_value( client.call( "Runtime.evaluate", { "returnByValue": True, "expression": """ (() => { %s const canvas = document.getElementById('GameCanvas') || document.querySelector('canvas'); const scene = window.cc && cc.director && cc.director.getScene && cc.director.getScene(); return { readyState: document.readyState, location: location.href, viewport: { width: innerWidth, height: innerHeight, dpr: devicePixelRatio }, canvas: canvas ? { width: canvas.width, height: canvas.height, clientWidth: canvas.clientWidth, clientHeight: canvas.clientHeight } : null, view: stage1ViewInfo(), ccSettingsScreen: window._CCSettings ? window._CCSettings.screen : null, hasCC: !!window.cc, sceneName: scene ? scene.name : null, childCount: scene ? scene.children.length : null }; })() """ % view_info_expression(), }, ) ) if not state.get("hasCC") or state.get("sceneName") != "stage1_world_camera": raise RuntimeError(f"Cocos runtime scene did not load correctly: {state}") canvas = state.get("canvas") or {} if int(canvas.get("width") or 0) != args.width or int(canvas.get("height") or 0) != args.height: raise RuntimeError(f"Unexpected canvas size: {state}") hitbox_diagnostics = extract_eval_value( client.call( "Runtime.evaluate", {"returnByValue": True, "expression": hitbox_diagnostics_expression()}, ) ) decor_state = extract_eval_value( client.call( "Runtime.evaluate", {"returnByValue": True, "expression": decor_state_expression()}, ) ) top_hud_state = extract_eval_value( client.call( "Runtime.evaluate", {"returnByValue": True, "expression": top_hud_state_expression()}, ) ) if not top_hud_state.get("exists"): raise RuntimeError(f"Top HUD did not exist in the runtime scene: {top_hud_state}") for key, expected in expected_labels.items(): actual = (top_hud_state.get("labels") or {}).get(key) if actual != expected: raise RuntimeError(f"Top HUD label {key} expected {expected!r}, got {actual!r}: {top_hud_state}") runtime_progress = top_hud_state.get("progress") or {} if abs(float(runtime_progress.get("progress", -1)) - expected_progress) > 0.001: raise RuntimeError(f"Top HUD progress expected {expected_progress}, got {runtime_progress}: {top_hud_state}") client.call( "Runtime.evaluate", { "returnByValue": True, "expression": """ (() => { if (window.cc && cc.profiler && cc.profiler.hideStats) { cc.profiler.hideStats(); } return true; })() """, }, ) time.sleep(0.3) interaction_checks: dict[str, dict] = {} if args.locked_grid: locked_position = extract_eval_value( client.call( "Runtime.evaluate", {"returnByValue": True, "expression": click_position_expression(args.locked_grid)}, ) ) if not locked_position.get("ok"): raise RuntimeError(f"Could not resolve locked grid click position: {locked_position}") dispatch_click(client, locked_position["screen"]["x"], locked_position["screen"]["y"]) time.sleep(0.4) interaction_checks["afterLockedGridClick"] = { "clickPosition": locked_position, "state": extract_eval_value( client.call( "Runtime.evaluate", {"returnByValue": True, "expression": interaction_state_expression()}, ) ), } if args.click_grid: click_position = extract_eval_value( client.call( "Runtime.evaluate", {"returnByValue": True, "expression": click_position_expression(args.click_grid)}, ) ) if not click_position.get("ok"): raise RuntimeError(f"Could not resolve grid click position: {click_position}") dispatch_click(client, click_position["screen"]["x"], click_position["screen"]["y"]) time.sleep(0.6) interaction_checks["afterSelectableGridClick"] = { "clickPosition": click_position, "state": extract_eval_value( client.call( "Runtime.evaluate", {"returnByValue": True, "expression": interaction_state_expression()}, ) ), } if args.click_seed_slot: seed_position = extract_eval_value( client.call( "Runtime.evaluate", {"returnByValue": True, "expression": seed_click_position_expression(args.click_seed_slot)}, ) ) if not seed_position.get("ok"): raise RuntimeError(f"Could not resolve seed click position: {seed_position}") dispatch_click(client, seed_position["screen"]["x"], seed_position["screen"]["y"]) time.sleep(max(args.after_seed_wait, 0)) interaction_checks["afterSeedClick"] = { "clickPosition": seed_position, "interactionState": extract_eval_value( client.call( "Runtime.evaluate", {"returnByValue": True, "expression": interaction_state_expression()}, ) ), "cropState": extract_eval_value( client.call( "Runtime.evaluate", {"returnByValue": True, "expression": crop_state_expression()}, ) ), } for wait_index, wait_seconds in enumerate(args.crop_wait, start=1): time.sleep(max(wait_seconds, 0)) interaction_checks[f"afterCropWait{wait_index}"] = { "waitSeconds": wait_seconds, "cropState": extract_eval_value( client.call( "Runtime.evaluate", {"returnByValue": True, "expression": crop_state_expression()}, ) ), } if args.blank_click: blank_position = {"screen": {"x": args.width * 0.5, "y": args.height * 0.18}} dispatch_click(client, blank_position["screen"]["x"], blank_position["screen"]["y"]) time.sleep(0.4) interaction_checks["afterBlankClick"] = { "clickPosition": blank_position, "state": extract_eval_value( client.call( "Runtime.evaluate", {"returnByValue": True, "expression": interaction_state_expression()}, ) ), } console = [] for event in client.events: method = event.get("method") if method in {"Runtime.consoleAPICalled", "Log.entryAdded"}: console.append(event) try: screenshot_bytes, capture_info = capture_current_frame(client, args.width, args.height) args.output.write_bytes(screenshot_bytes) except Exception as exc: report = { "status": "needs_solution", "url": args.url, "output": str(args.output), "browser": str(browser), "viewport": {"width": args.width, "height": args.height}, "runtimeState": state, "decorState": decor_state, "topHudState": top_hud_state, "hitboxDiagnostics": hitbox_diagnostics, "interactionChecks": interaction_checks, "captureError": str(exc), "consoleEventCount": len(console), "consoleEvents": console[-40:], } args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(json.dumps(report, ensure_ascii=False, indent=2)) raise report = { "status": "pass", "url": args.url, "output": str(args.output), "browser": str(browser), "viewport": {"width": args.width, "height": args.height}, "runtimeState": state, "capture": capture_info, "decorState": decor_state, "topHudState": top_hud_state, "hitboxDiagnostics": hitbox_diagnostics, "interactionChecks": interaction_checks, "consoleEventCount": len(console), "consoleEvents": console[-20:], } args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(json.dumps(report, ensure_ascii=False, indent=2)) finally: if client is not None: client.close() proc.terminate() try: proc.communicate(timeout=5) except subprocess.TimeoutExpired: proc.kill() try: proc.communicate(timeout=5) except subprocess.TimeoutExpired: pass user_data.cleanup() if __name__ == "__main__": main()