# pgames-manager — архитектура Локальная библиотека визуальных новелл (сначала Ren'Py). Игры не запускаются оригинальным движком: архив разбирается на сервере, скрипт и ассеты перекладываются в собственный формат **PGVN**, а играется копия в нашем веб-плеере. ## Развёртывание ``` Proxmox host ── Caddy (TLS, домен) ──► http://:8080 │ VM: docker compose│ ┌─────────────────────────────────────┴───────────┐ │ web (nginx) : SPA, API → backend, media internal │ │ backend : FastAPI (uvicorn) │ │ worker : конвертер, тот же образ │ │ db : PostgreSQL 16 │ └──────────────────────────────────────────────────┘ volume data/: blobs/ (ассеты), uploads/ (временные архивы) ``` Приложение отдаёт только HTTP, TLS — на Caddy. Пример Caddyfile: ``` games.example.lan { reverse_proxy 192.168.1.50:8080 } ``` Очередь задач — сама таблица версий в Postgres (`SELECT … FOR UPDATE SKIP LOCKED`), Redis не нужен. ## Пользователи и права * При первом старте создаётся `admin` (пароль из `ADMIN_PASSWORD`, по умолчанию `admin`) с флагом `must_change_credentials` — при первом входе он обязан сменить логин и пароль. * Регистрации нет. Админ создаёт пользователя, сервер генерирует случайный пароль и показывает его **один раз**; админ передаёт его человеку. У нового пользователя стоит `must_change_password`. * Роль = именованный набор прав. Права зашиты в коде (`backend/app/permissions.py`), роли редактируются в UI. Системные роли: `admin` (все права, нельзя удалить), `moderator`, `player`. * Сессия — JWT в httpOnly-cookie; в токене `token_version`, при смене пароля или деактивации он увеличивается и старые сессии умирают. ## Игры и версии * `game` — карточка в каталоге (название, описание, обложка, теги). * `game_version` — конкретная играбельная сборка: `owner`, `parent_id`, `status` (`uploaded → converting → ready | failed`), `manifest`, отчёт конвертации. Оригинал — версия без родителя, созданная загрузкой архива. * Форк версии копирует только манифест (килобайты). Хранилище блобов общее, поэтому мод, меняющий код, хранит только изменённые файлы скрипта. ### Хранилище блобов `data/blobs/ab/cd/` — неизменяемые файлы. Доступ проверяет backend, затем nginx отдаёт файл через закрытый internal location с `private, no-cache`. Манифест версии: ```json { "files": { "game.json": {"hash": "…", "size": 1234}, "script/script.json": {"hash": "…", "size": 5678}, "images/bg room.webp": {"hash": "…", "size": 91011} } } ``` Удаление версии не удаляет блобы сразу — их чистит сборщик мусора (блобы, на которые не ссылается ни один манифест). ## Конвейер конвертации (worker) 1. Распаковка архива (zip — stdlib, остальное — `7zz`), защита от zip-slip. 2. Поиск каталога `game/`. 3. Распаковка `.rpa` (RPA-2/3) внутрь `game/`. 4. Для `.rpyc` без `.rpy` — декомпиляция через unrpyc (best effort). 5. Парсинг `.rpy` собственным парсером → PGVN IR. Python-выражения разбираются модулем `ast` и транслируются в JSON-дерево выражений; неподдерживаемое попадает в отчёт как предупреждение. 6. Ассеты: изображения → WebP (с альфой), аудио → Opus (ogg), видео → WebM, шрифты копируются. Всё складывается в блобы. 7. Манифест сохраняется, исходники удаляются. При ошибке архив остаётся для повторной попытки. ## Формат PGVN v1 `game.json`: ```json { "format": "pgvn/1", "title": "…", "resolution": [1920, 1080], "entry": "start", "scripts": ["script/script.json", "script/ch1.json"], "characters": {"e": {"name": "Eileen", "color": "#c8ffc8"}}, "images": {"bg room": {"file": "images/bg room.webp"}, "black": {"color": "#000000"}}, "files": {"bg/room.jpg": "bg/room.webp"}, "defaults": [["points", {"k": "const", "v": 0}]] } ``` Скрипт-модуль (`script/*.json`) — упорядоченный список меток, у каждой тело из узлов-операторов. Структура остаётся деревом (близко к исходнику, удобно редактировать), состояние плеера — стек `{label, path}` где `path` — индексы внутри вложенных блоков. Новые сохранения дополнительно содержат ревизию и ID узлов для проверки позиции при изменении скрипта; совместимость переменных после изменения игровой логики требует отдельной проверки. У каждого узла есть постоянный `id`, у реплик — ещё `tid` для перевода. Переводы лежат отдельно, в `locales/<язык>/…` (см. [LOCALIZATION.md](LOCALIZATION.md)). Узлы (`op`): `label` (точка входа внутри блока), `camera`, `say`, `scene`, `show`, `hide`, `with`, `play`, `stop`, `queue`, `pause`, `menu`, `if`, `while`, `jump`, `call`, `return`, `exec`, `window`, `noop`. Выражения: `const`, `name`, `attr`, `sub`, `bin`, `bool`, `cmp`, `unary`, `if`, `list`, `tuple`, `dict`, `call`, `fmt`. Поддерживаемые встроенные функции в выражениях — белый список (`len`, `min`, `max`, `int`, `str`, `abs`, `round`, `renpy.random.*` и т.п.). ## Плеер Отдельный TS-модуль без привязки к React (`frontend/src/player`): * интерпретатор IR + вычислитель выражений; * DOM-рендер слоёв (`` с GPU-композицией), сцена масштабируется под окно, переходы через CSS; * текстбокс с эффектом печати, теги `{b} {i} {color} {size} {w} {p} {nw}`, интерполяция `[var]`; * меню выборов, история реплик, skip/auto, быстрые и слотовые сохранения на сервере, громкость каналов, предзагрузка ближайших картинок. ## Состояние и планы Реализовано: * редактор PGVN-форка, замена ассетов, ревизии, трёхстороннее слияние и перенос на новый оригинал — см. [MODDING.md](MODDING.md); * импорт Naninovel и адаптер Tomie, пакетный импорт и строгая проверка совместимости — см. [IMPORT.md](IMPORT.md); * редактор переводов — см. [LOCALIZATION.md](LOCALIZATION.md); * сборщик мусора (`python -m app.gc`, есть периодический режим); * ограничение неудачных попыток входа (`app/ratelimit.py`), CI в GitHub Actions. Планы: * ATL `function`, `matrixcolor`, шейдеры и `im.*` (экраны и ATL с движением уже есть: [SCREENS.md](SCREENS.md), `backend/app/converter/atl.py`, `frontend/src/player/atl.ts`); * другие движки (TyranoBuilder, RPG Maker MV) через тот же IR; * дальнейшее дробление `engine.ts` на модули (рендер сцены, UI, сохранения); * типизация backend под `mypy` (сейчас проверка справочная).