Add immutable revisions, PGVN editor, asset replacement, merge and rebase workflows, authenticated media delivery and reachability GC. Harden the script interpreter, pin unrpyc and remove tracked build metadata. Include current compatibility checks, Naninovel import and regression tests.
141 lines
8.4 KiB
Markdown
141 lines
8.4 KiB
Markdown
# pgames-manager — архитектура
|
||
|
||
Локальная библиотека визуальных новелл (сначала Ren'Py). Игры не запускаются
|
||
оригинальным движком: архив разбирается на сервере, скрипт и ассеты
|
||
перекладываются в собственный формат **PGVN**, а играется копия в нашем
|
||
веб-плеере.
|
||
|
||
## Развёртывание
|
||
|
||
```
|
||
Proxmox host ── Caddy (TLS, домен) ──► http://<IP VM>: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/<sha256>` — неизменяемые файлы. Доступ проверяет 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-рендер слоёв (`<img>` с GPU-композицией), сцена масштабируется под
|
||
окно, переходы через CSS;
|
||
* текстбокс с эффектом печати, теги `{b} {i} {color} {size} {w} {p} {nw}`,
|
||
интерполяция `[var]`;
|
||
* меню выборов, история реплик, skip/auto, быстрые и слотовые сохранения
|
||
на сервере, громкость каналов, предзагрузка ближайших картинок.
|
||
|
||
## Дорожная карта
|
||
|
||
* Реализованы редактор PGVN-форка, замена ассетов, ревизии, трёхстороннее слияние
|
||
и перенос на новый оригинал — см. [MODDING.md](MODDING.md).
|
||
* Поддержка screens/ATL-трансформаций в IR.
|
||
* Другие движки (TyranoBuilder, RPG Maker MV) через тот же IR.
|
||
* Сборщик мусора доступен как `python -m app.gc`, включая периодический режим.
|