# Моды, ревизии и обслуживание ## Работа с модом 1. На карточке готовой версии нажмите «Форк». Откроется собственный черновик мода. 2. Выберите модуль и метку. Редактор позволяет менять реплики и переходы, добавлять, удалять и перемещать команды, редактировать условия и вложенные варианты выбора. Дополнительные параметры выражений и команд редактируются как PGVN JSON AST. Это редактор PGVN, а не редактор исходного Ren'Py. 3. Сохраните мод. Сервер проверит IDs, метки, ссылки, структуру команд и точку входа; позиции вложенных меток (`anchors`) будут пересчитаны. 4. Откройте предпросмотр всей игры или отдельной команды. Запускается последнее сохранённое состояние. Изменение переменных при запуске с середины не воспроизводится. 5. В «Замене ассетов» загрузите файл соответствующего типа. Изображения, аудио и видео проходят конвертацию, шрифты проверяются по сигнатуре. Лимит одного файла — 512 МБ. Логический путь остаётся прежним; хэш меняется только в этом моде. 6. Посмотрите структурные отличия от базы и историю. Откат создаёт новую ревизию; предыдущие состояния остаются доступны. Сохранение отправляет ожидаемую ревизию. Если другая вкладка успела изменить мод, сервер отвечает 409; правки в открытом редакторе не сбрасываются. Перед закрытием страницы с несохранёнными изменениями браузер показывает предупреждение. Редактируются собственные черновики `kind=mod` при наличии `versions.fork`. Публикация требует `versions.publish`. Опубликованную версию сначала форкают. ## Слияние и перенос В редакторе выберите другую готовую версию той же игры и операцию: - «Объединить моды»: трёхстороннее слияние относительно ближайшего общего предка в графе ревизий. Независимые поля и вставки в разных местах объединяются. - «Перенести на новый оригинал»: база — зафиксированный оригинал мода. Новый оригинал сопоставляется с базой по уникальному содержимому команд внутри одноимённых меток и модулей. Совпадение порядкового номера или старого короткого ID не является доказательством. Неоднозначные узлы показываются для ручного сопоставления. Без соответствия узел считается удалённым в новом оригинале. Если мод менял удалённый узел, возникает конфликт. Автоматическое сопоставление переименованных модулей/меток не выполняется: такие случаи требуют ручного переноса правок в редакторе. Разные изменения одного поля, удаление изменённого узла и неоднозначный порядок вставок требуют выбора варианта. При замене одного ассета двумя модами выбирается файл целиком. После выбора можно повторить проверку. Результат всегда новый черновик. Перед применением сервер повторно проверяет обе исходные ревизии. Ревизия результата хранит обоих родителей. При переносе сопоставленные узлы и переводы принимают канонические ID нового оригинала; новые команды самого мода сохраняют свои ID. Поэтому новый оригинал становится корректной базой следующего переноса. Публикация перевода также использует зафиксированную базу и объединяет отдельные записи языка: разные строки не перезаписывают друг друга. Несовместимые изменения одной записи блокируют публикацию с указанием пути конфликта. Их нужно согласовать в редакторе перевода. ## Ревизии и совместимость `game_versions.revision_id` указывает на неизменяемый снимок `revisions`. `base_revision_id` фиксирует базу форка. Запись любого нового манифеста — через редактор, перевод, публикацию, откат или импорт — автоматически создаёт снимок. Снимок содержит автора, время, манифест и родителей и не зависит от удаления карточки родительской версии. Новые узлы, метки и ветки получают UUID; существующие ID не пересоздаются при обычной правке. Миграция `0003` создаёт текущие снимки существующих версий. Истинную базу старого форка восстановить нельзя: его `base_revision_id` остаётся пустым. Для надёжного переноса старого мода создайте новый форк нужного оригинала и перенесите правки. Автоматическое слияние таких старых независимых веток намеренно блокируется. Новые сохранения плеера содержат ревизию и ID текущих узлов/кадров вызовов. При изменении ревизии плеер ищет позиции по ID и просит подтвердить попытку загрузки: совместимость переменных и новой игровой логики автоматически не доказывается. Если позиция не найдена, загрузка блокируется. Старые сохранения без ревизии продолжают читаться в прежнем формате с индексными путями после предупреждения. Интерпретатор блокирует доступ к служебным свойствам JavaScript (`constructor`, `prototype`, `__proto__`), изменение прототипов и доступ к свойствам нативных функций из игрового AST. Это проверяется регрессионными тестами. ## Доступ к файлам Плеер использует `/api/versions/{id}/files/{hash}`. Сервер проверяет действующую сессию, `games.play`, видимость версии и наличие хэша в её манифесте. Совместимый маршрут `/blobs/{hash}` также проверяет доступ через видимые версии/обложки. Сам по себе известный хэш прав не даёт. В Docker файл отправляет nginx через `X-Accel-Redirect` и `internal` location. Прямое обращение к `/_protected_blobs/` запрещено. Без nginx работает FileResponse. Ответы имеют `private, no-cache`, ETag и `nosniff`; проверка прав предшествует 304. Уже скачанные файлы отозвать нельзя. Старые публичные кэшированные ответы до обновления могут оставаться у клиентов до истечения их прежнего срока кэширования. ## GC Предварительный отчёт, без удаления: ```sh docker compose --profile maintenance run --rm gc ``` Удаление недостижимых блобов и осиротевших записей ревизий: ```sh docker compose --profile maintenance run --rm gc --execute ``` Периодический запуск отдельным процессом (например, раз в сутки): ```sh docker compose --profile maintenance run -d --name pgames-gc gc --execute --interval 86400 ``` GC берёт эксклюзивную файловую блокировку общего `DATA_DIR/maintenance.lock`. API-записи и конвертация удерживают совместимые блокировки от начала работы до коммита БД. Поэтому GC дожидается активных операций и не удаляет ни новые блобы, ни старые блобы, повторно использованные ещё не зафиксированным манифестом. На Windows блокировки сериализуют доступ; production-контейнеры используют POSIX flock. Корни: текущие манифесты всех версий, базовые и текущие ревизии с предками, `version_history`, обложки. Черновики и история сохраняются. Независимые снимки удалённой версии, не нужные другим веткам, могут быть удалены. История живой версии не сокращается автоматически. При отсутствующей ревизии или живом блобе сборка мусора прерывается. Символические ссылки не обходятся. Все процессы должны использовать один и тот же DATA_DIR на файловой системе, поддерживающей согласованные блокировки. Ручные записи в БД/блобы и старые контейнеры, не использующие блокировки, несовместимы с одновременным GC. При обновлении остановите старые backend/worker перед запуском нового GC. ## Сборка и проверки unrpyc закреплён полным SHA в `backend/Dockerfile`; сборка проверяет HEAD. Отчёт импорта содержит `converterVersion` и `unrpycRevision`. Обновление SHA должно сопровождаться проверкой реальных `.rpyc` нужных версий Ren'Py. `*.tsbuildinfo` исключены из Git. ```sh cd backend pip install -r requirements-dev.txt python -m pytest # Дополнительно: PostgreSQL, отдельные временные schema/database, библиотека не меняется TEST_POSTGRES=1 python -m pytest cd ../frontend npm run build ``` Интеграционные тесты требуют PostgreSQL-пользователя с правами CREATE DATABASE и CREATE SCHEMA. Они удаляют только созданные ими временные базы/схемы. ## Другие движки Unity, GameMaker (FF692) и Godot (Beguiled) остаются отдельным направлением. Сначала нужны доступные экземпляры игр и исследование способа хранения сценария: извлечение ассетов само по себе не восстанавливает логику. Будущие адаптеры должны выдавать PGVN и отчёт неподдерживаемых возможностей. Универсальные конвертеры этих движков в текущий этап не входят.