"""Builds /Game/Maps/L_World: a world-partitioned level with the landscape that RawContent/World/World.json describes, imported from the PNGs in RawContent/World/Heightmaps/ and dressed as Elite_RockyMeadows dresses its demo maps: the pack's landscape material and three paint layers, its sun with the moving cloud shadows, its skybox dome, sky light, height fog and post-process settings (numbers read from the pack's maps with dump_level.py), plus a sea plane at sea level and two player starts on the spawn pad. Rebuilds from scratch every run; the level is a product of this script, the manifest and the PNGs, never hand-edited. UnrealEditor-Cmd.exe Salty.uproject -run=pythonscript -script=Scripts/Authoring/create_world.py -AllowCommandletRendering -AllowCommandletRendering matters: the landscape's render heightmaps come from the edit-layer merge on the GPU, and a commandlet without it silently skips that, leaving a landscape with collision but no visible surface. Swapping the terrain: point the manifest's source at a real heightmap (or change the seed), rerun generate_heightmap.py, then rerun this. If the PNGs are missing or the wrong size this script generates them first. The landscape's component layout is the engine's choice for the resolution: 4081 vertices a side gives 16x16 components of 255 quads, each its own streaming proxy (see RawContent/World/README.md before changing it). """ import os import shutil import sys import unreal HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) sys.path.insert(0, os.path.join(HERE, ".pylib")) import heightmap_io # noqa: E402 import rocky_meadows # noqa: E402 from world_manifest import LAYER_FILES, load_manifest # noqa: E402 # The pack's kit — its landscape material, layer infos, sun, skybox, fog and grade — is in rocky_meadows.py, # shared with create_region_world.py so one set of numbers dresses both worlds. manifest = load_manifest() LEVEL_PATH = manifest.level level_subsystem = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem) actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem) asset_lib = unreal.EditorAssetLibrary def ensure_heightmaps(): files = [manifest.heightmap_path] + [manifest.weightmap_path(name) for name in LAYER_FILES] reason = None missing = [f for f in files if not os.path.isfile(f)] if missing: reason = f"missing {missing}" else: width, height, depth, _ = heightmap_io.read_png_header(manifest.heightmap_path) if (width, height, depth) != (manifest.vertices_per_side, manifest.vertices_per_side, 16): reason = f"{manifest.heightmap_path} is {width}x{height} at {depth} bit, the manifest wants {manifest.vertices_per_side} at 16" if reason is None: return unreal.log(f"generating heightmaps: {reason}") import generate_heightmap generate_heightmap.main([]) def spawn(actor_class, label, location=unreal.Vector(0, 0, 0), rotation=unreal.Rotator(0, 0, 0)): actor = actor_subsystem.spawn_actor_from_class(actor_class, location, rotation) actor.set_actor_label(label) return actor KEEP_CLASSES = {"WorldSettings", "WorldDataLayers", "WorldPartitionMiniMap"} # what a fresh partitioned level has def content_path(): return os.path.abspath(unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_content_dir())) def external_actor_dir(): return os.path.join(content_path(), "__ExternalActors__", LEVEL_PATH.replace("/Game/", "", 1)) def prepare_level(): """An empty level to build into. An existing level is loaded and emptied rather than deleted and recreated: recreating it resaves its two HLOD layer assets, and an open editor that has had the level loaded keeps those files locked, so the recreation ends in a nameless temp world whose save goes nowhere (2026-09-16). Streaming proxies the editor does not load cannot be destroyed here; sweep_stale_actor_packages removes their files after the save.""" if asset_lib.does_asset_exist(LEVEL_PATH): if not level_subsystem.load_level(LEVEL_PATH): raise RuntimeError(f"could not load {LEVEL_PATH}") removed = 0 for actor in actor_subsystem.get_all_level_actors(): if actor.get_class().get_name() in KEEP_CLASSES: continue actor_subsystem.destroy_actor(actor) removed += 1 unreal.log(f"{LEVEL_PATH} loaded and emptied: {removed} loaded actors removed") else: folder = external_actor_dir() if os.path.isdir(folder): shutil.rmtree(folder, ignore_errors=True) level_subsystem.new_level(LEVEL_PATH, True) # world-partitioned: 200 km2 must stream world_path = editor_subsystem.get_editor_world().get_path_name() if not world_path.startswith(LEVEL_PATH): raise RuntimeError(f"the editor world is {world_path}, not {LEVEL_PATH}; a save would go nowhere") def sweep_stale_actor_packages(): """After the save: every file under the level's external actor folder that belongs to none of the level's actors is a leftover of an earlier build (a proxy the editor never loaded, so it could not be destroyed). Left there, it would come back as a second landscape the next time the level loads.""" keep = set() for actor in actor_subsystem.get_all_level_actors(): package = actor.get_outermost().get_path_name() # /Game/__ExternalActors__/Maps/L_World/1/YM/XXXX keep.add(os.path.normcase(os.path.join(content_path(), package.replace("/Game/", "", 1).replace("/", os.sep) + ".uasset"))) removed, failed = 0, 0 for root, _, files in os.walk(external_actor_dir()): for name in files: path = os.path.join(root, name) if os.path.normcase(path) in keep: continue try: os.remove(path) removed += 1 except OSError as error: failed += 1 unreal.log_warning(f"stale actor package {path} could not be removed: {error}") unreal.log(f"stale actor packages: {removed} removed, {failed} left, {len(keep)} kept") if failed: raise RuntimeError(f"{failed} stale actor packages remain under {external_actor_dir()}; the level would load duplicates") def create_landscape(): material = rocky_meadows.load_or_raise(rocky_meadows.LANDSCAPE_MATERIAL) weightmaps = rocky_meadows.weightmap_entries(manifest.weightmap_path) world = editor_subsystem.get_editor_world() unreal.log(f"landscape: {manifest.describe()}") landscape = unreal.LandscapeAuthoringLibrary.create_landscape_from_heightmap( world, manifest.heightmap_path, weightmaps, material, unreal.Vector(0.0, 0.0, manifest.landscape_z_cm), unreal.Vector(manifest.quad_cm, manifest.quad_cm, manifest.z_scale), manifest.streaming_grid_components) if not landscape: raise RuntimeError("landscape creation failed; see LogSaltyEditor") return landscape def ground_height_at(x, y, fallback): world = editor_subsystem.get_editor_world() reach = manifest.elevation_max_m * 100.0 + 100000.0 start, end = unreal.Vector(x, y, reach), unreal.Vector(x, y, -reach) hit = unreal.SystemLibrary.line_trace_single(world, start, end, unreal.TraceTypeQuery.ECC_VISIBILITY, False, [], unreal.DrawDebugTrace.NONE, True) if hit and hit.to_tuple()[0]: return hit.to_tuple()[4].z unreal.log_warning("no landscape hit under the spawn; using the fallback height") return fallback def ensure_player_starts(): # The heightmap has a flat pad at its centre; two starts so two PIE clients spawn without a warning. fallback = manifest.sea_level_z_cm + 15000.0 z = ground_height_at(0.0, 0.0, fallback) + 120.0 for index, y in enumerate((-200.0, 200.0)): spawn(unreal.PlayerStart, f"World_PlayerStart_{index}", unreal.Vector(0.0, y, z)) def main(): ensure_heightmaps() prepare_level() landscape = create_landscape() rocky_meadows.dress(spawn, manifest) rocky_meadows.ensure_sea(spawn, manifest) ensure_player_starts() if not level_subsystem.save_current_level(): raise RuntimeError(f"saving {LEVEL_PATH} failed; see the log above") unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True) sweep_stale_actor_packages() unreal.log(f"{LEVEL_PATH} saved with landscape {landscape.get_actor_label()}") main()