#!/usr/bin/env python3 """Extract source-backed evidence for the top HUD pass. This pass is intentionally limited to the original Cocos `main_ui_v2` nodes needed for the top-left exit button, user information, experience text, gold, and diamond display. Mock user values are recorded separately from source layout and source SpriteFrame metadata. """ from __future__ import annotations import json from pathlib import Path from typing import Any from PIL import Image ROOT = Path(__file__).resolve().parents[1] SOURCE_ROOT = Path("/Users/hy/Documents/dev/qq_farm_godot_rebuild_20260616") RAW_GAME = SOURCE_ROOT / "source_raw/6A3BB7D3D4349E2050E7B701AAD51600_726750140f8084ebcbe5190fe9c198c9" MAIN_ELEMENTS = SOURCE_ROOT / "source_first/out/main_scene_elements.json" MAIN_UI_PREFAB = SOURCE_ROOT / "downloads/raw/remote/extraRes/import/0d/0d7027270.4bc05.json" EXTRARES_CONFIG = SOURCE_ROOT / "downloads/raw/remote/extraRes/config.229e7.json" V2_ATLAS_IMPORT = SOURCE_ROOT / "downloads/raw/remote/extraRes/import/03/03f07fafe.d2787.json" V2_ATLAS_NATIVE = SOURCE_ROOT / "downloads/raw/remote/extraRes/native/17/17b0f7c92.2875b.astc" SPRITEFRAME_IMAGES = SOURCE_ROOT / "downloads/spriteframes/images" MOCK_AVATAR = RAW_GAME / "openDataContext/render/avatar.png" OUT_PATH = ROOT / "source_export/stage1_top_hud_source.json" REPORT_PATH = ROOT / "source_export/stage1_top_hud_extract_report.json" V2_ASSETS = { "btn_quit": "gui/texture/v2/btn_quit/spriteFrame", "img_head_di": "gui/texture/v2/img_head_di/spriteFrame", "img_head_di1": "gui/texture/v2/img_head_di1/spriteFrame", "img_level_bar": "gui/texture/v2/img_level_bar/spriteFrame", "img_level": "gui/texture/v2/img_level/spriteFrame", "img_source_di_v2": "gui/texture/v2/img_source_di_v2/spriteFrame", "img_gold": "gui/texture/v2/img_gold/spriteFrame", "img_diamond": "gui/texture/v2/img_diamond/spriteFrame", } LABEL_NODE_IDS = { 94: ("txtName", "nickname"), 95: ("txtLevel", "level"), 96: ("txtExp", "experience"), 98: ("txtGold", "goldDisplay"), 99: ("txtDiamond", "diamond"), } def load_json(path: Path) -> Any: return json.loads(path.read_text(encoding="utf-8")) def write_json(path: Path, data: Any) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") def image_info(path: Path) -> dict[str, Any]: with Image.open(path) as image: bbox = image.getbbox() return { "width": image.size[0], "height": image.size[1], "mode": image.mode, "alphaBoundingBox": list(bbox) if bbox else None, } def element_by_path(elements: list[dict[str, Any]], path: str) -> dict[str, Any]: for item in elements: if item.get("full_path") == path: return item raise KeyError(path) def vec3(value: dict[str, Any] | None) -> dict[str, float]: if not value: return {"x": 0.0, "y": 0.0, "z": 0.0} return {"x": float(value.get("x", 0)), "y": float(value.get("y", 0)), "z": float(value.get("z", 0))} def vec2(value: dict[str, Any] | None, default_x: float = 0.5, default_y: float = 0.5) -> dict[str, float]: if not value: return {"x": default_x, "y": default_y} return {"x": float(value.get("x", default_x)), "y": float(value.get("y", default_y))} def node_from_element( elements: list[dict[str, Any]], path: str, *, size: dict[str, float] | None = None, anchor: dict[str, float] | None = None, sprite_asset_id: str | None = None, sprite_type: int = 0, label: dict[str, Any] | None = None, mask: dict[str, Any] | None = None, progress_bar: dict[str, Any] | None = None, widget: dict[str, Any] | None = None, source_note: str | None = None, ) -> dict[str, Any]: element = element_by_path(elements, path) result = { "sourceIndex": int(element["index"]), "path": element["full_path"], "name": element["name"], "parentPath": element.get("parent_path"), "localPosition": vec3(element.get("local_position")), "localScale": vec3(element.get("local_scale")), "localEuler": vec3(element.get("local_euler")), "worldPosition": vec3(element.get("world_position_transformed")), "size": size or ( {"x": float(element["primary_size"]["width"]), "y": float(element["primary_size"]["height"])} if element.get("primary_size") else None ), "anchor": anchor or (vec2(element.get("anchor")) if element.get("anchor") else None), "active": True if element.get("active") is None else bool(element.get("active")), "componentCount": int(element.get("component_count", 0)), "spriteAssetId": sprite_asset_id, "spriteType": sprite_type, "label": label, "mask": mask, "progressBar": progress_bar, "widget": widget, "sourceNote": source_note, } return {key: value for key, value in result.items() if value is not None} def decode_compact(prefab: list[Any], value: Any) -> dict[str, Any] | None: if not isinstance(value, list) or not value or not isinstance(value[0], int): return None template_id = value[0] templates = prefab[4] classes = prefab[3] if template_id < 0 or template_id >= len(templates): return None template = templates[template_id] if not template or not isinstance(template[0], int): return None class_index = template[0] if class_index < 0 or class_index >= len(classes): return None class_def = classes[class_index] fields = class_def[1] if len(class_def) > 1 and isinstance(class_def[1], list) else [] props: dict[str, Any] = {} for prop_index, prop_value in zip(template[1:], value[1:]): if isinstance(prop_index, int) and 0 <= prop_index < len(fields): props[fields[prop_index]] = prop_value return {"type": class_def[0], "template": template_id, "props": props, "raw": value} def collect_compact(prefab: list[Any], value: Any, result: list[dict[str, Any]]) -> None: decoded = decode_compact(prefab, value) if decoded: result.append(decoded) if isinstance(value, list): for item in value: collect_compact(prefab, item, result) elif isinstance(value, dict): for item in value.values(): collect_compact(prefab, item, result) def packed_color(value: list[Any] | None, fallback: int) -> int: if isinstance(value, list) and len(value) == 2 and value[0] == 4: return int(value[1]) return fallback def extract_labels(prefab: list[Any]) -> dict[int, dict[str, Any]]: decoded: list[dict[str, Any]] = [] collect_compact(prefab, prefab[5][2], decoded) labels: dict[int, dict[str, Any]] = {} for item in decoded: if item["type"] != "cc.Label": continue props = item["props"] node_id = props.get("node") if node_id not in LABEL_NODE_IDS: continue _, mock_field = LABEL_NODE_IDS[int(node_id)] labels[int(node_id)] = { "sourceNodeIndex": int(node_id), "mockField": mock_field, "string": str(props.get("_string", "")), "actualFontSize": float(props.get("_actualFontSize", props.get("_fontSize", 20))), "fontSize": float(props.get("_fontSize", props.get("_actualFontSize", 20))), "lineHeight": float(props.get("_lineHeight", props.get("_fontSize", 20))), "horizontalAlign": int(props.get("_horizontalAlign", 1)), "verticalAlign": int(props.get("_verticalAlign", 1)), "overflow": int(props.get("_overflow", 0)), "enableWrapText": bool(props.get("_enableWrapText", True)), "bold": bool(props.get("_isBold", False)), "enableOutline": bool(props.get("_enableOutline", False)), "colorUint32": packed_color(props.get("_color"), 0xFFFFFFFF), "outlineColorUint32": packed_color(props.get("_outlineColor"), 0xFFFFFFFF), "sourceTemplate": item["template"], "sourceRaw": item["raw"], } return labels def extract_progress_bar(prefab: list[Any]) -> dict[str, Any]: decoded: list[dict[str, Any]] = [] collect_compact(prefab, prefab[5][2], decoded) for item in decoded: if item["type"] == "cc.ProgressBar" and item["props"].get("node") == 44: props = item["props"] return { "sourceNodeIndex": 44, "componentType": "cc.ProgressBar", "totalLength": float(props["_totalLength"]), "progress": float(props["_progress"]), "barSpriteSourceRef": props.get("_barSprite"), "sourceTemplate": item["template"], "sourceRaw": item["raw"], } raise RuntimeError("Source cc.ProgressBar for expBar was not found.") def extract_widget_from_node(prefab: list[Any], source_node_index: int) -> dict[str, Any]: # The compact main_ui_v2 object table keeps source node rows in # prefab[5][2][0]. # A node's component array contains inline component records; the Widget # component's `node` field is a negative back-reference generated by Cocos, # so ownership must be resolved by reading the component list of the source # node row itself. try: node_record = prefab[5][2][0][source_node_index] except IndexError as exc: raise RuntimeError(f"Source node row {source_node_index} is missing from main_ui_v2 prefab.") from exc decoded_node = decode_compact(prefab, node_record) if not decoded_node or decoded_node["type"] != "cc.Node": raise RuntimeError(f"Source row {source_node_index} is not a cc.Node.") compressed_components = decoded_node["props"].get("_components", []) components = compressed_components[0] if compressed_components and isinstance(compressed_components[0], list) else compressed_components for component in components: decoded_component = decode_compact(prefab, component) if decoded_component and decoded_component["type"] == "cc.Widget": props = decoded_component["props"] return { "componentType": "cc.Widget", "sourceNodeIndex": source_node_index, "alignFlags": int(props.get("_alignFlags", 0)), "alignMode": int(props.get("_alignMode", 0)), "left": float(props["_left"]) if "_left" in props else None, "right": float(props["_right"]) if "_right" in props else None, "top": float(props["_top"]) if "_top" in props else None, "bottom": float(props["_bottom"]) if "_bottom" in props else None, "originalWidth": float(props["_originalWidth"]) if "_originalWidth" in props else None, "originalHeight": float(props["_originalHeight"]) if "_originalHeight" in props else None, "sourceTemplate": decoded_component["template"], "sourceRaw": decoded_component["raw"], } raise RuntimeError(f"Source cc.Widget for node row {source_node_index} was not found.") def extract_ui_transform_from_node(prefab: list[Any], source_node_index: int) -> dict[str, Any]: try: node_record = prefab[5][2][0][source_node_index] except IndexError as exc: raise RuntimeError(f"Source node row {source_node_index} is missing from main_ui_v2 prefab.") from exc decoded_node = decode_compact(prefab, node_record) if not decoded_node or decoded_node["type"] != "cc.Node": raise RuntimeError(f"Source row {source_node_index} is not a cc.Node.") compressed_components = decoded_node["props"].get("_components", []) components = compressed_components[0] if compressed_components and isinstance(compressed_components[0], list) else compressed_components for component in components: decoded_component = decode_compact(prefab, component) if decoded_component and decoded_component["type"] == "cc.UITransform": props = decoded_component["props"] raw_size = props.get("_contentSize") raw_anchor = props.get("_anchorPoint") has_serialized_size = isinstance(raw_size, list) and len(raw_size) >= 3 has_serialized_anchor = isinstance(raw_anchor, list) and len(raw_anchor) >= 3 return { "componentType": "cc.UITransform", "sourceNodeIndex": source_node_index, "size": { "x": float(raw_size[1]) if has_serialized_size else 100.0, "y": float(raw_size[2]) if has_serialized_size else 100.0, }, "anchor": { "x": float(raw_anchor[1]) if has_serialized_anchor else 0.5, "y": float(raw_anchor[2]) if has_serialized_anchor else 0.5, }, "usesEngineDefaults": not has_serialized_size or not has_serialized_anchor, "engineDefaultEvidence": ( "Cocos Creator 3.8 UITransform initializes _contentSize to Size(100,100) " "and _anchorPoint to Vec2(0.5,0.5) when the compact component omits those fields." ), "sourceTemplate": decoded_component["template"], "sourceRaw": decoded_component["raw"], } raise RuntimeError(f"Source cc.UITransform for node row {source_node_index} was not found.") def sprite_frame_by_name(atlas_data: list[Any], name: str) -> tuple[int, dict[str, Any]]: for index, row in enumerate(atlas_data[5]): frame = row[0][0] if isinstance(row, list) and row and isinstance(row[0], list) and row[0] else None if isinstance(frame, dict) and frame.get("name") == name: return index, frame raise KeyError(name) def config_entry_for_asset(config: dict[str, Any], asset_path: str) -> tuple[int, str]: for raw_index, value in config["paths"].items(): if value[0] == asset_path: index = int(raw_index) return index, config["uuids"][index] raise KeyError(asset_path) def asset_record(config: dict[str, Any], atlas_data: list[Any], asset_id: str, resource: str) -> dict[str, Any]: config_index, compressed_uuid = config_entry_for_asset(config, resource) frame_name = resource.split("/")[-2] payload_index, frame = sprite_frame_by_name(atlas_data, frame_name) output = SPRITEFRAME_IMAGES / f"extraRes__gui__texture__v2__{frame_name}__03f07fafe_{payload_index}.png" return { "id": asset_id, "bundle": "extraRes", "resource": resource, "configIndex": config_index, "compressedUuid": compressed_uuid, "atlasPackKey": "03f07fafe", "sourceMetadataFile": str(V2_ATLAS_IMPORT), "sourceAtlasFile": str(V2_ATLAS_NATIVE), "sourcePath": str(output), "spriteFrame": frame, "pixelSize": image_info(output), } def main() -> int: main_elements = load_json(MAIN_ELEMENTS) elements = main_elements["prefabs"]["main_ui_v2"]["elements"] prefab = load_json(MAIN_UI_PREFAB) config = load_json(EXTRARES_CONFIG) atlas = load_json(V2_ATLAS_IMPORT) labels = extract_labels(prefab) progress = extract_progress_bar(prefab) main_widget = extract_widget_from_node(prefab, 1) head_info_widget = extract_widget_from_node(prefab, 7) source_widget = extract_widget_from_node(prefab, 31) source_ui_transform = extract_ui_transform_from_node(prefab, 31) source_files = [ MAIN_ELEMENTS, MAIN_UI_PREFAB, EXTRARES_CONFIG, V2_ATLAS_IMPORT, V2_ATLAS_NATIVE, MOCK_AVATAR, ] assets = [asset_record(config, atlas, asset_id, resource) for asset_id, resource in V2_ASSETS.items()] assets.append( { "id": "mock_avatar", "bundle": "runtime_mock", "resource": "openDataContext/render/avatar.png", "sourcePath": str(MOCK_AVATAR), "spriteFrame": None, "pixelSize": image_info(MOCK_AVATAR), "sourceNote": "Runtime mock avatar image from the extracted package. It fills the source headMask/head runtime mount point and does not define layout.", } ) source_files.extend(Path(item["sourcePath"]) for item in assets) missing = [str(path) for path in source_files if not path.exists()] node_label_by_name = { "txtName": labels[94], "txtLevel": labels[95], "txtExp": labels[96], "txtGold": labels[98], "txtDiamond": labels[99], } mock_user = { "nickname": "黄昏", "level": 4, "exp": 201, "expMax": 600, "gold": 58000, "goldDisplay": "5.8万", "diamond": 260, "avatarAssetId": "mock_avatar", } nodes = [ { "sourceIndex": 1, "path": "main_ui_v2", "name": "main_ui_v2", "parentPath": "root/ui", "localPosition": {"x": 0.0, "y": 0.0, "z": 0.0}, "localScale": {"x": 1.0, "y": 1.0, "z": 1.0}, "localEuler": {"x": 0.0, "y": 0.0, "z": 0.0}, "size": {"x": 720.0, "y": 1280.0}, "anchor": {"x": 0.5, "y": 0.5}, "active": True, "widget": main_widget, "sourceNote": "The compact prefab root serializes a cc.UITransform of 720 x 1280.", }, node_from_element( elements, "main_ui_v2/HeadInfo", size={"x": 200.0, "y": 200.0}, anchor={"x": 0.0, "y": 1.0}, widget=head_info_widget, ), node_from_element(elements, "main_ui_v2/HeadInfo/SubHeadInfo"), node_from_element(elements, "main_ui_v2/HeadInfo/btnExit", sprite_asset_id="btn_quit"), node_from_element(elements, "main_ui_v2/HeadInfo/SubHeadInfo/head_di", sprite_asset_id="img_head_di", sprite_type=1), node_from_element(elements, "main_ui_v2/HeadInfo/SubHeadInfo/img_head_di1", sprite_asset_id="img_head_di1"), node_from_element( elements, "main_ui_v2/HeadInfo/SubHeadInfo/headMask", size={"x": 71.0, "y": 71.0}, mask={"type": 1, "segments": 30, "fillColorUint32": 16777215}, ), node_from_element(elements, "main_ui_v2/HeadInfo/SubHeadInfo/headMask/head", size={"x": 71.0, "y": 71.0}, sprite_asset_id="mock_avatar"), node_from_element( elements, "main_ui_v2/HeadInfo/SubHeadInfo/node_head_frame", source_note="The source node has UITransform 112 x 112 and a Sprite component without a serialized SpriteFrame; recover the avatar-frame runtime skin mapping before adding frame texture.", ), node_from_element(elements, "main_ui_v2/HeadInfo/SubHeadInfo/expBar", sprite_asset_id="img_level_bar", sprite_type=1, progress_bar=progress), node_from_element(elements, "main_ui_v2/HeadInfo/SubHeadInfo/expBar/Bar"), node_from_element(elements, "main_ui_v2/HeadInfo/SubHeadInfo/img_level", sprite_asset_id="img_level"), node_from_element(elements, "main_ui_v2/HeadInfo/SubHeadInfo/txtName", label=node_label_by_name["txtName"]), node_from_element(elements, "main_ui_v2/HeadInfo/SubHeadInfo/txtLevel", label=node_label_by_name["txtLevel"]), node_from_element(elements, "main_ui_v2/HeadInfo/SubHeadInfo/txtExp", label=node_label_by_name["txtExp"]), node_from_element( elements, "main_ui_v2/Source", size=source_ui_transform["size"], anchor=source_ui_transform["anchor"], widget=source_widget, source_note=source_ui_transform["engineDefaultEvidence"], ), node_from_element(elements, "main_ui_v2/Source/root"), node_from_element(elements, "main_ui_v2/Source/root/img_source_di", sprite_asset_id="img_source_di_v2", sprite_type=1), node_from_element(elements, "main_ui_v2/Source/root/img_gold", sprite_asset_id="img_gold"), node_from_element(elements, "main_ui_v2/Source/root/txtGold", label=node_label_by_name["txtGold"]), node_from_element(elements, "main_ui_v2/Source/root/img_source_di1", sprite_asset_id="img_source_di_v2", sprite_type=1), node_from_element(elements, "main_ui_v2/Source/root/img_diamond", sprite_asset_id="img_diamond"), node_from_element(elements, "main_ui_v2/Source/root/txtDiamond", label=node_label_by_name["txtDiamond"]), ] evidence = { "metadata": { "module": "stage1_top_hud", "sourceFiles": [str(path) for path in source_files], "sourcePrefab": str(MAIN_UI_PREFAB), "bundle": "extraRes", "assetPath": "gui/prefab/main_ui_v2", "extractionRule": "Recover only the top HUD subset from main_ui_v2 source elements, compact Label/ProgressBar components, extraRes config path entries, and the 03f07fafe v2 SpriteFrame atlas. Do not infer layout from the reference screenshot.", }, "customComponents": { "headInfo": { "classIndex": 35, "uuid": prefab[3][35][0], "fields": prefab[3][35][1], "bindingReason": "HeadInfo custom component explicitly binds headImg, txtName, txtLevel, txtExp, expBar, btnQuit, btnHead, imgLevel, node_head_frame, node_month_card, and subNode.", }, "mainUi": { "classIndex": 39, "uuid": prefab[3][39][0], "fields": prefab[3][39][1], "bindingReason": "Main UI custom component binds headComp and sourceComp; this pass recovers the child nodes consumed by those components.", }, }, "nodes": nodes, "assets": assets, "mockUser": mock_user, "solutionDetails": [ "node_head_frame serializes a Sprite component but no static SpriteFrame in the compact prefab. The runtime avatar-frame skin mapping is still unresolved, so this pass creates the source node without inventing a frame texture.", "expBar has a source cc.ProgressBar with totalLength=135.5 and barSprite ref=158, but the bar sprite's separate SpriteFrame is not serialized in the recovered static subset. The mock script updates progress value; recover the exact foreground fill art from runtime source before adding it.", "txtLevel and txtExp serialize enableOutline and outlineColor in compact cc.Label data, but no outline-width field is present in the recovered source subset. This pass preserves label layout, color, text, and mock values without inventing a Creator LabelOutline width.", ], } write_json(OUT_PATH, evidence) report = { "status": "pass" if not missing and len(labels) == 5 else "needs_solution", "sourceEvidence": str(OUT_PATH), "sourceFileCount": len(source_files), "nodeCount": len(nodes), "assetCount": len(assets), "labelCount": len(labels), "missing": missing, } write_json(REPORT_PATH, report) print(json.dumps(report, ensure_ascii=False, indent=2)) return 0 if report["status"] == "pass" else 1 if __name__ == "__main__": raise SystemExit(main())