# LoVisual Проект: клиентский мод (`mod/`, бывш. `combatant-client-26.2/`) + платформа (`backend/`, `frontend/` — сайт+бэкенд), см. Фазу 10/Подсистемы 1-3 ниже. > Старый лаунчер (Rust + Slint) и старый backend (Rust + axum, другой дизайн) > были удалены раньше (решение 2026-09-05) — `backend/` и `frontend/` сейчас > строятся заново по дизайну из Фазы 10 / Подсистем 1-3, это не восстановление > старого кода. > **Коммиты — на английском** (решение 2026-09-23, действует с этого момента > вперёд; более старые коммиты на русском не переписываем). > **TEMP-NOTE (удалить этот блок, когда сделано):** после окончания текущего > рефакторинга владелец squash'ит историю git с 100+ коммитов до ~20, затем > пушит и открывает репозиторий из приватного в публичный (сейчас приват — > ранний код был спижен). Это план владельца, не техническая задача агента — > просто держать коммиты чистыми/по делу до этого момента. --- ## Правила (для мода, продублированы в его TODO) - KISS / DRY / SOLID - Макс. 200 строк на файл (фасады-делегаты исключение) - Макс. 4 файла на папку - Никакого статика, никаких this-escape, никаких мёртвых миксинов - Без пустышек; каждый этап компилируется (`./gradlew build`) - Тесты обязательны для новых модулей (JUnit) --- ## Правила платформы (`backend/`, `frontend/`) — исследовано 2026-09-23 > KISS/DRY/SOLID и "без пустышек, тесты обязательны" из правил мода действуют > и здесь без изменений. Ниже — то, что отличается или уточняется под > Rust/Axum и React/Vite, по итогам ресёрча актуальных практик и антипаттернов. **Лимит на файл: ≤250 строк (не 200, как у мода)** — сознательно шире: у мода лимит калибровался под Java (многословный синтаксис, много boilerplate на класс), а в Rust/TypeScript тот же объём логики обычно компактнее на строку, но модуль (структуры + impl + тесты в одном файле, как в `backend/PLAN.md`) естественно тянет чуть больше. Фасады-делегаты — то же исключение, что и у мода. **Макс. 4 файла на папку** — то же правило, что у мода, без изменений: превышение — сигнал выносить смысловую подпапку. Считаются только файлы, подпапки в лимит не входят (проверено на живом коде мода: `addon/` — 0 файлов + 4 подпапки). **Backend (Rust/Axum) — паттерны:** - Модуль на домен (`auth/`, `accounts/`, `device/`, `avatars/`), а не по техническому слою (`handlers/`, `models/`, `services/` вперемешку) — так одну фичу не размазывает по всему дереву папок - Repository-паттерн: SQL живёт только в `*/repo.rs`, хендлеры его не видят напрямую (уже так в `backend/PLAN.md`) - Собственный `enum AppError` вместо `String`/`anyhow` как типа ошибки на границе HTTP — `String` теряет контекст и заставляет аллоцировать на каждой ошибке - Axum-экстракторы (`Json`, `State`) — валидация на границе запроса компилятором, а не руками в теле хендлера **Антипаттерны — сознательно избегаем:** - `.unwrap()`/`.expect()` на данных из запроса — только `?` с конвертацией через `From<...> for AppError` (уже в `Global Constraints` плана) - Чрезмерный `.clone()` вместо ссылок — первый признак борьбы с borrow checker'ом не по делу - Блокирующий I/O внутри `async fn` (напр. синхронный файловый API) — душит рантайм tokio целиком, а не только один запрос - "Бог-структуры" (`AppState` со всем на свете) — состояние делится по доменам (`AuthState`, `DeviceState`, `AvatarState` — уже так в плане) **Frontend (React/Vite) — структура фичами, не по типу файла:** Один из самых частых промахов — раскладывать по `/components`, `/hooks`, `/services` вперемешку: фича размазывается по всему дереву, и удалить её целиком нельзя, не зацепив другие фичи. Вместо этого — `src/features/<имя>/` хранит свои компоненты+хуки+API-вызовы+типы вместе; общее (`src/shared/`) только для того, что реально используют 2+ фичи. Детали — в `frontend/ARCHITECTURE.md`. Источники: [Rustify — Axum Guide 2026](https://rustify.rs/articles/rust-backend-development-axum-2026), [Rust FAQ — Common Anti-Patterns](https://www.rustfaq.org/en/common-rust-anti-patterns-and-how-to-avoid-them/), [Rust Design Patterns — Anti-patterns](https://rust-unofficial.github.io/patterns/anti_patterns/), [Robin Wieruch — React Folder Structure 2026](https://www.robinwieruch.de/react-folder-structure/). --- ## Статус - [x] Фаза 1: Ребрендинг `combatant` → `lovisual` - [x] Фаза 2: Выкинуть Runtime/JarReplacement + Panic - [x] Фаза 3: Разбивка 12 файлов-гигантов - [x] Фаза 3.5: Удаление читерских модулей (легит-направление) - [x] Фаза 4: Security (BackdoorProtection, StaffTracker) - [x] Фаза 5: Нормализация модулей (ленивый Minecraft, GameClock) - [x] Фаза 6: Тесты (EventBus, конфиг, утилиты) — см. `mod/TODO.md` - [x] Фаза 7: Новые модули по мотивам SoupVisuals — ЗАВЕРШЕНА 2026-09-16 (Cosmetics Overhaul 2026-09-11; Gps/HitBubbles/AmbientParticles/EndCrystal/Balancer/Trinket/ HandsShader — 2026-09-15…16, см. ниже) - [ ] Фаза 8.5: Аудит структуры ≤200/≤4 — 8.5.1 (папки >4) ЗАВЕРШЕНА 2026-09-10 (0 папок >4, коммиты 8846d53…303b17e); 8.5.2 (гиганты >600) ЗАВЕРШЕНА 2026-09-28 (0 файлов >600, крупнейший — Renderer2D 474, фасад-исключение); план в `mod/TODO.md` - [x] Задача «Скачивание» (2026-09-29, ЗАВЕРШЕНА, детали в конце файла) — страница `/download` с двумя кнопками: прямая скачка jar с нашего бэкенда + кнопка GitHub под лентой «временно не работает»; jar 64,4 МБ живёт на VDS и отдаётся gateway (`GET /downloads/*` → `/api/downloads/lovisual.jar`), проверено curl'ом и в браузере; кнопки лендинга/футера ведут на страницу - [ ] Фаза 11: Серверный плагин — сервер может отключать функции мода (идея владельца 2026-09-29, скетч в конце файла) — дизайн не написан, реализация не начата - [ ] Фаза 8.5.3: остаток разбивки гигантов мода (файлы >200 → ≤200) — топ-4 закрыт 2026-09-30 (крупнейшие: ConfigDiffPanelComponent 393→167, RotationManager 391→199, ScriptedTriangulatorHudPanel 386→189, BetterButtons 381→197; коммиты 7e8b6b64, 3e57542b, 76bd8e95, 71f779c8, build+test ✓); остаток — 151 файл >200, не марафон, по мере касания, план в `mod/TODO.md` - [x] Фаза 9: Оптимизация FPS — 9.1 (мёртвые грузы) ЗАВЕРШЕНА 2026-09-11 (137121e…4d60d3d); 9.2 Optimize ЗАВЕРШЕНА 2026-09-25 (ade82f06…8746074d, 4 тумблера + OptimizeState); 9.3 A/B-мерка убрана из плана (решение владельца 2026-09-25: нужен запуск Minecraft на Windows, задачей не является) — фаза закрыта - [ ] Фаза 10: Платформа (сайт + бэкенд) — Подсистемы 1/2/3 спроектированы 2026-09-23 (см. ниже). Реализация: план backend Подсистемы 1 в `backend/PLAN.md` (написан 2026-09-23), frontend/Подсистема 2/3 — планы позже. Бэкенд 2026-09-25: Подсистема 1 backend ЗАВЕРШЕНА — `accounts-service`, `common`, `gateway` (identity, рейт-лимиты, CORS, refresh-ротация, device links) и `configs-service` (4 слота, share-коды, витрина publish/browse/detail/copy, публичный `/users/{id}` с бейджем `early`); планы: `backend/PLAN.md`, `backend/gateway/PLAN.md`, `backend/configs-service/PLAN.md`; e2e через gateway проверен вручную. Следующий шаг — фронтенд (`frontend/PLAN.md`) и интеграция мода. Решения 2026-09-25: мод бесплатный (никаких цен/подписок/ключей на сайте); сайт двуязычный ru/en (i18n); кнопка «Скачать» — прямая ссылка на `releases/latest/download/lovisual.jar` в GitHub; в план сайта добавлены редактор тем в браузере, страница «Скачать» + ченджлог, публичные профили; лендинг — насыщенный (живой ClickGui/HUD, частицы), см. `frontend/PLAN.md`. Интеграция мода 2026-09-27: первый шаг — URL бэкенда стал runtime-настройкой, а не константой сборки. Новый `PlatformConfig` (`mod/src/main/java/dev/loki/lovisual/features/platform/`, + `BackendEndpoint` нормализатор) хранит `backendBaseUrl` через тот же `ConfigSerializer`, редактируется в ClickGui (категория UTILITY главных настроек, рядом со `StyleConfig`). Значение по умолчанию — плейсхолдер `https://api.lovisual.example` (реальный прод-домен ещё нигде в репо не зафиксирован). HTTP-клиент и сама интеграция (auth/device-link/витрина) — отдельная будущая задача. Решение 2026-09-29 (см. задачу «Скачивание» ниже): прямая ссылка на GitHub из кнопок убрана — jar отдаёт наш бэкенд, GitHub остаётся второй кнопкой под лентой «временно не работает». --- ## Фаза 7: Новые модули — референс SoupVisuals > Референс: `ref/soupvisuals-src/` — SoupVisuals 3.2.0 (MC 1.21.11, лицензия SOUP-1.0). > Код референса НЕ копируем (чужая лицензия + декомпил) — только пере-реализация идей > под наши правила (≤200 строк/файл, ≤4 файла/папку, без статики, тесты). > Модули референса лежат в `ref/soupvisuals-src/padej/soup/implement/features/modules/`. - [x] **Hitboxes** (visuals) — цветные хитбоксы сущностей, как у Soup: ТОЛЬКО рендер контейнера с настройкой цвета/видимости, БЕЗ увеличения хитбокса (увеличение — чит, удалён в Фазе 3.5). Референс: `visuals/Hitboxes.java` - [x] **Watermark** (HUD, draggable) — обещан в README, в коде отсутствует - [x] **Gps** (world) — ЗАВЕРШЕНО 2026-09-15: компас-стрелка + маркер к точке (`visuals/world/Gps.java`, `util/world/GpsBearing.java` чистая математика + тесты, команда `%gps | clear`). Референс: `world/Gps.java` — идея взята (стрелка+маркер+дистанция+авто-удаление), код не копировался. Xaero-связка НЕ сделана (была опциональной идеей, не в скоупе запроса). Коммит fb0b288. - [x] **Light** (world) — кастомный цвет освещения (у Soup 41 стр.). Референс: `world/Light.java` - [x] **HitBubbles** (particles) — ЗАВЕРШЕНО 2026-09-15: отдельный модуль `visuals/effects/HitBubbles.java` (не режим HitEffect.BUBBLES — независимое включение, трёхфазная анимация появление/жизнь/исчезновение через новый `ThreeStageProgress`). Заодно вынесены общие хелперы из HitEffect (DRY): `AttackHitPoint`, `OrientedQuadRenderer`, `AnimatedColorModeOptions`. Референс: `particles/HitBubbles.java` (идея взята, код не копировался; интерполяции/множественные текстуры Soup не портированы). Коммит 5f2fc70. - [x] **AmbientParticles + FireFlyParticle** (particles) — ЗАВЕРШЕНО 2026-09-15: модуль `visuals/particles/AmbientParticles.java`, два режима — Ambient (плывущие мошки) и Firefly (блуждание + кольцевой буфер шлейфа вместо Trail-объектов референса). 19 визуальных стилей референса (Stars/Hearts/Bloom/.../Pyramid, asset-тяжёлые) НЕ портированы — фича-крип чит-клиента вне направления проекта; цвет через уже существующий AnimatedRenderColors вместо своей Sync/Custom-системы. Коммит 28ec166. - [x] **NameProtect** (misc) — подмена своего ника и ников друзей в тексте чата/скорборда (у Soup 42 стр.). Референс: `other/NameProtect.java` - [x] **EndCrystal** (visuals) — ЗАВЕРШЕНО 2026-09-15: `visuals/scene/EndCrystal.java` + `mixins/render/entity/EndCrystalRendererMixin.java` (WrapOperation на ModelPart.render, различает 2 вызова glass + 1 cube счётчиком-ordinal — тонировка/скрытие частей) + inject в `ClientLevelMixin.removeEntity` (осколки-вспышка на месте разрушения, т.к. ваниль убирает сущность мгновенно). Референс: `other/EndCrystal.java` — идея взята (кастомный цвет, "доиграть" разрушение), Sync/Custom×4-цвета+Wave/Vertex упрощены до общего AnimatedRenderColors. Коммит 50db8e3. - [x] **Trinket** (не "2.0" — CosmeticDisplay удалён откатом 2026-09-11, апгрейдить нечего) — ЗАВЕРШЕНО 2026-09-16: отдельный модуль `visuals/items/Trinket.java` в стиле текущих ChinaHat/TazikHat. `util/physics/TrinketPhysics` (чистая, тесты) — шарик на пружине у ног игрока (гравитация/отскок/тетер). Рендер — тонированный billboard (наш AnimatedRenderColors) + затухающий шлейф (кольцевой буфер) + звук на отскок (ванильный SoundEvents.SLIME_JUMP). Библиотека 10 иконок-спутников референса и 8 кастомных звуковых тем НЕ портированы — нужны новые ассеты. Референс: `other/Trinket.java` (там тоже почти нет реализации — только настройки). Коммит 840a716. - [x] **Cosmetics Overhaul (Unified Cosmetics)** — ЗАВЕРШЁН 2026-09-11: слоты `CosmeticSlot` (MODEL/PET/HEAD/SHOULDERS/BACK/WAIST/LEGS/HELD), парсинг `slot` из avatar.json + эвристика по имени папки, `CosmeticManager` на EnumMap (аксессуары — несколько одновременно), `CosmeticsScreen` (3D-превью drag/zoom + хотспоты + правая панель live-apply с "None" первым), per-slot offset/rot/scale через NumberValue-поля, built-in ChinaHat/TazikHat/BackSword в `cosmetics/builtin/` (старые модули chinahat/tazikhat/backsword удалены), i18n en+ru, тесты CosmeticSlotTest/CosmeticsHotspotTest. - ~~CosmeticDisplay 232 / TazikHatRenderer 242 / BackSwordRenderer 231 >200~~ — разбиты: CosmeticDisplay 196 + CosmeticBuiltins (значения+диспетчер built-in), TazikHat 186 (палитра-математика переиспользует ChinaHatPalette), BackSword 200 (edge/glow слиты в параметризованный drawKatana). - Не сделано из плана (следующий заход): иконки-превью built-in в списке слота, правка слотов SHOULDERS/LEGS/PET из экрана (сейчас только HEAD/BACK built-in), предпросмотр до эквипа. - [x] **Balancer** (mctiers) — ЗАВЕРШЕНО 2026-09-16: `features/module/modules/misc/Balancer.java` (fixType Vanilla/UHC/Pot/NethOP/SMP/Sword/Axe/Mace) + `util/mctiers/MCTiersCache` (асинхронный кэш, паттерн SkinManager) + `util/mctiers/MCTiersTier` (форматирование, тесты) + команда `%tier <ник> [режим]`. Референс `mctiers/Balancer.java` почти пуст — логика была в недоступной McTiersModule-базе; реальный публичный API mctiers.com найден и проверен через сторонний open-source плагин (sinnayuh/MCTier): `GET https://mctiers.com/api/search_profile/`. Коммит cf81ee4. - [x] **HandsShader** — ЗАВЕРШЕНО 2026-09-16: сверка показала, что Chams уже реализует 9/10 режимов Soup (Solid/Chroma/Balatro/Smoke/Stripes/Glow/Glass/Snow + свои Metallic/Noise) и полный dual/blend-набор (Mix/Add/Multiply/Screen/Overlay/ Difference, `HandBlendUniforms`) — сверх ожиданий. Не хватало только "Invert": добавлен `hand_chams_invert.frag` (RGB-инверт под маской руки через уже существующий UBO HandChams/u_Mix.x, без HSV-тумблеров и edge-glow референса — это дублировало бы наш Glow) + регистрация пайплайна + вписан в оба mode-списка Chams (основной и dual shader1/shader2). Референс: `visuals/HandsShader.java`. Коммит 76ffdec. **Это был последний пункт Фазы 7** — SoupVisuals-раздел закрыт полностью (Gps/HitBubbles/AmbientParticles/EndCrystal/Balancer/Trinket/HandsShader). - [x] **Фикс ChinaHat** — текущая геометрия кривая (смахивает на самбреро/зонт); переделать модель в нормальную конусную шляпу по референсу Soup (`visuals/ChinaHat.java` + `ChinaHatModel.java`). --- ## Фаза 10: Платформа — сайт + бэкенд (дизайн от 2026-09-23) > Прим.: лаунчер и старый backend (Rust+axum) были удалены ранее (см. шапку файла) — > это НЕ восстановление старого кода, а новый дизайн с нуля под текущие требования. > Статус: дизайн готов, backend Подсистемы 1 реализован 2026-09-25 (планы в > `backend/PLAN.md`, `backend/gateway/PLAN.md`, `backend/configs-service/PLAN.md`); > дальше — фронтенд (`frontend/PLAN.md`). > Код — в выделенных `backend/` и `frontend/` (созданы 2026-09-23), отдельно от `mod/`. ### Границы (декомпозиция) - **Подсистема 1 (этот дизайн)**: аккаунты, облачные конфиги, аватарки, публичная витрина конфигов. - **Подсистема 2 (backlog, отдельный дизайн)**: RPC-чат (переименован из рабочего названия "RVS" — отдельная чат-группа для владельцев клиента, не привязана к конкретному minecraft-серверу), друзья/статус онлайн, виджеты-телеметрия друга (HP/координаты/сервер/в бою или нет поверх HUD). Требует realtime-слой (WebSocket/presence) — не влезает в REST-модель Подсистемы 1. ### Архитектура Микросервисы с самого старта (осознанный выбор владельца проекта, не default-рекомендация): `accounts-service`, `configs-service`, `chat-service` (зарезервирован под Фазу 11), каждый — отдельный Rust/Axum процесс, общение между сервисами по gRPC (tonic). Перед ними — отдельный **API Gateway** (тоже Axum): единственная публичная точка (`api.`), терминирует TLS, проверяет JWT один раз, роутит по gRPC во внутренние сервисы; сайт и мод не знают о микросервисах и стучатся только в gateway. Для будущего chat-service gateway дополнительно проксирует WebSocket (`wss://api./chat/ws`). Один физический Postgres-инстанс/порт, у каждого сервиса — своя отдельная БД внутри этого инстанса (`accounts_db`, `configs_db`, `chat_db`) — логическая изоляция без отдельных серверов на старте. Аватарки и файлы конфигов — в S3-совместимом хранилище (MinIO), не в Postgres. Фронтенд сайта: React + Vite + Tailwind CSS. Дизайн/палитра — плагин `design-assets` (jezweb/claude-skills маркетплейс, skills `color-palette`, `favicon-gen`, `icon-set-generator`, `image-processing`, `ai-image-generator`) + встроенный skill `frontend-design`. ### Модель данных **accounts_db** - `accounts`: id, email (unique), password_hash (argon2id), display_nick (произвольный, НЕ привязан к minecraft-нику), created_at - `avatars`: account_id, s3_key, uploaded_at - `device_links`: id, account_id, device_token_hash, linked_at, last_seen — привязанные устройства (моды), видны/отзываемы в личном кабинете **configs_db** - `config_slots`: id, account_id, slot_index (1..4 — фиксированный лимит на аккаунт), name, data (jsonb), updated_at - `share_codes`: config_slot_id, code (unique, криптографически случайный, постоянный, владелец может регенерировать — старый код инвалидируется) - `showcase_listings`: config_slot_id, title, description, published_at — публичная витрина (фича из brainstorm-опроса) Свой конфиг грузится по имени/индексу напрямую. Чужой — только по коду через `%config load <код>` в игре (см. API ниже); одноразовость кода осознанно отклонена — код обязан быть постоянным (ссылка-приглашение), пока владелец сам не перевыпустит. ### Auth — два независимых пути **Сайт**: обычный email + пароль. - `POST /auth/register` {email, password, nick} - `POST /auth/login` → access JWT (~15 мин) + refresh-токен (httpOnly cookie) - `POST /auth/refresh`, `POST /auth/logout` **Мод → аккаунт**: OAuth 2.0 Device Authorization Grant (как Twitch/PlayStation), пароль в мод никогда не вводится. 1. Мод: `POST /device/code` → `{device_code, user_code, expires_in}` (`user_code` короткий, напр. `ABCD-1234`) 2. Мод показывает `user_code` в настройках, игрок вводит его на `site./link` (уже залогинен на сайте) → `POST /device/confirm` 3. Мод параллельно поллит `POST /device/token` {device_code} каждые 2-3с 4. После подтверждения — долгоживущий `device_token` (хранится у мода локально), используется как Bearer-заголовок ко всем запросам gateway 5. Личный кабинет → список привязанных устройств с `last_seen` и кнопкой revoke (единичный отзыв, не ломает остальные устройства) ### API конфигов и аватарок - `GET /configs` → 4 слота аккаунта (name, updated_at, share_code) - `PUT /configs/{slot}` {name, data} → сохранить, last-write-wins - `GET /configs/{slot}` → своё содержимое - `POST /configs/{slot}/regenerate-code` - `GET /configs/shared/{code}` → чужой конфиг по коду (код = секрет, отдельной авторизации получателя не требует) — этот путь дёргает `%config load <код>` - `POST|DELETE /configs/{slot}/publish` → витрина - `GET /showcase?sort=new|popular&page=`, `GET /showcase/{listing_id}` — публично, без авторизации; "скопировать к себе" → копия в свободный слот - `POST /avatars` (multipart, ≤5MB, png/jpg/webp, валидация по magic bytes, не по расширению) → ресайз до 256×256, S3 + CDN-URL в `accounts.avatar_url` ### Безопасность - Rate-limit на `/auth/login` и `/device/*` (брутфорс / спам кодов) - `password_hash` — argon2id - `device_token` хранится хэшированным в БД, как пароль - Share-код — криптографически случайный, не инкрементный ID (не перебираем) ## Пасхалки (решение владельца 2026-09-25) 1. **`.env`-ловушка** (gateway): запросы на `.env` (включая через `../`) → 200 + фейковый шуточный `.env` («nice_try_skiddie…»), в апстрим не уходят, IP в лог. Делается вместе с фиксом обхода лимитов через dot-segments. 2. **Konami code на сайте** (↑↑↓↓←→←→BA): ClickGui на главной включает TrollfaceMask, всё уходит в радужную тему. 3. **Сообщение в консоли DevTools**: ASCII-логотип LoVisual + «Шаришь? Исходники аддонов — на витрине». 4. **`IDDQD`**: `%config load IDDQD` в моде и `/configs/shared/IDDQD` на сайте отдают шуточный конфиг «God mode» — всё выключено, кроме ChinaHat. Код не пересекается с настоящими (в алфавите share-кодов нет `I`, длина 5 ≠ 8). 5. **404 = пустой мир**: падает блок, по клику «ломается» с частицами, как в игре. 6. **`/coffee` → 418 I'm a teapot** (gateway) со ссылкой на мод. Планы: `frontend/PLAN.md` Task 11, `backend/configs-service/PLAN.md` Task 7, `backend/gateway/PLAN.md` Task 12; мод-часть `%config load IDDQD` — в плане интеграции мода. --- ## Подсистема 2: RPC-чат + друзья + виджеты-телеметрия (дизайн от 2026-09-23) > Статус: спроектировано, реализация не начата. Требует realtime-слой > (WebSocket + presence), поэтому — отдельный `chat-service` (см. Фазу 10, > раздел "Архитектура"), а не REST как у Подсистем 1/3. > > Важно не путать с уже существующей в моде системой "друзей" > (`NameProtect` и т.д., `features/module/modules/misc/protect/NameProtect.java`) > — это **локальный** список ников на клиенте для защиты от пинга/подсветки, > не связан с аккаунтами. Ниже — отдельная, новая система друзей на аккаунтах. ### Структура чата Один глобальный канал ("все онлайн-владельцы клиента") + личные сообщения 1-на-1 только между взаимными друзьями. Без тематических каналов/групп — не усложняем на старте. - Только live-доставка, **без хранения истории на сервере** (как IRC) — ушёл из чата/оффлайн — старое не восстановить. Упрощает `chat_db` до почти пустой схемы (не нужна retention-политика, GDPR-очистка и т.п.) - `chat_db` хранит только: список текущих online-подключений (для presence) и friend-графа (см. ниже) — сами сообщения в БД не пишутся, гоняются напрямую через WebSocket-хаб в памяти chat-service ### Друзья (новая, account-based система) - Добавление — запрос/принятие по нику аккаунта (не по minecraft-нику): поиск на сайте или в клиенте → `POST /friends/request` {target_nick} → получатель видит входящий запрос (на сайте и/или в клиенте) → `POST /friends/accept|decline` - `chat_db.friendships`: account_id_a, account_id_b, status (pending/accepted), created_at (симметричная связь после accepted) - Личные сообщения в чате доступны только между accepted-друзьями ### Виджеты-телеметрия друга (HUD) - Данные: HP, координаты, minecraft-сервер (адрес/имя), в бою (да/нет) — каждое поле передаётся отдельно, не пакетом "всё или ничего" - **Настраиваемый шаринг по полям** (выбор пользователя, приватность): `chat_db.telemetry_sharing_prefs`: account_id, share_hp (bool), share_coords (bool), share_server (bool), share_combat_state (bool) — дефолт консервативный (напр. только HP+combat включены, координаты/сервер выключены by default, т.к. чувствительны) - Транспорт: тот же WebSocket-канал chat-service, отдельный тип сообщения `telemetry_update` (broadcast только подписанным взаимным друзьям, с фильтром по `sharing_prefs` получателя-источника на сервере — клиент никогда не получает поле, которое источник не разрешил шарить) - Отображение — новые draggable HUD-элементы по образцу существующих (`registerDraggableHudElement`), один виджет на друга или агрегированный список — решить на этапе UI-дизайна перед реализацией ### Presence (онлайн-статус) - Мод коннектится к `wss://api./chat/ws` с `device_token` при старте клиента (не требует отдельного логина — токен уже есть после device-link из Подсистемы 1) - `online` = есть открытый WebSocket; `last_seen` обновляется на disconnect - Список друзей на сайте/в клиенте показывает online/offline живым статусом через тот же сокет (site тоже может подключиться, если залогинен — видеть друзей онлайн из браузера) ### Профили/статусы (лёгкая версия, backlog внутри backlog) Идея из brainstorm-опроса ("уровень/бейджи/тайтлы", "давно с нами", "шарил N конфигов") — не проектируется в деталях сейчас, т.к. это чисто косметическая надстройка без технических рисков — легко добавить в `accounts_db` (поля `created_at` уже есть, `badges` — просто jsonb-массив) на любом этапе позже, не блокирует остальную Подсистему 2. ### Рейт-лимиты Подсистемы 2 (дополнение к общей таблице в Фазе 10) | Действие | Лимит | |---|---| | Отправка сообщения (глобальный канал) | 1 / сек / аккаунт, burst 5 | | Отправка личного сообщения | 2 / сек / аккаунт | | `POST /friends/request` | 20 / час / аккаунт (защита от спама заявками) | | `telemetry_update` (от клиента к серверу) | не чаще 1 раз / 2 сек / аккаунт — сервер троттлит и досылает клиентам с той же частотой, не завязан на тик-рейт игры | --- ## Подсистема 3: Аддоны 2.0 + витрина аддонов (дизайн от 2026-09-23) > Статус: спроектировано, реализация не начата. Расширяет существующий Java > addon-API (`docs/api/addons.md`, `addon/core/AddonManager.java`) — тот API > и дальше можно ломать (решение владельца 2026-09-05, см. верх файла), новый > скриптовый слой — надстройка сбоку, не замена. ### 1. Скриптовые аддоны (новый тип, для витрины) - JavaScript через GraalVM, отдельный от Java-`AddonManager` загрузчик рядом с `addon/core` - Точка входа — `manifest.json` (не `fabric.mod.json`: скрипт-аддону не нужен Fabric-loader): `id, name, version (semver), author, tags, entry.js` - Ограниченный whitelisted API — обёртки над тем же, что доступно Java-аддонам сейчас: `registerModule` (JS-класс с полями-настройками), `registerCommand`, `addRenderCallback` (только 2D HUD-отрисовка). Никакого прямого доступа к рефлексии/файловой системе/сети — весь I/O только через явно предоставленные хелперы контекста - Java-аддоны (текущие Fabric-моды) остаются для продвинутых случаев (мировой рендер, миксины), устанавливаются вручную; в витрине идут отдельным помеченным разделом от скриптовых ### 2. Больше точек расширения в существующем Java API Добавить в `LoVisualAddonContext`: - Доступ к событиям (input, world tick, packet) - Кастомные конфиг-виджеты в ClickGui - Регистрация собственных рендер-пайплайнов (не только callback) - API для чтения/записи через облачную конфиг-систему Подсистемы 1 ### 3. Метаданные / версии / автообновления - `manifest.json` получает `version` (semver) — и у скриптовых, и у Java-аддонов (в `custom.lovisual:addon` добавляется поле) - `AddonManager` при старте сверяет установленную версию с версией на сайте (если аддон пришёл оттуда — манифест хранит `source_url`/`addon_id`) → бейдж "обновление доступно" в `LoVisualAddonManagerScreen`; апдейт — по клику пользователя, никогда не втихую ### 4. Витрина аддонов на сайте - Публикация обязательно требует исходники (архив или ссылка на репозиторий) — открытость как единственный механизм доверия. Читерские аддоны **не блокируются** (решение владельца) — пользователь сам выбирает, что качать - Теги: обязательный `type: cheat | legit`; необязательные категории (мультивыбор, для фильтров, не для модерации): `visuals | hud | new-module | utility | qol | other` - Данные: `addons_db` (отдельная БД в том же Postgres-инстансе, как `accounts_db`/`configs_db`) — таблицы `addons` (id, owner_account_id, latest_version, tags[], type, source_url, downloads_count) и `addon_versions` (addon_id, version, changelog, file_s3_key, published_at) - Скачивание — прямая ссылка на файл/архив в S3; установка не автоматическая и не тихая — юзер жмёт "добавить" в клиенте, клиент качает и кладёт в addons-папку, но не запускает без явного включения в `LoVisualAddonManagerScreen` ### 5. Админка на сайте (`/admin`) - Новая роль `accounts.role: admin` (по умолчанию `user`); доступ к admin-эндпоинтам проверяется на gateway, не на фронте — обычные пользователи даже не видят `/admin` в UI - Модерация аддонов: снять с публикации / пометить проблемным (не за cheat/legit — только за реальные нарушения: сигналы о malware от юзеров, нерабочие/поддельные ссылки на исходники, спам) / **удалить** аддон целиком (со всеми версиями, если, например, DMCA или явный malware) - `accounts.can_publish_addons: bool` (default `true`) — отдельный флаг, отзываемый админом независимо от общего бана аккаунта: юзер продолжает пользоваться сайтом/конфигами/чатом, но не может публиковать новые аддоны (существующие остаются видны, если явно не удалены отдельно) - Модерация витрины конфигов (Подсистема 1): аналогично, скрыть листинг - Управление аккаунтами: бан/разбан (полный), отзыв `can_publish_addons`, просмотр жалоб - Статистика: кол-во аккаунтов/аддонов/конфигов, загрузок за период; график регистраций по дням/неделям/месяцам; активные аккаунты DAU/MAU (по `device_links.last_seen` + активности на сайте) ### 6. Рейт-лимиты (сквозной механизм, все Подсистемы) Реализуются на **API Gateway** (единая точка — не дублируются в каждом сервисе), token bucket на Redis (или in-memory при однонодовом деплое на старте), ключ — `account_id` для авторизованных запросов, `IP` для неавторизованных (регистрация, логин). | Эндпоинт(ы) | Лимит | Почему | |---|---|---| | `POST /auth/login` | 5 / мин / IP | брутфорс пароля | | `POST /auth/register` | 3 / час / IP | спам-аккаунты | | `POST /device/code`, `/device/token` | 10 / мин / IP | спам device-кодов | | `PUT /configs/{slot}` | 20 / мин / аккаунт | защита БД от спама сохранений (мод может дёргать часто) | | `GET /configs/shared/{code}` | 30 / мин / IP | перебор share-кодов | | `POST /avatars` | 5 / час / аккаунт | не файлопомойка | | `POST /addons` (публикация версии) | 10 / день / аккаунт | не спамить витрину | | `GET /showcase`, `GET /addons` (листинги) | 60 / мин / IP | защита от скрейпинга/DoS чтения | | Chat WebSocket: отправка сообщения | 1 / сек / аккаунт (burst 5) | защита от флуда в RPC-чате | Превышение → `429 Too Many Requests` с `Retry-After`; для WebSocket — локальный drop сообщения с уведомлением клиенту, без разрыва соединения. --- ## Задача «Скачивание»: две кнопки + jar с нашего бэкенда (2026-09-29, ЗАВЕРШЕНА) Проблема: кнопка «Скачать» вела на `/downloads/lovisual.jar`, но на проде такого файла нет — nginx по `try_files` отдаёт `index.html`, то есть вместо мода скачивалась HTML-страница (проверено 2026-09-29: `content-length: 2458`). Решение владельца 2026-09-29: 1. Все кнопки «Скачать» (hero лендинга, FaqDownload, футер) ведут на страницу `/download`, а не на файл напрямую. 2. На `/download` две кнопки: - **«Скачать напрямую»** — рабочая, тянет jar с нашего бэкенда (`/api/downloads/lovisual.jar` → nginx `/api/` → gateway → файл на диске); - **«Скачать с GitHub»** — закрыта лентой «временно не работает» (релизы в GitHub сейчас не публикуются), пока её не уберут вручную. 3. Gateway: новый маршрут `GET /downloads/*` — `tower-http` `ServeDir` на каталог из env `DOWNLOADS_DIR` (в docker: `/srv/downloads`, том `./downloads:ro`), отдаёт потоком (60 МБ не буферизуются в памяти), `Content-Type` гаджетом по расширению (`application/java-archive`), `Accept-Ranges: bytes`, 404 на отсутствующий файл, каталог не отдаётся. Обход `..` отсекает существующий guard (`reject_ambiguous_paths`, 400); nginx не менялся — фронт ходит через существующий `location /api/` (срезает префикс → `GET /downloads/*` на gateway). 4. Артефакт: `./gradlew build` в `mod/`, jar 64 415 945 байт (61,4 МБ), sha256 `359166d2…4156f9`, кладётся на VDS в `/opt/lovisual/backend/downloads/lovisual.jar`, `docker compose build gateway` + `up -d gateway`, проверка curl'ом. 5. Релизный процесс: пересобрать мод → положить jar на VDS вручную (мод остаётся отдельным Gradle-проектом, в `deploy.sh` не встраивается). Проверка (2026-09-29, прод): `curl -I https://visual.loki-code.dev/api/downloads/lovisual.jar` → 200, `content-type: application/java-archive`, `content-length: 64415945`, байты `PK\x03\x04`, Range-запрос работает, неизвестное имя → 404; страница `/download` (снимок в браузере) показывает обе кнопки, GitHub — под лентой; `bun run test` 114/114, `cargo test --workspace` зелёный, clippy/oxlint чистые. **Готово (коммиты):** `54af7158` gateway-маршрут + тесты `tests/downloads.rs`, `fc440f0c` страница/лендинг/футер/i18n/тесты, `70e33f29` лента не перекрывает надпись кнопки, `eb6e38e2` `.dockerignore` (контекст сборки без `target/` и jar), `192ef52e` этот план. Деплой на VDS — rsync изменённых файлов (рабочее деревие `/opt/lovisual` не синхронизировано с origin и содержимо одних и тех же коммитов — уникальных правок на сервере нет), `.env` на сервере не трогался. --- ## Фаза 11: Серверный плагин — сервер может отключать функции мода (скетч 2026-09-29) > Идея владельца 2026-09-29. Статус: обсуждение, дизайн не написан, кода нет. **Что это.** Отдельный серверный плагин (Paper/Spigot или Fabric-сервер), который публикует подключившимся игрокам «политику»: какие возможности LoVisual на этом сервере запрещены. Мод применяет её автоматически — честный игрок не может включить запрещённое, пока находится на этом сервере. **Как связываемся.** Plugin message channel (custom payload) `lovisual:policy`: сервер шлёт политику при входе и по запросу. Надёжнее HTTP к нашему бэкенду — работает и без интернета, и без бэкенда. **Формат (черновик):** ```json { "deny": ["reach", "esp", "freecam"], "mode": "lock", "reason": "правила сервера" } ``` - `mode: lock` — модули выключены и не включаются, ClickGui показывает замок с подписью «отключено сервером»; - `mode: suggest` — только предупреждение, игрок сам решает. **Где код.** - `server-plugin/` — новый Gradle-проект в корне репо (серверная Java, не Fabric-клиент, поэтому отдельно от `mod/`); - в моде — `features/platform/serverpolicy/` рядом с существующей платформенной папкой (`PlatformHttpClient`, `BackendEndpoint` переиспользуем, если политика однажды будет тянуться с бэкенда); - конфиг политики сервера — YAML в папке плагина. **Честные ограничения (записать заранее):** применение политики — на клиенте, читер может её игнорировать. Это не античит: цель — правила сервера и удобство честных игроков, а не защита от нарушителей. Настоящая защита — серверный античит, он отдельно. **Открытые вопросы:** целевой серверный софт (Paper vs Fabric-server vs Velocity); публикация политики через наш бэкенд (`/policy/{server}`) или только локальным конфигом плагина; команды `%policy` в чате; не конфликтует ли это с Подсистемой 2 (там realtime-слой и друзья, здесь достаточно одного payload при входе).