507 lines
23 KiB
Python
507 lines
23 KiB
Python
#!/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())
|