Root cause of the empty review frames: EditorLevelLibrary.load_level/new_level are deprecated no-ops in -nullrhi commandlets, so staged actors landed in the current world and got saved over the wrong map. Switched both map builders to the LevelEditorSubsystem and reordered the review builder to save ReviewMap before the DemoMap section (the switch-back to a map that only exists in memory silently failed, writing demo content over ReviewMap). Rebuilt both maps, re-rendered every screenshot; user visual check passed. Screenshots/README.md cube naming corrected (A grounded, B floating), approval states updated; TODO.md visual-review notes closed. MEMORY.md records the deprecated-call and save-order traps.
138 lines
5.5 KiB
Python
138 lines
5.5 KiB
Python
# Build /Game/Hollywood/DemoMap: the walkable demo level.
|
|
#
|
|
# <engine>/Engine/Binaries/Linux/UnrealEditor-Cmd <uproject> -run=pythonscript \
|
|
# -script=<repo>/Tools/ue_build_demo_map.py -nullrhi -unattended -nopause -stdout
|
|
#
|
|
# Content this map expects (built by earlier steps):
|
|
# /Game/Hollywood/ElCapitan/SM_HeroFacade Tools/ue_hero_facade_import.py
|
|
# /Game/Hollywood/Site/... Tools/ue_import_site.py
|
|
#
|
|
# Map conventions, from Geo/manifests/local-origin.md:
|
|
# X east, Y south, Z up, centimetres, origin at Hollywood x Highland.
|
|
# The player starts on the north sidewalk opposite the hero frontage, looking
|
|
# south at it, which is the view the reference photographs were taken from.
|
|
#
|
|
# Every actor is (re)created by label, so rerunning the script rebuilds the map
|
|
# deterministically instead of stacking duplicates.
|
|
|
|
import os
|
|
|
|
import unreal
|
|
|
|
MAP = "/Game/Hollywood/DemoMap"
|
|
RESULT = "/tmp/gigline/demo-map.txt"
|
|
lines = []
|
|
|
|
|
|
# EditorLevelLibrary.load_level/new_level are deprecated no-ops in -nullrhi
|
|
# commandlets (they return without switching worlds, so staged actors land in
|
|
# whatever world is current and get saved over the wrong map). The
|
|
# LevelEditorSubsystem actually switches.
|
|
LES = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
|
|
|
|
|
|
def switch_level(path):
|
|
return LES.load_level(path)
|
|
|
|
def log(msg):
|
|
lines.append(str(msg))
|
|
unreal.log(str(msg))
|
|
|
|
|
|
def spawn(cls, loc, label, rot=None):
|
|
actor = unreal.EditorLevelLibrary.spawn_actor_from_class(cls, loc)
|
|
if actor is None:
|
|
log(f"spawn {cls} failed")
|
|
return None
|
|
if rot is not None:
|
|
actor.set_actor_rotation(rot, False)
|
|
actor.set_actor_label(label)
|
|
return actor
|
|
|
|
|
|
def clear(prefixes=()):
|
|
removed = 0
|
|
for actor in unreal.EditorLevelLibrary.get_all_level_actors():
|
|
label = actor.get_actor_label()
|
|
if label.startswith(prefixes):
|
|
unreal.EditorLevelLibrary.destroy_actor(actor)
|
|
removed += 1
|
|
return removed
|
|
|
|
|
|
def build():
|
|
# --- level ------------------------------------------------------------
|
|
if unreal.EditorAssetLibrary.does_asset_exist(MAP):
|
|
if not switch_level(MAP):
|
|
log(f"could not load {MAP}")
|
|
return
|
|
else:
|
|
if not switch_level(MAP):
|
|
log(f"could not create {MAP}")
|
|
return
|
|
|
|
removed = clear(("Demo_", "PlayerStart", "Hero_Facade"))
|
|
log(f"cleared {removed} previous demo actors")
|
|
|
|
# --- lighting ---------------------------------------------------------
|
|
spawn(unreal.DirectionalLight, unreal.Vector(0, 0, 2000), "Demo_Sun",
|
|
unreal.Rotator(pitch=-35.0, yaw=200.0, roll=0.0))
|
|
spawn(unreal.SkyLight, unreal.Vector(0, 0, 2000), "Demo_SkyLight")
|
|
spawn(unreal.SkyAtmosphere, unreal.Vector(0, 0, 0), "Demo_Sky")
|
|
log("staged: sun, sky light, sky atmosphere")
|
|
|
|
# --- hero facade on the measured building line ------------------------
|
|
hero = unreal.EditorAssetLibrary.load_asset("/Game/Hollywood/ElCapitan/SM_HeroFacade")
|
|
if hero:
|
|
actor = spawn(unreal.StaticMeshActor, unreal.Vector(-10722.5, -1443.0, 0.0), "Hero_Facade")
|
|
if actor:
|
|
actor.static_mesh_component.set_static_mesh(hero)
|
|
actor.static_mesh_component.set_mobility(unreal.ComponentMobility.STATIC)
|
|
log("staged: Hero_Facade at the building line")
|
|
else:
|
|
log("MISSING: SM_HeroFacade — run Tools/ue_hero_facade_import.py")
|
|
|
|
# --- site meshes, if they have been imported --------------------------
|
|
imported = 0
|
|
for name in ("Terrain_DEM", "Road_Hollywood", "Sidewalk_South", "Sidewalk_North",
|
|
"Kerb_South", "Kerb_North", "Crossing_Markings"):
|
|
mesh = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Site/{name}")
|
|
if mesh is None:
|
|
continue
|
|
actor = spawn(unreal.StaticMeshActor, unreal.Vector(0, 0, 0), f"Demo_{name}")
|
|
if actor:
|
|
actor.static_mesh_component.set_static_mesh(mesh)
|
|
actor.static_mesh_component.set_mobility(unreal.ComponentMobility.STATIC)
|
|
imported += 1
|
|
log(f"staged {imported} site meshes from /Game/Hollywood/Site")
|
|
|
|
# --- player start: north sidewalk, facing the hero frontage -------------
|
|
# Signs were measured from the imported meshes rather than assumed: the FBX
|
|
# preset flips north, so in Unreal the road spans Y -1382..+1047, the NORTH
|
|
# sidewalk is at Y -1912..-1053 and the SOUTH sidewalk at Y +718..+1447. The
|
|
# facade stands at Y -1450. Stand on the north sidewalk and look north (-Y).
|
|
spawn(unreal.PlayerStart, unreal.Vector(-10722.5, -3500.0, 120.0), "Demo_PlayerStart",
|
|
unreal.Rotator(pitch=0.0, yaw=0.0, roll=0.0))
|
|
log("staged: Demo_PlayerStart on the north sidewalk facing south")
|
|
|
|
# A lockable LFS .umap checks out read-only, which makes save_map silently
|
|
# return False — chmod u+w first or the staged actors never persist.
|
|
umap = os.path.join(unreal.Paths.project_dir(), MAP.replace("/Game/", "Content/") + ".umap")
|
|
if os.path.exists(umap) and not os.access(umap, os.W_OK):
|
|
os.chmod(umap, 0o644)
|
|
log(f"map writable: chmod u+w {umap}")
|
|
saved = unreal.EditorLoadingAndSavingUtils.save_map(
|
|
unreal.EditorLevelLibrary.get_editor_world(), MAP)
|
|
log(f"save_map {MAP}: {saved}")
|
|
|
|
|
|
try:
|
|
build()
|
|
except Exception as exc: # noqa: BLE001
|
|
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")
|