Initial MVP: local visual novel library with Ren'Py → PGVN converter

- FastAPI backend: admin bootstrap with forced credential change, admin-created
  users with one-time generated passwords, roles as permission sets
- Games and versions with content-addressed blob storage; forks copy only the
  manifest, so mods share assets with the original
- Worker: archive/RPA extraction, unrpyc for .rpyc, own .rpy parser, Python
  subset translated via ast, media converted to WebP/Opus/WebM
- React panel (library search, game page, upload, users, roles) and a
  framework-free web player with saves, history, skip/auto, transitions
- docker compose: postgres, backend, worker, nginx on a single HTTP port

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-25 02:41:32 +03:00
commit 364b312064
72 files changed
+9707

No files matched your search

+133
View File
@@ -0,0 +1,133 @@
# pgames-manager — архитектура
Локальная библиотека визуальных новелл (сначала Ren'Py). Игры не запускаются
оригинальным движком: архив разбирается на сервере, скрипт и ассеты
перекладываются в собственный формат **PGVN**, а играется копия в нашем
веб-плеере.
## Развёртывание
```
Proxmox host ── Caddy (TLS, домен) ──► http://<IP VM>:8080
│
VM: docker compose│
┌─────────────────────────────────────┴───────────┐
│ web (nginx) : SPA, /api → backend, /blobs → том │
│ 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/<sha256>` — неизменяемые файлы, отдаются nginx с
`Cache-Control: immutable`. Манифест версии:
```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` — индексы
внутри вложенных блоков, так что сохранения стабильны и компактны.
Узлы (`op`): `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-рендер слоёв (`<img>` с GPU-композицией), сцена масштабируется под
окно, переходы через CSS;
* текстбокс с эффектом печати, теги `{b} {i} {color} {size} {w} {p} {nw}`,
интерполяция `[var]`;
* меню выборов, история реплик, skip/auto, быстрые и слотовые сохранения
на сервере, громкость каналов, предзагрузка ближайших картинок.
## Дорожная карта
* Веб-редактор форка: правка скриптов и замена ассетов (манифест уже готов).
* Поддержка screens/ATL-трансформаций в IR.
* Другие движки (TyranoBuilder, RPG Maker MV) через тот же IR.
* Сборщик мусора блобов по расписанию.