feat(simulation): add Worker AI polygon runtime and terrain navigation
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user