#!/usr/bin/env python3 """Extract Stage 1D house and doghouse evidence from the original Cocos source.""" 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") MAIN_ELEMENTS = SOURCE_ROOT / "source_first" / "out" / "main_scene_elements.json" SKIN_CONFIG = SOURCE_ROOT / "downloads/raw/remote/delayRes/import/2d/2d201122-b5a7-4181-8f60-d67a399ec757.edfbe.json" EXTRARES_IMPORT = SOURCE_ROOT / "downloads/raw/remote/extraRes/import" EXTRARES_NATIVE = SOURCE_ROOT / "downloads/raw/remote/extraRes/native" FARM_SCENE_PREFAB = EXTRARES_IMPORT / "0e/0ec3b3100.19268.json" CONVERTED_IMAGES = SOURCE_ROOT / "downloads" / "converted_png" / "images" SPINE_REGION_IMAGES = SOURCE_ROOT / "downloads" / "spine_regions" / "images" OUT_PATH = ROOT / "source_export" / "stage1_scene_decor_source.json" REPORT_PATH = ROOT / "source_export" / "stage1_scene_decor_extract_report.json" def load_json(path: Path) -> Any: return json.loads(path.read_text(encoding="utf-8")) def write_json(path: Path, data: dict[str, 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 compact_json_asset_records(path: Path) -> list[dict[str, Any]]: data = load_json(path) return data[5][0][2] 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 compact_node(element: dict[str, Any]) -> dict[str, Any]: return { "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")), "anchor": vec2(element.get("anchor")) if element.get("anchor") else None, "componentCount": int(element.get("component_count", 0)), } 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 children_of(elements: list[dict[str, Any]], parent_path: str) -> list[dict[str, Any]]: return sorted( [compact_node(item) for item in elements if item.get("parent_path") == parent_path], key=lambda item: item["sourceIndex"], ) def compact_child_order(prefab_data: list[Any], parent_index: int) -> list[int]: """Resolve a prefab node's serialized _children order from the compact ref table.""" flat_refs = prefab_data[5][1][2] for offset in range(0, len(flat_refs) - 2): if flat_refs[offset : offset + 3] != [0, parent_index, 0]: continue child_indices: list[int] = [] cursor = offset + 3 while cursor <= len(flat_refs) - 3: key, value, terminator = flat_refs[cursor : cursor + 3] if key == 0 and isinstance(value, int) and value >= 0 and terminator == 0: break if isinstance(key, int) and key < 0 and isinstance(value, int) and terminator == 0: child_indices.append(value) cursor += 3 if child_indices: return child_indices return [] def compact_ordered_children(elements: list[dict[str, Any]], prefab_data: list[Any], parent_path: str) -> list[dict[str, Any]]: element = element_by_path(elements, parent_path) by_index = {int(item["index"]): item for item in elements} child_indices = compact_child_order(prefab_data, int(element["index"])) if not child_indices: return children_of(elements, parent_path) return [compact_node(by_index[index]) for index in child_indices if index in by_index] def skin_record(records: list[dict[str, Any]], record_id: int) -> dict[str, Any]: for record in records: if int(record["id"]) == record_id: return record raise KeyError(record_id) 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 read_atlas_region(atlas_path: Path, name: str) -> dict[str, Any]: lines = atlas_path.read_text(encoding="utf-8").splitlines() for index, line in enumerate(lines): if line.strip() != name: continue region: dict[str, Any] = {"name": name} for raw in lines[index + 1 : index + 8]: if not raw.startswith(" "): break key, value = raw.strip().split(":", 1) value = value.strip() if key == "rotate": region[key] = value == "true" elif key in {"xy", "size", "orig", "offset"}: left, right = [int(item.strip()) for item in value.split(",")] region[key] = {"x": left, "y": right} if key in {"xy", "offset"} else {"width": left, "height": right} elif key == "index": region[key] = int(value) else: region[key] = value return region raise KeyError(f"missing atlas region {name} in {atlas_path}") def find_metadata_dict(obj: Any, name: str) -> dict[str, Any] | None: if isinstance(obj, dict): if obj.get("name") == name and "rect" in obj and "originalSize" in obj: return obj for value in obj.values(): found = find_metadata_dict(value, name) if found: return found elif isinstance(obj, list): for value in obj: found = find_metadata_dict(value, name) if found: return found return None def find_sprite_metadata(name: str) -> tuple[Path, dict[str, Any]]: for path in sorted(EXTRARES_IMPORT.rglob("*.json")): text = path.read_text(encoding="utf-8", errors="ignore") if name not in text: continue found = find_metadata_dict(json.loads(text), name) if found: return path, found raise FileNotFoundError(f"missing SpriteFrame metadata for {name}") def main() -> int: main_elements = load_json(MAIN_ELEMENTS) farm_elements = main_elements["prefabs"]["farm_scene_v3"]["elements"] farm_prefab_data = load_json(FARM_SCENE_PREFAB) skin_records = compact_json_asset_records(SKIN_CONFIG) house_record = skin_record(skin_records, 201001) doghouse_record = skin_record(skin_records, 205001) house_skeleton_bin = EXTRARES_NATIVE / "8e/8ee07a72-8385-4d93-8be2-a8791f4aaf0c.8cce8.bin" house_skeleton_import = EXTRARES_IMPORT / "8e/8ee07a72-8385-4d93-8be2-a8791f4aaf0c.452f3.json" house_atlas = EXTRARES_NATIVE / "97/9796faf5-f7c6-4147-94fd-eeb7c9f5c57a.7bba5.atlas" house_atlas_image = CONVERTED_IMAGES / "extraRes__spine__v2__costume__house__201001__64e30914-d5c1-4b98-8440-08cca7bef2ac.fcb93.png" house_region = SPINE_REGION_IMAGES / "spine__v2__costume__house__201001__xiaowu.png" doghouse_skeleton_bin = EXTRARES_NATIVE / "02/0219f9b8-75c9-470f-8a7a-7e0c9e98d3ee.cba76.bin" doghouse_skeleton_import = EXTRARES_IMPORT / "02/0219f9b8-75c9-470f-8a7a-7e0c9e98d3ee.54862.json" doghouse_atlas = EXTRARES_NATIVE / "6d/6d1e77d2-8c9a-4154-91ff-14403b217d82.f6adc.atlas" doghouse_atlas_image = CONVERTED_IMAGES / "extraRes__spine__v2__costume__doghouse__205001__7286d010-5123-4bef-a533-615e130f281f.a6021.png" doghouse_static_image = CONVERTED_IMAGES / "extraRes__model__v3__dogHouse__0e2ba560-1545-422c-a5d6-4140e12bf076.becf4.png" doghouse_sprite_meta_file, doghouse_sprite_frame = find_sprite_metadata("dogHouse") source_files = [ MAIN_ELEMENTS, SKIN_CONFIG, house_skeleton_import, house_skeleton_bin, house_atlas, house_atlas_image, house_region, FARM_SCENE_PREFAB, doghouse_skeleton_import, doghouse_skeleton_bin, doghouse_atlas, doghouse_atlas_image, doghouse_static_image, doghouse_sprite_meta_file, ] missing = [str(path) for path in source_files if not path.exists()] fg_layer_children = compact_ordered_children(farm_elements, farm_prefab_data, "farm_scene_v3/Scene/FgLayer") farm_skin_layers = compact_ordered_children(farm_elements, farm_prefab_data, "farm_scene_v3/Scene/FgLayer/FarmSkin") evidence = { "metadata": { "module": "stage1_scene_decor", "sourceFiles": [str(path) for path in source_files], "extractionRule": "Recover house and doghouse from farm_scene_v3 source parent chain, DogLayer source node, SkinCfg default records, and compact prefab _children order. Do not use farm_scene_v3_sprite positions.", }, "sourceParentNodes": { "compactChildrenResolution": { "sourcePrefab": str(FARM_SCENE_PREFAB), "rule": "Read the compact _children reference table from data[5][1][2]. For parent source index N, the sequence after [0,N,0] maps negative child keys to concrete node row indexes in sibling order.", }, "fgLayerChildrenOrder": fg_layer_children, "farmSkin": compact_node(element_by_path(farm_elements, "farm_scene_v3/Scene/FgLayer/FarmSkin")), "farmSkinLayers": farm_skin_layers, "dogLayer": compact_node(element_by_path(farm_elements, "farm_scene_v3/Scene/FgLayer/DogLayer")), "dogHouseSourceNode": compact_node(element_by_path(farm_elements, "farm_scene_v3/Scene/FgLayer/DogLayer/dogHouse")), }, "skinDefaults": [ { "id": int(house_record["id"]), "skinType": int(house_record["skin_type"]), "skinName": house_record["skin_name"], "parentNode": house_record["parent_node"], "position": {"x": float(house_record["position"]["x"]), "y": float(house_record["position"]["y"]), "z": 0.0}, "spineAsset": house_record["spine_asset"], "iconAsset": house_record["icon_asset"], }, { "id": int(doghouse_record["id"]), "skinType": int(doghouse_record["skin_type"]), "skinName": doghouse_record["skin_name"], "parentNode": doghouse_record["parent_node"], "position": {"x": float(doghouse_record["position"]["x"]), "y": float(doghouse_record["position"]["y"]), "z": 0.0}, "spineAsset": doghouse_record["spine_asset"], "iconAsset": doghouse_record["icon_asset"], }, ], "decorSpineAssets": [ { "id": "house_201001", "skinCfgId": 201001, "name": "默认小屋", "sourceAsset": "spine/v2/costume/house/201001", "skeletonImportJson": str(house_skeleton_import), "skeletonBinaryPath": str(house_skeleton_bin), "atlasPath": str(house_atlas), "atlasImagePath": str(house_atlas_image), "atlasImageName": "201001.png", "atlasImageInfo": image_info(house_atlas_image), "defaultSkin": "default", "defaultAnimation": "idle_01", "staticPreview": { "region": "xiaowu", "path": str(house_region), "imageInfo": image_info(house_region), "atlasRegion": read_atlas_region(house_atlas, "xiaowu"), }, }, { "id": "doghouse_205001", "skinCfgId": 205001, "name": "默认狗屋", "sourceAsset": "spine/v2/costume/doghouse/205001", "skeletonImportJson": str(doghouse_skeleton_import), "skeletonBinaryPath": str(doghouse_skeleton_bin), "atlasPath": str(doghouse_atlas), "atlasImagePath": str(doghouse_atlas_image), "atlasImageName": "205001.png", "atlasImageInfo": image_info(doghouse_atlas_image), "defaultSkin": "default", "defaultAnimation": "idle_01", "staticPreview": { "sourceAsset": "model/v3/dogHouse/spriteFrame", "path": str(doghouse_static_image), "imageInfo": image_info(doghouse_static_image), "spriteFrameMetadataFile": str(doghouse_sprite_meta_file), "spriteFrame": doghouse_sprite_frame, }, }, ], "generatedNodes": [ { "path": "farm_scene_v3/Scene/FgLayer/FarmSkin/Layer2/house_201001", "name": "house_201001", "parentPath": "farm_scene_v3/Scene/FgLayer/FarmSkin/Layer2", "source": "SkinCfg id=201001", "localPosition": {"x": float(house_record["position"]["x"]), "y": float(house_record["position"]["y"]), "z": 0.0}, "spineAssetId": "house_201001", }, { "path": "farm_scene_v3/Scene/FgLayer/DogLayer/dogHouse", "name": "dogHouse", "parentPath": "farm_scene_v3/Scene/FgLayer/DogLayer", "source": "farm_scene_v3 source node plus SkinCfg id=205001", "localPosition": vec3(element_by_path(farm_elements, "farm_scene_v3/Scene/FgLayer/DogLayer/dogHouse").get("local_position")), "anchor": vec2(element_by_path(farm_elements, "farm_scene_v3/Scene/FgLayer/DogLayer/dogHouse").get("anchor")), "spineAssetId": "doghouse_205001", "staticSpriteAssetId": "dogHouse_model_v3", }, ], "solutionDetails": [ "House and doghouse original assets are binary Spine 3.8 data. Stage 1D generates Cocos Creator spine-data assets from the original .bin/.atlas/.png; if Creator rejects binary spine import, record the import solution before using static preview regions.", ], } write_json(OUT_PATH, evidence) report = { "status": "pass" if not missing else "needs_solution", "sourceEvidence": str(OUT_PATH), "sourceFileCount": len(source_files), "missing": missing, "decorNodeCount": len(evidence["generatedNodes"]), "spineAssetCount": len(evidence["decorSpineAssets"]), "fgLayerRecoveredChildOrder": [item["name"] for item in fg_layer_children], } write_json(REPORT_PATH, report) print(json.dumps(report, ensure_ascii=False, indent=2)) return 0 if not missing else 1 if __name__ == "__main__": raise SystemExit(main())