"""Puts the current sea material on a level that already exists, and saves it. UnrealEditor-Cmd.exe /Salty.uproject -run=pythonscript \ -script="/Scripts/Authoring/fix_sea_material.py --level /Game/Maps/L_World" \ -AllowCommandletRendering -unattended -nopause -abslog=/Saved/Logs/sea.log Why this is a script of its own rather than a flag on the world builders. `ensure_dressing` spawns the sea only when the level has none, which is the right rule - a rerun must not leave a second sun behind it - and the consequence is that changing `rocky_meadows.SEA_GREY` cannot reach a world that already has a sea. The alternative to this file is rebuilding ninety-eight landscapes to change one material reference, which is about forty minutes to avoid writing twenty lines. It changes exactly one thing: the material on every actor labelled `World_Sea_Proto`. The plane's size and height are a product of the manifest and are already correct, so they are not touched, and neither is anything else in the level. The arguments go *inside* the quoted `-script=` value. UPythonScriptCommandlet::Main reads `-Script=` as one string and hands it to the Python plugin, which splits it into a filename and arguments; anything put after it on the command line is parsed by the engine and never reaches Python, so a level named the wrong way round is silently ignored and the default is used instead. """ import os 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 rocky_meadows # noqa: E402 DEFAULT_LEVELS = ("/Game/Maps/L_World",) level_subsystem = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem) def parse_levels(): """`--level A --level B`, or every level in DEFAULT_LEVELS. Read out of sys.argv because the engine puts its own arguments there too.""" argv = sys.argv[1:] levels = [argv[i + 1] for i, item in enumerate(argv) if item == "--level" and i + 1 < len(argv)] return levels or list(DEFAULT_LEVELS) def level_file(level_path): content = os.path.abspath(unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_content_dir())) return os.path.join(content, level_path.replace("/Game/", "", 1) + ".umap") def require_writable(level_path): """The same probe create_region_world.py makes, and for the same reason: the save is the last thing this does, an editor holding the level makes it fail with a sharing violation, and finding that out at the end is finding it out too late. `r+b` asks the operating system what a save asks and writes nothing.""" path = level_file(level_path) if not os.path.exists(path): raise RuntimeError(f"{path} does not exist; build the level before repairing its sea") try: open(path, "r+b").close() except OSError as error: raise RuntimeError( f"{path} is locked by another process ({error.__class__.__name__}): an editor with {level_path} " f"open, or playing it, holds it. Load another level in the editor and run again." ) from error def main(): levels = parse_levels() unreal.log(f"sea material: {'grey placeholder' if rocky_meadows.SEA_GREY else 'the engine water material'}") for level_path in levels: require_writable(level_path) for level_path in levels: if not level_subsystem.load_level(level_path): raise RuntimeError(f"could not load {level_path}") world = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem).get_editor_world() if not world.get_path_name().startswith(level_path): raise RuntimeError(f"the editor world is {world.get_path_name()}, not {level_path}; " f"a save would go nowhere") changed = rocky_meadows.repair_sea(None) if not changed: continue 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) unreal.log(f"{level_path} saved: {changed} sea plane(s) repainted") main()