#!/usr/bin/env python3 """Normalize and diff the Unreal export, headless import, and pre-export fallback.""" from __future__ import annotations import difflib import json import hashlib from pathlib import Path ROOT = Path(__file__).resolve().parents[4] OUT = ROOT / "Saved/Linux/l70" REPORT_DIR = Path(__file__).resolve().parent OWNER = ROOT / "Standalone/data/owner_world.json" def load(path: Path): return json.loads(path.read_text(encoding="utf-8")) def normalized_layout(path: Path) -> dict: value = load(path) # Headless annotates imported entries with the owner's requested passage style. # That annotation is not part of the serialized Unreal resolved layout. value.pop("resolved_strate_passage_styles", None) return value def canonical(manifest_path: Path, layout_path: Path, *, include_owner_inputs: bool) -> dict: manifest = load(manifest_path) layout = normalized_layout(layout_path) result = { "manifest": manifest, "layout": layout, "native_floating_island_plans": [ {k: v for k, v in plan.items() if k in ("strate_index", "top_chunk_z", "bottom_chunk_z")} for plan in manifest.get("floating_islands", []) ], "generator_recipes": [ { "strate_index": s.get("strate_index"), "generator_kind": s.get("generator_kind"), "recipe_id": s.get("recipe_id"), "recipe_version": s.get("recipe_version"), "query_dispatch_path": s.get("query_dispatch_path"), } for s in layout.get("strates", []) ], } if include_owner_inputs: owner = load(OWNER) result["owner_slots"] = [ { "index": slot.get("index"), "top_chunk_z": slot.get("top_chunk_z"), "bottom_chunk_z": slot.get("bottom_chunk_z"), "height_chunks": slot.get("height_chunks"), "generator_recipe_setting": slot.get("asset", {}).get("CoreGeometry", {}).get("GeneratorRecipe"), "passage_config": slot.get("passage_config"), } for slot in owner.get("strates", []) ] result["owner_passages"] = owner.get("passages", []) result["effective_layout_width_q"] = manifest.get("recipe", {}).get("layout_chunk_width_q") stale = owner.get("settings_asset", {}).get("WorldAssemblyManifestJson", "") result["inactive_settings_manifest"] = { "sha256": hashlib.sha256(stale.encode("utf-8")).hexdigest() if stale else None, "active": owner.get("settings_asset", {}).get("bUseWorldAssemblyManifestOverride", False), } return result def json_text(value) -> str: return json.dumps(value, indent=2, sort_keys=True, ensure_ascii=False) + "\n" def write(name: str, value) -> Path: path = OUT / name path.write_text(json_text(value), encoding="utf-8") return path def main() -> int: unreal = canonical(OUT / "manifest-unreal-strict-core.json", OUT / "layout-unreal-strict-core.json", include_owner_inputs=True) headless = canonical(OUT / "manifest-headless-imported-final.json", OUT / "layout-headless-imported-final.json", include_owner_inputs=True) unreal_path = write("resolved-plan-unreal-canonical.json", unreal) headless_path = write("resolved-plan-headless-canonical.json", headless) exact = unreal == headless old = canonical(OUT / "manifest-headless-fallback-current.json", OUT / "layout-headless-fallback-current.json", include_owner_inputs=False) old_path = write("resolved-plan-headless-fallback-canonical.json", old) compared_keys = ("manifest", "layout", "native_floating_island_plans", "generator_recipes") unreal_core = {key: unreal[key] for key in compared_keys} old_core = {key: old[key] for key in compared_keys} old_diffs = list(difflib.unified_diff( json_text(unreal_core).splitlines(), json_text(old_core).splitlines(), fromfile="unreal-current", tofile="headless-pre-export-fallback", lineterm="")) diff_path = OUT / "resolved-plan-diff.txt" diff_path.write_text("\n".join(old_diffs) + "\n", encoding="utf-8") # Report compact high-value field deltas for the previous independently planned pair. old_layout = old["layout"] new_layout = unreal["layout"] old_slots = [(s.get("strate_index"), s.get("generator_kind"), s.get("interior_bottom_um"), s.get("interior_top_um")) for s in old_layout.get("strates", [])] new_slots = [(s.get("strate_index"), s.get("generator_kind"), s.get("interior_bottom_um"), s.get("interior_top_um")) for s in new_layout.get("strates", [])] route_rows = [] for label, layout in (("fallback", old_layout), ("unreal", new_layout)): passages = [] for passage in layout.get("passages", []): points = passage.get("control_points", []) passages.append({ "id": passage.get("stable_id"), "upper": passage.get("upper_strate_index"), "lower": passage.get("lower_strate_index"), "style": passage.get("style"), "control_point_count": len(points), "upper_mouth_q": passage.get("upper_mouth_q"), "lower_mouth_q": passage.get("lower_mouth_q"), }) route_rows.append((label, passages)) lines = [ f"final_unreal_equals_headless={exact}", f"final_manifest_equal={unreal['manifest'] == headless['manifest']}", f"final_layout_equal={unreal['layout'] == headless['layout']}", f"final_stable_identity={new_layout.get('stable_identity')}", f"layout_width_q={unreal['effective_layout_width_q']}", f"native_fi_plans={unreal['native_floating_island_plans']}", f"owner_slots_current={[(s.get('index'), s.get('top_chunk_z'), s.get('bottom_chunk_z')) for s in unreal['owner_slots']]}", f"pre_export_stable_identity={old_layout.get('stable_identity')}", f"pre_export_manifest_hash={old['manifest'].get('content_hash')}", f"unreal_manifest_hash={unreal['manifest'].get('content_hash')}", f"pre_export_slots={old_slots}", f"unreal_slots={new_slots}", f"routes={json.dumps(route_rows, sort_keys=True)}", f"pre_export_diff_lines={len(old_diffs)}", f"canonical_unreal={unreal_path}", f"canonical_headless={headless_path}", f"canonical_fallback={old_path}", f"diff={diff_path}", ] result = REPORT_DIR / "resolved-plan-comparison.txt" result.write_text("\n".join(lines) + "\n", encoding="utf-8") print("\n".join(lines), flush=True) return 0 if exact else 2 if __name__ == "__main__": raise SystemExit(main())