Files
NODEDC_MISSION_CORE/simulation/ai-polygon/prepare_terrain.py
T

79 lines
2.8 KiB
Python

"""Versioned Worker-only collision preparation; never uploads generated assets."""
import hashlib
import json
import subprocess
import time
from pathlib import Path
from local_state import write_json
from k1link.simulation.ai_polygon.terrain_contract import terrain_matches
def prepare_terrain(root: Path, episode: Path, world: dict, job=None, pulse=None) -> Path:
if world.get("storage", {}).get("kind") == "worker":
path = root / "assets/prepared-worlds" / world["sha256"] / "terrain.json"
manifest = json.loads(path.read_text(encoding="utf-8-sig"))
if (
manifest.get("generator") != "paired-source"
or manifest.get("collider_sha256") != world.get("collider_sha256")
or not terrain_matches(manifest, world)
):
raise ValueError("Paired collision asset does not match the admitted world")
return path # install_terrain hashes the actual collider before physics.
generator = Path(__file__).parent / "navigation/Prepare-Terrain.ps1"
generator_sha256 = hashlib.sha256(generator.read_bytes()).hexdigest()
def matching():
for path in (root / "assets/terrain-v1").glob(world["sha256"] + "-*/terrain.json"):
value = json.loads(path.read_text(encoding="utf-8-sig"))
if terrain_matches(value, world, generator_sha256):
return path
return None
found = matching()
if found:
return found
world_path = episode / "terrain-world.json"
write_json(world_path, world)
with (episode / "terrain-preparation.log").open("wb") as log:
process = subprocess.Popen(
[
"powershell.exe",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-File",
str(generator),
"-Root",
str(root),
"-WorldFile",
str(world_path),
],
stdout=log,
stderr=subprocess.STDOUT,
)
if job is not None:
job.assign(process)
deadline = time.monotonic() + 180
try:
while process.poll() is None:
if time.monotonic() > deadline:
raise TimeoutError("Terrain preparation timed out")
if pulse:
pulse()
time.sleep(1)
if process.returncode != 0:
raise RuntimeError("Terrain preparation failed; inspect the Worker episode log")
finally:
if process.poll() is None:
from worker import terminate_episode
terminate_episode(process)
found = matching()
if found is None:
raise RuntimeError("Terrain preparation did not produce a matching collider")
return found