Python sandbox - try/except/else/finally, raise (incl. re-raise), assert, for/while ... else - Multi-generator and dict comprehensions - Builtin exception hierarchy; user exceptions subclass it - Inside try, missing keys/indexes, division by zero, attribute access on None and unknown names raise Python exceptions; lookups stay lenient elsewhere - int()/float() raise ValueError on non-numeric strings ATL - converter/atl.py: set/move (linear, ease, easein, easeout), pause, repeat, parallel, choice, block, on show/hide and named transforms become a timeline shipped as game.json "atl"; Move() becomes a linear slide - player/atl.ts: timeline runner wired into the renderer; `on hide` plays before the picture is removed - function, time, contains, matrixcolor and expressions still block the import Tests: exceptions/ATL fixtures and runtime tests, converter ATL tests; README and ARCHITECTURE updated. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
153 lines
9.1 KiB
Markdown
153 lines
9.1 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);
|
||
* импорт Naninovel и адаптер Tomie, пакетный импорт и строгая проверка
|
||
совместимости — см. [IMPORT.md](IMPORT.md);
|
||
* редактор переводов — см. [LOCALIZATION.md](LOCALIZATION.md);
|
||
* сборщик мусора (`python -m app.gc`, есть периодический режим);
|
||
* ограничение неудачных попыток входа (`app/ratelimit.py`), CI в GitHub Actions.
|
||
|
||
Планы:
|
||
|
||
* поддержка `screen`-языка и ATL `function`/`matrixcolor`/шейдеров (базовый ATL
|
||
с движением уже есть: `backend/app/converter/atl.py`, `frontend/src/player/atl.ts`);
|
||
* другие движки (TyranoBuilder, RPG Maker MV) через тот же IR;
|
||
* дальнейшее дробление `engine.ts` на модули (рендер сцены, UI, сохранения);
|
||
* типизация backend под `mypy` (сейчас проверка справочная).
|