Files
gig-line/Tools/unreal/ue_recreate_maps.py
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

85 lines
3.0 KiB
Python

"""Empty both Unreal maps before a build, and verify they really are empty.
<engine>/UnrealEditor-Cmd <uproject> -run=pythonscript \
-script=<repo>/Tools/unreal/ue_recreate_maps.py -nullrhi -unattended -nopause -stdout
Why this does not delete the `.umap` files: `LevelEditorSubsystem.new_level()` refuses a path whose
package is still in memory, and `EditorAssetLibrary.delete_asset()` leaves the file on disk with the package
still registered, so deleting and recreating in one session always returns False. Measured:
new_level fresh -> True
delete_asset -> True
disk file still present -> True
new_level again -> False
What does work, and is what the maps actually need, is clearing every actor and saving: the builders stage
everything they use, so a level with no actors is the blank slate. Anything an experiment left behind - an
actor from a probe, a duplicated vehicle, a stale shot camera - is gone, which is the failure this step
exists to prevent.
Both maps are emptied and saved, and the script reports the actor count it left behind.
Result: /tmp/gigline/recreate-maps.txt
"""
import os
import unreal
MAPS = ("/Game/Hollywood/DemoMap", "/Game/Test/ReviewMap")
RESULT = "/tmp/gigline/recreate-maps.txt"
lines = []
def log(message):
lines.append(str(message))
unreal.log(str(message))
def make_writable(path):
"""A .umap under Content/ that is read-only cannot be saved; LFS checks it out that way."""
disk = os.path.join(unreal.Paths.project_content_dir(),
path.replace("/Game/", "") + ".umap")
try:
mode = os.stat(disk).st_mode & 0o777
if not mode & 0o200:
os.chmod(disk, 0o644)
log(f"{path}: chmod u+w ({oct(mode)} -> 0o644)")
except FileNotFoundError:
pass
def main():
les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
for path in MAPS:
make_writable(path)
if not les.load_level(path):
log(f"{path}: could not load, and new_level cannot replace an in-memory package")
continue
removed = 0
for actor in list(unreal.EditorLevelLibrary.get_all_level_actors()):
try:
unreal.EditorLevelLibrary.destroy_actor(actor)
removed += 1
except Exception: # noqa: BLE001
pass
left = len(unreal.EditorLevelLibrary.get_all_level_actors())
saved = unreal.EditorLoadingAndSavingUtils.save_map(
unreal.EditorLevelLibrary.get_editor_world(), path)
log(f"{path}: cleared {removed} actors, {left} left, save_map {saved}")
empty = all(" 0 left" in line for line in lines)
lines.append(f"VERDICT: {'PASS' if empty else 'FAIL'}")
log(f"maps emptied: {'yes' if empty else 'no'}")
try:
main()
except Exception as exc: # noqa: BLE001
import traceback
log(f"FAILED: {type(exc).__name__}: {exc}")
log(traceback.format_exc())
lines.append("VERDICT: FAIL")
with open(RESULT, "w") as handle:
handle.write("\n".join(lines) + "\n")