342 lines
15 KiB
Python
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())
|