Files
UnrealPrototyping/Scripts/Authoring/build_region.sh
T
2026-09-25 17:02:24 +03:00

109 lines
5.7 KiB
Bash

#!/usr/bin/env bash
# Builds the world level a few tiles at a time. Which level that is comes from Region.json's `level`
# (/Game/Maps/L_World today), never from a name written here.
#
# Scripts/Authoring/build_region.sh # the whole grid from Region.json, rebuilding the level first
# Scripts/Authoring/build_region.sh --append # add whatever is still missing, keep what is there
# BATCH=6 Scripts/Authoring/build_region.sh # bigger batches, if the machine has the memory
#
# One process per batch, because a landscape of a hundred components costs about a gigabyte that the editor
# does not give back while the level is open. Thirty-six in one process reached 14.7 GB by the ninth tile
# and would have exhausted this machine's commit long before the last; six at a time peaks at 13.6 GB, which
# left only 1.2 GB of commit free. Three is the default for that reason. The script itself skips tiles that are
# already in the level, so a batch that is rerun costs only the editor's start-up.
#
# Everything is passed as an absolute path and the log goes to a file of its own. With the editor open and a
# relative project path, UnrealEditor-Cmd exits immediately having written nothing at all, which looks exactly
# like success: the exit code is zero and there is no output to read.
#
# The script's own arguments go *inside* the quoted -script= value, not after it. UPythonScriptCommandlet::Main
# reads -Script= as one quoted string and hands the whole thing to the Python plugin, which splits it into a
# filename and arguments and sets sys.argv from them; anything put after it on the command line is parsed by
# the engine and never reaches Python. Passed the wrong way the script sees no arguments and quietly builds
# every tile in the grid in one process, which is exactly the run this batching exists to avoid.
set -u
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
UE_ROOT="${UE_ROOT:-D:/UE_5.8}"
CMD="$UE_ROOT/Engine/Binaries/Win64/UnrealEditor-Cmd.exe"
PROJECT="$ROOT/Salty.uproject"
SCRIPT="$ROOT/Scripts/Authoring/create_region_world.py"
LOGS="$ROOT/Saved/Logs"
BATCH="${BATCH:-3}"
# The grid comes from the manifest, never from a number written here: Region.json is the contract, and a
# driver that assumed a 6x6 square would quietly build thirty-six of the ninety-eight tiles and report success.
MANIFEST="$ROOT/RawContent/World/Region.json"
grid() { grep -oE "\"$1\"[[:space:]]*:[[:space:]]*[0-9]+" "$MANIFEST" | grep -oE '[0-9]+$'; }
COLUMNS="${COLUMNS:-$(grid columns)}"
ROWS="${ROWS:-$(grid rows)}"
if [ -z "$COLUMNS" ] || [ -z "$ROWS" ]; then
echo "could not read tiles.columns and tiles.rows from $MANIFEST" >&2
exit 1
fi
append=0
if [ "${1:-}" = "--append" ]; then
append=1
fi
# An editor with the level open holds a write lock on the .umap. The save is the last thing a batch does and
# --rebuild is the first, so a locked file does not fail the run harmlessly: it empties the level and *then*
# fails, which on 2026-09-20 left twelve of ninety-eight tiles and looked exactly like a corrupted world.
# create_region_world.py refuses to start for the same reason; this is the same question asked before thirty
# editor start-ups pay for the answer. Opening r+b writes nothing.
PYTHON="$UE_ROOT/Engine/Binaries/ThirdParty/Python3/Win64/python.exe"
# The level comes from the manifest, like the grid does. A name written here would go stale the moment
# Region.json's `level` changed, and the probe would then cheerfully clear a file nothing is about to touch.
LEVEL_PATH="$(grep -oE '"level"[[:space:]]*:[[:space:]]*"[^"]+"' "$MANIFEST" | head -1 | sed 's/.*"\([^"]*\)"$/\1/')"
LEVEL_FILE="$ROOT/Content/Maps/${LEVEL_PATH##*/}.umap"
if [ -f "$LEVEL_FILE" ] && [ -x "$PYTHON" ]; then
if ! "$PYTHON" -c "import sys; open(sys.argv[1], 'r+b').close()" "$LEVEL_FILE" 2>/dev/null; then
echo "$LEVEL_FILE is locked: an editor with that level open holds it." >&2
echo "Close the editor, or load another level in it, then run this again." >&2
exit 1
fi
fi
tiles=()
for ((ty = 0; ty < ROWS; ty++)); do
for ((tx = 0; tx < COLUMNS; tx++)); do
tiles+=("$tx,$ty")
done
done
mkdir -p "$LOGS"
total=${#tiles[@]}
echo "=== ${COLUMNS}x${ROWS} = $total tiles, $BATCH a batch"
added_all=0
batch_index=0
for ((i = 0; i < total; i += BATCH)); do
batch=("${tiles[@]:i:BATCH}")
# The first batch of a full run starts the level over, which is also what sweeps any stale actor package an
# interrupted run left behind. --append never rebuilds.
rebuild=""
if [ "$i" -eq 0 ] && [ "$append" -eq 0 ]; then
rebuild=" --rebuild"
fi
log="$LOGS/region_batch_$batch_index.log"
echo "=== batch $batch_index: ${batch[*]}${rebuild}"
"$CMD" "$PROJECT" -run=pythonscript \
-script="$SCRIPT$rebuild --tiles ${batch[*]}" \
-AllowCommandletRendering -unattended -nopause -abslog="$log" >/dev/null 2>&1
# Not the exit code: UnrealEditor-Cmd returns non-zero whenever anything logged an Error, and this project
# logs three on every start (no GameFeatureData asset rule, and the editor already holds MCP's port 8000).
# The line the script prints on a successful save is the honest signal.
saved=$(grep -c "saved: .* landscape(s) added this run" "$log" 2>/dev/null || true)
if [ "${saved:-0}" -eq 0 ]; then
echo "batch $batch_index did not save; see $log" >&2
grep -E "LogPython: Error|Python script executed with errors|Traceback" "$log" 2>/dev/null | tail -5 >&2
exit 1
fi
added=$(grep -cE "LogPython: \[" "$log" 2>/dev/null || true)
added_all=$((added_all + ${added:-0}))
echo " $added landscape(s) added"
batch_index=$((batch_index + 1))
done
echo "done: $added_all landscape(s) added over $batch_index batches; $total tiles in the grid"