This commit is contained in:
Rainer Leit
2026-09-25 17:02:24 +03:00
parent cc43ed8dc8
commit 9597629951
2149 changed files with 460234 additions and 1770 deletions
+87
View File
@@ -0,0 +1,87 @@
"""Puts the current sea material on a level that already exists, and saves it.
UnrealEditor-Cmd.exe <abs>/Salty.uproject -run=pythonscript \
-script="<abs>/Scripts/Authoring/fix_sea_material.py --level /Game/Maps/L_World" \
-AllowCommandletRendering -unattended -nopause -abslog=<abs>/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()