#!/usr/bin/env python3 """Dev server for the reikhelm playground, headless depth/FPV export, and the pre-baked first-person explorer. Pure stdlib — no deps. python3 tools/serve.py # serve reikhelm-web on :8000 Three things, one server: GET / static file from the web root (reikhelm-web/) GET /out/ static file from the output tree (tools/out/) — lets the explorer load baked atlas frames + manifests POST /save/.png write the raw request body (a PNG) under tools/out/; sub-dirs allowed and created (e.g. atlas/7/r3_N.png) → {"ok": true, "path": "...", "bytes": N} All generated images live under tools/out/ (git-ignored, organized): out/depth/ top-down + one-off depth maps out/fpv/ first-person stills out/atlas// a baked explorable dungeon (frames + manifest.json) """ import argparse import json import os import sys from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from urllib.parse import unquote REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) def safe_join(base, rel): """Join rel under base, collapsing any '..' so it can't escape base.""" rel = os.path.normpath("/" + rel).lstrip("/") return os.path.join(base, rel) class Handler(SimpleHTTPRequestHandler): out_dir = "." extensions_map = { **SimpleHTTPRequestHandler.extensions_map, ".js": "text/javascript", ".mjs": "text/javascript", ".wasm": "application/wasm", ".json": "application/json", } # Route /out/ to the output tree; everything else from the web root (cwd). def translate_path(self, path): p = unquote(path.split("?", 1)[0].split("#", 1)[0]) if p == "/out" or p.startswith("/out/"): return safe_join(self.out_dir, p[len("/out/"):]) if p != "/out" else self.out_dir return super().translate_path(path) def end_headers(self): self.send_header("Cache-Control", "no-store") # dev server: always serve fresh modules super().end_headers() def do_POST(self): if not self.path.startswith("/save/"): self.send_error(404, "only /save/.png is supported") return rel = unquote(self.path[len("/save/"):]) if not (rel.endswith(".png") or rel.endswith(".json")): self.send_error(400, "path must end in .png or .json") return dest = safe_join(self.out_dir, rel) length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(length) os.makedirs(os.path.dirname(dest), exist_ok=True) with open(dest, "wb") as f: f.write(body) payload = json.dumps({"ok": True, "path": dest, "bytes": len(body)}).encode() self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(payload))) self.end_headers() self.wfile.write(payload) sys.stderr.write(f"saved {os.path.relpath(dest, REPO)} ({len(body)} bytes)\n") def log_message(self, fmt, *args): # quiet GETs; POSTs/errors print via do_POST/send_error pass def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--port", type=int, default=8000) ap.add_argument("--root", default=os.path.join(REPO, "reikhelm-web"), help="static web root") ap.add_argument("--out", default=os.path.join(REPO, "tools", "out"), help="output tree (served at /out, target of /save)") args = ap.parse_args() Handler.out_dir = os.path.abspath(args.out) os.makedirs(Handler.out_dir, exist_ok=True) root = os.path.abspath(args.root) os.chdir(root) httpd = ThreadingHTTPServer(("127.0.0.1", args.port), Handler) print(f"serving {os.path.relpath(root, REPO)} at http://127.0.0.1:{args.port}") print(f" /out/ → {os.path.relpath(Handler.out_dir, REPO)} POST /save/.png writes there") try: httpd.serve_forever() except KeyboardInterrupt: httpd.shutdown() if __name__ == "__main__": main()