"""Lokalny edytor w przeglądarce. Stdlib http.server — zero zależności. ricsave --game "~/Reksio/Reksio i Czarodzieje" serve Serwer celowo słucha tylko na 127.0.0.1: daje pełny zapis do katalogu gry. """ from __future__ import annotations import json import webbrowser from functools import partial from http.server import HTTPServer, SimpleHTTPRequestHandler from pathlib import Path from urllib.parse import parse_qs, urlparse from ricsave import catalog, schema, slot as slotmod from ricsave.slot import Game, SaveError STATIC = Path(__file__).parent / "static" class Handler(SimpleHTTPRequestHandler): game: Game def __init__(self, *args, game: Game, **kwargs): self.game = game super().__init__(*args, directory=str(STATIC), **kwargs) def log_message(self, fmt, *args): # cisza w konsoli pass # ---- API ---- def do_GET(self): url = urlparse(self.path) if not url.path.startswith("/api/"): return super().do_GET() query = parse_qs(url.query) try: payload = self._get(url.path, query) except SaveError as exc: return self._json({"error": str(exc)}, status=400) except KeyError as exc: return self._json({"error": f"nie znaleziono: {exc}"}, status=404) return self._json(payload) def do_POST(self): url = urlparse(self.path) length = int(self.headers.get("Content-Length", 0)) body = json.loads(self.rfile.read(length) or b"{}") try: payload = self._post(url.path, body) except SaveError as exc: return self._json({"error": str(exc)}, status=400) except (KeyError, ValueError) as exc: return self._json({"error": str(exc)}, status=400) return self._json(payload) def _slot_card(self, number: int) -> dict: s = self.game.slot(number) card = {"number": number, "live": number == catalog.LIVE_SLOT, "exists": s.exists()} if card["exists"]: try: card.update(s.summary()) except SaveError as exc: # slot niekompletny — pokazujemy go, ale nie udajemy że da się otworzyć card["exists"] = False card["error"] = str(exc) return card def _get(self, path: str, query: dict) -> dict: if path == "/api/slots": return { "root": str(self.game.root), "slots": [ self._slot_card(n) for n in (catalog.LIVE_SLOT, *catalog.MENU_SLOTS) ], "scenes": list(catalog.SCENES), "characters": list(catalog.CHARACTERS), "minigames": list(catalog.MINIGAMES), } slot = self.game.slot(int(query["slot"][0])) if path == "/api/overview": spells = slot.spells exp = slot.experience return { "summary": slot.summary(), "problems": slot.validate(), # Osobny blok zamiast grzebania w summary — front nie zna indeksów # SPELLS i nie musi liczyć klatki klepsydry sam. "experience": { "value": exp, "threshold": schema.EXP_PER_LEVEL, "frame": exp // schema.EXP_PER_HOURGLASS_FRAME + 1, "frames": schema.EXP_PER_LEVEL // schema.EXP_PER_HOURGLASS_FRAME + 1, "level": int(spells[schema.LEVEL_INDEX]), "blinking": bool(spells[schema.HOURGLASS_BLINKING_INDEX]), "known": slot.known_spells(), }, "gamesets": [ { "index": i, "name": col.name, "note": col.note, "value": slot.gamesets[i] if i < len(slot.gamesets) else None, } for i, col in enumerate(schema.GAMESETS) ], "spells": [ {"index": i, "value": v, "note": schema.describe_spell_index(i)} for i, v in enumerate(slot.spells) ], "items": slot.items.rows(), } if path == "/api/scene": view = slot.scene(query["name"][0]) return { "name": view.name, "columns": [ {"name": c.name, "type": c.type, "note": c.note} for c in view.columns ], "rows": view.table.rows, "types": { row[2]: schema.describe_object_type(int(row[2])) for row in view.table.rows if row[2].lstrip("-").isdigit() }, } if path == "/api/raw": key = query["key"][0] entry = catalog.BY_KEY[key] value = slot.raw(key) return { "key": key, "kind": entry.kind, "label": entry.label, "value": value if entry.kind == "arr" else value.rows, } if path == "/api/manifest": return { "entries": [ { "key": e.key, "kind": e.kind, "label": e.label, "file": e.filename(slot.number), } for e in catalog.MANIFEST ] } raise KeyError(path) def _post(self, path: str, body: dict) -> dict: if path == "/api/write": slot = self.game.slot(int(body["slot"])) key = body["key"] entry = catalog.BY_KEY[key] current = slot.raw(key) if entry.kind == "arr": slot._cache[key] = body["value"] else: rows = [[str(cell) for cell in row] for row in body["value"]] if len(rows) != len(current.rows): raise SaveError( f"liczba wierszy musi zostać {len(current.rows)} — " "skrypty scen adresują wiersze po indeksie" ) current.rows = rows slot.touch(key) return {"written": slot.flush()} if path == "/api/copy": files = slotmod.copy_slot(self.game, int(body["src"]), int(body["dst"])) return {"copied": len(files)} if path == "/api/backup": return {"path": str(slotmod.backup(self.game, body["dest"]))} raise KeyError(path) def _json(self, payload: dict, status: int = 200) -> None: raw = json.dumps(payload, ensure_ascii=False).encode("utf-8") self.send_response(status) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Length", str(len(raw))) self.end_headers() self.wfile.write(raw) def serve(game: Game, host: str = "127.0.0.1", port: int = 8765, open_browser: bool = True) -> None: handler = partial(Handler, game=game) server = HTTPServer((host, port), handler) url = f"http://{host}:{port}/" print(f"edytor: {url}") print(f"gra: {game.common}") print("Ctrl-C kończy.") if open_browser: webbrowser.open(url) try: server.serve_forever() except KeyboardInterrupt: print("\nkoniec")