host.controller_web_editor.routes.hitboxes
GET/POST handler logic for controller hitbox layouts.
Layouts live as JSON files under host/controller_web_editor/hitboxes/,
one per controller type. v1 only ships xbox.json. Layout names are
allowlisted so the URL parameter can't traverse the filesystem.
1"""GET/POST handler logic for controller hitbox layouts. 2 3Layouts live as JSON files under ``host/controller_web_editor/hitboxes/``, 4one per controller type. v1 only ships ``xbox.json``. Layout names are 5allowlisted so the URL parameter can't traverse the filesystem. 6""" 7 8from __future__ import annotations 9 10import json 11from pathlib import Path 12 13 14HITBOXES_DIR = Path(__file__).resolve().parent.parent / "hitboxes" 15 16# Allowlist of layout names that may be read/written. Extend with 17# "ps5", "switch", etc. when those layouts arrive. 18ALLOWED_LAYOUTS = frozenset({"xbox"}) 19 20 21class LayoutNotAllowedError(ValueError): 22 """Raised when the client requests an unknown layout name.""" 23 24 25def _layout_path(layout: str) -> Path: 26 if layout not in ALLOWED_LAYOUTS: 27 raise LayoutNotAllowedError(f"unknown layout: {layout!r}") 28 return HITBOXES_DIR / f"{layout}.json" 29 30 31def load_layout(layout: str) -> dict: 32 """Read a hitbox JSON file and return it as a dict.""" 33 return json.loads(_layout_path(layout).read_text(encoding="utf-8")) 34 35 36def save_layout(layout: str, data: dict) -> None: 37 """Write a hitbox JSON file. Validates the payload shape minimally. 38 39 A timestamp-free ``.bak`` of the previous contents is written next to 40 the file before overwriting, so an accidental overwrite is recoverable. 41 """ 42 if not isinstance(data, dict): 43 raise ValueError("payload must be a JSON object") 44 if "regions" not in data or not isinstance(data["regions"], dict): 45 raise ValueError("payload missing 'regions' object") 46 if "viewBox" not in data or not isinstance(data["viewBox"], str): 47 raise ValueError("payload missing 'viewBox' string") 48 49 # Optional fields validated only if present. 50 labels = data.get("labels") 51 if labels is not None and not isinstance(labels, dict): 52 raise ValueError("'labels' must be an object") 53 fonts = data.get("fonts") 54 if fonts is not None and not isinstance(fonts, dict): 55 raise ValueError("'fonts' must be an object") 56 57 path = _layout_path(layout) 58 if path.exists(): 59 backup = path.with_suffix(path.suffix + ".bak") 60 backup.write_bytes(path.read_bytes()) 61 62 path.parent.mkdir(parents=True, exist_ok=True) 63 path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") 64 65 66def list_layouts() -> list[str]: 67 """Return all allowlisted layouts that currently have a file on disk.""" 68 return sorted( 69 name for name in ALLOWED_LAYOUTS 70 if (HITBOXES_DIR / f"{name}.json").is_file() 71 )
HITBOXES_DIR =
PosixPath('/home/runner/work/Robot-2026/Robot-2026/host/controller_web_editor/hitboxes')
ALLOWED_LAYOUTS =
frozenset({'xbox'})
class
LayoutNotAllowedError(builtins.ValueError):
22class LayoutNotAllowedError(ValueError): 23 """Raised when the client requests an unknown layout name."""
Raised when the client requests an unknown layout name.
def
load_layout(layout: str) -> dict:
32def load_layout(layout: str) -> dict: 33 """Read a hitbox JSON file and return it as a dict.""" 34 return json.loads(_layout_path(layout).read_text(encoding="utf-8"))
Read a hitbox JSON file and return it as a dict.
def
save_layout(layout: str, data: dict) -> None:
37def save_layout(layout: str, data: dict) -> None: 38 """Write a hitbox JSON file. Validates the payload shape minimally. 39 40 A timestamp-free ``.bak`` of the previous contents is written next to 41 the file before overwriting, so an accidental overwrite is recoverable. 42 """ 43 if not isinstance(data, dict): 44 raise ValueError("payload must be a JSON object") 45 if "regions" not in data or not isinstance(data["regions"], dict): 46 raise ValueError("payload missing 'regions' object") 47 if "viewBox" not in data or not isinstance(data["viewBox"], str): 48 raise ValueError("payload missing 'viewBox' string") 49 50 # Optional fields validated only if present. 51 labels = data.get("labels") 52 if labels is not None and not isinstance(labels, dict): 53 raise ValueError("'labels' must be an object") 54 fonts = data.get("fonts") 55 if fonts is not None and not isinstance(fonts, dict): 56 raise ValueError("'fonts' must be an object") 57 58 path = _layout_path(layout) 59 if path.exists(): 60 backup = path.with_suffix(path.suffix + ".bak") 61 backup.write_bytes(path.read_bytes()) 62 63 path.parent.mkdir(parents=True, exist_ok=True) 64 path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
Write a hitbox JSON file. Validates the payload shape minimally.
A timestamp-free .bak of the previous contents is written next to
the file before overwriting, so an accidental overwrite is recoverable.
def
list_layouts() -> list[str]:
67def list_layouts() -> list[str]: 68 """Return all allowlisted layouts that currently have a file on disk.""" 69 return sorted( 70 name for name in ALLOWED_LAYOUTS 71 if (HITBOXES_DIR / f"{name}.json").is_file() 72 )
Return all allowlisted layouts that currently have a file on disk.