Step 1: the Salty project, two modules, tests and the gym

Salty.uproject (UE 5.8) from the Third Person template with class redirects, the SaltyCore and Salty modules,
the three build targets, USaltyAssetManager calling InitGlobalData, Config/Tags with the 23 root namespaces,
Scripts/run-tests.sh, build.sh and Authoring/create_gym.py, L_Gym, Git LFS attributes and the placeholder test.
Template variants kept as reference (D-41). The four Fab packs stay out of the repository for now (~10 GB).
The editor serves the engine MCP plugin on 127.0.0.1:8000 through DefaultEditorPerProjectUserSettings.ini.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rainer Leit
2026-09-16 20:10:43 +03:00
co-authored by Claude Fable 5.1
parent 0e61a77346
commit 4f2c55cd2a
1434 changed files with 11358 additions and 27 deletions
+84
View File
@@ -0,0 +1,84 @@
"""Creates /Game/Maps/L_Gym if it does not exist: a flat floor, a player start and daylight. Idempotent.
Step 1 wants an empty gym; step 3 extends this script with the greybox sections from Movement.md (stairs, slopes,
gaps, ledges, doorways, beams, corridor, surfaces, arena). Run headless:
UnrealEditor-Cmd.exe Salty.uproject -run=pythonscript -script=Scripts/Authoring/create_gym.py
or from the editor's Python console. Never hand-edit the .umap; rerun this.
"""
import unreal
LEVEL_PATH = "/Game/Maps/L_Gym"
FLOOR_HALF_SIZE_CM = 5000.0 # a 100 m square; the cube mesh is 100 cm, so scale = size / 100
CUBE = "/Engine/BasicShapes/Cube.Cube"
FLOOR_MATERIAL = "/Game/LevelPrototyping/Materials/MI_PrototypeGrid_Gray.MI_PrototypeGrid_Gray"
level_subsystem = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
asset_lib = unreal.EditorAssetLibrary
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
def find_actor(label):
for actor in actor_subsystem.get_all_level_actors():
if actor.get_actor_label() == label:
return actor
return None
def ensure_floor():
if find_actor("Gym_Floor"):
return
floor = spawn(unreal.StaticMeshActor, "Gym_Floor", unreal.Vector(0, 0, -50))
mesh = unreal.load_asset(CUBE)
component = floor.static_mesh_component
component.set_editor_property("mobility", unreal.ComponentMobility.STATIC)
component.set_static_mesh(mesh)
scale = FLOOR_HALF_SIZE_CM * 2.0 / 100.0
floor.set_actor_scale3d(unreal.Vector(scale, scale, 1.0))
material = unreal.load_asset(FLOOR_MATERIAL) if asset_lib.does_asset_exist(FLOOR_MATERIAL) else None
if material:
component.set_material(0, material)
def ensure_daylight():
if not find_actor("Gym_Sun"):
sun = spawn(unreal.DirectionalLight, "Gym_Sun", unreal.Vector(0, 0, 500), unreal.Rotator(-45, 30, 0))
sun.light_component.set_editor_property("intensity", 8.0)
sun.light_component.set_editor_property("atmosphere_sun_light", True)
if not find_actor("Gym_Sky"):
spawn(unreal.SkyAtmosphere, "Gym_Sky", unreal.Vector(0, 0, 0))
if not find_actor("Gym_SkyLight"):
sky_light = spawn(unreal.SkyLight, "Gym_SkyLight", unreal.Vector(0, 0, 500))
sky_light.light_component.set_editor_property("real_time_capture", True)
if not find_actor("Gym_Fog"):
spawn(unreal.ExponentialHeightFog, "Gym_Fog", unreal.Vector(0, 0, 0))
def ensure_player_starts():
# Two starts so two PIE clients on a dedicated server both spawn without a collision warning.
for index, y in enumerate((-150, 150)):
label = f"Gym_PlayerStart_{index}"
if not find_actor(label):
spawn(unreal.PlayerStart, label, unreal.Vector(0, y, 100))
def main():
if asset_lib.does_asset_exist(LEVEL_PATH):
unreal.log(f"{LEVEL_PATH} exists; loading it to top up missing actors")
level_subsystem.load_level(LEVEL_PATH)
else:
unreal.log(f"creating {LEVEL_PATH}")
level_subsystem.new_level(LEVEL_PATH, False) # not world-partitioned: the gym is small and hand-laid
ensure_floor()
ensure_daylight()
ensure_player_starts()
level_subsystem.save_current_level()
unreal.log(f"{LEVEL_PATH} saved")
main()
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Builds and packages the game through UAT. Usage: Scripts/build.sh [Platform] [Configuration] [-server]
# Scripts/build.sh Win64 Development
# -server needs the engine built from source (Decisions OD-04); the launcher build refuses the Server target.
set -u
UE_ROOT="${UE_ROOT:-D:/UE_5.8}"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PLATFORM="${1:-Win64}"
CONFIG="${2:-Development}"
TARGET_ARGS=""
if [[ "${3:-}" == "-server" || "${1:-}" == "-server" ]]; then
if [ ! -f "$UE_ROOT/Engine/Build/SourceDistribution.txt" ]; then
echo "The engine at $UE_ROOT is a launcher build; it cannot compile SaltyServer (OD-04). Use the editor's Run Dedicated Server." >&2
exit 1
fi
TARGET_ARGS="-server -serverplatform=$PLATFORM -noclient"
fi
PROJECT_W="$(cygpath -w "$ROOT/Salty.uproject" 2>/dev/null || echo "$ROOT/Salty.uproject")"
ARCHIVE_W="$(cygpath -w "$ROOT/Build/$PLATFORM" 2>/dev/null || echo "$ROOT/Build/$PLATFORM")"
"$UE_ROOT/Engine/Build/BatchFiles/RunUAT.bat" BuildCookRun -project="$PROJECT_W" -platform="$PLATFORM" \
-clientconfig="$CONFIG" -serverconfig="$CONFIG" -build -cook -stage -pak -archive -archivedirectory="$ARCHIVE_W" \
-nop4 -utf8output $TARGET_ARGS
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Runs the automation tests headless. Usage: Scripts/run-tests.sh [Filter]
# Scripts/run-tests.sh -> Salty.* (everything)
# Scripts/run-tests.sh Core -> Salty.Core.*
# UE_ROOT points at the engine install (default D:/UE_5.8). Exit code is non-zero when any test fails.
set -u
UE_ROOT="${UE_ROOT:-D:/UE_5.8}"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PROJECT="$ROOT/Salty.uproject"
FILTER="Salty${1:+.$1}"
REPORT="$ROOT/Saved/Automation/Reports"
LOG="$ROOT/Saved/Logs/RunTests.log"
mkdir -p "$REPORT" "$(dirname "$LOG")"
"$UE_ROOT/Engine/Binaries/Win64/UnrealEditor-Cmd.exe" "$(cygpath -w "$PROJECT" 2>/dev/null || echo "$PROJECT")" \
-ExecCmds="Automation RunTests $FILTER; Quit" \
-ReportExportPath="$(cygpath -w "$REPORT" 2>/dev/null || echo "$REPORT")" \
-unattended -nopause -NullRHI -nosplash -log -abslog="$(cygpath -w "$LOG" 2>/dev/null || echo "$LOG")" >/dev/null 2>&1
ENGINE_EXIT=$?
# The engine's exit code is not a reliable verdict; the "Test Completed" lines are. Startup prints two
# "LogAutomationTest: Error: Condition failed" lines before any test runs; they are the engine's, not ours.
grep -E "Test Completed\. Result=" "$LOG" | sed -E 's/^.*Test Completed\. //'
PASSED=$(grep -c "Test Completed. Result={Success}" "$LOG")
FAILED=$(grep -c "Test Completed. Result={Fail" "$LOG")
echo "passed=$PASSED failed=$FAILED filter=$FILTER"
if [ "$FAILED" -gt 0 ]; then echo "RESULT: FAILED"; exit 1; fi
if [ "$PASSED" -eq 0 ]; then echo "RESULT: NO TESTS RAN (engine exit $ENGINE_EXIT). See $LOG"; exit 2; fi
echo "RESULT: PASSED"