cocos-farm/tools/extract_scene_decor_evidence.py

342 lines
15 KiB
Python

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