"""CLI: ricsave . Ścieżkę do gry bierze z --game albo ze zmiennej RIC_GAME.""" from __future__ import annotations import argparse import json import os import sys from pathlib import Path from . import catalog, schema, slot as slotmod from .slot import Game, SaveError def _game(args) -> Game: root = args.game or os.environ.get("RIC_GAME") if not root: raise SaveError("podaj --game albo ustaw RIC_GAME") return Game(root) def _print_table(headers: list[str], rows: list[list[str]]) -> None: widths = [len(h) for h in headers] for row in rows: for i, cell in enumerate(row): widths[i] = max(widths[i], len(cell)) line = " ".join(h.ljust(w) for h, w in zip(headers, widths)) print(line) print(" ".join("-" * w for w in widths)) for row in rows: print(" ".join(cell.ljust(w) for cell, w in zip(row, widths))) def cmd_info(args) -> int: game = _game(args) print(f"gra: {game.root}") print(f"common: {game.common}") print() rows = [] for number in (catalog.LIVE_SLOT, *catalog.MENU_SLOTS): s = game.slot(number) if not s.exists(): rows.append([str(number), "(pusty)", "", "", ""]) continue info = s.summary() rows.append([ str(number) + (" (żywy)" if number == catalog.LIVE_SLOT else ""), info["location"] or "?", info["previous"] or "?", "tak" if info["mole_active"] not in (None, "0") else "nie", str(len(info["items"])), ]) _print_table(["slot", "lokacja", "poprzednia", "kret", "przedm."], rows) return 0 def cmd_validate(args) -> int: game = _game(args) problems = game.slot(args.slot).validate() if not problems: print(f"slot {args.slot}: OK") return 0 print(f"slot {args.slot}: {len(problems)} problem(ów)") for p in problems: print(f" - {p}") return 1 def cmd_dump(args) -> int: game = _game(args) s = game.slot(args.slot) if args.key: data = {args.key: s.raw(args.key)} if catalog.BY_KEY[args.key].kind == "dta": data[args.key] = data[args.key].rows # type: ignore[union-attr] else: data = s.to_dict() json.dump(data, sys.stdout, ensure_ascii=False, indent=2) print() return 0 def cmd_scene(args) -> int: game = _game(args) view = game.slot(args.slot).scene(args.name) headers = [c.name for c in view.columns] rows = [[str(i)] + row for i, row in enumerate(view.table.rows)] _print_table(["#"] + headers, rows) if args.explain: print() seen = sorted({int(r[2]) for r in view.table.rows if r[2].lstrip("-").isdigit()}) for value in seen: print(f" TYPE {value:>3} = {schema.describe_object_type(value)}") return 0 def cmd_items(args) -> int: game = _game(args) view = game.slot(args.slot).items headers = [c.name for c in view.columns] _print_table(headers, [list(r) for r in view.table.rows]) return 0 def cmd_gamesets(args) -> int: game = _game(args) values = game.slot(args.slot).gamesets rows = [] for i, col in enumerate(schema.GAMESETS): value = str(values[i]) if i < len(values) else "(BRAK)" rows.append([str(i), col.name, value, col.note]) _print_table(["#", "zmienna", "wartość", "znaczenie"], rows) return 0 def cmd_spells(args) -> int: game = _game(args) s = game.slot(args.slot) values = s.spells rows = [] for i, value in enumerate(values): rows.append([str(i), str(value), schema.describe_spell_index(i)]) _print_table(["#", "wartość", "znaczenie"], rows) exp = s.experience frame = exp // schema.EXP_PER_HOURGLASS_FRAME + 1 print( f"\ndoświadczenie: {exp}/{schema.EXP_PER_LEVEL}" f" (klatka klepsydry {frame}/16)" f" · awansów: {values[schema.LEVEL_INDEX]}" f" · miga: {'tak' if values[schema.HOURGLASS_BLINKING_INDEX] else 'nie'}" ) known = s.known_spells() print("znane czary:", ", ".join(known) if known else "brak") return 0 def cmd_exp(args) -> int: game = _game(args) s = game.slot(args.slot) before = s.experience s.experience = args.value print(f"doświadczenie: {before} → {args.value}") print("zapisano:", ", ".join(s.flush())) return 0 def cmd_set(args) -> int: game = _game(args) s = game.slot(args.slot) view = s.scene(args.scene) index = view.find(args.idname) if not args.idname.isdigit() else int(args.idname) for assignment in args.assignments: column, _, value = assignment.partition("=") if not _: raise SaveError(f"oczekiwano KOLUMNA=WARTOŚĆ, dostałem {assignment!r}") before = view.get(index, column) view.set(index, column, value) print(f"{args.scene}[{index}].{column}: {before} → {value}") s.touch(f"scene:{view.name}") if args.dry_run: print("(--dry-run: nic nie zapisano)") return 0 print("zapisano:", ", ".join(s.flush())) return 0 def cmd_diff(args) -> int: game = _game(args) a, b = game.slot(args.a).to_dict(), game.slot(args.b).to_dict() differences = 0 for key in sorted(set(a) | set(b)): if a.get(key) != b.get(key): differences += 1 print(f"~ {key} ({catalog.BY_KEY[key].label})") if args.verbose: left, right = a.get(key), b.get(key) if isinstance(left, list) and isinstance(right, list): for i in range(max(len(left), len(right))): lv = left[i] if i < len(left) else "(brak)" rv = right[i] if i < len(right) else "(brak)" if lv != rv: print(f" [{i}] {lv!r} → {rv!r}") print(f"\nróżnic: {differences} (slot {args.a} vs {args.b})") return 0 def cmd_copy(args) -> int: game = _game(args) files = slotmod.copy_slot(game, args.src, args.dst) print(f"skopiowano {len(files)} plików: slot {args.src} → {args.dst}") return 0 def cmd_backup(args) -> int: game = _game(args) print("kopia:", slotmod.backup(game, args.dest)) return 0 def cmd_newgame(args) -> int: game = _game(args) files = slotmod.new_game(game) print(f"zresetowano żywy stan (slot 0), {len(files)} plików") return 0 def cmd_serve(args) -> int: from webapp.server import serve serve(_game(args), host=args.host, port=args.port) return 0 def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(prog="ricsave", description=__doc__) parser.add_argument("--game", help="katalog gry (albo RIC_GAME)") sub = parser.add_subparsers(dest="command", required=True) p = sub.add_parser("info", help="przegląd slotów") p.set_defaults(func=cmd_info) p = sub.add_parser("validate", help="sprawdź niezmienniki slotu") p.add_argument("slot", type=int) p.set_defaults(func=cmd_validate) p = sub.add_parser("dump", help="zrzut slotu do JSON") p.add_argument("slot", type=int) p.add_argument("--key", help="pojedynczy plik, np. game / items / scene:GULDRYK") p.set_defaults(func=cmd_dump) p = sub.add_parser("scene", help="tabela obiektów lokacji") p.add_argument("slot", type=int) p.add_argument("name") p.add_argument("--explain", action="store_true", help="rozwiń kolumnę TYPE") p.set_defaults(func=cmd_scene) p = sub.add_parser("items", help="ekwipunek") p.add_argument("slot", type=int) p.set_defaults(func=cmd_items) p = sub.add_parser("gamesets", help="GAME.ARR z opisem pól") p.add_argument("slot", type=int) p.set_defaults(func=cmd_gamesets) p = sub.add_parser("spells", help="czary i doświadczenie (klepsydra)") p.add_argument("slot", type=int) p.set_defaults(func=cmd_spells) p = sub.add_parser("exp", help="ustaw doświadczenie (0..149)") p.add_argument("slot", type=int) p.add_argument("value", type=int) p.set_defaults(func=cmd_exp) p = sub.add_parser("set", help="zmień pola obiektu w lokacji") p.add_argument("slot", type=int) p.add_argument("scene") p.add_argument("idname", help="IDNAME obiektu albo numer wiersza") p.add_argument("assignments", nargs="+", metavar="KOLUMNA=WARTOŚĆ") p.add_argument("--dry-run", action="store_true") p.set_defaults(func=cmd_set) p = sub.add_parser("diff", help="porównaj dwa sloty") p.add_argument("a", type=int) p.add_argument("b", type=int) p.add_argument("-v", "--verbose", action="store_true") p.set_defaults(func=cmd_diff) p = sub.add_parser("copy", help="kopiuj slot (0 = żywy stan)") p.add_argument("src", type=int) p.add_argument("dst", type=int) p.set_defaults(func=cmd_copy) p = sub.add_parser("backup", help="kopia zapasowa całego common/") p.add_argument("dest", type=Path) p.set_defaults(func=cmd_backup) p = sub.add_parser("newgame", help="zresetuj żywy stan z szablonów _DEF") p.set_defaults(func=cmd_newgame) p = sub.add_parser("serve", help="uruchom lokalny edytor w przeglądarce") p.add_argument("--host", default="127.0.0.1") p.add_argument("--port", type=int, default=8765) p.set_defaults(func=cmd_serve) return parser def main(argv: list[str] | None = None) -> int: args = build_parser().parse_args(argv) try: return args.func(args) except SaveError as exc: print(f"błąd: {exc}", file=sys.stderr) return 2 except (KeyError, ValueError) as exc: print(f"błąd: {exc}", file=sys.stderr) return 2 if __name__ == "__main__": raise SystemExit(main())