cocos-farm/tools/capture_creator_runtime.py

1420 lines
57 KiB
Python

#!/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()