79 lines
2.8 KiB
Python
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
|