Files
gig-line/Tools/unreal/ue_audit_scene.py
T
zwitschi 4d27109668 refactor(tools): group the sixty scripts by what they talk to
One directory with sixty scripts stopped being navigable, and the grouping is also the sandbox boundary:

  Tools/unreal/    commandlets and the two map builders, with ue_common.py and ue_placement.py as the
                   shared imports (they need the engine)
  Tools/blender/   builders, exporters, the FBX preset, the MPFB character pipeline, blender_screenshots
                   (they need the Blender Flatpak)
  Tools/review/    the visual loop: render_screenshots.sh, judge_screenshot.py, hero_compare.py,
                   build_comparison_md.py, e2e_asset_check.py, hero_iterate.sh, visual_loop.py,
                   visual_targets.json
  Tools/gen/       image and texture generation: the ComfyUI driver, deterministic texture writers,
                   reference fetching
  Tools/input/     OS-level input injection
  root             _common.py, check-lfs.sh, addon_compat_probe.py, sibling_asset_audit.py, tests/

Every moved script now resolves the repository root as ../.. and puts the tools root plus its siblings on
sys.path, so 'import ue_common' still works from unreal/. The test suite bootstrap does the same for the five
folders. render_screenshots.sh takes the folder as a third argument to run_cmdlet, and the e2e driver,
hero_iterate.sh and the comparison generator were repointed.

51 README rows now link to their new location, the README gains a Layout section naming each folder's
sandbox, and the architecture notes and Blender README follow the moves.

Verified: 108 tests pass, prettier is clean, judge_screenshot.py and hero_compare.py run from their new
homes, and the e2e run was aborted mid-way (it had been rebuilding an empty DemoMap for 15 minutes) and is
being rerun against the new layout.
2026-11-10 12:40:30 +01:00

115 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""Audit the DemoMap for staged actors that would render wrong.
Checks that are cheap and catch the failures this project actually had:
1. **No unmaterialed actors.** 604 actors once rendered with the default material
because the demo builder deleted and recreated the site materials, orphaning
every reference; the map looked merely "a bit flat".
2. **Placement spans.** Each verified frontage building is probed against
`Tools/unreal/ue_placement.py`; a building 13 m off its footprint still renders fine,
which is exactly why it needs a number, not an eye.
3. **No duplicate actor labels.** Two buildings' kit pieces sharing a label made
the outliner ambiguous and hid one of them.
UnrealEditor-Cmd <uproject> -run=pythonscript \
-script=Tools/unreal/ue_audit_scene.py -nullrhi -unattended -nopause -stdout
Result file: `/tmp/gigline/scene-audit.txt`. Exit code from the commandlet is
always 0, so **read the file** — `visual_loop.py --verify` does not depend on it.
"""
from __future__ import annotations
import collections
import os
import sys
import unreal
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import ue_placement # noqa: E402
MAP = "/Game/Hollywood/DemoMap"
RESULT = "/tmp/gigline/scene-audit.txt"
lines: list[str] = []
failures = 0
def log(message: str) -> None:
lines.append(message)
print(message)
def fail(message: str) -> None:
global failures
failures += 1
log("FAIL " + message)
def main() -> None:
if not unreal.EditorLevelLibrary.load_level(MAP):
log(f"could not load {MAP}")
return
actors = unreal.EditorLevelLibrary.get_all_level_actors()
# 1. materials
unmaterialed = []
for actor in actors:
comp = actor.get_component_by_class(unreal.StaticMeshComponent)
if not comp or not comp.static_mesh:
continue
if comp.get_material(0) is None:
unmaterialed.append(actor.get_actor_label())
log(f"mesh actors: {sum(1 for a in actors if a.get_component_by_class(unreal.StaticMeshComponent))}")
if unmaterialed:
fail(f"{len(unmaterialed)} actor(s) with no material, e.g. {unmaterialed[:6]}")
else:
log("OK every staged mesh has a material")
# 2. placement spans
for actor in actors:
label = actor.get_actor_label()
key = None
if label.startswith("Frontage_SM_"):
name = label[len("Frontage_SM_"):]
key = {"Hollywood_6922": "6922", "Hollywood_6902": "6902",
"ElCapitanTheatre": "6838", "Budget_6822": "6822",
"Hollywood_6806": "6806", "LeeDrug_6800": "6800"}.get(name)
elif label == ue_placement.HERO_ACTOR:
key = "hero"
if not key:
continue
origin, extent = actor.get_actor_bounds(False)
lo, hi = origin.x - extent.x, origin.x + extent.x
problem = ue_placement.describe_span_failure(key, lo, hi)
if problem:
fail(problem)
else:
log(f"OK {key}: X {lo:.0f}..{hi:.0f}")
# 3. duplicate labels
counts = collections.Counter(a.get_actor_label() for a in actors)
duplicates = {k: v for k, v in counts.items() if v > 1}
if duplicates:
sample = list(duplicates.items())[:6]
fail(f"{len(duplicates)} duplicated actor label(s), e.g. {sample}")
else:
log("OK no duplicate actor labels")
log(f"== {failures} failure(s) ==")
try:
main()
except Exception as exc: # noqa: BLE001 - surface in the result file
import traceback
log(f"FAILED: {type(exc).__name__}: {exc}")
log(traceback.format_exc())
os.makedirs(os.path.dirname(RESULT), exist_ok=True)
with open(RESULT, "w", encoding="utf-8") as handle:
handle.write("\n".join(lines) + "\n")