Files
gig-line/Tools/unreal/ue_import_north_buildings.py
T
zwitschi dbb42322ff fix(tools): make the e2e chain actually run after the move, and record what it found
The e2e asset check now completes: PASS on all six steps - empty maps 9 s, demo build 10 s, review build 9 s,
captures 80 s, judge 388 s, review file. Four breakages from the folder move had to be fixed first:

- ue_placement and ue_common are siblings of the scripts that import them, but the rewrite put only the tools
  root on sys.path, so every Unreal commandlet died with ModuleNotFoundError. Both paths are inserted now.
- render_screenshots.sh resolved the repository root one level up, so commandlet paths became Tools/Tools/...
  and the capture step failed in 0 s.
- the driver treated a result file left by an earlier run as success, which is how a ten-second 'demo build'
  passed without building. Result files are unlinked before each step and child output streams to a per-step
  log file rather than a pipe.
- two editors on one project hang instead of failing, so the driver refuses to start while one is running.

Also corrects ue_recreate_maps.py: LevelEditorSubsystem.new_level() refuses a path whose package is still in
memory and delete_asset() leaves the file on disk, measured create->delete->create returning False. The step
now loads each map, destroys every actor, saves, and reports the count it left (0), which is the property the
builders actually need.

Open question recorded in TODO.md: the rebuilt map holds 409 actors and 613 KB against 600 and 886 KB before,
and the ~190 missing ones look like the facade-kit and theatre-trim placements that run as separate steps.
2026-11-10 13:37:21 +01:00

137 lines
5.2 KiB
Python

# Import the two north-side background buildings (TCL Chinese Theatre, Madame
# Tussaud's) into Unreal and stage them in the demo map, across the street from
# the hero facade.
#
# Placement: these blockout meshes live in the same local frame as the site
# meshes (X east / Y north / Z up, metres, origin Hollywood x Highland), which
# are imported at the Unreal origin and land correctly via the `ue_metric_v1`
# FBX axis mapping (north -> -Y). So the buildings are spawned at Unreal
# (0,0,0) like the site meshes, not at the pivot-shifted hero facade position.
#
# UnrealEditor-Cmd <uproject> -run=pythonscript \
# -script=<repo>/Tools/unreal/ue_import_north_buildings.py -nullrhi -unattended -nopause -stdout
import os
import sys
import unreal
_HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(_HERE)) # tools root, for _common
sys.path.insert(0, _HERE) # siblings in this folder (ue_common, ue_placement)
import ue_common # noqa: E402
SRC = "/tmp/gigline/north"
DEST = "/Game/Hollywood/North"
LEVEL = "/Game/Hollywood/DemoMap"
RESULT = "/tmp/gigline/north-import.txt"
lines = []
def log(msg):
lines.append(str(msg))
unreal.log(str(msg))
IMPORTS = [
("TCL_Chinese_Theatre.fbx", "SM_TCL_Chinese_Theatre"),
("Madame_Tussauds.fbx", "SM_Madame_Tussauds"),
]
def import_meshes():
assets = unreal.EditorAssetLibrary.list_assets(DEST) if unreal.EditorAssetLibrary.does_directory_exist(DEST) else []
for fn, asset_name in IMPORTS:
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([unreal.AssetImportTask(
filename=os.path.join(SRC, fn),
destination_path=DEST,
destination_name=asset_name,
automated=True,
save=True,
replace_existing=True,
options=unreal.FbxImportUI(
import_mesh=True, import_materials=True, import_textures=False,
import_as_skeletal=False) if False else None,
)])
# FbxImportUI kwargs are unreliable across 5.8; configure explicitly below.
def stage():
if unreal.EditorAssetLibrary.does_asset_exist(LEVEL):
unreal.EditorLevelLibrary.load_level(LEVEL)
for actor in list(unreal.EditorLevelLibrary.get_all_level_actors()):
if actor.get_actor_label().startswith("North_"):
unreal.EditorLevelLibrary.destroy_actor(actor)
for asset_name, label in (("SM_TCL_Chinese_Theatre", "North_TCL_Chinese"), ("SM_Madame_Tussauds", "North_Madame_Tussauds")):
mesh = unreal.EditorAssetLibrary.load_asset(f"{DEST}/{asset_name}")
if mesh is None:
log(f"MISSING {DEST}/{asset_name}")
continue
actor = unreal.EditorLevelLibrary.spawn_actor_from_class(
unreal.StaticMeshActor, unreal.Vector(0, 0, 0))
actor.set_actor_label(label)
comp = actor.static_mesh_component
comp.set_static_mesh(mesh)
comp.set_mobility(unreal.ComponentMobility.STATIC)
# Report where the mesh actually lands in world space (site-frame → Unreal).
box = mesh.get_bounding_box()
mn, mx = box.min, box.max
log(f"{label}: mesh world-space X {mn.x:.0f}..{mx.x:.0f} Y {mn.y:.0f}..{mx.y:.0f} size "
f"{mx.x-mn.x:.0f}x{mx.y-mn.y:.0f} cm")
_make_map_writable(LEVEL)
saved = unreal.EditorLoadingAndSavingUtils.save_map(
unreal.EditorLevelLibrary.get_editor_world(), LEVEL)
log(f"save_map {LEVEL}: {saved}")
if not ue_common.require_saved(saved, LEVEL):
log(f"FAILED to save {LEVEL} — staged content will not persist")
def _make_map_writable(level: str) -> None:
"""Clear the read-only bit an LFS .umap checks out with.
Without this `save_map()` returns False and the staged actor is lost on the
next load — no exception, no non-zero exit (see MEMORY: 0.1.x trap).
"""
content = unreal.Paths.project_content_dir()
rel = level[len("/Game/"):] if level.startswith("/Game/") else level
umap = os.path.join(content, rel + ".umap")
note = ue_common.make_writable(umap)
if note:
log(f"map writable: {note}")
def main():
for fn, asset_name in IMPORTS:
task = unreal.AssetImportTask()
task.filename = os.path.join(SRC, fn)
task.destination_path = DEST
task.destination_name = asset_name
task.automated = True
task.save = True
task.replace_existing = True
ui = unreal.FbxImportUI()
ui.import_mesh = True
ui.import_materials = True
ui.import_textures = False
ui.import_as_skeletal = False
smd = ui.static_mesh_import_data
smd.import_uniform_scale = 1.0
smd.combine_meshes = True
smd.convert_scene_unit = False
smd.force_front_x_axis = False
smd.normal_import_method = unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS
task.options = ui
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
log(f"imported {asset_name}")
unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True)
stage()
try:
main()
except Exception as exc: # noqa
import traceback
log(f"FAILED: {type(exc).__name__}: {exc}")
log(traceback.format_exc())
with open(RESULT, "w") as f:
f.write("\n".join(lines) + "\n")